C# の SDK(../dotnet/SyncSing.Plugin.Sdk)・Python の雛形(../templates/python-basic/plugin.py)は、ここに書いたことを実装しています。別の言語で作るときの決まりです。
1. 起動と接続
- アプリが拡張機能の EXE を起動します(作業フォルダ = manifest.json のあるフォルダ・ウィンドウなし・優先度は「通常より下」)。
- 標準入力の 1 行目 に接続情報(JSON)が届き、そのあと標準入力は閉じます。
{"syncsing": {"protocol": 1, "pipe": "\\\\.\\pipe\\LinkStudio.SyncSing.studio.12345.9b1c…", "transport": "pipe", "token": "…", "plugin_uuid": "…", "app": "studio", "app_version": "0.28.0", "channel": "stable", "api_version": "1.0", "locale": "ja", "data_dir": "C:\\…\\data", "log_dir": "C:\\…\\logs"}}data_dir・log_dirは環境変数SYNCSING_PLUGIN_DATA_DIR・SYNCSING_PLUGIN_LOG_DIRにも入っています。
- 10 秒以内 に
pipe(名前付きパイプ)を開き(C# ならNamedPipeClientStream、ほかの言語ならファイルとして\\.\pipe\…を読み書き)、最初の 1 通としてhelloを送ります。 - パイプは今のユーザーだけが開け、アプリは接続してきたプロセスが自分の起動した EXE(またはその子プロセス)かを確かめます。
2. メッセージ
- UTF-8 の JSON を 1 行に 1 つ(改行区切り)。1 メッセージ 1 MiB まで(超えるとアプリは接続を切ります。SDK も送る前に断ります)。
- 引数と戻り値の形は
../schemas/api-1.1.json(JSON Schema)。アプリの窓口は呼ばれるたびに、知らない引数・必須の引数・型を確かめます。 - 要求:
{"id": 正の整数, "method": "mic.set", "params": {…}} - 返事(成功):
{"id": 同じ, "result": {…}, "meta": {"ms": 12}} - 返事(失敗):
{"id": 同じ, "error": {"code": "busy", "message": "…(アプリの言語・画面に出せる文)", "retryable": true, "data": {…}}} - イベント(返事は要らない):
{"event": "chat.message", "seq": 1042, "ts": "2026-10-16T09:12:33+09:00", "data": {…}} - ホストからの要求は
idが"h:"で始まる文字列です。必ず同じidで返事をしてください(pingは 10 秒以内。答えないと固まった扱いで再起動されます)。
hello
{"id": 0, "method": "hello", "params": {"token": "(標準入力で受け取ったもの)",
"plugin": {"uuid": "manifest の uuid", "version": "manifest の version"},
"api": {"version": "1.0"}, "sdk": {"name": "my-sdk", "version": "0.1.0"}}}返事の result: session・api_version・app {kind, version, channel, locale, features}・plugin {tier, name}・granted(許可された権限)・denied・settings・dirs {data, log}・limits・ping_interval_ms。 トークン・uuid・version・API の版が違うと error を返して切断し、再起動しません。
ホストからの要求
| メソッド | いつ | 返事 |
|---|---|---|
ping | 15 秒ごと | {} |
plugin.activate | hello の直後 | {"ok": true} |
plugin.shutdown | 停止・アプリの終了・無効化・更新(reason) | {"ok": true} を返して 5 秒以内に終了 |
command.invoke | パネルのボタン・設定の button・Stream Deck・ミニUI・ショートカット(command・source・pressed。hold: true のコマンドは押したとき down・離したとき up) | {"ok": true} か {"ok": false, "message": "…"} |
tool.run | ツール枠の「実行」(tool) | {"ok": true} |
settings.changed | 利用者が設定を変えた(settings・changed) | {} |
上の 6 つ(API 1.0 の要求)には必ず返事をしてください(command.invoke で失敗を伝えるときも {"ok": false, "message": …} の result で)。 command.invoke・tool.run は すぐ返事をしてください。時間のかかる処理(通信・計測・ファイルの書き出しなど)は、先に panel.update で「〜しています…」を出してから裏(別スレッド)で行い、終わったらもう一度 panel.update します。アプリは押したボタンを返事が来るまで「実行中…」にし、返事のあとすぐパネルを取り直します(tool.run は 10 秒で打ち切り)。 読み取り(返事を受け取る)スレッドの中で、アプリへの要求の返事を待たないでください。待つとその返事を読めず、必ず時間切れになって「押したのに何も起きない」になります。イベント(event)の処理も読み取りスレッドではなく別のスレッドで、届いた順に行ってください(1.2 の SDK はどちらも守ります)。 押した結果の伝え方(右下の通知): 返事が {"ok": false, "message": "…"} ならその文が出ます。{"ok": true, "message": "…"} なら成功の文としてその文が出ます。message を省いた {"ok": true} のときは、そのあと 8 秒のあいだにパネルの先頭の status 部品が変わればその文が出ます(warn / error は ⚠ 付き)。つまり「すぐ返事をして裏で確かめる」作りでも、裏で断った理由(「処理が止まっています」など)を先頭の status に出せば利用者に届きます。2 秒たっても状態が変わらなければ「実行しました」と出ます。 経過時間・残り時間(計測中のカウントなど)は timer 部品で出してください(elapsed_s・total_s・running)。画面は 2 秒ごとに取り直すので、秒数を文字として送り続けると飛び飛びに見えます。timer は画面が 1 秒ごとに自分で進めます。 panel.update を別々のスレッドから同時に送ると、届く順番が入れ替わって古い画面が残ることがあります。最後に作った内容だけを 1 本のスレッドで順番に送ってください(1.2 の SDK の Panel(...).Send / client.panel(...).send)。 押したのに画面が変わらないときの調べ方: アプリの app.log に command.invoke 〇〇 → ok(N ms) と、プラグインの plugin.log(plugins\<uuid>\logs\)に受け取った記録が残ります。アプリ側が ok でプラグイン側に記録が無ければ要求の処理、記録はあるが画面が変わらなければ panel.update(断られた理由も plugin.log に出ます)を疑ってください。 {"error": {"code": "not_supported"}} を返してよいのは、この版に無い 知らない要求 だけです(syncsing_plugin.py test も、6 つへの error は失敗として扱います)。
3. エラーコード
| code | 意味 | もう一度試してよいか |
|---|---|---|
parse_error | JSON として読めない(切断します) | × |
invalid_request | id・method・params の形が違う | × |
method_not_found | この版に無いメソッド | × |
invalid_params | 引数の型・範囲・長さが違う(data.field) | × |
unauthorized | hello の前・トークンの不一致・改ざん | × |
permission_denied | 権限が無い(data.permission) | × |
rate_limited | 回数の上限(data.retry_after_ms) | ○ |
busy | 今は受け付けない(処理中のデバイス変更・配信中・録音中など。data.reason) | ○ |
locked | 利用者の設定で禁止(キー変更ロック・規約に未同意など) | × |
not_available | この構成に無い(NC Lite で使えない機能・OBS 未接続など) | × |
not_found | 指定したものが無い | × |
user_declined | 利用者が断った | × |
timeout | 時間内に終わらなかった | ○ |
internal | その他 | ○ |
api_version_unsupported | API の版が合わない(hello だけ) | × |
4. 回数と量の上限
- 呼び出し 20 回/秒・同時 4 本まで(超えると
rate_limited)。 notify.show・notify.proposeは 5 秒に 1 件、tts.speakは 3 秒に 1 件、overlay.set_itemsは 1 秒に 10 回。- イベントは 1 本あたり 500 件まで溜め、読まないと古い順に捨てて
events.droppedを 1 通入れます。metersは最新だけ。5 秒以上まったく読まないと切断されます。 log.writeは 1 行 4 KiB・100 行/秒まで。
5. プロセスの決まり
- メモリはプラグインごとに既定 1 GiB(manifest の
resources.max_memory_mb、最大 2 GiB)、子プロセスを含めて 8 個まで。アプリが終了すると拡張機能も終了します。 - 落ちたとき・固まったときは 1 秒 → 5 秒 → 30 秒で再起動し、10 分に 3 回を超えると止めて利用者に知らせます。
- 管理者権限を要求する EXE は起動しません。
- 書いてよいのは
data_dir・log_dirの中だけです(約束。アプリは止められないので、審査と署名の取り消しで守ります)。 - 標準出力・標準エラーは
logs\plugin.logに書かれます(5 MB で回します)。
6. API の版
- 版は
メジャー.マイナー(今は1.1。1.1 は声まわり・Studio 0.30.5 / NC Lite 1.6.5 から)。hello のapi.versionには manifest のapi_versionを書きます(1.0の拡張機能は 1.1 のアプリでもそのまま動きます)。manifest のapi_versionのメジャーがアプリと違うと読み込まれません。マイナーがアプリより新しいときも読み込まれません(「アプリの更新が必要です」)。 - マイナー版で増えるのは、メソッド・任意の引数・戻り値の項目・イベントの 追加だけ です。
7. 音専用のパイプ(API 1.1)
audio.open の返事の pipe を、命令用のパイプと同じように開きます(その拡張機能のプロセスだけが開けます。10 秒以内に開かないと閉じます)。JSON ではなく、次の 塊 を続けて流します。どちら向きも同じ形です。
| 位置 | 大きさ | 中身 |
|---|---|---|
| 0 | 1 バイト | 種類: 1 = アプリ → 拡張機能(マイクの声)、2 = 拡張機能 → アプリ(乗せる音) |
| 1 | 1 バイト | 速さ: 1 = 16000 Hz、2 = 24000 Hz、3 = 48000 Hz |
| 2 | 2 バイト | 中身のバイト数(偶数・100 ms ぶんまで) |
| 4 | 4 バイト | 順番(1 ずつ増える) |
| 8 | 8 バイト | 時刻(µs。アプリの音の時計。拡張機能から送るときは 0 でよい) |
| 16 | 中身のバイト数 | 符号付き 16 ビット・モノラル PCM |
- 数はすべてリトルエンディアン。マイクの声は 20 ms ごとに 1 塊(16 kHz なら 640 バイト)。
- 頭の形が崩れた塊が来ると、アプリはパイプを切ります(もう一度
audio.openから)。 - 受け取りが遅いときは、アプリが古い塊から捨てます(アプリの音は止めません)。捨てた数は
audio.stats。