openclaw-zero-video 0.1.4 → 0.1.7

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/README.md CHANGED
@@ -15,7 +15,7 @@ The plugin is a thin CDP driver plus a durable job store. It does not use provid
15
15
  | `cancel` | Best-effort cancel; reports whether the credit may already be spent |
16
16
  | `providers` | `list`, `add`, `configure`, `remove`, `test` provider profiles |
17
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.
18
+ Jobs are asynchronous. A render can take 1–15 minutes, so `submit` returns immediately and `status` is polled. `waitSec` (0–20) lets one status call wait inside the tool. `providers list` returns a compact summary by default; pass `includeDetails: true` for selectors. `status` and `submit` return compact jobs by default; pass `verbose: true` for the full record. Tool calls stay under the Gateway's drain budget.
19
19
 
20
20
  ## Provider profiles
21
21
 
@@ -120,7 +120,7 @@ The ledger uses balance delta, not a calendar:
120
120
  | `mediaDir` | `~/.openclaw/media/zero-video` |
121
121
  | `maxParallel` | `2` |
122
122
  | `jobTimeoutMs` | `1200000` |
123
- | `downloadTimeoutMs` | `30000` |
123
+ | `downloadTimeoutMs` | `20000` |
124
124
  | `maxDownloadBytes` | `52428800` |
125
125
  | `maxPromptChars` | `2000` |
126
126
  | `sshHost` | `192.168.1.12` |
