ELC Remote の「画面の無いビューア」。自動で動くもの(AI や監視スクリプト)が、人と同じように端末の画面を見て操作するための常駐プログラムです。
人が使うビューア(Web / iOS / Android / デスクトップ)とまったく同じ端末間プロトコルで繋ぎますが、画面は描きません。最新の 1 枚を持っているだけで、取りに来たときに渡します。操作する側は 127.0.0.1 のローカル API を叩きます。
- 単一バイナリ。 cgo を使わないので、Linux / macOS / Windows のどれでもそのまま動きます。入れるものはありません。 - 443/WSS のリレーだけを使います。 UDP が塞がれた環境でも通ります。 - サーバーは中身を読めません。 鍵は端末どうしの ECDH でだけ作られます。人が使うビューアと同じです。 - 人が同じ画面に入れます。 自動操作の様子を人がそのまま見られ、必要ならその場で操作を取り上げたり切断したりできます(端末側のプロフィール窓)。
`` AI / スクリプト ELC Remote for Agent ELC Remote 操作される端末 ────────────── ──────────── ─────────── ────────────── HTTP 127.0.0.1 ──▶ 画面を持つだけ ──▶ リレー(素通し) ──▶ Windows / macOS 入力を送る / Android ``
---
配布ページから自分の OS のものを落として、実行権を付けるだけです。
curl -fsSLO https://remote-app.elc-gateway.com/download/agent/ELC-Remote-for-Agent-0.3.15-linux-amd64
chmod +x ELC-Remote-for-Agent-0.3.5-linux-amd64
sudo mv ELC-Remote-for-Agent-0.3.15-linux-amd64 /usr/local/bin/elc-remote-agent
elc-remote-agent version
| OS | ファイル |
|---|---|
| Linux (x86_64) | ELC-Remote-for-Agent-0.3.15-linux-amd64 |
| Linux (arm64) | ELC-Remote-for-Agent-0.3.15-linux-arm64 |
| macOS (Apple Silicon) | ELC-Remote-for-Agent-0.3.15-macos-arm64 |
| macOS (Intel) | ELC-Remote-for-Agent-0.3.15-macos-amd64 |
| Windows | ELC-Remote-for-Agent-0.3.15-windows-amd64.exe |
macOS では初回に Gatekeeper が止めることがあります。xattr -d com.apple.quarantine elc-remote-agent で外してください。
1 枚撮って終わります。これが通れば経路と認証は問題ありません。
elc-remote-agent shot 460161493 232364 shot.jpg
# w_sasaki-PC に繋がりました
# shot.jpg に書きました (1920x1200, 124076 bytes)
elc-remote-agent serve
# ELC Remote for Agent 0.3.15 — http://127.0.0.1:7900
# サーバー: https://remote-app.elc-gateway.com
# トークン: Yk3s...(API に付ける Bearer トークン)
# 設定: ~/.config/elc-remote/elc-remote-agent.json
# 画面: http://127.0.0.1:7900/?token=Yk3s...
最後の行の URL をブラウザで開くと、いまどの端末に繋いでいるかが見えます。 端末ごとに ID・OS・画面の大きさ・速さ・最後の 1 枚が来てからの時間・ 同時に見ている人・取り上げられた権限が並び、そのときの画面も出ます。 その場で切ることもできます。
動いているかを確かめるのに curl を打つ必要はありません。
画面もループバックでしか開きません。別のマシンから見るときは同じように ポート転送でつなぎ、http://127.0.0.1:7900/ を開いてください。 トークンは一度入れればそのタブの間だけ覚えます(閉じれば消えます)。
トークンは ~/.config/elc-remote/elc-remote-agent.json(Windows は %AppData%\elc-remote\)に書き出されます。呼ぶ側はここから読んでください。
TOKEN=$(python3 -c 'import json;print(json.load(open("'$HOME'/.config/elc-remote/elc-remote-agent.json"))["token"])')
--addr はループバックだけです。 この API は端末を操作できるので、外から届く場所では待ち受けません。別のマシンから使うときは SSH のポート転送を使ってください。
ssh -L 7900:127.0.0.1:7900 <ELC Remote for Agent を動かしているマシン>
| 既定 | ||
|---|---|---|
--addr | 127.0.0.1:7900 | 待ち受け先(ループバックのみ) |
--server | https://remote-app.elc-gateway.com | ELC Remote サーバー |
--token | 自動生成 | API トークン |
--key | — | コントローラー鍵。付けるとパスコード無しで繋げる |
--org-token | — | 組織トークン(マルチテナントのとき) |
--name | ELC Remote for Agent@<ホスト名> | 端末側に名乗る名前 |
--state | OS の設定ディレクトリ | 設定の置き場 |
環境変数 ELC_SERVER / ELC_TOKEN / ELC_ORG_TOKEN / ELC_KEY でも指定できます。
自動で動くものにパスコードを持たせたくない場合は、鍵で入れます。
elc-remote-agent keygen
# 秘密鍵を書きました: ~/.config/elc-remote/controller-key.json
# 公開鍵:
# 1logZF+gPa0+F2FJD9L7/IwUcNU4k9VhGb8EQedhAD8=
出てきた公開鍵を、繋ぎたい端末の ELC Remote アプリで「この端末 → パスコード無しで入れる相手」に登録します。あとは --key を付けるだけで、passcode を省略できます。
elc-remote-agent serve --key ~/.config/elc-remote/controller-key.json
---
すべて Authorization: Bearer <トークン> が要ります(/healthz だけ不要)。本文と応答は JSON、画面だけ image/jpeg です。
GET | /healthz | 生きているか |
POST | /sessions | 端末に繋ぐ |
GET | /sessions | 繋いでいる一覧 |
GET | /sessions/{sid} | 状態(画面の大きさ、モニタ、同席者、権限、カーソル) |
DELETE | /sessions/{sid} | 切る |
GET | /sessions/{sid}/frame | いまの画面(JPEG) |
POST | /sessions/{sid}/still | 高画質の 1 枚。領域も指定できる |
POST | /sessions/{sid}/input | 入力を 1 つ送る |
POST | /sessions/{sid}/wait | 画面が落ち着くまで待つ |
POST | /sessions/{sid}/act | 操作して、落ち着くまで待って、結果を返す |
POST | /sessions/{sid}/monitor | モニタを選ぶ |
POST | /sessions/{sid}/quality | 画質を変える |
GET POST | /sessions/{sid}/clipboard | クリップボード |
GET | /templates | 登録したテンプレートの一覧 |
PUT DELETE | /templates/{name} | テンプレートの登録と削除 |
POST | /sessions/{sid}/templates/{name} | いまの画面の一部を切り取ってテンプレートにする |
POST | /sessions/{sid}/match | テンプレートが写っている場所を探す |
POST | /sessions/{sid}/text | 画面(または一部)の文字を読む |
POST | /sessions/{sid}/find-text | その文字が画面のどこにあるかを探す |
GET POST | /sessions/{sid}/elements | 画面の部品表(構造)を取る |
POST | /sessions/{sid}/probe | 同じ狙いを 3 段すべてに当てて、効き目を比べる |
{ "device": "460161493", "passcode": "232364",
"monitor": "all", "fps": 4, "scale": 1.0, "quality": 80 }
passcode は --key を使っているなら要りません。monitor は "all" かモニタ番号。省略すると端末の既定のままです。
返りは状態です。sid を以降で使います。
{ "sid": "01e485c9", "device": "460161493", "name": "w_sasaki-PC",
"platform": "windows", "authed": true, "transport": "relay",
"monitors": [ { "id": 0, "name": "Display 1", "w": 1920, "h": 1200, "primary": true } ],
"peers": [ { "name": "agent@srv", "id": "vMJEZMT40", "self": true, "input": true } ],
"perm": { "input": true, "clip": true, "files": true },
"cursor": { "x": 960, "y": 600, "shape": "arrow" } }
端末がオフラインなら 409、ID が無ければ 400 を返します。25 秒待たされません。
いまの画面を JPEG で返します。
| クエリ | |
|---|---|
since_seq | この番号より新しい画面だけ欲しい。同じなら 204(変化なし) |
timeout_ms | 新しい画面が来るまで待つ(長時間ポーリング)。既定 0 = 待たない |
max_age_ms | これより古ければ X-Frame-Stale: 1 を付ける(画面が動いていない印) |
応答ヘッダに X-Frame-Seq / X-Frame-Width / X-Frame-Height / X-Frame-Age-Ms が付きます。
curl -s -H "Authorization: Bearer $TOKEN" \
"http://127.0.0.1:7900/sessions/$SID/frame?timeout_ms=5000&since_seq=41" -o now.jpg
端末は画面が変わったときだけ送ってきます。 だから since_seq を付けて待つと「変わるまで待つ」になり、204 が返れば「変わっていない」と分かります。
等倍の高画質を 1 枚もらいます。w/h を付けるとその領域だけを切り出すので、文字を読みたいところだけを小さな転送量で取れます。座標は frame で返る画像の座標です。
{ "x": 0, "y": 0, "w": 600, "h": 200 }
応答ヘッダ X-Still-Region: x,y,w,h に、実際に切り出した場所が入ります。
type | 使うもの |
|---|---|
move | x, y |
click / dblclick | x, y(省略可), button(0=左 1=中 2=右) |
down / up | x, y, button |
scroll | x, y, dx, dy |
key | code(例 Enter, KeyA, F5) |
keys | codes(同時押し。順に押して逆順に離す) |
text | text(文字列をそのまま打つ。日本語も可) |
cad | Ctrl+Alt+Del(Windows のサービス常駐時のみ効きます) |
code は DOM の KeyboardEvent.code(キーの物理位置)です。端末側が自分の配列に直します。
座標の代わりに template(登録した見た目)や text(画面に出ている文字)で狙えます。 見つからなければ 404 を返すので、「押したつもりで押せていない」が起きません。
# 「保存」と書かれているところを押す
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"type":"click","text":"保存"}' \
http://127.0.0.1:7900/sessions/$SID/input
# 貼り付け(Windows なら Ctrl+V)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"type":"keys","codes":["ControlLeft","KeyV"]}' \
http://127.0.0.1:7900/sessions/$SID/input
端末側で操作を取り上げられているときは 400 を返します(「端末側のプロフィール窓で外されています」)。黙って無視はしません。
画面が落ち着くまで待ちます。
{ "settle_ms": 600, "timeout_ms": 15000, "ignore_busy": false }
{ "settled": true, "waited_ms": 1830, "frames": 4, "busy": false }
settle_ms のあいだ新しい画面が来なければ「落ち着いた」とみなします。加えて、カーソルが砂時計の間は待ち続けます(Windows のみ判別できます)。画面の差分だけで判断すると、動く広告や時計で「まだ動いている」と誤判定するためです。
sleep 30 で当てずっぽうに待つ代わりにこれを使うと、実際にかかった時間だけ待てます。
text か template を渡すと、それが出るまで(gone: true なら消えるまで)待ちます。 プログレスバーのように動き続ける画面では落ち着きを待てないので、こちらを使います。
{ "text": "保存しました", "timeout_ms": 30000 }
{ "template": "done-icon", "gone": true, "timeout_ms": 60000 }
操作して、落ち着くまで待って、結果を返します。1 往復で済むので、押す→待つ→見るを別々に呼ぶより速く、AI の推論回数も減ります。
{ "inputs": [ { "type": "click", "x": 820, "y": 460 } ],
"settle_ms": 600, "timeout_ms": 15000 }
{ "settled": true, "waited_ms": 1840, "frames": 12, "busy": false,
"frame": { "seq": 34, "w": 1920, "h": 1200, "bytes": 133168 } }
そのあと GET /frame で最新の画面を取ります。inputs は複数並べられ、各要素に delay_ms を付けると間に待ちを挟めます。
# モニタを選ぶ
curl -X POST -d '{"mode":"single","id":1}' .../monitor
curl -X POST -d '{"mode":"all"}' .../monitor
# 画質(fps は上限。変化が無ければ送られてこない)
curl -X POST -d '{"fps":2,"scale":1.0,"quality":85}' .../quality
# クリップボード(日本語を打たせるときは text より確実)
curl .../clipboard # -> {"text":"..."}
curl -X POST -d '{"text":"貼り付ける文字"}' .../clipboard
---
座標を数えて押すと、画面の配置が変わるたびに壊れます。 押したいものを名前か見た目で指すほうが、取り違えもやり直しも減ります。
指し方には精度の段があり、ここでは上から順に試して、最初に当たった段で 決めます。どの段で当てたかと、その確からしさは必ず返します。
| 段 | 何を見るか | 強いところ | 弱いところ |
|---|---|---|---|
1. 構造 struct | OS の部品表(Windows=UI Automation / macOS=AX)と CDP | 矩形がピクセルそのもの。拡大率・JPEG の劣化・配色・言語に影響されない。速い | 古いホスト、ゲーム画面、許可の無い macOS では取れない |
2. ピクセル pixel | 見本の絵との正規化相互相関 | 文字を持たないものでも指せる | 見た目が変わると外れる |
3. 意味 text | 文字認識(tesseract) | 見本を用意しなくてよい | 小さい字と和文で崩れる。遅い |
text で指すと 1 と 3 を、template で指すと 2 を使います。両方書けば 1→2→3 の順です。
実測(Chrome の配布ページ、Windows 11):
| 狙い | 構造 | 文字認識 |
|---|---|---|
ELC Remote | 0.92 / 109ms | 0.85 / 6897ms(別の箇所を指した) |
Web ビューアを開く | 0.92 / 102ms | 見つからず / 5388ms |
ダウンロード | 0.92 / 109ms | 0.93 / 6311ms(構造とのずれ 8px) |
構造が使えるなら 50〜60 倍速く、外しません。自分の現場での効き目は /probe で測ってください(下記)。
# いま画面に出ている部品を並べる
curl -s -X POST -H "$H" -H 'Content-Type: application/json' -d '{}' $API/sessions/$SID/elements
# -> {"count":60,"sources":["uia","cdp"],"took_ms":115,
# "items":[{"name":"最小化","role":"button","x":810,"y":11,"w":45,"h":39,
# "enabled":true,"src":"uia"}, ...]}
| 引数 | 既定 | |
|---|---|---|
all | false | true で見えている窓すべて。既定は前面の窓だけ |
cdp | true | ブラウザと Electron の中身も見に行く |
max | 400 | 返す数の上限 |
timeout_ms | 2500 | これを過ぎたら諦める |
src はどこから取れたか。uia(Windows)、ax(macOS)、cdp(ブラウザの中身)。
ブラウザの中身を指すには、そのブラウザがデバッグ口を開いている必要があります。 開いていなければ notes にその旨が入り、OS の部品表だけで答えます。
# Chrome / Edge / Electron を起動するとき
chrome --remote-debugging-port=9222
覗きに行く口は 9222、9223、9229 です。
curl -s -X POST -H "$H" -H 'Content-Type: application/json' -d '{"text":"ダウンロード"}' $API/sessions/$SID/probe
# -> {"layers":[
# {"layer":"struct","found":true,"x":349,"y":147,"score":0.92,"took_ms":109,
# "extra":{"matched":"ダウンロード","role":"a","src":"cdp","elements":60}},
# {"layer":"text","found":true,"x":357,"y":152,"score":0.93,"took_ms":6311}],
# "agreement":[{"a":"struct","b":"text","dx":8,"dy":5}]}
probe は止めずに全段を走らせます。通常の操作より遅いので、較正と 原因調べに使ってください。agreement は段どうしの隔たりで、小さいほど その答えの裏が取れたことになります。
input と act は、狙いで指したとき必ず答えを返します。
curl -s -X POST -H "$H" -H 'Content-Type: application/json' -d '{"type":"click","text":"保存"}' $API/sessions/$SID/input
# -> {"ok":true,"via":"struct","score":0.92,"x":148,"y":385,
# "matched":"保存","src":"uia"}
どの段も外れたときは、段ごとの理由を並べて返します。
狙いが見つかりませんでした —— 構造: 部品が 1 つも取れませんでした
(OS のアクセシビリティ: 許可がありません) / 文字: "保存" は画面に見つかりませんでした
いまの画面から切り取って、名前を付けて残せます。画像を用意する必要はありません。
# いまの画面の (540,462) から 92x30 を「保存ボタン」として覚える
curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d '{"x":540,"y":462,"w":92,"h":30}' \
$API/sessions/$SID/templates/save-button
# 探す
curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d '{"template":"save-button","threshold":0.9}' $API/sessions/$SID/match
# -> {"count":1,"matches":[{"x":540,"y":462,"w":92,"h":30,"cx":586,"cy":477,"score":1}]}
template / image | 登録した名前、または base64 の画像(その場限り) |
threshold | 0..1。既定 0.9。0.9 くらいが「同じものが写っている」の目安 |
x y w h | 探す範囲(動く時計などを避けたいとき) |
max | 返す数の上限。既定 8 |
明るさの違いには強い(正規化相互相関)ですが、回転や拡大縮小には対応しません。 同じ端末の同じ画面を探す用途なので、等倍の一致で足ります。 {"still": true} を付けて切り取ると、配信フレームではなく等倍の画面から取ります。
速さの目安は 1920x1200 の画面から 92x30 を探して 0.25 秒、120x32 で 1.0 秒です。
文字認識は外部コマンドに任せます。 単一バイナリに保つためで、 画像を標準入力で渡して標準出力を読むだけの決まりなので、tesseract でも自前のものでも挿せます。
elc-remote-agent serve --ocr "tesseract stdin stdout -l jpn+eng --psm 6 tsv"
TSV で出す設定にしてください。 語ごとの位置が取れるので、文字を指して押せます。 TSV でなければ、ただの文字列として扱います(位置は分かりません)。
# 画面の文字を読む(領域を絞れる)
curl -s -X POST -H "$H" -d '{"x":500,"y":440,"w":500,"h":120}' $API/sessions/$SID/text
# -> {"text":"Android 8.0 以降 ELC-Remote-...", "words":[...], "count":6}
# その文字がどこにあるか
curl -s -X POST -H "$H" -d '{"text":"Android"}' $API/sessions/$SID/find-text
# -> {"count":2,"matches":[{"x":545,"y":465,"w":69,"h":27,"cx":579,"cy":478,...}]}
読むときは既定で等倍に取り直します。配信フレームは縮小されていて文字がつぶれるためです。 返す座標は配信フレームの画素にそろえてあるので、そのまま input の座標に使えます ({"still": false} で取り直しを止められます。速いかわりに読み落とします)。
文字認識を設定していなければ 501 と設定方法を返します。
TOKEN=$(python3 -c 'import json;print(json.load(open("'$HOME'/.config/elc-remote/elc-remote-agent.json"))["token"])')
API=http://127.0.0.1:7900
H="Authorization: Bearer $TOKEN"
SID=$(curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d '{"device":"460161493","passcode":"232364","fps":4}' $API/sessions \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["sid"])')
# 1. 見る
curl -s -H "$H" "$API/sessions/$SID/frame?timeout_ms=5000" -o screen.jpg
# 2. 押して、落ち着くまで待つ
curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d '{"inputs":[{"type":"click","x":820,"y":460}],"settle_ms":600}' \
$API/sessions/$SID/act
# 3. 文字を読みたいところだけ等倍で取る
curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d '{"x":600,"y":300,"w":700,"h":240}' \
$API/sessions/$SID/still -o label.jpg
curl -s -X DELETE -H "$H" $API/sessions/$SID
peers に自分以外が入っていれば、人が同じ画面を見ています。
curl -s -H "$H" $API/sessions/$SID | python3 -c '
import json,sys
d = json.load(sys.stdin)
others = [p["name"] for p in d["peers"] if not p["self"]]
print("人が見ています:", others) if others else print("自分だけです")'
0600)にあります。これを持っている人は端末を操作できます。--name で名乗った名前が出ます。自分が誰か分かる名前にしてください。sessions として記録が残ります。小さな文字や装飾の強い文字は読み落とします。確実に押したいものはテンプレートのほうが堅いです。