openclaw-zero-video 0.0.0-stage → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -2
- package/dist/config.d.ts +27 -0
- package/dist/config.js +114 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +154 -0
- package/dist/jobs.d.ts +81 -0
- package/dist/jobs.js +689 -0
- package/dist/providers.d.ts +16 -0
- package/dist/providers.js +268 -0
- package/dist/transport.d.ts +24 -0
- package/dist/transport.js +235 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +1 -0
- package/openclaw.plugin.json +103 -0
- package/package.json +35 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ghirne
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,148 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Zero Video
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate videos through signed-in consumer AI video sites on the estate Chromium lane.
|
|
4
|
+
|
|
5
|
+
The plugin is a thin CDP driver plus a durable job store. It does not use provider APIs, does not read cookies or credentials, and does not authenticate. Login is performed manually in the Webtop on cosmos; the plugin reuses that persistent profile.
|
|
6
|
+
|
|
7
|
+
## Tool
|
|
8
|
+
|
|
9
|
+
`zero_video` with four operations:
|
|
10
|
+
|
|
11
|
+
| Operation | Purpose |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `submit` | Fill the prompt, apply options, submit, return a `jobId` |
|
|
14
|
+
| `status` | Poll a job; auto-download and verify on success |
|
|
15
|
+
| `cancel` | Best-effort cancel; reports whether the credit may already be spent |
|
|
16
|
+
| `providers` | `list`, `add`, `configure`, `remove`, `test` provider profiles |
|
|
17
|
+
|
|
18
|
+
Jobs are asynchronous. A render can take 1–15 minutes, so `submit` returns immediately and `status` is polled. `waitSec` (0–10) lets one status call wait inside the tool. Tool calls are kept under the Gateway's 60 s plugin-drain budget; long waits are split across calls.
|
|
19
|
+
|
|
20
|
+
## Provider profiles
|
|
21
|
+
|
|
22
|
+
Providers live in `~/.openclaw/zero-video/providers.json`, managed by the tool. They are not in `openclaw.json`, so adding or changing a provider does not require a gateway restart.
|
|
23
|
+
|
|
24
|
+
Minimum profile:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"id": "pixverse-main",
|
|
29
|
+
"label": "PixVerse",
|
|
30
|
+
"url": "https://app.pixverse.ai/",
|
|
31
|
+
"promptInput": "textarea",
|
|
32
|
+
"submitButton": "button[type=submit]",
|
|
33
|
+
"downloadButton": "button[aria-label=Download]"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Full profile:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"id": "flow-main",
|
|
42
|
+
"label": "Google Flow",
|
|
43
|
+
"url": "https://labs.google/fx/tools/flow",
|
|
44
|
+
"enabled": true,
|
|
45
|
+
"loginCheck": "[data-testid=account-avatar]",
|
|
46
|
+
"promptInput": "textarea",
|
|
47
|
+
"submitButton": "button[data-testid=generate]",
|
|
48
|
+
"busyIndicator": "[data-testid=generating]",
|
|
49
|
+
"readyIndicator": "[data-testid=clip-ready]",
|
|
50
|
+
"errorText": "[role=alert]",
|
|
51
|
+
"downloadButton": "button[aria-label=Download]",
|
|
52
|
+
"downloadConfirm": "[role=menuitem]:has-text('720p')",
|
|
53
|
+
"creditBalance": "[data-testid=credits]",
|
|
54
|
+
"creditCost": 10,
|
|
55
|
+
"dailyCap": 50,
|
|
56
|
+
"timeoutMs": 1200000,
|
|
57
|
+
"readinessMs": 3000,
|
|
58
|
+
"options": [
|
|
59
|
+
{ "name": "duration", "value": "8", "selector": "[data-value='8s']", "action": "click" },
|
|
60
|
+
{ "name": "aspect", "value": "16:9", "selector": "[data-value='16:9']", "action": "click" }
|
|
61
|
+
],
|
|
62
|
+
"imageInput": "input[type=file]",
|
|
63
|
+
"cancelButton": "button[aria-label=Cancel]",
|
|
64
|
+
"libraryUrl": "https://labs.google/fx/tools/flow/project",
|
|
65
|
+
"libraryItem": "[data-testid=clip-card]",
|
|
66
|
+
"notes": "Visible watermark is forced in India.",
|
|
67
|
+
"verifiedAt": "2026-10-21"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`downloadConfirm` is for a second click after the download button opens a menu. `options` map a requested `durationSec`, `aspect` or `model` to a concrete control; if the request names a value that is not configured, the tool returns `OPTION_NOT_CONFIGURED` rather than guessing.
|
|
72
|
+
|
|
73
|
+
At least one of `busyIndicator` or `readyIndicator` is required before `submit`; `providers test` still works without them so selectors can be discovered first. `readinessMs` defaults to 3000 and is the SPA mount delay after the prompt input appears.
|
|
74
|
+
|
|
75
|
+
`imageInput` enables image-to-video. The image is copied to the Chromium container first, then set on the file input with CDP `DOM.setFileInputFiles`.
|
|
76
|
+
|
|
77
|
+
## Provider management examples
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{"op":"providers","providersAction":"list"}
|
|
81
|
+
{"op":"providers","providersAction":"add","provider":{"id":"pixverse-main","label":"PixVerse","url":"https://app.pixverse.ai/","promptInput":"textarea","submitButton":"button[type=submit]","downloadButton":"button[aria-label=Download]"}}
|
|
82
|
+
{"op":"providers","providersAction":"configure","providerId":"pixverse-main","provider":{"dailyCap":20,"creditCost":5}}
|
|
83
|
+
{"op":"providers","providersAction":"test","providerId":"pixverse-main"}
|
|
84
|
+
{"op":"providers","providersAction":"remove","providerId":"pixverse-main"}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`test` opens the provider URL, checks the login marker, prompt input, submit button and download button, then closes the tab. It does not spend credits. On success it stamps `verifiedAt`.
|
|
88
|
+
|
|
89
|
+
## Generation
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{"op":"submit","site":"pixverse-main","prompt":"A slow dolly shot through a rain-soaked market at dusk","durationSec":5,"aspect":"16:9"}
|
|
93
|
+
{"op":"status","jobId":"pixverse-main-m1abc2-3f9c11","waitSec":30}
|
|
94
|
+
{"op":"status","jobId":"pixverse-main-m1abc2-3f9c11","waitSec":60}
|
|
95
|
+
{"op":"cancel","jobId":"pixverse-main-m1abc2-3f9c11"}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
On success, `status` returns an artifact with `path`, `bytes`, `sha256`, `durationSec`, `width`, `height` and `codec`. Files are stored under `~/.openclaw/media/zero-video/<YYYY>/<MM>/` with a sidecar `.json` manifest.
|
|
99
|
+
|
|
100
|
+
If the tab is lost (gateway or Chromium restart), the job becomes `needs_attention` with `BROWSER_LOST`. The render may still exist in the provider library; the tool does not guess which item to download. Recover manually in the Webtop, or resubmit deliberately.
|
|
101
|
+
|
|
102
|
+
## Credit control
|
|
103
|
+
|
|
104
|
+
The ledger uses balance delta, not a calendar:
|
|
105
|
+
|
|
106
|
+
- `spentToday += max(0, balanceBefore - balanceAfter)`.
|
|
107
|
+
- A balance increase is treated as a refill and resets `spentToday`.
|
|
108
|
+
- `dailyCap` and `creditCost` are per provider. `dailyCap: 0` means no cap.
|
|
109
|
+
- One job per provider at a time, `maxParallel` globally, 5 s status polling.
|
|
110
|
+
- No automatic retry after submit. A failed download leaves the job in `needs_attention`; `status` with `retryDownload: true` retries the transfer.
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
| Key | Default |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `cdpUrl` | `http://127.0.0.1:9222` |
|
|
117
|
+
| `allowRemoteCdp` | `false` |
|
|
118
|
+
| `providersFile` | `~/.openclaw/zero-video/providers.json` |
|
|
119
|
+
| `stateDir` | `~/.openclaw/zero-video` |
|
|
120
|
+
| `mediaDir` | `~/.openclaw/media/zero-video` |
|
|
121
|
+
| `maxParallel` | `2` |
|
|
122
|
+
| `jobTimeoutMs` | `1200000` |
|
|
123
|
+
| `downloadTimeoutMs` | `30000` |
|
|
124
|
+
| `maxDownloadBytes` | `52428800` |
|
|
125
|
+
| `maxPromptChars` | `2000` |
|
|
126
|
+
| `sshHost` | `192.168.1.12` |
|
|
127
|
+
| `containerName` | `Chromium` |
|
|
128
|
+
| `sshKey` | `~/.ssh/ghirnemox_ed25519` |
|
|
129
|
+
| `cosmosDownloadsDir` | `/config/Downloads/zero-video` |
|
|
130
|
+
| `keepContainerCopyHours` | `24` |
|
|
131
|
+
| `storePrompts` | `true` |
|
|
132
|
+
| `retentionDays` | `30` |
|
|
133
|
+
|
|
134
|
+
## Error codes
|
|
135
|
+
|
|
136
|
+
`INVALID_REQUEST`, `UNKNOWN_PROVIDER`, `PROVIDER_DISABLED`, `PROVIDER_BUSY`, `PROVIDER_INCOMPLETE`, `TOO_MANY_JOBS`, `PROMPT_REQUIRED`, `PROMPT_TOO_LONG`, `LOGIN_REQUIRED`, `INPUT_NOT_FOUND`, `SUBMIT_DISABLED`, `MODERATION_BLOCKED`, `QUOTA_EXHAUSTED`, `DAILY_CAP_REACHED`, `OPTION_NOT_CONFIGURED`, `IMAGE_INPUT_NOT_CONFIGURED`, `DOWNLOAD_FAILED`, `TRANSFER_FAILED`, `VERIFY_FAILED`, `INVALID_MEDIA`, `TOO_LARGE`, `TIMEOUT`, `BROWSER_LOST`, `CANCELLED`.
|
|
137
|
+
|
|
138
|
+
## Build
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm install
|
|
142
|
+
npm run typecheck
|
|
143
|
+
npm test
|
|
144
|
+
npm run build
|
|
145
|
+
npm pack
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Runtime requires Node 22+, Chromium reachable over CDP, and `ffprobe` on the gateway.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
cdpUrl: string;
|
|
3
|
+
allowRemoteCdp: boolean;
|
|
4
|
+
providersFile: string;
|
|
5
|
+
stateDir: string;
|
|
6
|
+
mediaDir: string;
|
|
7
|
+
maxParallel: number;
|
|
8
|
+
jobTimeoutMs: number;
|
|
9
|
+
downloadTimeoutMs: number;
|
|
10
|
+
maxDownloadBytes: number;
|
|
11
|
+
maxPromptChars: number;
|
|
12
|
+
sshHost: string;
|
|
13
|
+
containerName: string;
|
|
14
|
+
sshKey: string;
|
|
15
|
+
cosmosDownloadsDir: string;
|
|
16
|
+
keepContainerCopyHours: number;
|
|
17
|
+
storePrompts: boolean;
|
|
18
|
+
retentionDays: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class ToolError extends Error {
|
|
21
|
+
readonly code: string;
|
|
22
|
+
constructor(code: string, message?: string);
|
|
23
|
+
}
|
|
24
|
+
export declare function expandHome(value: string, home?: string): string;
|
|
25
|
+
export declare function parseConfig(raw: unknown, home?: string): Config;
|
|
26
|
+
export declare function assertPrompt(prompt: unknown, config: Config): string;
|
|
27
|
+
export declare function assertJobId(jobId: unknown): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const CONFIG_KEYS = new Set([
|
|
4
|
+
'cdpUrl', 'allowRemoteCdp', 'providersFile', 'stateDir', 'mediaDir',
|
|
5
|
+
'maxParallel', 'jobTimeoutMs', 'downloadTimeoutMs', 'maxDownloadBytes',
|
|
6
|
+
'maxPromptChars', 'sshHost', 'sshKey', 'cosmosDownloadsDir', 'containerName',
|
|
7
|
+
'keepContainerCopyHours', 'storePrompts', 'retentionDays',
|
|
8
|
+
]);
|
|
9
|
+
export class ToolError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message = code) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = 'ToolError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function expandHome(value, home = homedir()) {
|
|
18
|
+
if (value === '~')
|
|
19
|
+
return home;
|
|
20
|
+
if (value.startsWith('~/'))
|
|
21
|
+
return path.join(home, value.slice(2));
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function text(value, fallback) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return fallback;
|
|
27
|
+
if (typeof value !== 'string')
|
|
28
|
+
throw new ToolError('INVALID_CONFIG');
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
throw new ToolError('INVALID_CONFIG');
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
function integer(value, fallback, min, max) {
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
return fallback;
|
|
37
|
+
if (!Number.isInteger(value) || Number(value) < min || Number(value) > max) {
|
|
38
|
+
throw new ToolError('INVALID_CONFIG');
|
|
39
|
+
}
|
|
40
|
+
return Number(value);
|
|
41
|
+
}
|
|
42
|
+
function boolean(value, fallback) {
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
return fallback;
|
|
45
|
+
if (typeof value !== 'boolean')
|
|
46
|
+
throw new ToolError('INVALID_CONFIG');
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
export function parseConfig(raw, home = homedir()) {
|
|
50
|
+
if (raw === undefined || raw === null)
|
|
51
|
+
raw = {};
|
|
52
|
+
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
53
|
+
throw new ToolError('INVALID_CONFIG');
|
|
54
|
+
const input = raw;
|
|
55
|
+
for (const key of Object.keys(input)) {
|
|
56
|
+
if (!CONFIG_KEYS.has(key))
|
|
57
|
+
throw new ToolError('UNKNOWN_CONFIG_KEY');
|
|
58
|
+
}
|
|
59
|
+
let cdp;
|
|
60
|
+
try {
|
|
61
|
+
cdp = new URL(text(input.cdpUrl, 'http://127.0.0.1:9222'));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new ToolError('INVALID_CDP_URL');
|
|
65
|
+
}
|
|
66
|
+
if (!['http:', 'https:'].includes(cdp.protocol) || cdp.username || cdp.password || cdp.search || cdp.hash) {
|
|
67
|
+
throw new ToolError('INVALID_CDP_URL');
|
|
68
|
+
}
|
|
69
|
+
const allowRemoteCdp = boolean(input.allowRemoteCdp, false);
|
|
70
|
+
const localHosts = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
71
|
+
if (!allowRemoteCdp && !localHosts.has(cdp.hostname)) {
|
|
72
|
+
throw new ToolError('REMOTE_CDP_DISABLED');
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
cdpUrl: cdp.toString().replace(/\/$/, ''),
|
|
76
|
+
allowRemoteCdp,
|
|
77
|
+
providersFile: expandHome(text(input.providersFile, '~/.openclaw/zero-video/providers.json'), home),
|
|
78
|
+
stateDir: expandHome(text(input.stateDir, '~/.openclaw/zero-video'), home),
|
|
79
|
+
mediaDir: expandHome(text(input.mediaDir, '~/.openclaw/media/zero-video'), home),
|
|
80
|
+
maxParallel: integer(input.maxParallel, 2, 1, 3),
|
|
81
|
+
jobTimeoutMs: integer(input.jobTimeoutMs, 1_200_000, 60_000, 7_200_000),
|
|
82
|
+
downloadTimeoutMs: integer(input.downloadTimeoutMs, 30_000, 10_000, 35_000),
|
|
83
|
+
maxDownloadBytes: integer(input.maxDownloadBytes, 52_428_800, 1_048_576, 104_857_600),
|
|
84
|
+
maxPromptChars: integer(input.maxPromptChars, 2000, 1, 8000),
|
|
85
|
+
sshHost: text(input.sshHost, '192.168.1.12'),
|
|
86
|
+
containerName: (() => {
|
|
87
|
+
const value = text(input.containerName, 'Chromium');
|
|
88
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/.test(value))
|
|
89
|
+
throw new ToolError('INVALID_CONFIG');
|
|
90
|
+
return value;
|
|
91
|
+
})(),
|
|
92
|
+
sshKey: expandHome(text(input.sshKey, '~/.ssh/ghirnemox_ed25519'), home),
|
|
93
|
+
cosmosDownloadsDir: text(input.cosmosDownloadsDir, '/config/Downloads/zero-video').replace(/\/+$/, ''),
|
|
94
|
+
keepContainerCopyHours: integer(input.keepContainerCopyHours, 24, 0, 168),
|
|
95
|
+
storePrompts: boolean(input.storePrompts, true),
|
|
96
|
+
retentionDays: integer(input.retentionDays, 30, 1, 3650),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function assertPrompt(prompt, config) {
|
|
100
|
+
if (typeof prompt !== 'string')
|
|
101
|
+
throw new ToolError('INVALID_REQUEST');
|
|
102
|
+
const trimmed = prompt.trim();
|
|
103
|
+
if (!trimmed)
|
|
104
|
+
throw new ToolError('PROMPT_REQUIRED');
|
|
105
|
+
if (trimmed.length > config.maxPromptChars)
|
|
106
|
+
throw new ToolError('PROMPT_TOO_LONG');
|
|
107
|
+
return trimmed;
|
|
108
|
+
}
|
|
109
|
+
export function assertJobId(jobId) {
|
|
110
|
+
if (typeof jobId !== 'string' || !/^[a-z0-9][a-z0-9-]{2,80}$/.test(jobId)) {
|
|
111
|
+
throw new ToolError('INVALID_JOB_ID');
|
|
112
|
+
}
|
|
113
|
+
return jobId;
|
|
114
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
|
|
2
|
+
import { parseConfig, ToolError } from './config.js';
|
|
3
|
+
import { ProviderStore, testProvider } from './providers.js';
|
|
4
|
+
import { CdpDriver } from './transport.js';
|
|
5
|
+
import { JobStore, LedgerStore, MediaStore, Runner, Ssh } from './jobs.js';
|
|
6
|
+
const PLUGIN_ID = 'zero-video';
|
|
7
|
+
export default definePluginEntry({
|
|
8
|
+
id: PLUGIN_ID,
|
|
9
|
+
name: 'Zero Video',
|
|
10
|
+
description: 'Generate videos through signed-in consumer AI video sites with runtime-managed providers.',
|
|
11
|
+
register(api) {
|
|
12
|
+
const config = parseConfig(api.pluginConfig);
|
|
13
|
+
const providers = new ProviderStore(config.providersFile);
|
|
14
|
+
const driver = new CdpDriver(config.cdpUrl);
|
|
15
|
+
const jobs = new JobStore(config);
|
|
16
|
+
const ledgers = new LedgerStore(config);
|
|
17
|
+
const ssh = new Ssh(config);
|
|
18
|
+
const media = new MediaStore(config);
|
|
19
|
+
const runner = new Runner(config, driver, providers, jobs, ledgers, media, ssh, (record) => api.logger.info(JSON.stringify(record)));
|
|
20
|
+
async function handleProviders(args, signal) {
|
|
21
|
+
const action = args.providersAction;
|
|
22
|
+
const id = args.providerId ?? (typeof args.provider?.id === 'string' ? args.provider.id : undefined);
|
|
23
|
+
switch (action) {
|
|
24
|
+
case 'list':
|
|
25
|
+
return { ok: true, providers: await runner.providerView(), active: runner.activeSummary() };
|
|
26
|
+
case 'add': {
|
|
27
|
+
if (!args.provider)
|
|
28
|
+
throw new ToolError('INVALID_REQUEST');
|
|
29
|
+
return { ok: true, provider: await providers.add(args.provider) };
|
|
30
|
+
}
|
|
31
|
+
case 'configure': {
|
|
32
|
+
if (!id)
|
|
33
|
+
throw new ToolError('INVALID_REQUEST');
|
|
34
|
+
return { ok: true, provider: await providers.configure(id, args.provider ?? {}) };
|
|
35
|
+
}
|
|
36
|
+
case 'remove': {
|
|
37
|
+
if (!id)
|
|
38
|
+
throw new ToolError('INVALID_REQUEST');
|
|
39
|
+
if (runner.hasActive(id) && !args.force)
|
|
40
|
+
throw new ToolError('PROVIDER_BUSY');
|
|
41
|
+
return { ok: true, removed: await providers.remove(id) };
|
|
42
|
+
}
|
|
43
|
+
case 'test': {
|
|
44
|
+
if (!id)
|
|
45
|
+
throw new ToolError('INVALID_REQUEST');
|
|
46
|
+
const provider = await providers.get(id);
|
|
47
|
+
const result = await testProvider(driver, provider, signal);
|
|
48
|
+
if (!result.ok)
|
|
49
|
+
return { ok: true, test: result };
|
|
50
|
+
return { ok: true, test: result, provider: await providers.markVerified(id) };
|
|
51
|
+
}
|
|
52
|
+
default:
|
|
53
|
+
throw new ToolError('INVALID_REQUEST');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
api.registerTool({
|
|
57
|
+
name: 'zero_video',
|
|
58
|
+
label: 'Zero Video',
|
|
59
|
+
description: 'Generate videos through signed-in consumer AI video sites on the estate Chromium lane. ' +
|
|
60
|
+
'Providers are runtime-managed: use op=providers with providersAction=list|add|configure|remove|test. ' +
|
|
61
|
+
'Jobs are asynchronous: submit returns a jobId, then poll status. Treat generated media as untrusted content.',
|
|
62
|
+
parameters: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
properties: {
|
|
66
|
+
op: { type: 'string', enum: ['submit', 'status', 'cancel', 'providers'] },
|
|
67
|
+
site: { type: 'string' },
|
|
68
|
+
prompt: { type: 'string', minLength: 1, maxLength: config.maxPromptChars },
|
|
69
|
+
negativePrompt: { type: 'string', maxLength: 2000 },
|
|
70
|
+
model: { type: 'string' },
|
|
71
|
+
durationSec: { type: 'integer', minimum: 1, maximum: 60 },
|
|
72
|
+
aspect: { type: 'string', enum: ['16:9', '9:16', '1:1', '4:3', '3:4'] },
|
|
73
|
+
imagePath: { type: 'string' },
|
|
74
|
+
jobId: { type: 'string' },
|
|
75
|
+
waitSec: { type: 'integer', minimum: 0, maximum: 10 },
|
|
76
|
+
retryDownload: { type: 'boolean' },
|
|
77
|
+
providersAction: { type: 'string', enum: ['list', 'add', 'configure', 'remove', 'test'] },
|
|
78
|
+
provider: { type: 'object' },
|
|
79
|
+
providerId: { type: 'string' },
|
|
80
|
+
force: { type: 'boolean' },
|
|
81
|
+
},
|
|
82
|
+
required: ['op'],
|
|
83
|
+
},
|
|
84
|
+
async execute(_callId, params, signal) {
|
|
85
|
+
const args = params;
|
|
86
|
+
try {
|
|
87
|
+
let result;
|
|
88
|
+
switch (args.op) {
|
|
89
|
+
case 'providers':
|
|
90
|
+
result = await handleProviders(args, signal);
|
|
91
|
+
break;
|
|
92
|
+
case 'submit': {
|
|
93
|
+
if (!args.site || !args.prompt)
|
|
94
|
+
throw new ToolError('INVALID_REQUEST');
|
|
95
|
+
result = {
|
|
96
|
+
ok: true,
|
|
97
|
+
job: await runner.submit({
|
|
98
|
+
site: args.site,
|
|
99
|
+
prompt: args.prompt,
|
|
100
|
+
negativePrompt: args.negativePrompt,
|
|
101
|
+
model: args.model,
|
|
102
|
+
durationSec: args.durationSec,
|
|
103
|
+
aspect: args.aspect,
|
|
104
|
+
imagePath: args.imagePath,
|
|
105
|
+
}, signal),
|
|
106
|
+
};
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'status': {
|
|
110
|
+
if (!args.jobId)
|
|
111
|
+
throw new ToolError('INVALID_REQUEST');
|
|
112
|
+
result = {
|
|
113
|
+
ok: true,
|
|
114
|
+
job: await runner.status({
|
|
115
|
+
jobId: args.jobId,
|
|
116
|
+
waitSec: args.waitSec,
|
|
117
|
+
retryDownload: args.retryDownload,
|
|
118
|
+
}, signal),
|
|
119
|
+
};
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 'cancel': {
|
|
123
|
+
if (!args.jobId)
|
|
124
|
+
throw new ToolError('INVALID_REQUEST');
|
|
125
|
+
result = { ok: true, job: await runner.cancel(args.jobId) };
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
throw new ToolError('INVALID_REQUEST');
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
133
|
+
details: result,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const code = error instanceof ToolError ? error.code : 'TOOL_FAILED';
|
|
138
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
+
const result = { ok: false, error: code, message: message.slice(0, 500) };
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
142
|
+
details: result,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
api.logger.info(JSON.stringify({
|
|
148
|
+
plugin: PLUGIN_ID,
|
|
149
|
+
cdpTarget: new URL(config.cdpUrl).host,
|
|
150
|
+
providersFile: config.providersFile,
|
|
151
|
+
maxParallel: config.maxParallel,
|
|
152
|
+
}));
|
|
153
|
+
},
|
|
154
|
+
});
|
package/dist/jobs.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Config } from './config.js';
|
|
2
|
+
import type { Driver } from './transport.js';
|
|
3
|
+
import type { ProviderStore } from './providers.js';
|
|
4
|
+
import type { JobArtifact, JobRecord, Ledger, ProviderSpec, StatusRequest, SubmitRequest } from './types.js';
|
|
5
|
+
export declare class JobStore {
|
|
6
|
+
private readonly config;
|
|
7
|
+
private readonly jobsDir;
|
|
8
|
+
constructor(config: Config);
|
|
9
|
+
init(): Promise<void>;
|
|
10
|
+
private write;
|
|
11
|
+
create(record: JobRecord): Promise<JobRecord>;
|
|
12
|
+
get(id: string): Promise<JobRecord>;
|
|
13
|
+
update(id: string, patch: Partial<JobRecord>): Promise<JobRecord>;
|
|
14
|
+
listRecent(limit?: number): Promise<JobRecord[]>;
|
|
15
|
+
findActiveByProvider(site: string): Promise<JobRecord | undefined>;
|
|
16
|
+
}
|
|
17
|
+
export declare class LedgerStore {
|
|
18
|
+
private readonly config;
|
|
19
|
+
private readonly ledgerDir;
|
|
20
|
+
constructor(config: Config);
|
|
21
|
+
private read;
|
|
22
|
+
private write;
|
|
23
|
+
get(providerId: string): Promise<Ledger>;
|
|
24
|
+
observe(providerId: string, balance: number | null): Promise<Ledger>;
|
|
25
|
+
canSpend(provider: ProviderSpec, ledger: Ledger, balance: number | null): void;
|
|
26
|
+
record(providerId: string, before: number | null, after: number | null, fallbackCost: number): Promise<Ledger>;
|
|
27
|
+
}
|
|
28
|
+
export declare class Ssh {
|
|
29
|
+
private readonly config;
|
|
30
|
+
constructor(config: Config);
|
|
31
|
+
private validate;
|
|
32
|
+
private run;
|
|
33
|
+
private container;
|
|
34
|
+
private transferDir;
|
|
35
|
+
mkdir(remote: string): Promise<void>;
|
|
36
|
+
list(remote: string): Promise<Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
size: number;
|
|
39
|
+
mtime: number;
|
|
40
|
+
}>>;
|
|
41
|
+
copyFrom(remote: string, local: string): Promise<void>;
|
|
42
|
+
put(local: string, remote: string): Promise<void>;
|
|
43
|
+
remove(remote: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export declare class MediaStore {
|
|
46
|
+
private readonly config;
|
|
47
|
+
constructor(config: Config);
|
|
48
|
+
private ffprobe;
|
|
49
|
+
private sha256;
|
|
50
|
+
verify(file: string): Promise<JobArtifact>;
|
|
51
|
+
store(tempFile: string, job: JobRecord): Promise<JobArtifact>;
|
|
52
|
+
}
|
|
53
|
+
export declare class Runner {
|
|
54
|
+
private readonly config;
|
|
55
|
+
private readonly driver;
|
|
56
|
+
private readonly providers;
|
|
57
|
+
private readonly jobs;
|
|
58
|
+
private readonly ledgers;
|
|
59
|
+
private readonly media;
|
|
60
|
+
private readonly ssh;
|
|
61
|
+
private readonly log;
|
|
62
|
+
private readonly active;
|
|
63
|
+
constructor(config: Config, driver: Driver, providers: ProviderStore, jobs: JobStore, ledgers: LedgerStore, media: MediaStore, ssh: Ssh, log?: (record: Record<string, unknown>) => void);
|
|
64
|
+
submit(request: SubmitRequest, signal?: AbortSignal): Promise<JobRecord>;
|
|
65
|
+
status(request: StatusRequest, signal?: AbortSignal): Promise<JobRecord>;
|
|
66
|
+
cancel(jobId: string): Promise<JobRecord>;
|
|
67
|
+
hasActive(providerId: string): boolean;
|
|
68
|
+
activeSummary(): Array<{
|
|
69
|
+
providerId: string;
|
|
70
|
+
jobId: string;
|
|
71
|
+
}>;
|
|
72
|
+
providerView(): Promise<Array<ProviderSpec & {
|
|
73
|
+
ledger: Ledger;
|
|
74
|
+
activeJobId: string | null;
|
|
75
|
+
}>>;
|
|
76
|
+
private applyOptions;
|
|
77
|
+
private readBalance;
|
|
78
|
+
private waitEnabled;
|
|
79
|
+
private waitForRemoteFile;
|
|
80
|
+
private download;
|
|
81
|
+
}
|