@@ -34,18 +34,28 @@ Enabled uploads pre-flight the candidate against the live routing generation and
`endpoints`, `routes[].renderer`, global `resolveRequest`, global `renderError`, and global `renderers` are rejected. `parseSubmitResponse` returns only `{taskId, taskData, immediate?, state?}`; `clientResponse` is rejected.
`icon` is an optional LobeHub icon name string (for example `Sora.Color`). The values `text` and `text:<label>` request a generated text avatar instead (label defaults to the first two characters of `name`). It is display-only and does not participate in routing, billing, or admission beyond type and length checks.
`icon` is an optional LobeHub icon name string (for example `Sora.Color`). The values `text` and `text:<label>` request a generated text avatar instead (label defaults to the first two characters of `name`). Inline data URIs and remote URLs are rejected. `icon` is display-only and does not participate in routing or billing.
A plugin whose vendor has no LobeHub icon ships its logo as a sidecar file, `icon.svg` or `icon.png`, next to `plugin.js`; the manifest stays readable and the image never appears in source diffs. Built-in plugins embed the sidecar from `plugins/tasks/<key>/`. For uploads the admin UI reads the file and sends it in the upload request's separate `icon` field as `data:image/png;base64,...` or `data:image/svg+xml;base64,...`; marketplace indexes declare it as `iconFile.path` and the installer fetches it from the index's own origin. The gateway stores the logo apart from the source and serves it from `GET /api/plugin/task/:key/icon` (optionally `?version=`), which the UI renders only through `<img>`. Limits: 512 KiB; PNG must carry the PNG signature; SVG must be well-formed XML with an `svg` root and no `script` or `foreignObject` elements, event-handler attributes, DOCTYPE, `javascript:` values, or absolute `http(s)` references. Supply artwork legible on both light and dark backgrounds (an SVG may use a `prefers-color-scheme` media query).
`baseUrl` is an optional default upstream address for channels of type 61 ("Task Plugin"). When an administrator binds the plugin and leaves the channel Base URL empty, the host copies this value onto the channel before validation and records `base_url_source: plugin_default` in the channel audit event; the persisted channel value is what every later request uses, so a later plugin update that changes `baseUrl` does not move existing channels. The value must be an absolute `http` or `https` URL without credentials, query, or fragment; the host is lowercased and must be ASCII (use punycode), trailing slashes are stripped, and the normalized value must not exceed 191 characters (the width of the channel `base_url` column on MySQL). Private, loopback, and plain `http` addresses are allowed for self-hosted upstreams; the admin UI flags them before binding. `baseUrl` is ignored on legacy `channelTypes` channels, which keep the built-in per-type default. The default host is not implicitly added to `allowedHosts`: if the administrator points the channel at a different host, plugin requests to the author's default host are rejected.
`allowedHosts` lists extra hosts that plugin requests may target besides the channel base URL host. Each entry is `host` or `host:port` (IPv6 literals bracketed, for example `[::1]:8080`); schemes, paths, credentials, and queries are rejected. Entries are lowercased and must be unique after normalization. An entry with a port matches only requests to that port (default ports `80`/`443` match requests that omit them).
| `icon` | string | Optional LobeHub icon or `text` / `text:<label>`. ≤ 128 characters. Image logos ship as a sidecar `icon.svg` / `icon.png`, not in the manifest. |
| `description` | LocalizedText | Optional plugin summary. See LocalizedText. ≤ 512 runes per locale. |
| `version` | string | Required semver. |
| `sortPriority` | `number` | Optional signed 32-bit integer, default `0`. Higher values appear first; ties use ascending plugin key. Display only: does not change routing or override precedence. |
| `website` | `string` | Optional plugin website, independent of `author.url` and `baseUrl`. Empty is allowed; otherwise an absolute HTTPS URL with a valid ASCII hostname (use punycode for internationalized domains), without credentials, whitespace, control characters, or backslashes. Paths, queries, and fragments are allowed. |
| `author` | `{name, url?}` | Required name; `url`, when present, must be an absolute HTTP(S) URL. |
| `baseUrl` | string | Optional default Base URL for type-61 Task Plugin channels. Absolute `http(s)` URL, no credentials/query/fragment, ASCII host, trailing slash stripped, ≤ 191 characters. |
| `allowedHosts` | string[] | Optional extra request hosts, `host` or `host:port`. No schemes, paths, or credentials. |
`channelTypes` lists the legacy channel types this plugin's driver can drive (for example, sora declares `[55, 1]` because the same OpenAI-type base URL and bearer key serve both chat and video). Every entry equally participates in channel selection, historical `Task.Platform` matching, and the `byChannelType` routing index; the same type value may not appear on two plugins. Third-party plugins normally omit `channelTypes` and live on type-59 "Task Plugin" channels bound by `task_plugin_key`. The previous split identity/compatibility field names are rejected.
`channelTypes` lists the legacy channel types this plugin's driver can drive (for example, sora declares `[55, 1]` because the same OpenAI-type base URL and bearer key serve both chat and video). Every entry equally participates in channel selection, historical `Task.Platform` matching, and the `byChannelType` routing index; the same type value may not appear on two plugins. Third-party plugins normally omit `channelTypes` and live on type-61 "Task Plugin" channels bound by `task_plugin_key`. The previous split identity/compatibility field names are rejected.
Numeric `usageSchema` fields declare a host-owned unit of `second`, `count`, `token`, or `credit`. Boolean fields declare `{type: "boolean"}`.
...
...
@@ -55,6 +65,25 @@ Numeric `usageSchema` fields declare a host-owned unit of `second`, `count`, `to
`meta.description` and each `usageSchema` field `description` accept LocalizedText. A bare string is equivalent to `{en: <string>}`. A map must include a non-empty `en` value. The host normalizes both forms to a map; API responses always emit an object.
Enum fields may additionally declare `enumLabels`, a map from enum values to LocalizedText display names:
```js
video_input:{
enum:["none","video"],
description:{en:"Reference video input",zh:"参考视频输入"},
enumLabels:{
none:{en:"No reference video",zh:"无参考视频"},
video:{en:"With reference video",zh:"有参考视频"},
},
}
```
`enumLabels` is optional and may cover only some options. It is valid only on enum fields, and every key must exactly match a declared enum value. Each label follows the same locale normalization, required English fallback and 256-rune-per-locale limit as a field description. Labels are short phrases without trailing punctuation. They never change usage facts, expression conditions or values submitted by UI controls. Boolean fields use their description with a localized yes/no state and cannot declare `enumLabels`.
The UI selects the current language, then its primary language, then English. If no option label is provided, it displays the original enum value; a missing field description falls back to the field name. Labels are plugin data, not frontend translation keys.
This is an additive extension of `apiVersion: 1`. Updated gateways continue to load plugins without `enumLabels`; older gateways with strict property validation reject plugins that declare it.
```js
description:"Video generation via the vendor API"
description:{en:"Video generation via the vendor API",zh:"通过厂商接口生成视频"}
...
...
@@ -69,6 +98,24 @@ Rules:
- The frontend resolves a locale with exact tag → primary subtag → `en` (for example `zh-TW` → `zh` → `en`).
- Description copy in any language must not include vendor currency prices. The same prohibition applies to `usageExamples` labels.
### Description writing and translation conventions
Descriptions are short, user-facing phrases without trailing punctuation in any language. Keep translations equivalent in meaning. For `usageSchema` fields, use the following wording:
| Field meaning | Wording | English example | Chinese example |
| --- | --- | --- | --- |
| Numeric billing quantity | Billing subject + unit price | Song generation unit price | 生成歌曲单价 |
| Boolean | Whether a state is enabled or something is present | Whether audio is generated / Reference video present | 是否生成音频 / 存在参考视频 |
| Other enum condition | Short condition name | Output video resolution | 输出视频分辨率 |
- A shared description must cover all supported actions. For Suno, `clips.description` is `Song or lyrics generation unit price` / `生成歌曲或歌词单价`, and `action.description` is `Generate songs or lyrics` / `生成歌曲或歌词`.
- Boolean descriptions name the affirmative state represented by `true`; do not add a question mark or invert the meaning of the field.
- Preserve distinctions such as input versus output. Units belong in `unit`; do not repeat unit explanations or numeric prices in the description.
- Keep protocol limits, usage sources, estimation and settlement details in code comments or technical documentation, rather than in display descriptions.
- For example, replace `Requested video duration in seconds.` / `请求的视频时长,单位为秒。` with `Video generation unit price` / `视频生成单价`; replace `Whether audio is generated. Default true.` / `是否生成音频。默认为 true。` with `Whether audio is generated` / `是否生成音频`.
- These are authoring conventions for plugin metadata, not additional runtime validation rules. Existing `LocalizedText` wire types, usage quantities, enum values and billing semantics are unchanged; a numeric field still contains usage, not a price.
## Request body
Every decoder receives one host-parsed body:
...
...
@@ -175,3 +222,5 @@ The host classifies the HTTP status before trusting a non-terminal parse:
| Other 4xx | Call the parse hook with `response.status`. A still-non-terminal result is unrecognized and increments `PollFailures`. |
A valid 2xx non-terminal parse resets `PollFailures` to 0. After `TASK_POLL_MAX_FAILURES` (default 20) consecutive failures the task becomes `FAILURE` and follows the existing refund chain. The 24h `TASK_TIMEOUT_MINUTES` sweep remains the outer deadline.
Marketplace index v1 plugin entries also accept optional `sortPriority` and `website` fields. Each source is sorted independently by descending priority and ascending key. Missing or invalid priorities fall back to `0`; invalid websites are hidden. Installed plugins and channel binding options use plugin metadata. Website links open only on user interaction; the host does not fetch them.
"website":{"oneOf":[{"const":""},{"type":"string","format":"uri","pattern":"^[Hh][Tt][Tt][Pp][Ss]://[^/@?#\\s]+(?:[/?#]|$)"}],"description":"Absolute HTTPS URL with a valid ASCII hostname (use punycode for internationalized domains) and no credentials; validated by the host."},
{name:"approved host with port",requestURL:"http://192.168.1.10:8080/task",baseURL:"http://192.168.1.10:8000",allowedHosts:[]string{"192.168.1.10:8080"}},
{name:"approved host with explicit default port",requestURL:"https://upload.example.com/task",baseURL:"https://api.example.com",allowedHosts:[]string{"upload.example.com:443"}},
{name:"approved host port mismatch",requestURL:"http://192.168.1.10:9000/task",baseURL:"http://192.168.1.10:8000",allowedHosts:[]string{"192.168.1.10:8080"},wantError:"not allowed"},
{name:"subdomain is not implicit",requestURL:"https://evil.api.example.com/task",baseURL:"https://api.example.com",wantError:"not allowed"},