package/dist/config.js CHANGED
@@ -79,7 +79,7 @@ export function parseConfig(raw, home = homedir()) {
79
79
  mediaDir: expandHome(text(input.mediaDir, '~/.openclaw/media/zero-video'), home),
80
80
  maxParallel: integer(input.maxParallel, 2, 1, 3),
81
81
  jobTimeoutMs: integer(input.jobTimeoutMs, 1_200_000, 60_000, 7_200_000),
82
- downloadTimeoutMs: integer(input.downloadTimeoutMs, 30_000, 10_000, 35_000),
82
+ downloadTimeoutMs: integer(input.downloadTimeoutMs, 20_000, 10_000, 25_000),
83
83
  maxDownloadBytes: integer(input.maxDownloadBytes, 52_428_800, 1_048_576, 104_857_600),
84
84
  maxPromptChars: integer(input.maxPromptChars, 2000, 1, 8000),
85
85
  sshHost: text(input.sshHost, '192.168.1.12'),
package/dist/index.js CHANGED
@@ -21,8 +21,23 @@ export default definePluginEntry({
21
21
  const action = args.providersAction;
22
22
  const id = args.providerId ?? (typeof args.provider?.id === 'string' ? args.provider.id : undefined);
23
23
  switch (action) {
24
- case 'list':
25
- return { ok: true, providers: await runner.providerView(), active: runner.activeSummary() };
24
+ case 'list': {
25
+ const rows = await runner.providerView();
26
+ if (args.includeDetails)
27
+ return { ok: true, providers: rows, active: runner.activeSummary() };
28
+ return {
29
+ ok: true,
30
+ providers: rows.map((p) => ({
31
+ id: p.id,
32
+ label: p.label,
33
+ enabled: p.enabled,
34
+ verifiedAt: p.verifiedAt,
35
+ ledger: p.ledger,
36
+ activeJobId: p.activeJobId,
37
+ })),
38
+ active: runner.activeSummary(),
39
+ };
40
+ }
26
41
  case 'add': {
27
42
  if (!args.provider)
28
43
  throw new ToolError('INVALID_REQUEST');
@@ -72,8 +87,10 @@ export default definePluginEntry({
72
87
  aspect: { type: 'string', enum: ['16:9', '9:16', '1:1', '4:3', '3:4'] },
73
88
  imagePath: { type: 'string' },
74
89
  jobId: { type: 'string' },
75
- waitSec: { type: 'integer', minimum: 0, maximum: 10 },
90
+ waitSec: { type: 'integer', minimum: 0, maximum: 20 },
76
91
  retryDownload: { type: 'boolean' },
92
+ includeDetails: { type: 'boolean' },
93
+ verbose: { type: 'boolean' },
77
94
  providersAction: { type: 'string', enum: ['list', 'add', 'configure', 'remove', 'test'] },
78
95
  provider: { type: 'object' },
79
96
  providerId: { type: 'string' },
@@ -92,30 +109,48 @@ export default definePluginEntry({
92
109
  case 'submit': {
93
110
  if (!args.site || !args.prompt)
94
111
  throw new ToolError('INVALID_REQUEST');
112
+ const job = await runner.submit({
113
+ site: args.site,
114
+ prompt: args.prompt,
115
+ negativePrompt: args.negativePrompt,
116
+ model: args.model,
117
+ durationSec: args.durationSec,
118
+ aspect: args.aspect,
119
+ imagePath: args.imagePath,
120
+ }, signal);
95
121
  result = {
96
122
  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),
123
+ job: args.verbose ? job : {
124
+ id: job.id,
125
+ site: job.site,
126
+ state: job.state,
127
+ submittedAt: job.submittedAt,
128
+ creditsBefore: job.creditsBefore,
129
+ },
106
130
  };
107
131
  break;
108
132
  }
109
133
  case 'status': {
110
134
  if (!args.jobId)
111
135
  throw new ToolError('INVALID_REQUEST');
136
+ const job = await runner.status({
137
+ jobId: args.jobId,
138
+ waitSec: args.waitSec,
139
+ retryDownload: args.retryDownload,
140
+ }, signal);
112
141
  result = {
113
142
  ok: true,
114
- job: await runner.status({
115
- jobId: args.jobId,
116
- waitSec: args.waitSec,
117
- retryDownload: args.retryDownload,
118
- }, signal),
143
+ job: args.verbose ? job : {
144
+ id: job.id,
145
+ site: job.site,
146
+ state: job.state,
147
+ errorCode: job.errorCode,
148
+ artifact: job.artifact,
149
+ creditsBefore: job.creditsBefore,
150
+ creditsAfter: job.creditsAfter,
151
+ submittedAt: job.submittedAt,
152
+ completedAt: job.completedAt,
153
+ },
119
154
  };
120
155
  break;
121
156
  }
package/dist/jobs.d.ts CHANGED
@@ -74,6 +74,8 @@ export declare class Runner {
74
74
  activeJobId: string | null;
75
75
  }>>;
76
76
  private applyOptions;
77
+ private readTextMatch;
78
+ private readBlockedText;
77
79
  private readBalance;
78
80
  private waitEnabled;
79
81
  private waitForRemoteFile;
package/dist/jobs.js CHANGED
@@ -419,6 +419,7 @@ export class Runner {
419
419
  try {
420
420
  job = await this.jobs.update(job.id, { state: 'opening' });
421
421
  page = await this.driver.open(provider.url, signal);
422
+ await page.bringToFront();
422
423
  const promptReady = await page.waitFor(provider.promptInput, 20_000, signal);
423
424
  if (!promptReady) {
424
425
  if (provider.loginCheck && !(await page.isVisible(provider.loginCheck).catch(() => false))) {
@@ -431,6 +432,9 @@ export class Runner {
431
432
  }
432
433
  // Let the SPA finish mounting the editor and submit handlers before typing.
433
434
  await sleep(provider.readinessMs ?? 3000, signal);
435
+ const blocked = await this.readBlockedText(page, provider);
436
+ if (blocked)
437
+ throw new ToolError('PROVIDER_BLOCKED', blocked);
434
438
  if (request.imagePath) {
435
439
  if (!provider.imageInput)
436
440
  throw new ToolError('IMAGE_INPUT_NOT_CONFIGURED');
@@ -440,7 +444,8 @@ export class Runner {
440
444
  await page.setFileInput(provider.imageInput, remote);
441
445
  await sleep(750, signal);
442
446
  }
443
- await page.fill(provider.promptInput, prompt);
447
+ const promptToFill = provider.promptPrefix ? `${provider.promptPrefix} ${prompt}` : prompt;
448
+ await page.fill(provider.promptInput, promptToFill);
444
449
  await sleep(300, signal);
445
450
  await this.applyOptions(page, provider, request);
446
451
  const balance = await this.readBalance(page, provider);
@@ -449,9 +454,18 @@ export class Runner {
449
454
  const enabled = await this.waitEnabled(page, provider.submitButton, 8_000, signal);
450
455
  if (!enabled)
451
456
  throw new ToolError('SUBMIT_DISABLED');
452
- if (!(await page.click(provider.submitButton)))
457
+ if (!(await page.realClick(provider.submitButton)))
453
458
  throw new ToolError('SUBMIT_DISABLED');
454
459
  await sleep(1000, signal);
460
+ if (provider.adapter === 'flow') {
461
+ const confirmSelector = provider.confirmSelector ?? 'div[role="radio"][aria-label="Approve"]';
462
+ const confirmed = await page.waitFor(confirmSelector, 45_000, signal);
463
+ if (!confirmed)
464
+ throw new ToolError('FLOW_CONFIRMATION_TIMEOUT');
465
+ if (!(await page.realClick(confirmSelector)))
466
+ throw new ToolError('FLOW_CONFIRMATION_FAILED');
467
+ await sleep(1500, signal);
468
+ }
455
469
  if (provider.errorText && (await page.isVisible(provider.errorText).catch(() => false))) {
456
470
  const message = (await page.text(provider.errorText).catch(() => '')).slice(0, 500);
457
471
  throw new ToolError(classifySubmitError(message), message || 'SUBMIT_FAILED');
@@ -482,8 +496,12 @@ export class Runner {
482
496
  }
483
497
  async status(request, signal) {
484
498
  let job = await this.jobs.get(assertJobId(request.jobId));
485
- if (isTerminal(job.state))
499
+ if (isTerminal(job.state)) {
500
+ const rt = this.active.get(job.site);
501
+ if (rt?.jobId === job.id)
502
+ this.active.delete(job.site);
486
503
  return job;
504
+ }
487
505
  const provider = await this.providers.get(job.site);
488
506
  const runtime = this.active.get(provider.id);
489
507
  if (!runtime || runtime.jobId !== job.id) {
@@ -496,10 +514,30 @@ export class Runner {
496
514
  if (request.retryDownload) {
497
515
  return this.download(job, provider, runtime.page);
498
516
  }
499
- const deadline = Date.now() + Math.min(Math.max(request.waitSec ?? 0, 0), 10) * 1000;
517
+ const deadline = Date.now() + Math.min(Math.max(request.waitSec ?? 0, 0), 20) * 1000;
500
518
  do {
501
519
  if (signal?.aborted)
502
520
  throw new ToolError('CANCELLED');
521
+ const blocked = await this.readBlockedText(runtime.page, provider);
522
+ if (blocked) {
523
+ this.active.delete(provider.id);
524
+ return this.jobs.update(job.id, {
525
+ state: 'failed',
526
+ errorCode: 'PROVIDER_BLOCKED',
527
+ errorMessage: blocked,
528
+ completedAt: nowIso(),
529
+ });
530
+ }
531
+ const failure = await this.readTextMatch(runtime.page, provider.failureText);
532
+ if (failure) {
533
+ this.active.delete(provider.id);
534
+ return this.jobs.update(job.id, {
535
+ state: 'failed',
536
+ errorCode: 'PROVIDER_FAILED',
537
+ errorMessage: failure,
538
+ completedAt: nowIso(),
539
+ });
540
+ }
503
541
  if (provider.errorText) {
504
542
  const visible = await runtime.page.isVisible(provider.errorText).catch(() => false);
505
543
  if (visible) {
@@ -550,7 +588,7 @@ export class Runner {
550
588
  const runtime = this.active.get(provider.id);
551
589
  if (runtime && runtime.jobId === job.id) {
552
590
  if (provider.cancelButton)
553
- await runtime.page.click(provider.cancelButton).catch(() => false);
591
+ await runtime.page.realClick(provider.cancelButton).catch(() => false);
554
592
  await runtime.page.close().catch(() => undefined);
555
593
  this.active.delete(provider.id);
556
594
  }
@@ -597,12 +635,27 @@ export class Runner {
597
635
  el.dispatchEvent(new Event('change', { bubbles: true }));
598
636
  }`, { selector: option.selector, value: option.value });
599
637
  }
600
- else if (!(await page.click(option.selector))) {
638
+ else if (!(await page.realClick(option.selector))) {
601
639
  throw new ToolError('SELECTOR_CHANGED');
602
640
  }
603
641
  await sleep(300);
604
642
  }
605
643
  }
644
+ async readTextMatch(page, pattern) {
645
+ if (!pattern)
646
+ return null;
647
+ const text = await page.text('body').catch(() => '');
648
+ try {
649
+ const match = text.match(new RegExp(pattern, 'i'));
650
+ return match ? match[0].slice(0, 200) : null;
651
+ }
652
+ catch {
653
+ return null;
654
+ }
655
+ }
656
+ async readBlockedText(page, provider) {
657
+ return this.readTextMatch(page, provider.blockedText);
658
+ }
606
659
  async readBalance(page, provider) {
607
660
  if (!provider.creditBalance)
608
661
  return null;
@@ -646,12 +699,13 @@ export class Runner {
646
699
  const remoteDir = `${this.config.cosmosDownloadsDir}/${job.id}`;
647
700
  try {
648
701
  await this.ssh.mkdir(remoteDir);
702
+ await page.bringToFront();
649
703
  await page.setDownloadBehavior(remoteDir);
650
- if (!(await page.click(provider.downloadButton)))
704
+ if (!(await page.realClick(provider.downloadButton)))
651
705
  throw new ToolError('DOWNLOAD_FAILED');
652
706
  if (provider.downloadConfirm) {
653
707
  await sleep(700);
654
- await page.click(provider.downloadConfirm);
708
+ await page.realClick(provider.downloadConfirm);
655
709
  }
656
710
  const remoteName = await this.waitForRemoteFile(remoteDir);
657
711
  const tempDir = path.join(this.config.stateDir, 'tmp');
package/dist/providers.js CHANGED
@@ -4,6 +4,7 @@ import { ToolError } from './config.js';
4
4
  const PROVIDER_KEYS = new Set([
5
5
  'id', 'label', 'url', 'enabled', 'promptInput', 'submitButton', 'downloadButton',
6
6
  'downloadConfirm', 'loginCheck', 'busyIndicator', 'readyIndicator', 'errorText',
7
+ 'blockedText', 'adapter', 'confirmSelector', 'failureText', 'promptPrefix',
7
8
  'creditBalance', 'creditCost', 'dailyCap', 'timeoutMs', 'readinessMs', 'options', 'imageInput',
8
9
  'cancelButton', 'libraryUrl', 'libraryItem', 'notes', 'createdAt', 'updatedAt',
9
10
  'verifiedAt',
@@ -111,6 +112,17 @@ export function normalizeProvider(input, existing, touch = true) {
111
112
  busyIndicator: optionalSelector(merged.busyIndicator, 'busyIndicator'),
112
113
  readyIndicator: optionalSelector(merged.readyIndicator, 'readyIndicator'),
113
114
  errorText: optionalSelector(merged.errorText, 'errorText'),
115
+ blockedText: optionalString(merged.blockedText, 'blockedText', 500),
116
+ adapter: (() => {
117
+ if (merged.adapter === undefined)
118
+ return undefined;
119
+ if (merged.adapter === 'flow')
120
+ return 'flow';
121
+ throw new ToolError('INVALID_PROVIDER');
122
+ })(),
123
+ confirmSelector: optionalSelector(merged.confirmSelector, 'confirmSelector'),
124
+ failureText: optionalString(merged.failureText, 'failureText', 500),
125
+ promptPrefix: optionalString(merged.promptPrefix, 'promptPrefix', 500),
114
126
  creditBalance: optionalSelector(merged.creditBalance, 'creditBalance'),
115
127
  creditCost: numberValue(merged.creditCost, 1, 0, 10_000),
116
128
  dailyCap: numberValue(merged.dailyCap, 6, 0, 10_000, true),
@@ -6,6 +6,8 @@ export interface Page {
6
6
  enabled(selector: string): Promise<boolean>;
7
7
  text(selector: string): Promise<string>;
8
8
  click(selector: string): Promise<boolean>;
9
+ realClick(selector: string): Promise<boolean>;
10
+ bringToFront(): Promise<void>;
9
11
  fill(selector: string, text: string): Promise<void>;
10
12
  setFileInput(selector: string, filePath: string): Promise<void>;
11
13
  setDownloadBehavior(downloadPath: string): Promise<void>;
package/dist/transport.js CHANGED
@@ -142,19 +142,56 @@ export class CdpDriver {
142
142
  return true;
143
143
  }`, selector);
144
144
  },
145
+ async realClick(selector) {
146
+ const rect = await evaluate(`(s) => {
147
+ const el = document.querySelector(s);
148
+ if (!el) return null;
149
+ const r = el.getBoundingClientRect();
150
+ const st = getComputedStyle(el);
151
+ return { x: r.x, y: r.y, w: r.width, h: r.height,
152
+ visible: r.width > 1 && r.height > 1 && st.display !== 'none' && st.visibility !== 'hidden' && Number(st.opacity || '1') > 0.01 };
153
+ }`, selector);
154
+ if (!rect?.visible)
155
+ return false;
156
+ const x = rect.x + rect.w / 2;
157
+ const y = rect.y + rect.h / 2;
158
+ await command('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y }, sessionId);
159
+ await command('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 }, sessionId);
160
+ await command('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }, sessionId);
161
+ return true;
162
+ },
163
+ async bringToFront() {
164
+ await command('Page.bringToFront', {}, sessionId);
165
+ },
145
166
  async fill(selector, value) {
146
- await evaluate(`(a) => {
147
- const el = document.querySelector(a.selector);
148
- if (!el) throw new Error('INPUT_NOT_FOUND');
149
- el.focus();
150
- if ('value' in el) {
167
+ const isValue = await evaluate(`(s) => { const el = document.querySelector(s); return !!el && 'value' in el; }`, selector);
168
+ if (isValue) {
169
+ await evaluate(`(a) => {
170
+ const el = document.querySelector(a.selector);
171
+ if (!el) throw new Error('INPUT_NOT_FOUND');
172
+ el.focus();
151
173
  const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value');
152
174
  descriptor?.set?.call(el, a.text);
153
175
  el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: a.text }));
154
- } else {
155
- document.execCommand('insertText', false, a.text);
156
- }
157
- }`, { selector, text: value });
176
+ }`, { selector, text: value });
177
+ return;
178
+ }
179
+ await evaluate(`(s) => {
180
+ const el = document.querySelector(s);
181
+ if (!el) throw new Error('INPUT_NOT_FOUND');
182
+ el.focus();
183
+ const selection = window.getSelection();
184
+ const range = document.createRange();
185
+ range.selectNodeContents(el);
186
+ selection?.removeAllRanges();
187
+ selection?.addRange(range);
188
+ }`, selector);
189
+ try {
190
+ await command('Input.insertText', { text: value }, sessionId);
191
+ }
192
+ catch {
193
+ await evaluate(`(a) => document.execCommand('insertText', false, a.text)`, { text: value });
194
+ }
158
195
  },
159
196
  async setFileInput(selector, filePath) {
160
197
  await command('DOM.enable', {}, sessionId);
package/dist/types.d.ts CHANGED
@@ -19,6 +19,11 @@ export interface ProviderSpec {
19
19
  busyIndicator?: string;
20
20
  readyIndicator?: string;
21
21
  errorText?: string;
22
+ blockedText?: string;
23
+ adapter?: 'flow';
24
+ confirmSelector?: string;
25
+ failureText?: string;
26
+ promptPrefix?: string;
22
27
  creditBalance?: string;
23
28
  creditCost: number;
24
29
  dailyCap: number;
@@ -2,7 +2,7 @@
2
2
  "id": "zero-video",
3
3
  "name": "Zero Video",
4
4
  "description": "Generate videos through signed-in consumer AI video sites, with runtime-managed provider profiles.",
5
- "version": "0.1.4",
5
+ "version": "0.1.7",
6
6
  "providers": [],
7
7
  "contracts": {
8
8
  "tools": [
@@ -51,8 +51,8 @@
51
51
  "downloadTimeoutMs": {
52
52
  "type": "integer",
53
53
  "minimum": 10000,
54
- "maximum": 35000,
55
- "default": 30000
54
+ "maximum": 25000,
55
+ "default": 20000
56
56
  },
57
57
  "maxDownloadBytes": {
58
58
  "type": "integer",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-zero-video",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "Generate videos through signed-in consumer AI video sites on the estate Chromium lane.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",