@petercjl/infinite-canvas-cli 0.1.0 → 0.1.1
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 +7 -5
- package/bin/infinite-canvas.mjs +8 -4
- package/capabilities.json +22 -3
- package/package.json +1 -1
- package/skills/infinite-canvas-video/SKILL.md +6 -3
- package/skills/infinite-canvas-video/adapters/codex.json +1 -1
- package/skills/infinite-canvas-video/adapters/generic.json +1 -1
- package/skills/infinite-canvas-video/references/runtime.md +11 -3
- package/src/browser-runtime.mjs +105 -0
- package/src/browser.mjs +4 -7
- package/src/contracts.mjs +11 -6
- package/src/core.mjs +1 -1
package/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`@petercjl/infinite-canvas-cli` uses an existing SealSeek Infinite Canvas board to generate videos through its native Agent conversation. It imports ordered reference images, sends the supplied prompt in a fresh conversation, downloads the video, and groups the original image nodes and video node in a Frame.
|
|
4
4
|
|
|
5
|
-
This package requires Node.js 22+,
|
|
5
|
+
This package requires Node.js 22+, installed Google Chrome on macOS or Windows and an installed `sealseek-media` CLI supporting image upload and canvas get/add. The package discovers and launches Chrome itself on loopback port 9223 with a persistent current-user dedicated profile. Browser login is completed in that window. The website supplies the selected model and authenticated account. Each authorized generation can incur charges.
|
|
6
6
|
|
|
7
7
|
Each board file uses exactly one browser tab. Concurrent videos run in separate Agent conversations within that tab; UI submissions and canvas writes are queued while remote generation runs in parallel. Duplicate tabs must be resolved before submission or saving.
|
|
8
8
|
|
|
9
|
+
`doctor` distinguishes Chrome installation from runtime state. An installed executable passes the browser dependency check while Chrome is stopped. `browser open` launches or reuses the dedicated instance; video execution prepares it automatically. A port/profile conflict is reported explicitly. Chrome outside the standard paths can be selected with `INFINITE_CANVAS_CHROME_BIN`.
|
|
10
|
+
|
|
9
11
|
## Agent usage
|
|
10
12
|
|
|
11
13
|
Provide the board ID, prompt and reference image attachments in their intended order. Send the Agent:
|
|
@@ -14,19 +16,19 @@ Provide the board ID, prompt and reference image attachments in their intended o
|
|
|
14
16
|
|
|
15
17
|
To install:
|
|
16
18
|
|
|
17
|
-
> 安装 @petercjl/infinite-canvas-cli
|
|
19
|
+
> 安装 @petercjl/infinite-canvas-cli 及其配套 Skill。
|
|
18
20
|
|
|
19
21
|
To update:
|
|
20
22
|
|
|
21
|
-
> 更新 @petercjl/infinite-canvas-cli
|
|
23
|
+
> 更新 @petercjl/infinite-canvas-cli 及其配套 Skill。
|
|
22
24
|
|
|
23
25
|
To recover:
|
|
24
26
|
|
|
25
27
|
> 使用 infinite-canvas,恢复任务「任务ID」。
|
|
26
28
|
|
|
27
|
-
To
|
|
29
|
+
To synchronize the current Agent’s Skill:
|
|
28
30
|
|
|
29
|
-
> 将 infinite-canvas
|
|
31
|
+
> 将 infinite-canvas 当前安装包的配套 Skill 同步到当前 Agent。
|
|
30
32
|
|
|
31
33
|
Codex/macOS has completed six live video tasks across two boards. Other Agent adapters require runtime verification.
|
|
32
34
|
|
package/bin/infinite-canvas.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
4
|
import crypto from 'node:crypto';
|
|
6
5
|
import {META,ROOT,need,parse,readJSON,execute,stateRoot,newJSON,redact,safeId} from '../src/core.mjs';
|
|
7
6
|
import {manifest,HELP} from '../src/contracts.mjs';
|
|
@@ -32,11 +31,16 @@ async function main(argv){
|
|
|
32
31
|
const {options:o,args}=parse(argv.slice(1),{values:['canvas'],booleans:['live','json']});need(!args.length,'INVALID_ARGUMENT','Unexpected arguments.');await automaticCheck();const checks={node:process.version,entry:path.join(ROOT,'bin/infinite-canvas.mjs'),platform:process.platform,skill:SKILL};
|
|
33
32
|
for(const library of ['playwright-core','image-size']){try{await import(library);checks[library]={ok:true};}catch{checks[library]={ok:false};}}
|
|
34
33
|
try{checks.media={ok:true,version:(await execute(process.env.INFINITE_CANVAS_MEDIA_BIN||'sealseek-media',['--version'],{timeout:15000})).stdout.trim()};}catch{checks.media={ok:false};}
|
|
35
|
-
|
|
34
|
+
const {ChromeRuntime}=await import('../src/browser-runtime.mjs');const runtime=new ChromeRuntime();
|
|
35
|
+
try{checks.browser=await runtime.configuredLauncher('status')||await runtime.status();}catch(e){checks.browser={ok:false,error:redact(e)};}
|
|
36
36
|
if(o.live){need(o.canvas,'INVALID_ARGUMENT','Live doctor requires --canvas ID.');const {BrowserDriver,LOCATORS}=await import('../src/browser.mjs');const driver=new BrowserDriver();try{await driver.connect(o.canvas,{open:false});const board=await driver.getBoard();checks.live={canvas_id:String(board.id),title:board.title,locators:{}};for(const [k,s]of Object.entries(LOCATORS))checks.live.locators[k]=await driver.page.locator(s).count();}finally{await driver.close();}}
|
|
37
|
-
const ok=checks.media.ok&&checks['playwright-core'].ok&&checks['image-size'].ok&&checks.browser.
|
|
37
|
+
const ok=checks.media.ok&&checks['playwright-core'].ok&&checks['image-size'].ok&&checks.browser.ok;if(!ok)process.exitCode=1;return out({ok,checks,browser_ready:!!checks.browser.ready,paid_action:false,end_to_end:'not-tested-by-doctor'});
|
|
38
|
+
}
|
|
39
|
+
if(command==='browser'){
|
|
40
|
+
const {args}=parse(rest,{booleans:['json']});need(!args.length&&['status','open'].includes(action),'INVALID_ARGUMENT','Use browser status/open --json.');
|
|
41
|
+
const {ChromeRuntime}=await import('../src/browser-runtime.mjs');const runtime=new ChromeRuntime();
|
|
42
|
+
const result=action==='open'?await runtime.open():await runtime.configuredLauncher('status')||await runtime.status();if(!result.ok)process.exitCode=1;return out(result);
|
|
38
43
|
}
|
|
39
|
-
if(command==='browser'&&action==='status'){const {args}=parse(rest,{booleans:['json']});need(!args.length,'INVALID_ARGUMENT','Unexpected arguments.');const r=await execute(process.env.INFINITE_CANVAS_BROWSER_BIN||path.join(os.homedir(),'.local','bin','ecommerce-browser'),['status','--json'],{timeout:15000});return out(JSON.parse(r.stdout));}
|
|
40
44
|
if(command==='board'&&action==='inspect'){const {options:o,args}=parse(rest,{values:['canvas'],booleans:['json']});need(!args.length&&o.canvas,'INVALID_ARGUMENT','Provide --canvas ID.');const {MediaCLI}=await import('../src/media-cli.mjs');return out(await new MediaCLI().get(o.canvas));}
|
|
41
45
|
if(command==='video'&&action==='generate'){
|
|
42
46
|
const {options:o,args}=parse(rest,{values:['canvas','prompt','prompt-file','output','request-id'],repeat:['reference'],booleans:['new','submit','dry-run','json']});need(!args.length&&!(o.submit&&o['dry-run']),'INVALID_ARGUMENT','Conflicting options or extra arguments.');need(!!o.prompt!==!!o['prompt-file'],'INVALID_ARGUMENT','Provide exactly one prompt source.');need(!(o.new&&o['request-id']),'INVALID_ARGUMENT','Use new or request-id.');
|
package/capabilities.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema": "infinite-canvas-capabilities",
|
|
3
3
|
"schema_version": "1.0.0",
|
|
4
4
|
"package": "@petercjl/infinite-canvas-cli",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"input": {
|
|
7
7
|
"required": [
|
|
8
8
|
"canvas_id",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"operations": [
|
|
29
29
|
"browser.status",
|
|
30
|
+
"browser.open",
|
|
30
31
|
"board.inspect",
|
|
31
32
|
"video.generate.native-agent",
|
|
32
33
|
"task.inspect",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"node": ">=22",
|
|
50
|
-
"browser": "
|
|
51
|
+
"browser": "Installed Google Chrome; built-in macOS/Windows discovery and startup on loopback 9223 with current-user dedicated profile",
|
|
51
52
|
"canvasAPI": "sealseek-media >=0.6.7, public CLI commands",
|
|
52
53
|
"libraries": [
|
|
53
54
|
"playwright-core",
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
"codex:macos": "tested: six native Agent video tasks across two boards; one tab per board; downloads, deduplication and Frame readback verified",
|
|
62
63
|
"generic:macos": "spec-compatible, runtime untested",
|
|
63
64
|
"sealseek:macos": "spec-compatible, runtime untested",
|
|
64
|
-
"generic:windows": "
|
|
65
|
+
"generic:windows": "built-in Chrome discovery/startup implemented; actual host video end-to-end verification pending"
|
|
65
66
|
},
|
|
66
67
|
"commandContracts": {
|
|
67
68
|
"video generate": {
|
|
@@ -138,6 +139,24 @@
|
|
|
138
139
|
"sideEffects": "local-write",
|
|
139
140
|
"externalMutation": "With --submit: reparent existing nodes and preserve their identities."
|
|
140
141
|
},
|
|
142
|
+
"browser status": {
|
|
143
|
+
"options": {
|
|
144
|
+
"--json": {
|
|
145
|
+
"type": "boolean"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"sideEffects": "none",
|
|
149
|
+
"output": "Chrome availability and separate CDP runtime readiness"
|
|
150
|
+
},
|
|
151
|
+
"browser open": {
|
|
152
|
+
"options": {
|
|
153
|
+
"--json": {
|
|
154
|
+
"type": "boolean"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"sideEffects": "local-write",
|
|
158
|
+
"externalMutation": "Launch or reuse system Chrome on loopback 9223 with the current-user dedicated profile; no generation."
|
|
159
|
+
},
|
|
141
160
|
"doctor": {
|
|
142
161
|
"options": {
|
|
143
162
|
"--canvas": {
|
package/package.json
CHANGED
|
@@ -17,11 +17,11 @@ Return the local MP4 from `artifacts[].path`, task ID, board ID and Frame ID. Su
|
|
|
17
17
|
|
|
18
18
|
Resolve `infinite-canvas` from PATH. Run `version --json`, `capabilities --json` and `skill source --json` to inspect the installed implementation and current canonical Skill. Minimum contract: native Agent Send, ordered references, existing-node grouping, task get/wait/resume, local video output. Load the selected host adapter in `adapters/`; `generic.json` is the portable fallback. If the entry or required feature is missing, return `CAPABILITY_UNAVAILABLE`; report the missing dependency instead of substituting another generation provider.
|
|
19
19
|
|
|
20
|
-
External dependencies are Node.js 22+, `playwright-core` and `image-size` (package managed),
|
|
20
|
+
External dependencies are Node.js 22+, `playwright-core` and `image-size` (package managed), installed Google Chrome on macOS or Windows, and `sealseek-media` with image upload, canvas get/add capabilities. This Skill invokes the helper through the CLI contract; it does not compose another generation Skill. Browser authentication stays in the user's dedicated Chrome profile. Never copy cookies or tokens into a prompt, output or package.
|
|
21
21
|
|
|
22
22
|
## Main line
|
|
23
23
|
|
|
24
|
-
1. Validate required inputs and authorization. Run `doctor --json`;
|
|
24
|
+
1. Validate required inputs and authorization. Run `doctor --json`: its browser check means Chrome is available to launch; `browser_ready` separately reports runtime readiness. Use `browser open --json` to prepare Chrome. The CLI discovers and launches the system browser itself with fixed loopback 9223 and the persistent current-user `.dianshang-chrome-profile`; a separate launcher installation is unnecessary. For a UI compatibility check use `doctor --live --canvas ID --json`. On missing login, have the user sign in in that Chrome window, then return here. The CLI uses CDP port 9223 and the dedicated ecommerce profile. Enforce the single-tab constraint below before any submission or canvas write.
|
|
25
25
|
2. Prepare the exact prompt in a new file when needed; preserve any existing file. Run `video generate --canvas ID --prompt-file FILE --reference IMAGE1 --reference IMAGE2 --dry-run --json`, repeating `--reference` in order. Dry run checks local inputs; it does not prove provider availability.
|
|
26
26
|
3. For an authorized real request, run the same inputs with `--submit --json`. Save the returned task ID immediately. Repeated identical inputs reuse the same task. `--request-id KEY` provides caller-controlled idempotency; `--new` deliberately creates a new paid task and requires explicit intent to generate another video.
|
|
27
27
|
4. Follow `task wait TASK_ID --timeout 30 --json` or `task get TASK_ID --json`. The worker imports references, creates a new native Agent conversation, attaches images, pastes the exact prompt and clicks Send. Status/history reads track the captured session and run. Generation is submitted only through the native UI.
|
|
@@ -41,6 +41,9 @@ If duplicate tabs for a board are detected, stop new submissions and writes with
|
|
|
41
41
|
|
|
42
42
|
## Branches and recovery
|
|
43
43
|
|
|
44
|
+
- `CHROME_NOT_FOUND`: install Google Chrome or set `INFINITE_CANVAS_CHROME_BIN` to its actual executable; return to step 1.
|
|
45
|
+
- `BROWSER_PORT_CONFLICT` / `BROWSER_OWNERSHIP_UNVERIFIED`: preserve existing browser processes/profile files, resolve the 9223 ownership conflict, then return to step 1.
|
|
46
|
+
- `BROWSER_START_TIMEOUT`: inspect the dedicated profile’s existing Chrome and startup permissions; preserve task IDs and return to step 1 after readiness is restored.
|
|
44
47
|
- `waiting`: the monitoring window or a connection ended; use `task resume TASK_ID --json`, then return to step 4. Remote runs continue independently.
|
|
45
48
|
- `uncertain` / `SUBMISSION_UNCERTAIN`: preserve the task; resume inspects the captured session for exactly one run. If identity cannot be resolved, report the uncertainty and request inspection. Never start another paid task as an automatic retry.
|
|
46
49
|
- `canvas_pending`: return any downloaded video as a partial result, then resume the same task for import/grouping. It does not resubmit generation.
|
|
@@ -56,4 +59,4 @@ Read [runtime reference](references/runtime.md) only when command details or rec
|
|
|
56
59
|
|
|
57
60
|
Check one clean user message, captured native session/run IDs, attachment order at submission, exactly one result URL, returned reference metadata when provided, artifact bytes/hash and saved Frame membership. Report unavailable reference metadata or absent ffprobe verification honestly. Scope claims to actually tested hosts and operating systems. The current browser adapter needs live validation after webpage changes; independent clean-context regression is user gated.
|
|
58
61
|
|
|
59
|
-
Install/update only the requested Agent through package-managed `skill install/update --agent
|
|
62
|
+
Install/update only the executing/requested Agent through package-managed `skill install/update --agent AGENT_NAME --path RESOLVED_SKILL_DIR`. A named adapter supplies a default only for that host; other valid Agent names use the generic adapter and their own explicitly resolved Skill directory. The package is the source of truth. Automatic update detection is bounded and can be disabled; installation is explicit. Keep model defaults and private assets outside the package.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"implementation": {
|
|
18
18
|
"kind": "cli",
|
|
19
19
|
"command": "infinite-canvas",
|
|
20
|
-
"instructions": "Resolve from PATH. Run version, capabilities and doctor. Requires Node.js >=22,
|
|
20
|
+
"instructions": "Resolve from PATH. Run version, capabilities and doctor. Requires Node.js >=22, installed Google Chrome and sealseek-media; Chrome discovery/startup is bundled for macOS and Windows. Use structured JSON. Codex/macOS end-to-end verified with six native Agent videos across two boards, one tab per board, downloads and saved Frame readback. Other hosts require their own runtime validation."
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"implementation": {
|
|
18
18
|
"kind": "cli",
|
|
19
19
|
"command": "infinite-canvas",
|
|
20
|
-
"instructions": "Resolve from PATH. Run version, capabilities and doctor. Requires Node.js >=22,
|
|
20
|
+
"instructions": "Resolve from PATH. Run version, capabilities and doctor. Requires Node.js >=22, installed Google Chrome and sealseek-media; Chrome discovery/startup is bundled for macOS and Windows. Use structured JSON. Named-host runtime end-to-end verification is pending."
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -8,10 +8,12 @@ Browser invariant: one board file, one tab. Reuse the existing tab for that canv
|
|
|
8
8
|
infinite-canvas version --json
|
|
9
9
|
infinite-canvas capabilities --json
|
|
10
10
|
infinite-canvas doctor --json
|
|
11
|
+
infinite-canvas browser status --json
|
|
12
|
+
infinite-canvas browser open --json
|
|
11
13
|
infinite-canvas skill source --json
|
|
12
|
-
infinite-canvas skill status --agent
|
|
13
|
-
infinite-canvas skill install --agent
|
|
14
|
-
infinite-canvas skill update --agent
|
|
14
|
+
infinite-canvas skill status --agent AGENT_NAME --path RESOLVED_SKILL_DIR --json
|
|
15
|
+
infinite-canvas skill install --agent AGENT_NAME --path RESOLVED_SKILL_DIR --json
|
|
16
|
+
infinite-canvas skill update --agent AGENT_NAME --path RESOLVED_SKILL_DIR --json
|
|
15
17
|
infinite-canvas video generate --canvas BOARD_ID --prompt-file PROMPT_FILE --reference IMAGE_1 --reference IMAGE_2 --dry-run --json
|
|
16
18
|
infinite-canvas video generate --canvas BOARD_ID --prompt-file PROMPT_FILE --reference IMAGE_1 --reference IMAGE_2 --submit --json
|
|
17
19
|
infinite-canvas task get TASK_ID --json
|
|
@@ -28,3 +30,9 @@ infinite-canvas update auto off --json
|
|
|
28
30
|
State resides under the user's application-data directory (`INFINITE_CANVAS_STATE_DIR` can select another private location). It contains task state, reference copies, readback backups, download metadata and worker logs. Preserve it for recovery; treat media URLs and prompts as private. `INFINITE_CANVAS_AUTO_UPDATE=0` disables automatic registry detection. Explicit update installation requires `update install --yes`, refuses source checkouts and backs up the installed package. Unpublished development versions have no registry update available.
|
|
29
31
|
|
|
30
32
|
The CLI never prints browser credentials. Changing source, a live webpage deployment or a helper CLI version may require revalidation. JSON failures include `error.code` and `error.message`; keep the original task ID in user-facing recovery advice.
|
|
33
|
+
|
|
34
|
+
## Built-in Chrome runtime
|
|
35
|
+
|
|
36
|
+
Only Google Chrome is an external browser prerequisite. macOS discovery checks system and current-user Applications; Windows checks per-user and Program Files locations, then App Paths registry entries. `INFINITE_CANVAS_CHROME_BIN` selects an actual executable for a custom installation. No extra launcher is required. `INFINITE_CANVAS_BROWSER_BIN` remains an explicit legacy override and is verified against the same port/profile contract.
|
|
37
|
+
|
|
38
|
+
`browser status` is read-only. `chrome.installed` and `available` describe installation; `running` and `ready` describe the dedicated instance. A stopped but installed Chrome passes the browser dependency check in `doctor`. `browser open` and normal video execution launch/reuse it, verify process ownership, loopback CDP 9223 and the current-user profile. On Windows this profile is under the current home directory; on macOS it is under the current home directory with the same `.dianshang-chrome-profile` name. Windows uses built-in PowerShell process/TCP queries; macOS uses bounded nonblocking system process queries. Startup is queued so concurrent jobs share the browser. `doctor --live --canvas ID` requires the running, authenticated, editable board as well.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import {constants} from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import {spawn} from 'node:child_process';
|
|
7
|
+
import {execute,Fault,need,lock,sleep} from './core.mjs';
|
|
8
|
+
|
|
9
|
+
export const CDP='http://127.0.0.1:9223';
|
|
10
|
+
export const PROFILE_NAME='.dianshang-chrome-profile';
|
|
11
|
+
const escapeRE=s=>s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
|
12
|
+
const powershell=env=>path.win32.join(env.SystemRoot||env.WINDIR||'C:\\Windows','System32','WindowsPowerShell','v1.0','powershell.exe');
|
|
13
|
+
const psPrefix='$ErrorActionPreference="Stop"; [Console]::OutputEncoding=[Text.UTF8Encoding]::new($false); ';
|
|
14
|
+
export function windowsListeners(output){
|
|
15
|
+
return String(output).split(/\r?\n/).map(line=>line.trim().split(/\s+/)).filter(row=>row.length===5&&/^TCP$/i.test(row[0])&&row[1].endsWith(':9223')&&/^\d+$/.test(row[4])&&['0.0.0.0:0','[::]:0'].includes(row[2])).map(row=>({pid:Number(row[4]),address:row[1].slice(0,-5).replace(/^\[|\]$/g,'')}));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function chromeCandidates({platform=process.platform,env=process.env,home=os.homedir()}={}){
|
|
19
|
+
const p=platform==='win32'?path.win32:path.posix;
|
|
20
|
+
if(env.INFINITE_CANVAS_CHROME_BIN)return [env.INFINITE_CANVAS_CHROME_BIN];
|
|
21
|
+
if(platform==='darwin')return ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',p.join(home,'Applications/Google Chrome.app/Contents/MacOS/Google Chrome')];
|
|
22
|
+
if(platform==='win32')return [...new Set([env.LOCALAPPDATA||p.join(home,'AppData','Local'),env.ProgramFiles||env.PROGRAMFILES||'C:\\Program Files',env['ProgramFiles(x86)']||env['PROGRAMFILES(X86)']||'C:\\Program Files (x86)'].map(root=>p.join(root,'Google','Chrome','Application','chrome.exe')))];
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
export function dedicatedCommand(command,profile,platform=process.platform){
|
|
26
|
+
let c=String(command||''),expected=profile;
|
|
27
|
+
if(platform==='win32'){c=c.replaceAll('\\','/').toLowerCase();expected=expected.replaceAll('\\','/').toLowerCase();}
|
|
28
|
+
const flag=(name,value)=>new RegExp('(?:^|[\\s"])--'+name+'(?:=|\\s+)"?'+escapeRE(value)+'"?(?=$|[\\s"])').test(c);
|
|
29
|
+
return flag('remote-debugging-port','9223')&&flag('user-data-dir',expected)&&!/(?:^|[\s"])--no-sandbox(?:[\s"]|$)/.test(c)&&(!c.includes('--remote-debugging-address')||flag('remote-debugging-address','127.0.0.1'));
|
|
30
|
+
}
|
|
31
|
+
export function chromeArgs(profile,{headless=false,startupUrl='https://sealseek.cn/board/'}={}){
|
|
32
|
+
return ['--remote-debugging-address=127.0.0.1','--remote-debugging-port=9223','--user-data-dir='+profile,'--no-first-run','--no-default-browser-check',...(headless?['--headless=new']:[]),startupUrl];
|
|
33
|
+
}
|
|
34
|
+
async function listening(){return new Promise(resolve=>{const socket=net.createConnection({host:'127.0.0.1',port:9223});let done=false;const finish=v=>{if(done)return;done=true;socket.destroy();resolve(v);};socket.setTimeout(800,()=>finish(false));socket.once('connect',()=>finish(true));socket.once('error',()=>finish(false));});}
|
|
35
|
+
async function launch(executable,args){
|
|
36
|
+
const child=spawn(executable,args,{detached:true,stdio:'ignore',windowsHide:false});
|
|
37
|
+
await new Promise((resolve,reject)=>{child.once('spawn',resolve);child.once('error',reject);});child.unref();return child.pid;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ChromeRuntime {
|
|
41
|
+
constructor({platform=process.platform,env=process.env,home=os.homedir(),access=async file=>{const stat=await fs.stat(file);if(!stat.isFile())return false;await fs.access(file,platform==='win32'?constants.F_OK:constants.X_OK);return true;},exec=execute,probe=listening,fetcher=fetch,launchProcess=launch,mkdir=dir=>fs.mkdir(dir,{recursive:true,mode:0o700}),pause=sleep,headless=false,startupUrl}={}){
|
|
42
|
+
Object.assign(this,{platform,env,home,access,exec,probe,fetcher,launchProcess,mkdir,pause,headless,startupUrl});
|
|
43
|
+
this.paths=platform==='win32'?path.win32:path.posix;this.profile=this.paths.join(home,PROFILE_NAME);
|
|
44
|
+
}
|
|
45
|
+
async discover(){
|
|
46
|
+
for(const file of chromeCandidates(this)){try{if(await this.access(file))return {installed:true,executable:file,source:this.env.INFINITE_CANVAS_CHROME_BIN?'configured':'standard-location'};}catch{}}
|
|
47
|
+
if(this.env.INFINITE_CANVAS_CHROME_BIN)return {installed:false,executable:this.env.INFINITE_CANVAS_CHROME_BIN,source:'configured',error:{code:'CHROME_NOT_FOUND',message:'Configured Chrome executable is unavailable.'}};
|
|
48
|
+
if(this.platform==='win32'){
|
|
49
|
+
for(const key of ['HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe','HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe']){
|
|
50
|
+
try{const {stdout}=await this.exec('reg.exe',['query',key,'/ve'],{timeout:3000,windowsHide:true});let file=stdout.match(/REG_(?:EXPAND_)?SZ\s+(.+)/)?.[1]?.trim().replace(/^"|"$/g,'');if(file){file=file.replace(/%([^%]+)%/g,(_,name)=>Object.entries(this.env).find(([k])=>k.toLowerCase()===name.toLowerCase())?.[1]||'%'+name+'%');if(await this.access(file))return {installed:true,executable:file,source:'windows-app-paths'};}}catch{}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {installed:false,source:'system-discovery',error:{code:['darwin','win32'].includes(this.platform)?'CHROME_NOT_FOUND':'UNSUPPORTED_PLATFORM',message:'Install Google Chrome, or provide its executable with INFINITE_CANVAS_CHROME_BIN.'}};
|
|
54
|
+
}
|
|
55
|
+
async owners(){
|
|
56
|
+
if(this.platform==='win32'){
|
|
57
|
+
const network=await this.exec(path.win32.join(this.env.SystemRoot||this.env.WINDIR||'C:\\Windows','System32','netstat.exe'),['-ano','-p','tcp'],{timeout:5000,maxBuffer:2*1024*1024,windowsHide:true});
|
|
58
|
+
const connections=windowsListeners(network.stdout),pids=[...new Set(connections.map(p=>p.pid))];if(!pids.length)return [];
|
|
59
|
+
const script=psPrefix+'$result=@(foreach($id in @('+pids.join(',')+')){ $p=Get-CimInstance Win32_Process -Filter ("ProcessId = "+$id); [pscustomobject]@{pid=$p.ProcessId;name=$p.Name;command=$p.CommandLine} }); ConvertTo-Json -InputObject $result -Compress';
|
|
60
|
+
const {stdout}=await this.exec(powershell(this.env),['-NoProfile','-NonInteractive','-Command',script],{timeout:30000,maxBuffer:1024*1024,windowsHide:true});const data=JSON.parse(stdout.replace(/^\uFEFF/,''));return (Array.isArray(data)?data:[data]).map(p=>({...p,addresses:connections.filter(c=>c.pid===p.pid).map(c=>c.address)}));
|
|
61
|
+
}
|
|
62
|
+
if(this.platform==='darwin'){
|
|
63
|
+
const {stdout}=await this.exec('/usr/sbin/lsof',['-b','-nP','-t','-iTCP:9223','-sTCP:LISTEN'],{timeout:5000});const pids=[...new Set(stdout.trim().split(/\s+/).filter(v=>/^\d+$/.test(v)))];
|
|
64
|
+
return Promise.all(pids.map(async pid=>({pid:Number(pid),command:(await this.exec('/bin/ps',['-p',pid,'-o','command='],{timeout:3000})).stdout.trim()})));
|
|
65
|
+
}
|
|
66
|
+
throw new Fault('UNSUPPORTED_PLATFORM','Built-in Chrome startup currently supports macOS and Windows.');
|
|
67
|
+
}
|
|
68
|
+
async inspect(chrome){
|
|
69
|
+
if(!await this.probe())return {state:'stopped',running:false,ready:false};
|
|
70
|
+
let owners;
|
|
71
|
+
try{owners=await this.owners();}catch{return {state:'conflict',running:true,ready:false,error:{code:'BROWSER_OWNERSHIP_UNVERIFIED',message:'Port 9223 is occupied, but its process/profile could not be verified.'}};}
|
|
72
|
+
const matches=owners.length>0&&owners.every(p=>dedicatedCommand(p.command,this.profile,this.platform)&&(this.platform==='win32'?p.name?.toLowerCase()==='chrome.exe'&&p.addresses?.length&&p.addresses.every(a=>['127.0.0.1','::1'].includes(a)):p.command.startsWith(chrome.executable+' ')||p.command.startsWith('"'+chrome.executable+'" ')));
|
|
73
|
+
if(!matches)return {state:'conflict',running:true,ready:false,error:{code:'BROWSER_PORT_CONFLICT',message:'Port 9223 belongs to another process, profile, or non-loopback listener. Preserve it and resolve the conflict.'}};
|
|
74
|
+
try{
|
|
75
|
+
const response=await this.fetcher(CDP+'/json/version',{signal:AbortSignal.timeout(1500),redirect:'error'});const data=await response.json(),ws=new URL(data.webSocketDebuggerUrl);
|
|
76
|
+
need(response.ok&&/^(?:Headless)?Chrome\//.test(data.Browser)&&ws.protocol==='ws:'&&ws.hostname==='127.0.0.1'&&ws.port==='9223','BROWSER_UNAVAILABLE','Chrome CDP response is invalid.');
|
|
77
|
+
return {state:'ready',running:true,ready:true,pids:owners.map(p=>p.pid),browser:data.Browser};
|
|
78
|
+
}catch{return {state:'starting',running:true,ready:false,pids:owners.map(p=>p.pid)};}
|
|
79
|
+
}
|
|
80
|
+
async status(){
|
|
81
|
+
const chrome=await this.discover();const runtime=await this.inspect(chrome);
|
|
82
|
+
return {ok:(chrome.installed||runtime.ready)&&!runtime.error,available:chrome.installed||runtime.ready,chrome,...runtime,cdp:CDP,profile:this.profile,source:'built-in',...(chrome.error&&!runtime.ready?{error:chrome.error}:{}),next_action:runtime.ready?'open-or-reuse-board':chrome.installed&&!runtime.error?'browser open':runtime.error?'resolve-port-conflict':'install-google-chrome'};
|
|
83
|
+
}
|
|
84
|
+
async configuredLauncher(action){
|
|
85
|
+
const file=this.env.INFINITE_CANVAS_BROWSER_BIN;if(!file)return null;
|
|
86
|
+
let value;try{const r=await this.exec(file,[action,'--json'],{timeout:30000});value=JSON.parse(r.stdout);}catch(e){try{value=JSON.parse(e.stdout);}catch{throw new Fault('BROWSER_UNAVAILABLE','Configured browser launcher is unavailable.');}}
|
|
87
|
+
need(value.cdp===CDP&&this.paths.resolve(value.profile||'')===this.profile,'PROFILE_MISMATCH','Configured launcher must use port 9223 and the current-user dedicated profile.');
|
|
88
|
+
const own=await this.status();need(!own.error,own.error?.code||'BROWSER_UNAVAILABLE',own.error?.message||'Browser verification failed.');return {...own,source:'configured-launcher'};
|
|
89
|
+
}
|
|
90
|
+
async open({timeoutMs=60000}={}){
|
|
91
|
+
if(this.env.INFINITE_CANVAS_BROWSER_BIN){await this.configuredLauncher('open');const result=await this.status();need(result.ready,'BROWSER_UNAVAILABLE','Configured launcher did not make Chrome ready.');return result;}
|
|
92
|
+
const end=Date.now()+timeoutMs;
|
|
93
|
+
while(Date.now()<end){
|
|
94
|
+
try{return await lock('browser-start:'+this.profile,async()=>{
|
|
95
|
+
let state=await this.status();if(state.ready)return {...state,reused:true,started:false};
|
|
96
|
+
need(!state.error,state.error?.code||'BROWSER_UNAVAILABLE',state.error?.message||'Browser is unavailable.');
|
|
97
|
+
need(state.chrome.installed,'CHROME_NOT_FOUND','Google Chrome is unavailable.');
|
|
98
|
+
if(!state.running){await this.mkdir(this.profile);await this.launchProcess(state.chrome.executable,chromeArgs(this.profile,{headless:this.headless,startupUrl:this.startupUrl}));}
|
|
99
|
+
const started=!state.running;while(Date.now()<end){await this.pause(300);state=await this.status();need(!state.error,state.error?.code||'BROWSER_UNAVAILABLE',state.error?.message||'Browser start conflict.');if(state.ready)return {...state,started,reused:!started};}
|
|
100
|
+
throw new Fault('BROWSER_START_TIMEOUT','Chrome did not become ready. Check whether this dedicated profile is already open without debugging; preserve its files and existing browser processes.');
|
|
101
|
+
});}catch(e){if(e.code!=='BUSY')throw e;await this.pause(300);}
|
|
102
|
+
}
|
|
103
|
+
throw new Fault('BUSY','Another process is preparing the dedicated Chrome browser.');
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/browser.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import crypto from 'node:crypto';
|
|
4
3
|
import {chromium} from 'playwright-core';
|
|
5
|
-
import {
|
|
4
|
+
import {need,Fault,sleep,newJSON,lock} from './core.mjs';
|
|
6
5
|
import {sceneSignature,verifyGrouping} from './frame.mjs';
|
|
6
|
+
import {ChromeRuntime} from './browser-runtime.mjs';
|
|
7
7
|
|
|
8
8
|
export const LOCATORS={newChat:'button:has(svg.lucide-square-plus)',upload:'input[type=file][accept="image/*,video/mp4,video/webm,video/*"]',editor:'[contenteditable=true][role=textbox]',send:'button[aria-label="发送消息"]'};
|
|
9
9
|
export async function handleBrowserDialog(dialog){try{if(dialog.type()==='beforeunload'||dialog.type()==='alert')await dialog.accept();else await dialog.dismiss();return true;}catch{return false;}}
|
|
@@ -25,12 +25,9 @@ export function nativeSubmission(body,expectedPrompt,files){
|
|
|
25
25
|
export class BrowserDriver {
|
|
26
26
|
async connect(canvasId,{open=true}={}){
|
|
27
27
|
this.canvasId=canvasId;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let status=JSON.parse(r.stdout);if(status.state!=='ready'&&open){await execute(launcher,['open'],{timeout:30000});status=JSON.parse((await execute(launcher,['status','--json'],{timeout:15000})).stdout);}
|
|
28
|
+
const runtime=new ChromeRuntime();
|
|
29
|
+
const status=open?await runtime.open():await runtime.configuredLauncher('status')||await runtime.status();
|
|
31
30
|
need(status.state==='ready'&&status.cdp==='http://127.0.0.1:9223','BROWSER_UNAVAILABLE','The fixed ecommerce CDP browser is unavailable.');
|
|
32
|
-
const profile=path.join(os.homedir(),'.dianshang-chrome-profile');need(path.resolve(status.profile)===profile,'PROFILE_MISMATCH','Ecommerce browser profile mismatch.');
|
|
33
|
-
if(process.platform!=='win32'){let verified=false;for(const pid of status.pids||[]){try{const ps=await execute('ps',['-p',String(pid),'-o','command=']);verified ||= ps.stdout.includes('--remote-debugging-port=9223')&&ps.stdout.includes('--user-data-dir='+profile);}catch{}}need(verified,'PROFILE_MISMATCH','Chrome process arguments could not be verified.');}
|
|
34
31
|
this.status=status;this.browser=await chromium.connectOverCDP(status.cdp,{timeout:15000});
|
|
35
32
|
for(const context of this.browser.contexts()){for(const page of context.pages())page.on('dialog',handleBrowserDialog);context.on('page',page=>page.on('dialog',handleBrowserDialog));}
|
|
36
33
|
await this.composerLock(()=>this.selectBoardTab({open}));
|
package/src/contracts.mjs
CHANGED
|
@@ -4,6 +4,8 @@ export const commandContracts={
|
|
|
4
4
|
'video generate':{options:{'--canvas':s,'--prompt':s,'--prompt-file':s,'--reference':{...s,repeatable:true},'--output':s,'--request-id':s,'--new':b,'--submit':b,'--dry-run':b,'--json':b},sideEffects:'local-write',externalMutation:'With --submit: import references, send native Agent conversation, import a missing video, group selected elements in a Frame.',paid:'Only native webpage sending may charge credits.'},
|
|
5
5
|
'task get|wait|resume|sync':{options:{'--timeout':s,'--json':b},sideEffects:'local-write',externalMutation:'resume continues original task; sync reapplies existing video archival and grouping; never a second generation.'},
|
|
6
6
|
'frame group':{options:{'--canvas':s,'--elements':s,'--frame-id':s,'--name':s,'--submit':b,'--json':b},sideEffects:'local-write',externalMutation:'With --submit: reparent existing nodes and preserve their identities.'},
|
|
7
|
+
'browser status':{options:{'--json':b},sideEffects:'none',output:'Chrome availability and separate CDP runtime readiness'},
|
|
8
|
+
'browser open':{options:{'--json':b},sideEffects:'local-write',externalMutation:'Launch or reuse system Chrome on loopback 9223 with the current-user dedicated profile; no generation.'},
|
|
7
9
|
'doctor':{options:{'--canvas':s,'--live':b,'--json':b},sideEffects:'none'},
|
|
8
10
|
'skill source':{options:{'--json':b},sideEffects:'none'},
|
|
9
11
|
'skill status|install|update':{options:{'--agent':s,'--path':s,'--json':b},sideEffects:'local-write'},
|
|
@@ -13,10 +15,10 @@ export const commandContracts={
|
|
|
13
15
|
export const manifest={schema:'infinite-canvas-capabilities',schema_version:'1.0.0',package:META.name,version:META.version,
|
|
14
16
|
input:{required:['canvas_id','prompt','references'],references:'ordered PNG/JPEG/WebP local files or HTTPS URLs'},
|
|
15
17
|
output:{terminal:'local MP4 + saved Frame containing original reference nodes and one video node',fields:['task_id','status','canvas_id','session_id','run_id','frame_id','reference_element_ids','video_element_id','artifacts']},
|
|
16
|
-
operations:['browser.status','board.inspect','video.generate.native-agent','task.inspect','task.wait','task.resume','task.sync','frame.group-existing'],
|
|
18
|
+
operations:['browser.status','browser.open','board.inspect','video.generate.native-agent','task.inspect','task.wait','task.resume','task.sync','frame.group-existing'],
|
|
17
19
|
generationTransport:{kind:'native-browser-ui',newConversationPerLogicalVideo:true,singleTabPerCanvas:true,concurrency:'separate conversations in the same tab; queued UI submissions and canvas writes; parallel remote runs',submission:'click native Send',directGenerationAPI:false,recovery:'saved sessionId/runId; read-only run and history APIs',model:'inherit webpage settings'},
|
|
18
|
-
dependencies:{node:'>=22',browser:'
|
|
19
|
-
platforms:{'codex:macos':'tested: six native Agent video tasks across two boards; one tab per board; downloads, deduplication and Frame readback verified','generic:macos':'spec-compatible, runtime untested','sealseek:macos':'spec-compatible, runtime untested','generic:windows':'
|
|
20
|
+
dependencies:{node:'>=22',browser:'Installed Google Chrome; built-in macOS/Windows discovery and startup on loopback 9223 with current-user dedicated profile',canvasAPI:'sealseek-media >=0.6.7, public CLI commands',libraries:['playwright-core','image-size'],optional:['ffprobe']},
|
|
21
|
+
platforms:{'codex:macos':'tested: six native Agent video tasks across two boards; one tab per board; downloads, deduplication and Frame readback verified','generic:macos':'spec-compatible, runtime untested','sealseek:macos':'spec-compatible, runtime untested','generic:windows':'built-in Chrome discovery/startup implemented; actual host video end-to-end verification pending'},
|
|
20
22
|
commandContracts,
|
|
21
23
|
automaticUpdates:{enabledByDefault:true,defaultMode:'notify',intervalHours:24,timeoutMs:1500,trigger:'ordinary video/frame/doctor/capabilities entry; excludes help/version/skill/task polling',configuration:'private update-policy.json; update auto on|off|status',disable:'INFINITE_CANVAS_AUTO_UPDATE=0',recovery:'offline backoff preserves installation; source checkout updates through Git; explicit install keeps a package backup',installation:'explicit --yes only; no automatic installation'},
|
|
22
24
|
limitations:['No service-side atomic canvas revision check; stop on detected user edits.','Frame grouping first version supports unrotated image/video nodes.','Do not submit another generation after unknown submission.']
|
|
@@ -36,7 +38,8 @@ task resume TASK_ID --json (same session/run, no resubmission)
|
|
|
36
38
|
task sync TASK_ID --json (reapply existing output to canvas, no generation)
|
|
37
39
|
frame group --canvas ID --elements IDS.json --frame-id ID [--name TEXT]
|
|
38
40
|
[--submit] --json (preview by default)
|
|
39
|
-
browser status --json
|
|
41
|
+
browser status --json (read-only installation and runtime state)
|
|
42
|
+
browser open --json (built-in system Chrome startup/reuse)
|
|
40
43
|
board inspect --canvas ID --json
|
|
41
44
|
doctor [--live --canvas ID] --json
|
|
42
45
|
capabilities --json
|
|
@@ -47,8 +50,10 @@ update check --json
|
|
|
47
50
|
update install --yes [--agent AGENT_NAME --path DIR] --json
|
|
48
51
|
update auto on|off|status --json
|
|
49
52
|
|
|
50
|
-
Runtime: Node >=22;
|
|
51
|
-
|
|
53
|
+
Runtime: Node >=22; installed Google Chrome on macOS/Windows; sealseek-media.
|
|
54
|
+
Chrome startup is bundled. doctor checks availability; doctor --live verifies a running editable board.
|
|
55
|
+
Configuration: INFINITE_CANVAS_STATE_DIR, INFINITE_CANVAS_CHROME_BIN,
|
|
56
|
+
INFINITE_CANVAS_BROWSER_BIN (optional legacy launcher override),
|
|
52
57
|
INFINITE_CANVAS_MEDIA_BIN. Credentials remain with their existing providers.
|
|
53
58
|
Package update detection is throttled and can be disabled using
|
|
54
59
|
INFINITE_CANVAS_AUTO_UPDATE=0. No generation is submitted by installation.
|
package/src/core.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
|
|
33
33
|
export function alive(pid){try{process.kill(pid,0);return true;}catch{return false;}}
|
|
34
34
|
export async function lock(key,fn,{root=stateRoot()}={}){
|
|
35
35
|
const p=path.join(root,'locks',hash(key)+'.json');await mkdir(path.dirname(p));
|
|
36
|
-
if(await exists(p)){
|
|
36
|
+
if(await exists(p)){let old;try{old=await readJSON(p);}catch(e){if(e.code==='ENOENT')return lock(key,fn,{root});if(e instanceof SyntaxError)throw new Fault('BUSY','Another process is committing its lock.');throw e;}need(old.owner===META.name,'UNMANAGED_STATE','Unrecognized lock.');need(!alive(old.pid),'BUSY','Another process owns this operation.',{pid:old.pid});await fs.rename(p,p+'.'+crypto.randomUUID()+'.abandoned');}
|
|
37
37
|
try{await newJSON(p,{owner:META.name,pid:process.pid,key,createdAt:new Date().toISOString()});}catch(e){if(['EEXIST','OUTPUT_EXISTS'].includes(e.code))throw new Fault('BUSY','Operation is already locked.');throw e;}
|
|
38
38
|
try{return await fn();}finally{await fs.unlink(p);}
|
|
39
39
|
}
|