diff --git a/bin/ocx.mjs b/bin/ocx.mjs index dbf63b4ae2f..ee7e0494b6e 100755 --- a/bin/ocx.mjs +++ b/bin/ocx.mjs @@ -447,6 +447,9 @@ function runPackageManagerSelfUpdate(manager) { } const env = mutationChildEnvironment(); delete env.OCX_SERVICE; + // The restarted proxy is an ordinary owner; only a sibling's own replacement carries this. + delete env.OCX_SIBLING_OF_PORT; + delete env.OCX_SIBLING_HANDOFF_NONCE; console.log(`Attempting to restart the proxy on port ${bakePort}.`); const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(bakePort)], { detached: true, diff --git a/docs-site/src/content/docs/fr/guides/remote-link.md b/docs-site/src/content/docs/fr/guides/remote-link.md index 6c99cde0d98..4fe3cc878bc 100644 --- a/docs-site/src/content/docs/fr/guides/remote-link.md +++ b/docs-site/src/content/docs/fr/guides/remote-link.md @@ -9,20 +9,19 @@ Une liaison entre machines connecte un ordinateur OpenCodex **Home** à un ordin - Home peut se connecter à Child avec une clé OpenSSH. - Pour une liaison initiée par Child, Child peut se connecter à Home avec une clé OpenSSH (la connexion par mot de passe n’est pas prise en charge). -- OpenCodex est installé sur Child. +- OpenCodex 2.66.0 ou ultérieur est installé sur Child (et sur Home pour une liaison initiée par Child). - Les deux ordinateurs utilisent macOS ou Linux. -- Le tableau de bord Home dispose d’une session appairée complète. +- La liaison se lance depuis Home : son tableau de bord est ouvert sur l’ordinateur Home lui-même (navigateur ou application de bureau, installation autonome) ou via une session Hub appairée. -SSH par mot de passe et Windows restent hors du flux actuel. Pour démarrer une liaison depuis Child, ouvrez le tableau de bord du Child autonome, choisissez **Enfant** → **Trouver le Home**, sélectionnez l’hôte SSH de Home, vérifiez puis confirmez l’empreinte de la clé hôte, et choisissez **Connecter comme Enfant**. Child doit pouvoir se connecter à Home avec une clé SSH (les mots de passe ne sont pas pris en charge), et `ocx` doit être en cours d’exécution sur Home. Le port du tunnel client est `1024` ou supérieur. Après la jonction, Child redémarre et se connecte via Home. Cette option est disponible uniquement en mode autonome. +SSH par mot de passe et Windows restent hors du flux actuel. Connecter un ordinateur comme Child depuis le tableau de bord (liaison initiée par Child) n’est pas disponible dans cette version : la jonction redémarre OpenCodex sur cet ordinateur, ce qui couperait les connexions Codex déjà ouvertes ; le tableau de bord affiche donc le rôle **Enfant** comme indisponible. La liaison initiée par Home est la voie prise en charge : sur l’ordinateur qui doit servir de Home, choisissez **Home** et ajoutez l’autre ordinateur comme Child, comme décrit ci-dessous. ## Ajouter un Child depuis `#remote` 1. Ouvrez le tableau de bord sur `#remote` et activez Remote Link. -2. Choisissez **Home**. -3. Sélectionnez **Add child**. -4. Choisissez un hôte parmi les candidats SSH, ou saisissez un alias de configuration SSH. -5. Lancez le test de connexion et comparez l’empreinte proposée avec celle de l’ordinateur visé. Cette comparaison aide à détecter un mauvais hôte ou une clé d’hôte modifiée avant que SSH ne lui fasse confiance. -6. Confirmez l’empreinte, puis connectez Child. +2. Choisissez **Home**, puis **Continue**. La liste des hôtes SSH s’ouvre. +3. Choisissez un hôte parmi les candidats SSH, ou saisissez un alias de configuration SSH. +4. Lancez le test de connexion et comparez l’empreinte proposée avec celle de l’ordinateur visé. Cette comparaison aide à détecter un mauvais hôte ou une clé d’hôte modifiée avant que SSH ne lui fasse confiance. +5. Confirmez l’empreinte, puis connectez Child. Le tableau de bord ne demande pas de saisir un jeton. Il sonde d’abord l’hôte et ne peut appliquer la liaison qu’après votre confirmation explicite de l’empreinte. diff --git a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md index 7a1b6cfedbb..59d53766063 100644 --- a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md @@ -15,7 +15,7 @@ Assistant de configuration interactif (`setup` est un alias de `init`). Il deman ### `ocx start [--port ] [--socks5 [host:port] | --socks5-off]` -Démarre le serveur proxy, de préférence sur le port `10100`. La commande écrit l’état du PID et du port d’exécution, et refuse de démarrer une deuxième instance active. Lorsque le port préféré est occupé, `start` interroge le processus qui l’occupe puis s’arrête dans tous les cas : elle refuse de démarrer si un processus opencodex y répond et signale sinon que le processus est inconnu. Elle ne déplace jamais l’écouteur vers un autre port d’elle-même, car cela laisserait le premier proxy en cours d’exécution et redirigerait Codex vers le second. Un autre `--port` explicite est également refusé avec le même `OPENCODEX_HOME`, car les modes d’observation et de plafond écrivent tous deux dans le même journal de dépenses. Utilisez un `OPENCODEX_HOME` distinct pour une instance sœur indépendante ; `port: 0` ne sépare que l’attribution du port, pas l’état. Au démarrage, elle synchronise dans le catalogue Codex les modèles de chaque fournisseur. À l’arrêt, elle rétablit le fonctionnement natif de Codex, sauf si le proxy a été lancé comme service géré (`OCX_SERVICE=1`). +Démarre le serveur proxy, de préférence sur le port `10100`. La commande écrit l’état du PID et du port d’exécution, et refuse de démarrer une deuxième instance active. Lorsque le port préféré est occupé, `start` interroge le processus qui l’occupe puis s’arrête dans tous les cas : elle refuse de démarrer si un processus opencodex y répond et signale sinon que le processus est inconnu. Elle ne déplace jamais l’écouteur vers un autre port d’elle-même, car cela laisserait le premier proxy en cours d’exécution et redirigerait Codex vers le second. Un autre `--port` explicite est également refusé avec le même `OPENCODEX_HOME`, car les modes d’observation et de plafond écrivent tous deux dans le même journal de dépenses. Utilisez un `OPENCODEX_HOME` distinct pour une instance sœur indépendante ; `port: 0` ne sépare que l’attribution du port, pas l’état. Au démarrage, elle synchronise dans le catalogue Codex les modèles de chaque fournisseur. À l’arrêt, elle rétablit le fonctionnement natif de Codex, sauf si le proxy a été lancé comme service géré (`OCX_SERVICE=1`). Une instance sœur démarrée à côté d’un proxy déjà actif ne fait ni l’un ni l’autre, même lorsqu’elle est arrêtée avec `ocx stop` ou par un signal : elle ne sert que les requêtes directes sur son propre port, et Codex, Grok et Claude restent dirigés vers le proxy qui tournait déjà. `--socks5` (par défaut `127.0.0.1:10808`) enregistre l’URL SOCKS5 dans `config.proxy` et achemine les requêtes HTTP(S) sortantes dans un véritable tunnel SOCKS5. `--socks5-off` supprime uniquement diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index fa02f36a8db..46329eff947 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -284,8 +284,10 @@ desktop and the wrong one in two common cases: you need a different browser prof identity, a second account), or the dashboard is open against a proxy running somewhere else. Every login surface shows the authorization URL with a copy button, the device code when the -provider issues one, and a field to paste the redirect URL or authorization code back. So you can -always finish a login by hand. +provider issues one, and the current instructions. Browser callback flows also show a field to +paste the redirect URL or authorization code back. During device approval that field is hidden: +enter the displayed code on the provider's verification page instead. If the provider switches +to manual input, the dashboard replaces the old code and instructions on its next status poll. To stop the proxy from opening a browser at all, tick **Don't open a browser on the proxy machine** beside the login button, or set it permanently: @@ -302,7 +304,7 @@ Two cases behave differently, and it is worth knowing which you are in: - **A different browser profile on the same machine** works with the copied link alone. The loopback callback on `127.0.0.1` still completes the flow. -- **A browser on a different machine** also needs the paste fallback, because the redirect URI is +- **A browser callback flow on a different machine** also needs the paste fallback, because the redirect URI is still `http://127.0.0.1:/callback` on the proxy's host. Finish the login there, then paste the redirect URL (or just the code) back into the dashboard or `ocx account code`. @@ -774,6 +776,9 @@ including add-account and reauthentication. A raw admin token or forged GUI head `403 oauth_consent_required` before a credential is read or a grant starts. This gate uses the server-resolved session principal, not a separately recorded warning-checkbox receipt. Direct `ocx login meta-muse` and other OAuth providers keep their existing login policies. +The management OAuth provider list therefore omits Meta Muse for raw-admin-token dashboards; +open a session-authenticated dashboard to use that login flow. This changes discovery only, +not the admission checks on login start or manual continuation. Both seeded `meta-muse` models expose `minimal`/`low`/`medium`/`high`/`xhigh`/`max` to routed clients, including Grok's effort picker. Requests use diff --git a/docs-site/src/content/docs/guides/remote-link.md b/docs-site/src/content/docs/guides/remote-link.md index 7219f1210ed..6481d520dc0 100644 --- a/docs-site/src/content/docs/guides/remote-link.md +++ b/docs-site/src/content/docs/guides/remote-link.md @@ -9,20 +9,19 @@ A machine link connects an OpenCodex **Home** computer to a **Child** computer o - The Home computer can log in to the Child with an OpenSSH key. - For a Child-initiated link, the Child can log in to Home with an OpenSSH key (password login is not supported). -- OpenCodex is installed on the Child computer. +- OpenCodex 2.66.0 or later is installed on the Child computer, and on Home for a Child-initiated link. - Both computers run macOS or Linux. -- The Home dashboard has a full paired session. +- Links are started from the Home: its dashboard is opened on the Home computer itself (browser or desktop app, standalone install) or through a paired Hub session. -Password SSH and Windows are outside the current flow. For a Child-initiated link, open the standalone Child dashboard, choose **Child** → **Find Home**, select the SSH host for Home, check and confirm the host-key fingerprint, then choose **Connect as Child**. The Child must be able to log in to Home with an SSH key (password login is not supported), and `ocx` must be running on Home. The client tunnel port is `1024` or higher. After joining, the Child restarts and connects through Home. This option is available only on a standalone runtime. +Password SSH and Windows are outside the current flow. Connecting a computer as a Child from the dashboard (a Child-initiated link) is not available in this release: joining restarts OpenCodex on that computer, which would drop the Codex connections already running there, so the dashboard shows the **Child** role as unavailable. Home-initiated linking is the supported path: on the computer that should be Home, choose **Home** and add the other computer as a Child, as described below. ## Add a Child from `#remote` 1. Open the dashboard at `#remote` and switch Remote Link on. -2. Choose **Home**. -3. Select **Add child**. -4. Choose a host from the SSH candidates, or enter an SSH config alias. -5. Run the connection test and compare the offered host fingerprint with the fingerprint for the machine you intend to use. Comparing it helps detect a wrong host or a changed host key before SSH trusts the host. -6. Confirm the fingerprint, then connect the Child. +2. Choose **Home**, then **Continue**. The SSH host list opens. +3. Choose a host from the SSH candidates, or enter an SSH config alias. +4. Run the connection test and compare the offered host fingerprint with the fingerprint for the machine you intend to use. Comparing it helps detect a wrong host or a changed host key before SSH trusts the host. +5. Confirm the fingerprint, then connect the Child. The dashboard does not ask you to enter a token. It probes the host first, and it cannot apply the link until you explicitly confirm the fingerprint. @@ -46,6 +45,15 @@ ocx disconnect To disconnect a Child-initiated link, run `ocx disconnect` on the Child. It disconnects the client tunnel and revokes the link on Home over SSH. If Home revocation fails, it prints: `Home revoke failed; run ocx link revoke --link-id on the home.` +## Troubleshooting + +When a step fails, the dashboard shows the reason and, when SSH reported one, the last line of its error output under the message. + +- **Could not connect to the SSH host**: the host must accept your SSH key without a password prompt; `ssh -o BatchMode=yes true` must succeed from a terminal. A `ProxyCommand` helper such as `cloudflared` must be installed in `/opt/homebrew/bin`, `/usr/local/bin`, `~/.bun/bin`, `~/.local/bin` or another directory on the PATH OpenCodex runs with. +- **ocx was not found on the remote computer**: OpenCodex looks for `ocx` on the PATH of a non-interactive SSH session first, then in `~/.bun/bin`, `~/.local/bin`, `/opt/homebrew/bin` and `/usr/local/bin`. If it is installed elsewhere, add that directory to PATH in a file the remote shell reads for non-interactive sessions, such as `~/.zshenv` for zsh. +- **OpenCodex on the remote computer is too old**: run `ocx update` on that computer. Remote Link needs 2.66.0 or later. +- **The remote computer did not report an OpenCodex version**: `ocx --version` on that computer printed something else, for example the usage text of an unsupported Windows install. + ## Security The Child uses the Home computer's providers and provider credentials through the link. The Home creates a separate link key for each Child; removing the link revokes that key. Compare the host fingerprint before confirmation so a wrong machine or changed host key is not accepted by mistake. Dashboard sessions issued from a Tailscale identity cannot manage machine links. diff --git a/docs-site/src/content/docs/ja/guides/remote-link.md b/docs-site/src/content/docs/ja/guides/remote-link.md index 0a959bb31b1..da3c499e3e0 100644 --- a/docs-site/src/content/docs/ja/guides/remote-link.md +++ b/docs-site/src/content/docs/ja/guides/remote-link.md @@ -9,20 +9,19 @@ description: SSH で OpenCodex の Home コンピューターと Child コンピ - Home から Child に OpenSSH キーでログインできること。 - Child から開始するリンクでは、Child から Home に OpenSSH キーでログインできる必要があります(パスワードログインには対応していません)。 -- Child に OpenCodex がインストールされていること。 +- Child に OpenCodex 2.66.0 以降がインストールされていること(Child から開始するリンクでは Home にも)。 - 両方のコンピューターが macOS または Linux であること。 -- Home のダッシュボードに完全なペアリング済みセッションがあること。 +- リンクは Home 側から開始すること。使うダッシュボードは、Home のコンピューター上で直接開いたもの(スタンドアロン環境のブラウザーまたはデスクトップアプリ)か、ペアリング済みの Hub セッションです。 -パスワード SSH と Windows は現在のフローに含まれません。Child からリンクを開始するには、スタンドアロンの Child ダッシュボードで **子** → **Home を探す** を選び、Home の SSH ホストを選択し、ホストキーのフィンガープリントを確認してから **子として接続** を選びます。Child から Home へ SSH キーでログインできる必要があり(パスワードログインには対応していません)、Home では `ocx` が実行中である必要があります。クライアントトンネルのポートは `1024` 以上です。参加後、Child は再起動して Home に接続します。この項目はスタンドアロンランタイムでのみ使用できます。 +パスワード SSH と Windows は現在のフローに含まれません。このリリースでは、ダッシュボードからコンピューターを Child として接続すること(Child から開始するリンク)はできません。参加するとそのコンピューターの OpenCodex が再起動し、すでに動いている Codex 接続が切断されるため、ダッシュボードでは **子** の役割を選択できません。サポートされているのは Home から開始するリンクです。Home にするコンピューターで **Home** を選び、下記の手順でもう一方のコンピューターを Child として追加してください。 ## `#remote` から Child を追加する 1. ダッシュボードで `#remote` を開き、Remote Link をオンにします。 -2. **Home** を選びます。 -3. **Add child** を選びます。 -4. SSH の候補からホストを選ぶか、SSH 設定のエイリアスを入力します。 -5. 接続テストを実行し、表示されたホストフィンガープリントを接続先コンピューターのものと比較します。比較すると、SSH がホストを信頼する前に、別のコンピューターや変更されたホストキーを検出できます。 -6. フィンガープリントを確認して Child を接続します。 +2. **Home** を選び、**Continue** を押します。SSH ホストの一覧が開きます。 +3. SSH の候補からホストを選ぶか、SSH 設定のエイリアスを入力します。 +4. 接続テストを実行し、表示されたホストフィンガープリントを接続先コンピューターのものと比較します。比較すると、SSH がホストを信頼する前に、別のコンピューターや変更されたホストキーを検出できます。 +5. フィンガープリントを確認して Child を接続します。 ダッシュボードはトークンの入力を求めません。先にホストをプローブし、フィンガープリントを明示的に確認するまでリンクを適用しません。 diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md index b6c5fe4a7db..c56f93e2d4b 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -15,7 +15,7 @@ description: セットアップ、開始、停止、サービス、診断、同 ### `ocx start [--port ] [--socks5 [host:port] | --socks5-off]` -プロキシ サーバー (優先ポート `10100`) を起動します。PID/ランタイムポートの状態を書き込み、2 番目のライブインスタンスの起動を拒否します。優先ポートが使用中の場合、`start` はそのポートを使用しているプロセスを確認して、どちらの場合も停止します。opencodex が応答していれば起動を拒否し、それ以外は使用しているプロセスを特定できないと報告します。最初のプロキシを実行したまま Codex を 2 番目のプロキシへ向けることになるため、自動でリスナーを別のポートへ移すことはありません。同じ `OPENCODEX_HOME` では別の `--port` を明示しても拒否されます。監視のみの構成も上限を適用する構成も同じ支出ジャーナルへ書き込むためです。独立した sibling には別の `OPENCODEX_HOME` を使用してください。`port: 0` はポートだけを OS に割り当てさせ、状態を分離しません。開始時に、各プロバイダーのモデルを Codex のカタログに同期します。マネージド サービス (`OCX_SERVICE=1`) として起動されていない限り、シャットダウン時にネイティブ Codex が復元されます。 +プロキシ サーバー (優先ポート `10100`) を起動します。PID/ランタイムポートの状態を書き込み、2 番目のライブインスタンスの起動を拒否します。優先ポートが使用中の場合、`start` はそのポートを使用しているプロセスを確認して、どちらの場合も停止します。opencodex が応答していれば起動を拒否し、それ以外は使用しているプロセスを特定できないと報告します。最初のプロキシを実行したまま Codex を 2 番目のプロキシへ向けることになるため、自動でリスナーを別のポートへ移すことはありません。同じ `OPENCODEX_HOME` では別の `--port` を明示しても拒否されます。監視のみの構成も上限を適用する構成も同じ支出ジャーナルへ書き込むためです。独立した sibling には別の `OPENCODEX_HOME` を使用してください。`port: 0` はポートだけを OS に割り当てさせ、状態を分離しません。開始時に、各プロバイダーのモデルを Codex のカタログに同期します。マネージド サービス (`OCX_SERVICE=1`) として起動されていない限り、シャットダウン時にネイティブ Codex が復元されます。既に稼働中のプロキシの横で起動した sibling は、`ocx stop` やシグナルで停止した場合も含めてそのどちらも行わず、自身のポートで直接のリクエストを処理するだけで、Codex、Grok、Claude は既に稼働していたプロキシを指したままになります。 `--socks5`(デフォルト `127.0.0.1:10808`)は SOCKS5 URL を `config.proxy` に保存し、送信 HTTP(S) リクエストを実際の SOCKS5 トンネル経由で送信します。`--socks5-off` は保存された SOCKS5 プロキシだけを削除し、HTTP プロキシは削除しません。値は設定に保存されるため、`ocx update` 後も保持されます。URL にユーザー名とパスワードを含めることはできますが、起動ログでは非表示になります。 diff --git a/docs-site/src/content/docs/ko/guides/remote-link.md b/docs-site/src/content/docs/ko/guides/remote-link.md index 8ef2559b0e7..457b95e8cfa 100644 --- a/docs-site/src/content/docs/ko/guides/remote-link.md +++ b/docs-site/src/content/docs/ko/guides/remote-link.md @@ -9,20 +9,19 @@ description: SSH로 OpenCodex Home 컴퓨터와 Child 컴퓨터를 연결합니 - Home 컴퓨터에서 OpenSSH 키 로그인으로 Child 컴퓨터에 접속할 수 있어야 합니다. - 자식이 시작하는 링크에서는 자식에서 OpenSSH 키 로그인으로 홈에 접속할 수 있어야 합니다(비밀번호 로그인은 지원하지 않음). -- Child 컴퓨터에 OpenCodex가 설치되어 있어야 합니다. +- Child 컴퓨터에 OpenCodex 2.66.0 이상이 설치되어 있어야 합니다(자식이 시작하는 링크에서는 Home에도). - 두 컴퓨터 모두 macOS 또는 Linux여야 합니다. -- Home 대시보드에 완전한 페어링 세션이 있어야 합니다. +- 링크는 홈에서 시작합니다. 홈 컴퓨터에서 직접 연 대시보드(독립형 설치의 브라우저 또는 데스크톱 앱)나 페어링된 Hub 세션을 사용해야 합니다. -비밀번호 SSH와 Windows는 현재 흐름에서 지원하지 않습니다. 자식이 연결을 시작하려면 독립형 런타임으로 실행 중인 자식의 대시보드에서 **자식** → **홈 찾기**를 선택하고, 홈(Home)으로 사용할 SSH 호스트를 고른 다음 호스트 키 지문을 확인하고 **자식으로 연결**을 누릅니다. 자식에서 홈으로 SSH 키 로그인을 할 수 있어야 하며(비밀번호 로그인은 지원하지 않음), 홈에서 `ocx`가 실행 중이어야 합니다. 클라이언트 터널 포트는 `1024` 이상이어야 합니다. 연결이 완료되면 자식이 재시작되고 홈에 연결됩니다. 이 메뉴는 독립형 런타임에서만 사용할 수 있습니다. +비밀번호 SSH와 Windows는 현재 흐름에서 지원하지 않습니다. 이번 릴리스에서는 대시보드에서 컴퓨터를 자식으로 연결하는 방식(자식이 시작하는 링크)을 쓸 수 없습니다. 자식으로 참여하면 그 컴퓨터의 OpenCodex가 다시 시작되어 이미 쓰고 있는 Codex 연결이 끊어지기 때문에, 대시보드에서는 **자식** 역할을 선택할 수 없습니다. 지원되는 방법은 홈에서 시작하는 링크입니다. 홈이 될 컴퓨터에서 **Home**을 선택하고, 아래 순서대로 다른 컴퓨터를 자식으로 추가하세요. ## `#remote`에서 Child 추가하기 1. 대시보드에서 `#remote`를 열고 Remote Link를 켭니다. -2. **Home**을 선택합니다. -3. **Add child**를 선택합니다. -4. SSH 후보에서 호스트를 선택하거나 SSH 설정의 alias를 입력합니다. -5. 연결 테스트를 실행하고 표시된 호스트 지문을 연결하려는 컴퓨터의 지문과 비교합니다. 비교하면 SSH가 호스트를 신뢰하기 전에 잘못된 컴퓨터나 변경된 호스트 키를 발견할 수 있습니다. -6. 지문을 확인한 뒤 Child를 연결합니다. +2. **Home**을 선택한 뒤 **Continue**를 누릅니다. SSH 호스트 목록이 열립니다. +3. SSH 후보에서 호스트를 선택하거나 SSH 설정의 alias를 입력합니다. +4. 연결 테스트를 실행하고 표시된 호스트 지문을 연결하려는 컴퓨터의 지문과 비교합니다. 비교하면 SSH가 호스트를 신뢰하기 전에 잘못된 컴퓨터나 변경된 호스트 키를 발견할 수 있습니다. +5. 지문을 확인한 뒤 Child를 연결합니다. 대시보드는 토큰 입력을 요구하지 않습니다. 먼저 호스트를 검사하며, 지문을 명시적으로 확인하기 전에는 링크를 적용하지 않습니다. diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index b9ec56615fb..74e4abca98d 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -26,7 +26,9 @@ Codex 자동 시작 shim도 설치합니다. 시작을 거부합니다. 관찰 전용과 제한 적용 모드 모두 같은 지출 저널에 기록하기 때문입니다. 독립된 형제 인스턴스에는 별도의 `OPENCODEX_HOME`을 사용하세요. `port: 0`은 포트만 OS에 맡기며 상태를 분리하지 않습니다. 시작할 때는 각 공급자의 모델을 Codex 카탈로그로 동기화합니다. 종료할 때는 -기본 Codex를 복원합니다. 단, 관리형 서비스로 실행한 경우(`OCX_SERVICE=1`)는 예외입니다. +기본 Codex를 복원합니다. 단, 관리형 서비스로 실행한 경우(`OCX_SERVICE=1`)는 예외입니다. 이미 실행 중인 +프록시 옆에서 시작한 형제 인스턴스는 `ocx stop`이나 시그널로 멈출 때도 동기화와 복원을 하지 않고 자신의 +포트에서 직접 요청만 처리하며, Codex, Grok, Claude는 원래 실행 중이던 프록시를 계속 가리킵니다. `--socks5`(기본값 `127.0.0.1:10808`)는 SOCKS5 URL을 `config.proxy`에 저장하고 실제 SOCKS5 터널을 통해 송신 HTTP(S) 요청을 전달합니다. `--socks5-off`는 저장된 SOCKS5 프록시만 지우며 diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index 21dbee3153a..6f0505b09db 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -27,7 +27,10 @@ would leave the first proxy running and re-point Codex at the second. An explici and enforced spend accounting both write the same journal. Use a separate `OPENCODEX_HOME` for an independent sibling; `port: 0` only asks the OS for that instance's port and does not separate its state. On start it syncs each provider's models into Codex's catalog. On shutdown it restores -native Codex — unless it was launched as a managed service (`OCX_SERVICE=1`). +native Codex — unless it was launched as a managed service (`OCX_SERVICE=1`). A sibling started +beside a running proxy does neither: it serves direct requests on its own port only, and Codex, +Grok and Claude stay pointed at the proxy that was already running. Stopping that sibling, with +`ocx stop` or a signal, leaves their configuration alone as well. `--socks5` (default `127.0.0.1:10808`) saves `config.proxy` as a SOCKS5 URL and routes outbound HTTP(S) through a real SOCKS5 tunnel. `--socks5-off` clears only that saved SOCKS5 proxy; it diff --git a/docs-site/src/content/docs/ru/guides/remote-link.md b/docs-site/src/content/docs/ru/guides/remote-link.md index 1db5c571256..44906145861 100644 --- a/docs-site/src/content/docs/ru/guides/remote-link.md +++ b/docs-site/src/content/docs/ru/guides/remote-link.md @@ -9,20 +9,19 @@ description: Подключите компьютер OpenCodex Home к комп - Home может войти на Child по ключу OpenSSH. - Для связи, инициированной со стороны Child, Child должен входить на Home по ключу OpenSSH (вход по паролю не поддерживается). -- На Child установлен OpenCodex. +- На Child установлен OpenCodex 2.66.0 или новее (для связи со стороны Child — и на Home). - Оба компьютера работают под macOS или Linux. -- В панели Home есть полноценная сопряжённая сессия. +- Связь начинают со стороны Home: панель открыта на самом компьютере Home (браузер или настольное приложение в автономной установке) или через сопряжённую сессию Hub. -SSH с паролем и Windows сейчас не поддерживаются. Чтобы начать связь со стороны Child, откройте панель автономного Child, выберите **Дочерний** → **Найти Home**, укажите SSH-хост Home, проверьте и подтвердите отпечаток ключа хоста, затем выберите **Подключить как Child**. Child должен входить на Home по ключу SSH (вход по паролю не поддерживается), а на Home должен работать `ocx`. Порт клиентского туннеля должен быть `1024` или выше. После подключения Child перезапускается и подключается через Home. Этот пункт доступен только в автономном режиме. +SSH с паролем и Windows сейчас не поддерживаются. В этой версии подключить компьютер как Child из панели (связь со стороны Child) нельзя: подключение перезапускает OpenCodex на этом компьютере, из-за чего оборвутся уже работающие подключения Codex, поэтому в панели роль **Дочерний** недоступна. Поддерживаемый путь — связь со стороны Home: на компьютере, который должен стать Home, выберите **Home** и добавьте другой компьютер как Child, как описано ниже. ## Добавление Child из `#remote` 1. Откройте `#remote` в панели и включите Remote Link. -2. Выберите **Home**. -3. Выберите **Add child**. -4. Выберите хост среди кандидатов SSH или введите псевдоним из конфигурации SSH. -5. Запустите проверку соединения и сравните показанный отпечаток хоста с отпечатком нужного компьютера. Сравнение помогает обнаружить неправильный компьютер или изменённый ключ хоста до того, как SSH начнёт ему доверять. -6. Подтвердите отпечаток и подключите Child. +2. Выберите **Home**, затем **Continue**. Откроется список SSH-хостов. +3. Выберите хост среди кандидатов SSH или введите псевдоним из конфигурации SSH. +4. Запустите проверку соединения и сравните показанный отпечаток хоста с отпечатком нужного компьютера. Сравнение помогает обнаружить неправильный компьютер или изменённый ключ хоста до того, как SSH начнёт ему доверять. +5. Подтвердите отпечаток и подключите Child. Панель не просит вводить токен. Сначала выполняется проверка хоста, и применить связь можно только после явного подтверждения отпечатка. diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index 3b7caf84a68..77b62ddc79b 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -30,7 +30,10 @@ PID/runtime-port, а попытка поднять второй живой эк Для независимого соседнего экземпляра используйте отдельный `OPENCODEX_HOME`; `port: 0` поручает ОС выбрать только порт и не разделяет состояние. На старте прокси синхронизирует модели каждого провайдера в каталог Codex. При shutdown он восстанавливает native Codex — если только прокси не -был запущен как managed service (`OCX_SERVICE=1`). +был запущен как managed service (`OCX_SERVICE=1`). Соседний экземпляр, запущенный рядом с уже работающим +прокси, не делает ни того, ни другого, в том числе при остановке через `ocx stop` или сигналом: он +обслуживает только прямые запросы на своём порту, а Codex, Grok и Claude остаются направленными на +прокси, который уже работал. `--socks5` (по умолчанию `127.0.0.1:10808`) сохраняет SOCKS5 URL в `config.proxy` и направляет исходящие HTTP(S)-запросы через настоящий SOCKS5-туннель. `--socks5-off` удаляет только сохранённый diff --git a/docs-site/src/content/docs/tr/guides/remote-link.md b/docs-site/src/content/docs/tr/guides/remote-link.md index a4b85cb2277..764ee6cd3bd 100644 --- a/docs-site/src/content/docs/tr/guides/remote-link.md +++ b/docs-site/src/content/docs/tr/guides/remote-link.md @@ -9,20 +9,19 @@ Makine bağlantısı, bir OpenCodex **Home** bilgisayarını bir **Child** bilgi - Home bilgisayarı, Child bilgisayarına OpenSSH anahtarıyla giriş yapabilir. - Child tarafından başlatılan bağlantı için Child, Home bilgisayarına OpenSSH anahtarıyla giriş yapabilmelidir (parola girişi desteklenmez). -- Child bilgisayarında OpenCodex kuruludur. +- Child bilgisayarında OpenCodex 2.66.0 veya sonrası kuruludur (Child tarafından başlatılan bağlantıda Home üzerinde de). - Her iki bilgisayar da macOS veya Linux çalıştırır. -- Home kontrol panelinde tam bir eşleştirilmiş oturum vardır. +- Bağlantı Home tarafından başlatılır: kontrol paneli Home bilgisayarının kendisinde (bağımsız kurulumda tarayıcı veya masaüstü uygulaması) ya da eşleştirilmiş bir Hub oturumu üzerinden açılır. -Parolalı SSH ve Windows mevcut akışın dışındadır. Child üzerinden bağlantı başlatmak için bağımsız çalışan Child kontrol panelinde **Çocuk** → **Home'u bul** seçeneklerini izleyin, Home için SSH ana bilgisayarını seçin, ana bilgisayar anahtarı parmak izini kontrol edip onaylayın ve ardından **Çocuk olarak bağlan** seçeneğini seçin. Child, Home bilgisayarına SSH anahtarıyla giriş yapabilmelidir (parola girişi desteklenmez) ve Home üzerinde `ocx` çalışıyor olmalıdır. İstemci tüneli portu `1024` veya daha yüksek olmalıdır. Katılma işleminden sonra Child yeniden başlar ve Home bilgisayarına bağlanır. Bu seçenek yalnızca standalone çalışma zamanında kullanılabilir. +Parolalı SSH ve Windows mevcut akışın dışındadır. Bu sürümde bir bilgisayarı kontrol panelinden Child olarak bağlamak (Child tarafından başlatılan bağlantı) kullanılamaz: katılmak o bilgisayardaki OpenCodex'i yeniden başlatır ve çalışan Codex bağlantılarını keser; bu yüzden kontrol panelinde **Çocuk** rolü kullanılamaz. Desteklenen yol, Home tarafından başlatılan bağlantıdır: Home olacak bilgisayarda **Home** seçeneğini seçin ve diğer bilgisayarı aşağıda anlatıldığı gibi Child olarak ekleyin. ## `#remote` üzerinden Child ekleme 1. Kontrol panelinde `#remote` sayfasını açın ve Remote Link'i açın. -2. **Home** seçeneğini seçin. -3. **Add child** seçeneğini seçin. -4. SSH adaylarından bir ana bilgisayar seçin veya SSH yapılandırmasındaki diğer adı girin. -5. Bağlantı testini çalıştırın ve gösterilen ana bilgisayar parmak izini bağlanmak istediğiniz bilgisayarın parmak iziyle karşılaştırın. Karşılaştırma, SSH ana bilgisayara güvenmeden önce yanlış bilgisayarı veya değişmiş anahtarını fark etmenize yardımcı olur. -6. Parmak izini onaylayın, ardından Child'ı bağlayın. +2. **Home** seçeneğini seçin, ardından **Continue** düğmesine basın. SSH ana bilgisayar listesi açılır. +3. SSH adaylarından bir ana bilgisayar seçin veya SSH yapılandırmasındaki diğer adı girin. +4. Bağlantı testini çalıştırın ve gösterilen ana bilgisayar parmak izini bağlanmak istediğiniz bilgisayarın parmak iziyle karşılaştırın. Karşılaştırma, SSH ana bilgisayara güvenmeden önce yanlış bilgisayarı veya değişmiş anahtarını fark etmenize yardımcı olur. +5. Parmak izini onaylayın, ardından Child'ı bağlayın. Kontrol paneli belirteç girmenizi istemez. Önce ana bilgisayarı yoklar ve parmak izini açıkça onaylamadan bağlantıyı uygulamaz. diff --git a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md index d2c4a437743..700b7da51c4 100644 --- a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md @@ -32,7 +32,9 @@ yalnızca gözlem ve sınır uygulama kiplerinin ikisi de aynı harcama günlü bir kardeş örnek için ayrı bir `OPENCODEX_HOME` kullanın. `port: 0` yalnızca port seçimini işletim sistemine bırakır, durumu ayırmaz. Başlangıçta her sağlayıcının modellerini Codex'in kataloğuna senkronize eder. Kapatıldığında — yönetilen bir servis olarak başlatılmadığı sürece -(`OCX_SERVICE=1`) — yerel Codex'i geri yükler. +(`OCX_SERVICE=1`) — yerel Codex'i geri yükler. Çalışan bir proxy'nin yanında başlatılan kardeş örnek, +`ocx stop` ya da bir sinyalle durdurulduğunda da dahil ikisini de yapmaz: yalnızca kendi portundaki +doğrudan istekleri karşılar ve Codex, Grok ile Claude zaten çalışmakta olan proxy'yi göstermeye devam eder. `--socks5` (varsayılan `127.0.0.1:10808`) SOCKS5 URL'sini `config.proxy` içine kaydeder ve giden HTTP(S) isteklerini gerçek bir SOCKS5 tünelinden yönlendirir. `--socks5-off` yalnızca kaydedilmiş diff --git a/docs-site/src/content/docs/zh-cn/guides/remote-link.md b/docs-site/src/content/docs/zh-cn/guides/remote-link.md index be53fe62bcd..f72177fa2c7 100644 --- a/docs-site/src/content/docs/zh-cn/guides/remote-link.md +++ b/docs-site/src/content/docs/zh-cn/guides/remote-link.md @@ -9,20 +9,19 @@ description: 通过 SSH 将 OpenCodex 主机与子机连接起来。 - 主机可以使用 OpenSSH 密钥登录子机。 - 对于由子机发起的链接,子机必须能使用 OpenSSH 密钥登录主机(不支持密码登录)。 -- 子机已安装 OpenCodex。 +- 子机已安装 OpenCodex 2.66.0 或更高版本(由子机发起的链接还要求主机也满足)。 - 两台电脑运行 macOS 或 Linux。 -- 主机控制台拥有完整的已配对会话。 +- 链接从 Home 一侧发起:控制台需在 Home 电脑本机打开(独立安装的浏览器或桌面应用),或通过已配对的 Hub 会话打开。 -密码 SSH 和 Windows 不在当前流程中。要从子机发起连接,请在独立运行的子机控制台中选择 **子设备** → **查找 Home**,选择 Home 的 SSH 主机,检查并确认主机密钥指纹,然后选择 **以子设备身份连接**。子机必须能使用 SSH 密钥登录 Home(不支持密码登录),并且 Home 上正在运行 `ocx`。客户端隧道端口必须为 `1024` 或更高。加入后,子机会重启并连接到 Home。此入口仅在 standalone 运行时提供。 +密码 SSH 和 Windows 不在当前流程中。此版本不支持从控制台把电脑连接为子机(即由子机发起的链接):加入会重新启动这台电脑上的 OpenCodex,已在运行的 Codex 连接会因此中断,因此控制台中的 **子设备** 角色不可选。受支持的方式是由 Home 发起链接:在要作为 Home 的电脑上选择 **Home**,再按下文步骤把另一台电脑添加为子机。 ## 从 `#remote` 添加子机 1. 打开控制台的 `#remote`,开启 Remote Link。 -2. 选择 **Home**。 -3. 选择 **Add child**。 -4. 从 SSH 候选主机中选择主机,或输入 SSH 配置别名。 -5. 运行连接测试,并将显示的主机指纹与目标电脑的指纹进行比较。比较指纹可以在 SSH 信任主机前发现错误的电脑或已更换的主机密钥。 -6. 确认指纹,然后连接子机。 +2. 选择 **Home**,然后点击 **Continue**。SSH 主机列表会打开。 +3. 从 SSH 候选主机中选择主机,或输入 SSH 配置别名。 +4. 运行连接测试,并将显示的主机指纹与目标电脑的指纹进行比较。比较指纹可以在 SSH 信任主机前发现错误的电脑或已更换的主机密钥。 +5. 确认指纹,然后连接子机。 控制台不会要求输入令牌。它会先探测主机,只有明确确认指纹后才能应用链接。 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index c5caee6abb1..6f4ab51582d 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -15,7 +15,7 @@ description: 安装、启动、停止、服务、诊断、同步和更新命令 ### `ocx start [--port ] [--socks5 [host:port] | --socks5-off]` -启动代理服务器(首选端口 `10100`)。它会写入 PID/运行时端口状态,并拒绝启动第二个存活实例。当首选端口已被占用时,`start` 会探测占用者,并且无论结果如何都会停止:如果那里响应的是 opencodex,它会直接拒绝启动;否则会报告无法识别的占用者。它绝不会自行把监听地址移到其他端口,因为那会让第一个代理继续运行,并将 Codex 重新指向第二个代理。即使显式指定不同的 `--port`,共用同一个 `OPENCODEX_HOME` 时也会拒绝启动,因为仅观察模式和启用上限的模式都会写入同一个支出日志。独立的同级实例必须使用单独的 `OPENCODEX_HOME`;`port: 0` 只让操作系统分配端口,并不会隔离状态。启动时,它会把每个提供方的模型同步到 Codex 的目录中。关闭时,它会恢复原生 Codex,除非它是作为受管服务启动的(`OCX_SERVICE=1`)。 +启动代理服务器(首选端口 `10100`)。它会写入 PID/运行时端口状态,并拒绝启动第二个存活实例。当首选端口已被占用时,`start` 会探测占用者,并且无论结果如何都会停止:如果那里响应的是 opencodex,它会直接拒绝启动;否则会报告无法识别的占用者。它绝不会自行把监听地址移到其他端口,因为那会让第一个代理继续运行,并将 Codex 重新指向第二个代理。即使显式指定不同的 `--port`,共用同一个 `OPENCODEX_HOME` 时也会拒绝启动,因为仅观察模式和启用上限的模式都会写入同一个支出日志。独立的同级实例必须使用单独的 `OPENCODEX_HOME`;`port: 0` 只让操作系统分配端口,并不会隔离状态。启动时,它会把每个提供方的模型同步到 Codex 的目录中。关闭时,它会恢复原生 Codex,除非它是作为受管服务启动的(`OCX_SERVICE=1`)。在已运行的代理旁启动的同级实例两者都不做,即使通过 `ocx stop` 或信号停止也是如此:它只在自己的端口上处理直接请求,Codex、Grok 和 Claude 仍指向原本已在运行的代理。 `--socks5`(默认 `127.0.0.1:10808`)会将 SOCKS5 URL 保存到 `config.proxy`,并通过真正的 SOCKS5 隧道转发出站 HTTP(S) 请求。`--socks5-off` 只会清除已保存的 SOCKS5 代理,不会删除 HTTP 代理。该值保存在配置中,因此会在 `ocx update` 后保留。URL 可以包含用户名和密码,但启动日志会将其隐藏。 diff --git a/docs-site/src/content/docs/zh-tw/guides/remote-link.md b/docs-site/src/content/docs/zh-tw/guides/remote-link.md index b8b6b5c51f7..611c366f271 100644 --- a/docs-site/src/content/docs/zh-tw/guides/remote-link.md +++ b/docs-site/src/content/docs/zh-tw/guides/remote-link.md @@ -9,20 +9,19 @@ description: 透過 SSH 連接 OpenCodex Home 電腦與 Child 電腦。 - Home 可以使用 OpenSSH 金鑰登入 Child。 - 對於由 Child 發起的連結,Child 必須能使用 OpenSSH 金鑰登入 Home(不支援密碼登入)。 -- Child 已安裝 OpenCodex。 +- Child 已安裝 OpenCodex 2.66.0 或更新版本(由 Child 發起的連結也要求 Home 符合)。 - 兩台電腦執行 macOS 或 Linux。 -- Home 儀表板擁有完整的已配對工作階段。 +- 連結由 Home 端發起:儀表板需在 Home 電腦本機開啟(獨立安裝的瀏覽器或桌面應用程式),或透過已配對的 Hub 工作階段開啟。 -密碼 SSH 和 Windows 不在目前流程中。若要從 Child 發起連線,請在獨立執行的 Child 儀表板中選擇 **子裝置** → **尋找 Home**,選取 Home 的 SSH 主機,檢查並確認主機金鑰指紋,然後選擇 **以子裝置身分連線**。Child 必須能使用 SSH 金鑰登入 Home(不支援密碼登入),而且 Home 上正在執行 `ocx`。用戶端通道連接埠必須是 `1024` 或更高。加入後,Child 會重新啟動並連線到 Home。這個入口只在 standalone 執行個體中提供。 +密碼 SSH 和 Windows 不在目前流程中。此版本不支援從儀表板將電腦連線為 Child(即由 Child 發起的連結):加入會重新啟動這台電腦上的 OpenCodex,已在執行的 Codex 連線會因此中斷,因此儀表板中的 **子裝置** 角色無法選取。受支援的方式是由 Home 發起連結:在要作為 Home 的電腦上選擇 **Home**,再依下方步驟將另一台電腦新增為 Child。 ## 從 `#remote` 新增 Child 1. 開啟儀表板的 `#remote`,開啟 Remote Link。 -2. 選擇 **Home**。 -3. 選擇 **Add child**。 -4. 從 SSH 候選主機選擇主機,或輸入 SSH 設定別名。 -5. 執行連線測試,並將顯示的主機指紋與目標電腦的指紋比較。比較指紋可在 SSH 信任主機前發現錯誤的電腦或已變更的主機金鑰。 -6. 確認指紋,然後連接 Child。 +2. 選擇 **Home**,然後按 **Continue**。SSH 主機列表會開啟。 +3. 從 SSH 候選主機選擇主機,或輸入 SSH 設定別名。 +4. 執行連線測試,並將顯示的主機指紋與目標電腦的指紋比較。比較指紋可在 SSH 信任主機前發現錯誤的電腦或已變更的主機金鑰。 +5. 確認指紋,然後連接 Child。 儀表板不會要求輸入權杖。它會先探測主機,只有明確確認指紋後才能套用連結。 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md index 16899542c02..6bd593004ae 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md @@ -15,7 +15,7 @@ description: 安裝、啟動、停止、服務、診斷、同步與更新指令 ### `ocx start [--port ] [--socks5 [host:port] | --socks5-off]` -啟動代理伺服器(偏好連接埠 `10100`)。它寫入 PID/runtime-port 狀態,並拒絕啟動第二個即時實例。偏好連接埠被佔用時,`start` 會探測佔用者,且無論結果如何都會停止:若回應的是 opencodex,它會直接拒絕啟動;否則會回報無法識別的佔用者。它絕不會自行將監聽位置移到其他連接埠,因為這會讓第一個代理繼續執行,並將 Codex 重新指向第二個代理。即使明確指定不同的 `--port`,共用同一個 `OPENCODEX_HOME` 時仍會拒絕啟動,因為僅觀察模式和啟用上限的模式都會寫入同一份支出日誌。獨立的同層實例必須使用不同的 `OPENCODEX_HOME`;`port: 0` 只讓作業系統指派連接埠,不會隔離狀態。啟動時它將每個供應商的模型同步到 Codex 目錄。關閉時它還原原生 Codex——除非它是作為受管服務啟動的(`OCX_SERVICE=1`)。 +啟動代理伺服器(偏好連接埠 `10100`)。它寫入 PID/runtime-port 狀態,並拒絕啟動第二個即時實例。偏好連接埠被佔用時,`start` 會探測佔用者,且無論結果如何都會停止:若回應的是 opencodex,它會直接拒絕啟動;否則會回報無法識別的佔用者。它絕不會自行將監聽位置移到其他連接埠,因為這會讓第一個代理繼續執行,並將 Codex 重新指向第二個代理。即使明確指定不同的 `--port`,共用同一個 `OPENCODEX_HOME` 時仍會拒絕啟動,因為僅觀察模式和啟用上限的模式都會寫入同一份支出日誌。獨立的同層實例必須使用不同的 `OPENCODEX_HOME`;`port: 0` 只讓作業系統指派連接埠,不會隔離狀態。啟動時它將每個供應商的模型同步到 Codex 目錄。關閉時它還原原生 Codex——除非它是作為受管服務啟動的(`OCX_SERVICE=1`)。在已執行的代理旁啟動的同層實例兩者皆不做,即使透過 `ocx stop` 或訊號停止也一樣:它只在自己的連接埠上處理直接請求,Codex、Grok 和 Claude 仍指向原本已在執行的代理。 `--socks5`(預設 `127.0.0.1:10808`)會將 SOCKS5 URL 儲存到 `config.proxy`,並透過真正的 SOCKS5 通道轉送對外 HTTP(S) 請求。`--socks5-off` 只會清除已儲存的 SOCKS5 代理,不會刪除 HTTP 代理。此值儲存在設定中,因此會在 `ocx update` 後保留。URL 可以包含使用者名稱和密碼,但啟動記錄會隱藏它們。 diff --git a/gui/src/App.tsx b/gui/src/App.tsx index 0d28feeaa21..ef9f1a2b245 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -20,7 +20,7 @@ import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconH import { useI18n, useT, LOCALES, localeDisplayName, type Locale, type TKey } from "./i18n/shared"; import { Notice, Select, ToastNotice, type NoticeTone } from "./ui"; import { configureApiTargets, hasApiSession, installApiAuthFetch, installApiSessionFromHtml, logoutApiSession, SESSION_UNAVAILABLE_EVENT } from "./api"; -import { apiBaseForPlane, discoverApiTargets, isConnectedRuntime, standaloneApiTargets, type ApiTargets } from "./api-targets"; +import { adminTokenPromptAllowed, apiBaseForPlane, discoverApiTargets, isConnectedRuntime, runtimeRoleFromDocument, standaloneApiTargets, type ApiTargets } from "./api-targets"; import { ConnectPairingForm } from "./connect-pairing"; import { type Page } from "./app-routing"; import { readModelsTab, type ModelsTab } from "./pages/models-tab"; @@ -203,6 +203,12 @@ export default function App() { return () => controller.abort(); }, [page, sharedSessionReady, sharedBase]); const remoteWorkspaceAvailable = sharedSessionReady && remoteWorkspaceAvailableState; + // A standalone/hub dashboard exposed through an authenticated non-loopback origin can need a + // consent-bearing GUI session even though it is not a connected client. Remote Link requires + // that stronger principal, so offer the existing one-time pairing flow instead of a dead-end + // "sign in" warning. Other pages keep their ordinary admin-token flow unchanged. + const remotePairingRequired = page === "remote" && !sharedSessionReady + && runtimeRoleFromDocument() === "hub" && adminTokenPromptAllowed(); // Narrow screens: the sidebar becomes an off-canvas drawer behind a hamburger toggle. const [navOpen, setNavOpen] = useState(false); @@ -552,7 +558,7 @@ export default function App() { {targetError && (
{t("connection.machineUnavailable")}
)} - {targets.connected && !sharedSessionReady && ( + {((targets.connected && !sharedSessionReady) || remotePairingRequired) && ( { setSharedSessionReady(true); setSharedSessionEpoch(epoch => epoch + 1); @@ -567,7 +573,7 @@ export default function App() { {page === "logs" && } {page === "usage" && } {page === "storage" && } - {page === "remote" && navigateToPage("remote-workspace")} />} + {page === "remote" && !remotePairingRequired && navigateToPage("remote-workspace")} />} {page === "remote-workspace" && navigateToPage("remote")} />} {page === "codex-set" && } {page === "integrations" && } diff --git a/gui/src/components/login-url-block.tsx b/gui/src/components/login-url-block.tsx index 6f9d30f85cb..a56227954d0 100644 --- a/gui/src/components/login-url-block.tsx +++ b/gui/src/components/login-url-block.tsx @@ -86,7 +86,8 @@ export type LoginHintPaste = { * * Order is deliberate: the device code first because it is the short thing a * human has to type, then the URL, then any provider prose, then the paste - * fallback for when the browser cannot reach the loopback callback. + * fallback for when the browser cannot reach the loopback callback. Device + * grants poll for approval instead: their human code is not a callback code. */ export function LoginHint({ hint, paste }: { hint: LoginHintData; paste?: LoginHintPaste }) { const t = useT(); @@ -119,7 +120,7 @@ export function LoginHint({ hint, paste }: { hint: LoginHintData; paste?: LoginH )} {hint.instructions &&
{hint.instructions}
} - {paste && ( + {paste && !deviceCode && (
{t("prov.pasteRedirectHint")}
diff --git a/gui/src/components/use-add-provider-oauth.ts b/gui/src/components/use-add-provider-oauth.ts index b28fa4473c4..185e7f8ea55 100644 --- a/gui/src/components/use-add-provider-oauth.ts +++ b/gui/src/components/use-add-provider-oauth.ts @@ -3,6 +3,7 @@ import type { TFn } from "../i18n/shared"; import { readJsonIfOk } from "../fetch-json"; import { openBrowserRequestField } from "../oauth-open-browser-pref"; import { afterOAuthCancellation, cancelOAuthLogin } from "../oauth-cancellation-barrier"; +import type { LoginHintData } from "./login-url-block"; export const OAUTH_LOGIN_POLL_INTERVAL_MS = 2_000; @@ -123,7 +124,7 @@ export function useAddProviderOAuth({ await new Promise(r => setTimeout(r, OAUTH_LOGIN_POLL_INTERVAL_MS)); if (!aliveRef.current || !isCurrent()) return; const sRes = await fetch(`${apiBase}/api/oauth/status?provider=${providerId}`).catch(() => null); - const s = sRes ? await readJsonIfOk<{ loggedIn?: boolean; error?: string }>(sRes) : null; + const s = sRes ? await readJsonIfOk<{ loggedIn?: boolean; error?: string; hint?: LoginHintData }>(sRes) : null; if (!aliveRef.current || !isCurrent()) return; if (s?.error) { activeProvidersRef.current.delete(providerId); @@ -133,9 +134,16 @@ export function useAddProviderOAuth({ } if (s?.loggedIn) { activeProvidersRef.current.delete(providerId); + setOauthMsg(""); onAdded(providerId); return; } + if (s?.hint) { + setOauthUrl(s.hint.url ?? "", providerId, s.hint.deviceCode, s.hint.instructions); + setOauthMsg(s.hint.url || s.hint.deviceCode + ? t("modal.waitingLogin") + : (s.hint.instructions || t("modal.loggingIn"))); + } } await cancelServerLogin(providerId); if (!aliveRef.current || !isCurrent()) return; @@ -150,7 +158,10 @@ export function useAddProviderOAuth({ setOauthMsg(t("modal.networkError")); } } finally { - if (aliveRef.current && isCurrent()) setOauthBusy(false); + if (aliveRef.current && isCurrent()) { + setOauthBusy(false); + setOauthUrl("", providerId); + } } }, [aliveRef, apiBase, bumpLoginGeneration, cancelServerLogin, onAdded, t]); diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 2be9e6b24f8..34266a80d56 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -3323,6 +3323,7 @@ export const de: Record = { "link.close": "Schließen", "link.cancel": "Abbrechen", "remoteLink.childDisabled": "Kind-Verbindungen können nur von einer eigenständigen Laufzeit gestartet werden.", + "remoteLink.childJoinUnavailable": "Diesen Computer über das Dashboard als Kind zu verbinden ist in dieser Version nicht verfügbar: Dabei wird OpenCodex neu gestartet, und bestehende Codex-Verbindungen würden abbrechen. Starten Sie die Verbindung stattdessen vom Home aus: Wählen Sie auf dem Computer, der Home sein soll, „Zuhause“ und fügen Sie den anderen Computer als Kind hinzu.", "remoteLink.findHome.title": "Home suchen", "remoteLink.findHome.body": "Wählen Sie den Home-Computer für dieses Kind aus.", "remoteLink.findHome.action": "Home suchen", @@ -3354,8 +3355,8 @@ export const de: Record = { "remoteLink.direction.client": "Vom Client gestartet", "remoteLink.error.admission_timeout": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", "remoteLink.error.compensation_failed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", - "remoteLink.error.fingerprint_failed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", - "remoteLink.error.forbidden": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", + "remoteLink.error.fingerprint_failed": "Der SSH-Hostschlüssel konnte nicht gelesen werden. Testen Sie die Verbindung erneut.", + "remoteLink.error.forbidden": "Diese Dashboard-Sitzung kann keine Remote-Links verwalten. Öffnen Sie das Dashboard auf diesem Computer (eigenständige Installation) oder verwenden Sie eine gekoppelte Hub-Sitzung.", "remoteLink.error.host_confirmation_expired": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", "remoteLink.error.host_fingerprint_mismatch": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", "remoteLink.error.host_not_confirmed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", @@ -3368,14 +3369,17 @@ export const de: Record = { "remoteLink.error.link_exists": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", "remoteLink.error.link_not_found": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", "remoteLink.error.link_remove_failed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", - "remoteLink.error.link_unavailable": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", - "remoteLink.error.listener_unavailable": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", - "remoteLink.error.probe_failed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", - "remoteLink.error.remote_connect_failed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", + "remoteLink.error.link_unavailable": "Remote Link ist in dieser Laufzeit nicht verfügbar. Starten Sie OpenCodex neu und versuchen Sie es erneut.", + "remoteLink.error.listener_unavailable": "Der Remote-Link-Listener konnte auf diesem Computer nicht starten. Prüfen Sie, ob ein Portkonflikt besteht, und versuchen Sie es erneut.", + "remoteLink.error.probe_failed": "Keine Verbindung zum SSH-Host möglich. Prüfen Sie, ob er Ihren SSH-Schlüssel akzeptiert und ob ein benötigter ProxyCommand-Helfer installiert ist.", + "remoteLink.error.remote_connect_failed": "Der entfernte Computer konnte sich nicht mit diesem Home verbinden. Prüfen Sie dort den OpenCodex-Dienst und versuchen Sie es erneut.", "remoteLink.error.remote_disconnect_failed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", - "remoteLink.error.remote_port_failed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", + "remoteLink.error.remote_ocx_missing": "ocx wurde auf dem entfernten Computer nicht gefunden. Installieren Sie dort OpenCodex oder stellen Sie sicher, dass ocx im PATH nicht interaktiver SSH-Sitzungen liegt.", + "remoteLink.error.remote_ocx_outdated": "OpenCodex auf dem entfernten Computer ist für Remote Link zu alt. Führen Sie dort ocx update aus (2.66.0 oder neuer) und versuchen Sie es erneut.", + "remoteLink.error.remote_ocx_unrecognized": "Der entfernte Computer hat keine OpenCodex-Version gemeldet. Remote Link benötigt OpenCodex 2.66.0 oder neuer unter macOS oder Linux.", + "remoteLink.error.remote_port_failed": "Der entfernte Computer konnte seinen Link-Port nicht melden. Prüfen Sie, ob dort OpenCodex 2.66.0 oder neuer läuft, und versuchen Sie es erneut.", "remoteLink.error.tailscale_session_refused": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", - "remoteLink.error.version_probe_failed": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", + "remoteLink.error.version_probe_failed": "ocx konnte auf dem entfernten Computer nicht über SSH ausgeführt werden. Prüfen Sie den SSH-Zugang und versuchen Sie es erneut.", "remoteLink.error.generic": "Die Remote-Link-Anfrage konnte nicht abgeschlossen werden.", "remoteLink.reason.auth": "Die Authentifizierung ist fehlgeschlagen.", "remoteLink.reason.hostkey": "Der Hostschlüssel konnte nicht verifiziert werden.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 6e614cd9d45..ad00e8ad053 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -3357,6 +3357,7 @@ export const en = { "link.close": "Close", "link.cancel": "Cancel", "remoteLink.childDisabled": "Child links can only be started from a standalone runtime.", + "remoteLink.childJoinUnavailable": "Connecting this computer as a Child from the dashboard is not available in this release: joining restarts OpenCodex and would drop existing Codex connections. Start the link from the Home instead: on the computer that should be Home, choose Home and add the other computer as a Child.", "remoteLink.findHome.title": "Find Home", "remoteLink.findHome.body": "Choose the Home computer to connect this Child to.", "remoteLink.findHome.action": "Find Home", @@ -3388,8 +3389,8 @@ export const en = { "remoteLink.direction.client": "Client initiated", "remoteLink.error.admission_timeout": "Remote link request could not be completed.", "remoteLink.error.compensation_failed": "Remote link request could not be completed.", - "remoteLink.error.fingerprint_failed": "Remote link request could not be completed.", - "remoteLink.error.forbidden": "Remote link request could not be completed.", + "remoteLink.error.fingerprint_failed": "The SSH host key could not be read. Test the connection again.", + "remoteLink.error.forbidden": "This dashboard session cannot manage remote links. Open the dashboard on this computer (standalone install), or use a paired Hub session.", "remoteLink.error.host_confirmation_expired": "Remote link request could not be completed.", "remoteLink.error.host_fingerprint_mismatch": "Remote link request could not be completed.", "remoteLink.error.host_not_confirmed": "Remote link request could not be completed.", @@ -3402,14 +3403,17 @@ export const en = { "remoteLink.error.link_exists": "Remote link request could not be completed.", "remoteLink.error.link_not_found": "Remote link request could not be completed.", "remoteLink.error.link_remove_failed": "Remote link request could not be completed.", - "remoteLink.error.link_unavailable": "Remote link request could not be completed.", - "remoteLink.error.listener_unavailable": "Remote link request could not be completed.", - "remoteLink.error.probe_failed": "Remote link request could not be completed.", - "remoteLink.error.remote_connect_failed": "Remote link request could not be completed.", + "remoteLink.error.link_unavailable": "Remote Link is not available in this runtime. Restart OpenCodex and retry.", + "remoteLink.error.listener_unavailable": "The Remote Link listener could not start on this computer. Check for a port conflict and retry.", + "remoteLink.error.probe_failed": "Could not connect to the SSH host. Check that it accepts your SSH key and that any ProxyCommand helper is installed.", + "remoteLink.error.remote_connect_failed": "The remote computer could not connect to this Home. Check its OpenCodex service and retry.", "remoteLink.error.remote_disconnect_failed": "Remote link request could not be completed.", - "remoteLink.error.remote_port_failed": "Remote link request could not be completed.", + "remoteLink.error.remote_ocx_missing": "ocx was not found on the remote computer. Install OpenCodex there, or make sure ocx is on the PATH of non-interactive SSH sessions.", + "remoteLink.error.remote_ocx_outdated": "OpenCodex on the remote computer is too old for Remote Link. Run ocx update there (2.66.0 or later), then retry.", + "remoteLink.error.remote_ocx_unrecognized": "The remote computer did not report an OpenCodex version. Remote Link needs OpenCodex 2.66.0 or later on macOS or Linux.", + "remoteLink.error.remote_port_failed": "The remote computer could not report its link port. Check that OpenCodex 2.66.0 or later is running there and retry.", "remoteLink.error.tailscale_session_refused": "Remote link request could not be completed.", - "remoteLink.error.version_probe_failed": "Remote link request could not be completed.", + "remoteLink.error.version_probe_failed": "Could not run ocx on the remote computer over SSH. Check SSH access and retry.", "remoteLink.error.generic": "Remote link request could not be completed.", "remoteLink.reason.auth": "Authentication failed.", "remoteLink.reason.hostkey": "The host key could not be verified.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 8b1012a1797..e8068e096f4 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -3312,6 +3312,7 @@ export const fr: Record = { "link.close": "Fermer", "link.cancel": "Annuler", "remoteLink.childDisabled": "Les liens Enfant ne peuvent être lancés que depuis un runtime autonome.", + "remoteLink.childJoinUnavailable": "Connecter cet ordinateur comme Enfant depuis le tableau de bord n’est pas disponible dans cette version : la connexion redémarre OpenCodex et couperait les connexions Codex existantes. Lancez plutôt la liaison depuis le Home : sur l’ordinateur qui doit être le Home, choisissez « Accueil », puis ajoutez l’autre ordinateur comme Enfant.", "remoteLink.findHome.title": "Trouver le Home", "remoteLink.findHome.body": "Choisissez l’ordinateur Home auquel connecter cet Enfant.", "remoteLink.findHome.action": "Trouver le Home", @@ -3343,8 +3344,8 @@ export const fr: Record = { "remoteLink.direction.client": "Lancé par le client", "remoteLink.error.admission_timeout": "La demande de lien distant n’a pas pu aboutir.", "remoteLink.error.compensation_failed": "La demande de lien distant n’a pas pu aboutir.", - "remoteLink.error.fingerprint_failed": "La demande de lien distant n’a pas pu aboutir.", - "remoteLink.error.forbidden": "La demande de lien distant n’a pas pu aboutir.", + "remoteLink.error.fingerprint_failed": "La clé d’hôte SSH n’a pas pu être lue. Relancez le test de connexion.", + "remoteLink.error.forbidden": "Cette session du tableau de bord ne peut pas gérer les liens distants. Ouvrez le tableau de bord sur cet ordinateur (installation autonome) ou utilisez une session hub jumelée.", "remoteLink.error.host_confirmation_expired": "La demande de lien distant n’a pas pu aboutir.", "remoteLink.error.host_fingerprint_mismatch": "La demande de lien distant n’a pas pu aboutir.", "remoteLink.error.host_not_confirmed": "La demande de lien distant n’a pas pu aboutir.", @@ -3357,14 +3358,17 @@ export const fr: Record = { "remoteLink.error.link_exists": "La demande de lien distant n’a pas pu aboutir.", "remoteLink.error.link_not_found": "La demande de lien distant n’a pas pu aboutir.", "remoteLink.error.link_remove_failed": "La demande de lien distant n’a pas pu aboutir.", - "remoteLink.error.link_unavailable": "La demande de lien distant n’a pas pu aboutir.", - "remoteLink.error.listener_unavailable": "La demande de lien distant n’a pas pu aboutir.", - "remoteLink.error.probe_failed": "La demande de lien distant n’a pas pu aboutir.", - "remoteLink.error.remote_connect_failed": "La demande de lien distant n’a pas pu aboutir.", + "remoteLink.error.link_unavailable": "Remote Link n’est pas disponible dans ce runtime. Redémarrez OpenCodex, puis réessayez.", + "remoteLink.error.listener_unavailable": "L’écouteur Remote Link n’a pas pu démarrer sur cet ordinateur. Vérifiez qu’aucun conflit de port n’existe, puis réessayez.", + "remoteLink.error.probe_failed": "Impossible de se connecter à l’hôte SSH. Vérifiez qu’il accepte votre clé SSH et que l’assistant ProxyCommand éventuellement requis est installé.", + "remoteLink.error.remote_connect_failed": "L’ordinateur distant n’a pas pu se connecter à ce Home. Vérifiez son service OpenCodex, puis réessayez.", "remoteLink.error.remote_disconnect_failed": "La demande de lien distant n’a pas pu aboutir.", - "remoteLink.error.remote_port_failed": "La demande de lien distant n’a pas pu aboutir.", + "remoteLink.error.remote_ocx_missing": "ocx est introuvable sur l’ordinateur distant. Installez-y OpenCodex, ou vérifiez que ocx figure dans le PATH des sessions SSH non interactives.", + "remoteLink.error.remote_ocx_outdated": "OpenCodex est trop ancien sur l’ordinateur distant pour Remote Link. Exécutez-y ocx update (2.66.0 ou ultérieur), puis réessayez.", + "remoteLink.error.remote_ocx_unrecognized": "L’ordinateur distant n’a pas indiqué de version d’OpenCodex. Remote Link nécessite OpenCodex 2.66.0 ou ultérieur sous macOS ou Linux.", + "remoteLink.error.remote_port_failed": "L’ordinateur distant n’a pas pu indiquer son port de liaison. Vérifiez qu’OpenCodex 2.66.0 ou ultérieur y est en cours d’exécution, puis réessayez.", "remoteLink.error.tailscale_session_refused": "La demande de lien distant n’a pas pu aboutir.", - "remoteLink.error.version_probe_failed": "La demande de lien distant n’a pas pu aboutir.", + "remoteLink.error.version_probe_failed": "Impossible d’exécuter ocx sur l’ordinateur distant via SSH. Vérifiez l’accès SSH, puis réessayez.", "remoteLink.error.generic": "La demande de lien distant n’a pas pu aboutir.", "remoteLink.reason.auth": "L’authentification a échoué.", "remoteLink.reason.hostkey": "La clé d’hôte n’a pas pu être vérifiée.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 34df0413060..c3c6d6eb60d 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -3345,6 +3345,7 @@ export const ja: Record = { "link.close": "閉じる", "link.cancel": "キャンセル", "remoteLink.childDisabled": "子リンクを開始できるのはスタンドアロンランタイムだけです。", + "remoteLink.childJoinUnavailable": "このリリースでは、ダッシュボードからこのコンピューターを子として接続することはできません。接続すると OpenCodex が再起動し、既存の Codex 接続が切断されるためです。代わりに Home 側からリンクを開始してください。Home にするコンピューターで「ホーム」を選び、もう一方のコンピューターを子として追加します。", "remoteLink.findHome.title": "Home を探す", "remoteLink.findHome.body": "この子コンピューターを接続する Home を選択してください。", "remoteLink.findHome.action": "Home を探す", @@ -3376,8 +3377,8 @@ export const ja: Record = { "remoteLink.direction.client": "クライアント開始", "remoteLink.error.admission_timeout": "リモートリンクのリクエストを完了できませんでした。", "remoteLink.error.compensation_failed": "リモートリンクのリクエストを完了できませんでした。", - "remoteLink.error.fingerprint_failed": "リモートリンクのリクエストを完了できませんでした。", - "remoteLink.error.forbidden": "リモートリンクのリクエストを完了できませんでした。", + "remoteLink.error.fingerprint_failed": "SSH ホストキーを読み取れませんでした。接続テストをもう一度実行してください。", + "remoteLink.error.forbidden": "このダッシュボードセッションではリモートリンクを管理できません。このコンピューター上でダッシュボードを開く(スタンドアロン環境)か、ペアリング済みのハブセッションを使用してください。", "remoteLink.error.host_confirmation_expired": "リモートリンクのリクエストを完了できませんでした。", "remoteLink.error.host_fingerprint_mismatch": "リモートリンクのリクエストを完了できませんでした。", "remoteLink.error.host_not_confirmed": "リモートリンクのリクエストを完了できませんでした。", @@ -3390,14 +3391,17 @@ export const ja: Record = { "remoteLink.error.link_exists": "リモートリンクのリクエストを完了できませんでした。", "remoteLink.error.link_not_found": "リモートリンクのリクエストを完了できませんでした。", "remoteLink.error.link_remove_failed": "リモートリンクのリクエストを完了できませんでした。", - "remoteLink.error.link_unavailable": "リモートリンクのリクエストを完了できませんでした。", - "remoteLink.error.listener_unavailable": "リモートリンクのリクエストを完了できませんでした。", - "remoteLink.error.probe_failed": "リモートリンクのリクエストを完了できませんでした。", - "remoteLink.error.remote_connect_failed": "リモートリンクのリクエストを完了できませんでした。", + "remoteLink.error.link_unavailable": "このランタイムではリモートリンクを利用できません。OpenCodex を再起動してから再試行してください。", + "remoteLink.error.listener_unavailable": "このコンピューターでリモートリンクのリスナーを起動できませんでした。ポートの競合がないか確認してから再試行してください。", + "remoteLink.error.probe_failed": "SSH ホストに接続できませんでした。SSH キーで認証できるか、必要な ProxyCommand ヘルパーがインストールされているかを確認してください。", + "remoteLink.error.remote_connect_failed": "リモートのコンピューターがこの Home に接続できませんでした。そのコンピューターの OpenCodex サービスを確認してから再試行してください。", "remoteLink.error.remote_disconnect_failed": "リモートリンクのリクエストを完了できませんでした。", - "remoteLink.error.remote_port_failed": "リモートリンクのリクエストを完了できませんでした。", + "remoteLink.error.remote_ocx_missing": "リモートのコンピューターで ocx が見つかりませんでした。そこに OpenCodex をインストールするか、非対話 SSH セッションの PATH に ocx があることを確認してください。", + "remoteLink.error.remote_ocx_outdated": "リモートのコンピューターの OpenCodex が古く、リモートリンクに対応していません。そのコンピューターで ocx update を実行して 2.66.0 以降にしてから再試行してください。", + "remoteLink.error.remote_ocx_unrecognized": "リモートのコンピューターが OpenCodex のバージョンを返しませんでした。リモートリンクには macOS または Linux 上の OpenCodex 2.66.0 以降が必要です。", + "remoteLink.error.remote_port_failed": "リモートのコンピューターがリンクポートを返しませんでした。そのコンピューターで OpenCodex 2.66.0 以降が動作しているか確認してから再試行してください。", "remoteLink.error.tailscale_session_refused": "リモートリンクのリクエストを完了できませんでした。", - "remoteLink.error.version_probe_failed": "リモートリンクのリクエストを完了できませんでした。", + "remoteLink.error.version_probe_failed": "SSH 経由でリモートのコンピューターの ocx を実行できませんでした。SSH アクセスを確認してから再試行してください。", "remoteLink.error.generic": "リモートリンクのリクエストを完了できませんでした。", "remoteLink.reason.auth": "認証に失敗しました。", "remoteLink.reason.hostkey": "ホストキーを確認できませんでした。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 40d09ec0115..aa00df58565 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -3345,6 +3345,7 @@ export const ko: Record = { "link.close": "닫기", "link.cancel": "취소", "remoteLink.childDisabled": "자식 링크는 독립형 런타임에서만 시작할 수 있습니다.", + "remoteLink.childJoinUnavailable": "이번 릴리스에서는 대시보드에서 이 컴퓨터를 자식으로 연결할 수 없습니다. 자식으로 연결하면 OpenCodex가 다시 시작되어 지금 쓰고 있는 Codex 연결이 끊어지기 때문입니다. 대신 홈에서 연결을 시작하세요. 홈이 될 컴퓨터에서 홈을 선택한 뒤 다른 컴퓨터를 자식으로 추가하면 됩니다.", "remoteLink.findHome.title": "홈 찾기", "remoteLink.findHome.body": "이 자식 컴퓨터를 연결할 홈 컴퓨터를 선택하세요.", "remoteLink.findHome.action": "홈 찾기", @@ -3376,8 +3377,8 @@ export const ko: Record = { "remoteLink.direction.client": "클라이언트 시작", "remoteLink.error.admission_timeout": "원격 연결 요청을 완료하지 못했습니다.", "remoteLink.error.compensation_failed": "원격 연결 요청을 완료하지 못했습니다.", - "remoteLink.error.fingerprint_failed": "원격 연결 요청을 완료하지 못했습니다.", - "remoteLink.error.forbidden": "원격 연결 요청을 완료하지 못했습니다.", + "remoteLink.error.fingerprint_failed": "SSH 호스트 키를 읽지 못했습니다. 연결 테스트를 다시 실행하세요.", + "remoteLink.error.forbidden": "이 대시보드 세션으로는 원격 연결을 관리할 수 없습니다. 이 컴퓨터에서 대시보드를 열거나(독립형 설치), 페어링된 Hub 세션을 사용하세요.", "remoteLink.error.host_confirmation_expired": "원격 연결 요청을 완료하지 못했습니다.", "remoteLink.error.host_fingerprint_mismatch": "원격 연결 요청을 완료하지 못했습니다.", "remoteLink.error.host_not_confirmed": "원격 연결 요청을 완료하지 못했습니다.", @@ -3390,14 +3391,17 @@ export const ko: Record = { "remoteLink.error.link_exists": "원격 연결 요청을 완료하지 못했습니다.", "remoteLink.error.link_not_found": "원격 연결 요청을 완료하지 못했습니다.", "remoteLink.error.link_remove_failed": "원격 연결 요청을 완료하지 못했습니다.", - "remoteLink.error.link_unavailable": "원격 연결 요청을 완료하지 못했습니다.", - "remoteLink.error.listener_unavailable": "원격 연결 요청을 완료하지 못했습니다.", - "remoteLink.error.probe_failed": "원격 연결 요청을 완료하지 못했습니다.", - "remoteLink.error.remote_connect_failed": "원격 연결 요청을 완료하지 못했습니다.", + "remoteLink.error.link_unavailable": "이 런타임에서는 원격 연결을 사용할 수 없습니다. OpenCodex를 다시 시작한 뒤 다시 시도하세요.", + "remoteLink.error.listener_unavailable": "이 컴퓨터에서 원격 연결 리스너를 시작하지 못했습니다. 포트 충돌이 있는지 확인한 뒤 다시 시도하세요.", + "remoteLink.error.probe_failed": "SSH 호스트에 접속하지 못했습니다. SSH 키로 로그인할 수 있는지, ProxyCommand 도우미가 설치되어 있는지 확인하세요.", + "remoteLink.error.remote_connect_failed": "원격 컴퓨터를 이 홈에 연결하지 못했습니다. 그 컴퓨터의 OpenCodex 서비스를 확인한 뒤 다시 시도하세요.", "remoteLink.error.remote_disconnect_failed": "원격 연결 요청을 완료하지 못했습니다.", - "remoteLink.error.remote_port_failed": "원격 연결 요청을 완료하지 못했습니다.", + "remoteLink.error.remote_ocx_missing": "원격 컴퓨터에서 ocx를 찾지 못했습니다. 그 컴퓨터에 OpenCodex를 설치하거나, 비대화형 SSH 세션의 PATH에 ocx가 있는지 확인하세요.", + "remoteLink.error.remote_ocx_outdated": "원격 컴퓨터의 OpenCodex가 원격 연결을 쓰기에는 오래되었습니다. 그 컴퓨터에서 ocx update를 실행해 2.66.0 이상으로 올린 뒤 다시 시도하세요.", + "remoteLink.error.remote_ocx_unrecognized": "원격 컴퓨터가 OpenCodex 버전을 알려 주지 않았습니다. 원격 연결에는 macOS 또는 Linux의 OpenCodex 2.66.0 이상이 필요합니다.", + "remoteLink.error.remote_port_failed": "원격 컴퓨터의 링크 포트를 확인하지 못했습니다. 그 컴퓨터에서 OpenCodex 2.66.0 이상이 실행 중인지 확인한 뒤 다시 시도하세요.", "remoteLink.error.tailscale_session_refused": "원격 연결 요청을 완료하지 못했습니다.", - "remoteLink.error.version_probe_failed": "원격 연결 요청을 완료하지 못했습니다.", + "remoteLink.error.version_probe_failed": "SSH로 원격 컴퓨터의 ocx를 실행하지 못했습니다. SSH 접속을 확인한 뒤 다시 시도하세요.", "remoteLink.error.generic": "원격 연결 요청을 완료하지 못했습니다.", "remoteLink.reason.auth": "인증에 실패했습니다.", "remoteLink.reason.hostkey": "호스트 키를 확인하지 못했습니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 6f4a0a42778..10ea91ba4f0 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -3346,6 +3346,7 @@ export const ru: Record = { "link.close": "Закрыть", "link.cancel": "Отмена", "remoteLink.childDisabled": "Связь с дочерним компьютером можно начать только из автономного режима.", + "remoteLink.childJoinUnavailable": "В этой версии подключить этот компьютер как дочерний из панели нельзя: подключение перезапускает OpenCodex и оборвёт текущие подключения Codex. Начните связь со стороны Home: на компьютере, который должен стать Home, выберите «Главный» и добавьте другой компьютер как дочерний.", "remoteLink.findHome.title": "Найти Home", "remoteLink.findHome.body": "Выберите компьютер Home, к которому подключить этот Child.", "remoteLink.findHome.action": "Найти Home", @@ -3377,8 +3378,8 @@ export const ru: Record = { "remoteLink.direction.client": "Инициировано клиентом", "remoteLink.error.admission_timeout": "Не удалось завершить запрос удалённой связи.", "remoteLink.error.compensation_failed": "Не удалось завершить запрос удалённой связи.", - "remoteLink.error.fingerprint_failed": "Не удалось завершить запрос удалённой связи.", - "remoteLink.error.forbidden": "Не удалось завершить запрос удалённой связи.", + "remoteLink.error.fingerprint_failed": "Не удалось прочитать ключ SSH-хоста. Запустите проверку соединения ещё раз.", + "remoteLink.error.forbidden": "Этот сеанс панели не может управлять удалёнными связями. Откройте панель на этом компьютере (автономная установка) или используйте сопряжённый сеанс хаба.", "remoteLink.error.host_confirmation_expired": "Не удалось завершить запрос удалённой связи.", "remoteLink.error.host_fingerprint_mismatch": "Не удалось завершить запрос удалённой связи.", "remoteLink.error.host_not_confirmed": "Не удалось завершить запрос удалённой связи.", @@ -3391,14 +3392,17 @@ export const ru: Record = { "remoteLink.error.link_exists": "Не удалось завершить запрос удалённой связи.", "remoteLink.error.link_not_found": "Не удалось завершить запрос удалённой связи.", "remoteLink.error.link_remove_failed": "Не удалось завершить запрос удалённой связи.", - "remoteLink.error.link_unavailable": "Не удалось завершить запрос удалённой связи.", - "remoteLink.error.listener_unavailable": "Не удалось завершить запрос удалённой связи.", - "remoteLink.error.probe_failed": "Не удалось завершить запрос удалённой связи.", - "remoteLink.error.remote_connect_failed": "Не удалось завершить запрос удалённой связи.", + "remoteLink.error.link_unavailable": "Удалённая связь недоступна в этой среде выполнения. Перезапустите OpenCodex и повторите попытку.", + "remoteLink.error.listener_unavailable": "Не удалось запустить приёмник удалённой связи на этом компьютере. Проверьте, нет ли конфликта портов, и повторите попытку.", + "remoteLink.error.probe_failed": "Не удалось подключиться к SSH-хосту. Убедитесь, что он принимает ваш ключ SSH и что нужная вспомогательная программа ProxyCommand установлена.", + "remoteLink.error.remote_connect_failed": "Удалённый компьютер не смог подключиться к этому Home. Проверьте службу OpenCodex на нём и повторите попытку.", "remoteLink.error.remote_disconnect_failed": "Не удалось завершить запрос удалённой связи.", - "remoteLink.error.remote_port_failed": "Не удалось завершить запрос удалённой связи.", + "remoteLink.error.remote_ocx_missing": "На удалённом компьютере не найден ocx. Установите там OpenCodex или убедитесь, что ocx есть в PATH неинтерактивных SSH-сеансов.", + "remoteLink.error.remote_ocx_outdated": "OpenCodex на удалённом компьютере слишком старый для удалённой связи. Выполните там ocx update (версия 2.66.0 или новее) и повторите попытку.", + "remoteLink.error.remote_ocx_unrecognized": "Удалённый компьютер не сообщил версию OpenCodex. Для удалённой связи нужен OpenCodex 2.66.0 или новее на macOS или Linux.", + "remoteLink.error.remote_port_failed": "Удалённый компьютер не сообщил порт связи. Убедитесь, что на нём работает OpenCodex 2.66.0 или новее, и повторите попытку.", "remoteLink.error.tailscale_session_refused": "Не удалось завершить запрос удалённой связи.", - "remoteLink.error.version_probe_failed": "Не удалось завершить запрос удалённой связи.", + "remoteLink.error.version_probe_failed": "Не удалось запустить ocx на удалённом компьютере по SSH. Проверьте доступ по SSH и повторите попытку.", "remoteLink.error.generic": "Не удалось завершить запрос удалённой связи.", "remoteLink.reason.auth": "Ошибка аутентификации.", "remoteLink.reason.hostkey": "Не удалось проверить ключ хоста.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 563823498a4..59eba015435 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -3346,6 +3346,7 @@ export const tr: Record = { "link.close": "Kapat", "link.cancel": "İptal", "remoteLink.childDisabled": "Çocuk bağlantıları yalnızca bağımsız çalışma zamanından başlatılabilir.", + "remoteLink.childJoinUnavailable": "Bu sürümde bu bilgisayarı panodan Çocuk olarak bağlamak kullanılamaz: bağlanmak OpenCodex'i yeniden başlatır ve mevcut Codex bağlantılarını keser. Bağlantıyı bunun yerine Home tarafından başlatın: Home olacak bilgisayarda “Ana” seçeneğini seçin ve diğer bilgisayarı Çocuk olarak ekleyin.", "remoteLink.findHome.title": "Home\u0027u bul", "remoteLink.findHome.body": "Bu Çocuk bilgisayarının bağlanacağı Home bilgisayarını seçin.", "remoteLink.findHome.action": "Home'u bul", @@ -3377,8 +3378,8 @@ export const tr: Record = { "remoteLink.direction.client": "İstemci tarafından başlatıldı", "remoteLink.error.admission_timeout": "Uzak bağlantı isteği tamamlanamadı.", "remoteLink.error.compensation_failed": "Uzak bağlantı isteği tamamlanamadı.", - "remoteLink.error.fingerprint_failed": "Uzak bağlantı isteği tamamlanamadı.", - "remoteLink.error.forbidden": "Uzak bağlantı isteği tamamlanamadı.", + "remoteLink.error.fingerprint_failed": "SSH ana bilgisayar anahtarı okunamadı. Bağlantıyı yeniden test edin.", + "remoteLink.error.forbidden": "Bu pano oturumu uzak bağlantıları yönetemez. Panoyu bu bilgisayarda açın (bağımsız kurulum) veya eşleştirilmiş bir Merkez oturumu kullanın.", "remoteLink.error.host_confirmation_expired": "Uzak bağlantı isteği tamamlanamadı.", "remoteLink.error.host_fingerprint_mismatch": "Uzak bağlantı isteği tamamlanamadı.", "remoteLink.error.host_not_confirmed": "Uzak bağlantı isteği tamamlanamadı.", @@ -3391,14 +3392,17 @@ export const tr: Record = { "remoteLink.error.link_exists": "Uzak bağlantı isteği tamamlanamadı.", "remoteLink.error.link_not_found": "Uzak bağlantı isteği tamamlanamadı.", "remoteLink.error.link_remove_failed": "Uzak bağlantı isteği tamamlanamadı.", - "remoteLink.error.link_unavailable": "Uzak bağlantı isteği tamamlanamadı.", - "remoteLink.error.listener_unavailable": "Uzak bağlantı isteği tamamlanamadı.", - "remoteLink.error.probe_failed": "Uzak bağlantı isteği tamamlanamadı.", - "remoteLink.error.remote_connect_failed": "Uzak bağlantı isteği tamamlanamadı.", + "remoteLink.error.link_unavailable": "Uzak bağlantı bu çalışma zamanında kullanılamıyor. OpenCodex'i yeniden başlatıp tekrar deneyin.", + "remoteLink.error.listener_unavailable": "Uzak bağlantı dinleyicisi bu bilgisayarda başlatılamadı. Port çakışması olup olmadığını kontrol edip tekrar deneyin.", + "remoteLink.error.probe_failed": "SSH ana bilgisayarına bağlanılamadı. SSH anahtarınızı kabul ettiğini ve gerekiyorsa ProxyCommand yardımcısının kurulu olduğunu kontrol edin.", + "remoteLink.error.remote_connect_failed": "Uzak bilgisayar bu Home'a bağlanamadı. Oradaki OpenCodex hizmetini kontrol edip tekrar deneyin.", "remoteLink.error.remote_disconnect_failed": "Uzak bağlantı isteği tamamlanamadı.", - "remoteLink.error.remote_port_failed": "Uzak bağlantı isteği tamamlanamadı.", + "remoteLink.error.remote_ocx_missing": "Uzak bilgisayarda ocx bulunamadı. Orada OpenCodex'i kurun veya ocx'in etkileşimsiz SSH oturumlarının PATH'inde olduğundan emin olun.", + "remoteLink.error.remote_ocx_outdated": "Uzak bilgisayardaki OpenCodex, uzak bağlantı için çok eski. Orada ocx update komutunu çalıştırıp 2.66.0 veya sonrasına güncelleyin, ardından yeniden deneyin.", + "remoteLink.error.remote_ocx_unrecognized": "Uzak bilgisayar bir OpenCodex sürümü bildirmedi. Uzak bağlantı için macOS veya Linux üzerinde OpenCodex 2.66.0 veya sonrası gerekir.", + "remoteLink.error.remote_port_failed": "Uzak bilgisayar bağlantı portunu bildiremedi. Orada OpenCodex 2.66.0 veya sonrasının çalıştığını kontrol edip tekrar deneyin.", "remoteLink.error.tailscale_session_refused": "Uzak bağlantı isteği tamamlanamadı.", - "remoteLink.error.version_probe_failed": "Uzak bağlantı isteği tamamlanamadı.", + "remoteLink.error.version_probe_failed": "Uzak bilgisayarda SSH üzerinden ocx çalıştırılamadı. SSH erişimini kontrol edip tekrar deneyin.", "remoteLink.error.generic": "Uzak bağlantı isteği tamamlanamadı.", "remoteLink.reason.auth": "Kimlik doğrulama başarısız oldu.", "remoteLink.reason.hostkey": "Ana bilgisayar anahtarı doğrulanamadı.", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 8ce937847a7..cbd565d9a78 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -3281,6 +3281,7 @@ export const vi: Record = { "link.close": "Đóng", "link.cancel": "Hủy", "remoteLink.childDisabled": "Chỉ có thể bắt đầu liên kết máy con từ runtime độc lập.", + "remoteLink.childJoinUnavailable": "Trong bản phát hành này, không thể kết nối máy này với vai trò máy con từ bảng điều khiển: việc kết nối sẽ khởi động lại OpenCodex và làm ngắt các kết nối Codex hiện có. Hãy bắt đầu liên kết từ phía Home: trên máy sẽ làm Home, chọn “Máy chủ” rồi thêm máy còn lại làm máy con.", "remoteLink.findHome.title": "Tìm Home", "remoteLink.findHome.body": "Chọn máy Home để kết nối máy con này.", "remoteLink.findHome.action": "Tìm Home", @@ -3312,8 +3313,8 @@ export const vi: Record = { "remoteLink.direction.client": "Do máy con khởi tạo", "remoteLink.error.admission_timeout": "Không thể hoàn tất yêu cầu liên kết từ xa.", "remoteLink.error.compensation_failed": "Không thể hoàn tất yêu cầu liên kết từ xa.", - "remoteLink.error.fingerprint_failed": "Không thể hoàn tất yêu cầu liên kết từ xa.", - "remoteLink.error.forbidden": "Không thể hoàn tất yêu cầu liên kết từ xa.", + "remoteLink.error.fingerprint_failed": "Không đọc được khóa máy chủ SSH. Hãy chạy lại bước kiểm tra kết nối.", + "remoteLink.error.forbidden": "Phiên bảng điều khiển này không thể quản lý liên kết từ xa. Hãy mở bảng điều khiển trên máy này (bản cài độc lập) hoặc dùng phiên Hub đã ghép nối.", "remoteLink.error.host_confirmation_expired": "Không thể hoàn tất yêu cầu liên kết từ xa.", "remoteLink.error.host_fingerprint_mismatch": "Không thể hoàn tất yêu cầu liên kết từ xa.", "remoteLink.error.host_not_confirmed": "Không thể hoàn tất yêu cầu liên kết từ xa.", @@ -3326,14 +3327,17 @@ export const vi: Record = { "remoteLink.error.link_exists": "Không thể hoàn tất yêu cầu liên kết từ xa.", "remoteLink.error.link_not_found": "Không thể hoàn tất yêu cầu liên kết từ xa.", "remoteLink.error.link_remove_failed": "Không thể hoàn tất yêu cầu liên kết từ xa.", - "remoteLink.error.link_unavailable": "Không thể hoàn tất yêu cầu liên kết từ xa.", - "remoteLink.error.listener_unavailable": "Không thể hoàn tất yêu cầu liên kết từ xa.", - "remoteLink.error.probe_failed": "Không thể hoàn tất yêu cầu liên kết từ xa.", - "remoteLink.error.remote_connect_failed": "Không thể hoàn tất yêu cầu liên kết từ xa.", + "remoteLink.error.link_unavailable": "Liên kết từ xa không khả dụng trong runtime này. Hãy khởi động lại OpenCodex rồi thử lại.", + "remoteLink.error.listener_unavailable": "Không thể khởi động trình lắng nghe liên kết từ xa trên máy này. Hãy kiểm tra xung đột cổng rồi thử lại.", + "remoteLink.error.probe_failed": "Không kết nối được tới máy chủ SSH. Hãy kiểm tra máy đó chấp nhận khóa SSH của bạn và trình trợ giúp ProxyCommand cần thiết đã được cài đặt.", + "remoteLink.error.remote_connect_failed": "Máy từ xa không kết nối được tới Home này. Hãy kiểm tra dịch vụ OpenCodex trên máy đó rồi thử lại.", "remoteLink.error.remote_disconnect_failed": "Không thể hoàn tất yêu cầu liên kết từ xa.", - "remoteLink.error.remote_port_failed": "Không thể hoàn tất yêu cầu liên kết từ xa.", + "remoteLink.error.remote_ocx_missing": "Không tìm thấy ocx trên máy từ xa. Hãy cài OpenCodex trên máy đó hoặc kiểm tra ocx có trong PATH của phiên SSH không tương tác.", + "remoteLink.error.remote_ocx_outdated": "OpenCodex trên máy từ xa quá cũ để dùng liên kết từ xa. Hãy chạy ocx update trên máy đó (2.66.0 trở lên) rồi thử lại.", + "remoteLink.error.remote_ocx_unrecognized": "Máy từ xa không báo phiên bản OpenCodex. Liên kết từ xa cần OpenCodex 2.66.0 trở lên trên macOS hoặc Linux.", + "remoteLink.error.remote_port_failed": "Máy từ xa không báo được cổng liên kết. Hãy kiểm tra máy đó đang chạy OpenCodex 2.66.0 trở lên rồi thử lại.", "remoteLink.error.tailscale_session_refused": "Không thể hoàn tất yêu cầu liên kết từ xa.", - "remoteLink.error.version_probe_failed": "Không thể hoàn tất yêu cầu liên kết từ xa.", + "remoteLink.error.version_probe_failed": "Không chạy được ocx trên máy từ xa qua SSH. Hãy kiểm tra quyền truy cập SSH rồi thử lại.", "remoteLink.error.generic": "Không thể hoàn tất yêu cầu liên kết từ xa.", "remoteLink.reason.auth": "Xác thực không thành công.", "remoteLink.reason.hostkey": "Không thể xác minh khóa máy chủ.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index c92a5c5d5c4..357be1e9410 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -3309,6 +3309,7 @@ export const zhTW: Record = { "link.close": "關閉", "link.cancel": "取消", "remoteLink.childDisabled": "只有獨立執行環境才能發起子裝置連線。", + "remoteLink.childJoinUnavailable": "此版本暫不支援從儀表板將這台電腦連線為子裝置:連線會重新啟動 OpenCodex,現有的 Codex 連線會因此中斷。請改由 Home 端發起連線:在要作為 Home 的電腦上選擇「主機」,再將另一台電腦新增為子裝置。", "remoteLink.findHome.title": "尋找 Home", "remoteLink.findHome.body": "選擇要連線此子裝置的 Home 電腦。", "remoteLink.findHome.action": "尋找 Home", @@ -3340,8 +3341,8 @@ export const zhTW: Record = { "remoteLink.direction.client": "用戶端發起", "remoteLink.error.admission_timeout": "無法完成遠端連線要求。", "remoteLink.error.compensation_failed": "無法完成遠端連線要求。", - "remoteLink.error.fingerprint_failed": "無法完成遠端連線要求。", - "remoteLink.error.forbidden": "無法完成遠端連線要求。", + "remoteLink.error.fingerprint_failed": "無法讀取 SSH 主機金鑰。請重新測試連線。", + "remoteLink.error.forbidden": "此儀表板工作階段無法管理遠端連線。請在這台電腦上開啟儀表板(獨立安裝),或使用已配對的中樞工作階段。", "remoteLink.error.host_confirmation_expired": "無法完成遠端連線要求。", "remoteLink.error.host_fingerprint_mismatch": "無法完成遠端連線要求。", "remoteLink.error.host_not_confirmed": "無法完成遠端連線要求。", @@ -3354,14 +3355,17 @@ export const zhTW: Record = { "remoteLink.error.link_exists": "無法完成遠端連線要求。", "remoteLink.error.link_not_found": "無法完成遠端連線要求。", "remoteLink.error.link_remove_failed": "無法完成遠端連線要求。", - "remoteLink.error.link_unavailable": "無法完成遠端連線要求。", - "remoteLink.error.listener_unavailable": "無法完成遠端連線要求。", - "remoteLink.error.probe_failed": "無法完成遠端連線要求。", - "remoteLink.error.remote_connect_failed": "無法完成遠端連線要求。", + "remoteLink.error.link_unavailable": "遠端連線在目前的執行環境中無法使用。請重新啟動 OpenCodex 後重試。", + "remoteLink.error.listener_unavailable": "無法在這台電腦上啟動遠端連線監聽器。請檢查是否有連接埠衝突後重試。", + "remoteLink.error.probe_failed": "無法連線到 SSH 主機。請確認該主機接受你的 SSH 金鑰,並已安裝所需的 ProxyCommand 輔助程式。", + "remoteLink.error.remote_connect_failed": "遠端電腦無法連線到此 Home。請檢查該電腦上的 OpenCodex 服務後重試。", "remoteLink.error.remote_disconnect_failed": "無法完成遠端連線要求。", - "remoteLink.error.remote_port_failed": "無法完成遠端連線要求。", + "remoteLink.error.remote_ocx_missing": "在遠端電腦上找不到 ocx。請在該電腦上安裝 OpenCodex,或確認 ocx 位於非互動式 SSH 工作階段的 PATH 中。", + "remoteLink.error.remote_ocx_outdated": "遠端電腦上的 OpenCodex 版本過舊,不支援遠端連線。請在該電腦上執行 ocx update 升級到 2.66.0 或更新版本後重試。", + "remoteLink.error.remote_ocx_unrecognized": "遠端電腦沒有回報 OpenCodex 版本。遠端連線需要 macOS 或 Linux 上的 OpenCodex 2.66.0 或更新版本。", + "remoteLink.error.remote_port_failed": "遠端電腦未能回報其連線連接埠。請確認該電腦上正在執行 OpenCodex 2.66.0 或更新版本後重試。", "remoteLink.error.tailscale_session_refused": "無法完成遠端連線要求。", - "remoteLink.error.version_probe_failed": "無法完成遠端連線要求。", + "remoteLink.error.version_probe_failed": "無法透過 SSH 在遠端電腦上執行 ocx。請檢查 SSH 存取權後重試。", "remoteLink.error.generic": "無法完成遠端連線要求。", "remoteLink.reason.auth": "驗證失敗。", "remoteLink.reason.hostkey": "無法驗證主機金鑰。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 69ef71c1ab5..791c6bdd116 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -3344,6 +3344,7 @@ export const zh: Record = { "link.close": "关闭", "link.cancel": "取消", "remoteLink.childDisabled": "只有独立运行时才能发起子设备连接。", + "remoteLink.childJoinUnavailable": "此版本暂不支持从仪表板将这台电脑连接为子设备:连接会重启 OpenCodex,现有的 Codex 连接会因此中断。请改为从 Home 一侧发起连接:在要作为 Home 的电脑上选择“主机”,然后将另一台电脑添加为子设备。", "remoteLink.findHome.title": "查找 Home", "remoteLink.findHome.body": "选择要连接此子设备的 Home 电脑。", "remoteLink.findHome.action": "查找 Home", @@ -3375,8 +3376,8 @@ export const zh: Record = { "remoteLink.direction.client": "客户端发起", "remoteLink.error.admission_timeout": "无法完成远程连接请求。", "remoteLink.error.compensation_failed": "无法完成远程连接请求。", - "remoteLink.error.fingerprint_failed": "无法完成远程连接请求。", - "remoteLink.error.forbidden": "无法完成远程连接请求。", + "remoteLink.error.fingerprint_failed": "无法读取 SSH 主机密钥。请重新测试连接。", + "remoteLink.error.forbidden": "此仪表板会话无法管理远程连接。请在这台电脑上打开仪表板(独立安装),或使用已配对的中心会话。", "remoteLink.error.host_confirmation_expired": "无法完成远程连接请求。", "remoteLink.error.host_fingerprint_mismatch": "无法完成远程连接请求。", "remoteLink.error.host_not_confirmed": "无法完成远程连接请求。", @@ -3389,14 +3390,17 @@ export const zh: Record = { "remoteLink.error.link_exists": "无法完成远程连接请求。", "remoteLink.error.link_not_found": "无法完成远程连接请求。", "remoteLink.error.link_remove_failed": "无法完成远程连接请求。", - "remoteLink.error.link_unavailable": "无法完成远程连接请求。", - "remoteLink.error.listener_unavailable": "无法完成远程连接请求。", - "remoteLink.error.probe_failed": "无法完成远程连接请求。", - "remoteLink.error.remote_connect_failed": "无法完成远程连接请求。", + "remoteLink.error.link_unavailable": "远程连接在当前运行时中不可用。请重启 OpenCodex 后重试。", + "remoteLink.error.listener_unavailable": "无法在这台电脑上启动远程连接监听器。请检查是否存在端口冲突后重试。", + "remoteLink.error.probe_failed": "无法连接到 SSH 主机。请确认该主机接受你的 SSH 密钥,并已安装所需的 ProxyCommand 辅助程序。", + "remoteLink.error.remote_connect_failed": "远程电脑无法连接到此 Home。请检查该电脑上的 OpenCodex 服务后重试。", "remoteLink.error.remote_disconnect_failed": "无法完成远程连接请求。", - "remoteLink.error.remote_port_failed": "无法完成远程连接请求。", + "remoteLink.error.remote_ocx_missing": "在远程电脑上找不到 ocx。请在该电脑上安装 OpenCodex,或确认 ocx 位于非交互式 SSH 会话的 PATH 中。", + "remoteLink.error.remote_ocx_outdated": "远程电脑上的 OpenCodex 版本过旧,不支持远程连接。请在该电脑上运行 ocx update 升级到 2.66.0 或更高版本后重试。", + "remoteLink.error.remote_ocx_unrecognized": "远程电脑没有报告 OpenCodex 版本。远程连接需要 macOS 或 Linux 上的 OpenCodex 2.66.0 或更高版本。", + "remoteLink.error.remote_port_failed": "远程电脑未能报告其连接端口。请确认该电脑上正在运行 OpenCodex 2.66.0 或更高版本后重试。", "remoteLink.error.tailscale_session_refused": "无法完成远程连接请求。", - "remoteLink.error.version_probe_failed": "无法完成远程连接请求。", + "remoteLink.error.version_probe_failed": "无法通过 SSH 在远程电脑上运行 ocx。请检查 SSH 访问后重试。", "remoteLink.error.generic": "无法完成远程连接请求。", "remoteLink.reason.auth": "身份验证失败。", "remoteLink.reason.hostkey": "无法验证主机密钥。", diff --git a/gui/src/pages/RemoteLink.tsx b/gui/src/pages/RemoteLink.tsx index ece988c680c..1ba6945625d 100644 --- a/gui/src/pages/RemoteLink.tsx +++ b/gui/src/pages/RemoteLink.tsx @@ -57,6 +57,9 @@ const ERROR_TKEY: Record = { probe_failed: "remoteLink.error.probe_failed", remote_connect_failed: "remoteLink.error.remote_connect_failed", remote_disconnect_failed: "remoteLink.error.remote_disconnect_failed", + remote_ocx_missing: "remoteLink.error.remote_ocx_missing", + remote_ocx_outdated: "remoteLink.error.remote_ocx_outdated", + remote_ocx_unrecognized: "remoteLink.error.remote_ocx_unrecognized", remote_port_failed: "remoteLink.error.remote_port_failed", standalone_required: "remoteLink.error.standalone_required", tailscale_session_refused: "remoteLink.error.tailscale_session_refused", @@ -102,6 +105,13 @@ function errorKey(error: unknown): TKey { return "remoteLink.error.generic"; } +type LinkActionError = { key: TKey; hint: string | null }; +function linkActionError(error: unknown): LinkActionError { return { key: errorKey(error), hint: error instanceof LinkApiError ? error.hint : null }; } +function LinkErrorNotice({ error }: { error: LinkActionError }): ReactElement { + const t = useT(); + return {t(error.key)}{error.hint && {t("remoteLink.reason.generic")} {error.hint}}; +} + export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = false, onOpenWorkspace }: RemoteLinkProps): ReactElement { const t = useT(); const [uiState, setUiState] = useState("off"); @@ -115,7 +125,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = const [confirmation, setConfirmation] = useState(null); const [checkedFingerprint, setCheckedFingerprint] = useState(false); const [busy, setBusy] = useState<"candidates" | "probe" | "confirm" | "apply" | "join" | "remove" | null>(null); - const [actionError, setActionError] = useState(null); + const [actionError, setActionError] = useState(null); const [failedAction, setFailedAction] = useState(null); const [confirming, setConfirming] = useState<{ row: LinkRowWire; force: boolean } | null>(null); const [forceError, setForceError] = useState(null); @@ -209,6 +219,9 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = // Cancelling the sheet abandons the attempt, so late responses cannot recreate its state. const closeSheet = () => { cancelLinkAttempt(); setSheetOpen(false); setCandidates([]); setProbe(null); setConfirmation(null); setCheckedFingerprint(false); setActionError(null); setFailedAction(null); setBusy(null); setUiState(current => ["failed", "adding-child", "confirming-host", "applying", "joining"].includes(current) ? "adding-child" : current); addButtonRef.current?.focus(); }; const standaloneRuntime = isStandaloneRuntime(); + // Joining restarts this OpenCodex and moves Codex routing to the Home, so the server offers it + // only to a paired session; the local dashboard can still run the Home side. + const childSelectable = standaloneRuntime && status?.joinAvailable === true; const openSheet = async () => { const attempt = startLinkAttempt(); setSheetOpen(true); setUiState("adding-child"); setCandidates([]); setProbe(null); setConfirmation(null); setCheckedFingerprint(false); setActionError(null); setFailedAction(null); setBusy("candidates"); @@ -218,7 +231,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = setCandidates(parseCandidates(result)); } catch (error) { if (!isCurrentLinkAttempt(attempt)) return; - setActionError(errorKey(error)); + setActionError(linkActionError(error)); } finally { if (isCurrentLinkAttempt(attempt)) setBusy(null); } @@ -233,7 +246,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = setProbe(parseProbe(result)); } catch (error) { if (!isCurrentLinkAttempt(attempt)) return; - setActionError(errorKey(error)); setFailedAction({ phase: "probe", alias: value }); setUiState("failed"); + setActionError(linkActionError(error)); setFailedAction({ phase: "probe", alias: value }); setUiState("failed"); } finally { if (isCurrentLinkAttempt(attempt)) setBusy(null); } @@ -249,7 +262,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = setConfirmation(parseConfirmation(result)); } catch (error) { if (!isCurrentLinkAttempt(attempt)) return; - setActionError(errorKey(error)); + setActionError(linkActionError(error)); } finally { if (isCurrentLinkAttempt(attempt)) setBusy(null); } @@ -265,7 +278,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = void refreshStatus(); } catch (error) { if (!isCurrentLinkAttempt(attempt)) return; - setActionError(errorKey(error)); setFailedAction({ phase: "apply", alias: confirmed.alias }); setUiState("failed"); + setActionError(linkActionError(error)); setFailedAction({ phase: "apply", alias: confirmed.alias }); setUiState("failed"); } finally { if (isCurrentLinkAttempt(attempt)) setBusy(null); } @@ -273,7 +286,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = const joinLink = async (requestedAlias = confirmation?.alias, attempt = linkAttemptRef.current ?? startLinkAttempt()) => { const value = requestedAlias?.trim(); - if (!value) return; + if (!value || !childSelectable) return; setBusy("join"); setActionError(null); setFailedAction(null); setUiState("joining"); try { await requestLinkJson<{ linkId: string; alias: string; restarting: true }>(apiBase, "/api/link/join", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ alias: value }), signal: attempt.controller.signal }); @@ -281,7 +294,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = closeSheet(); setUiState("restart-waiting"); } catch (error) { if (!isCurrentLinkAttempt(attempt)) return; - setActionError(errorKey(error)); setFailedAction({ phase: "join", alias: value }); setUiState("failed"); + setActionError(linkActionError(error)); setFailedAction({ phase: "join", alias: value }); setUiState("failed"); } finally { if (isCurrentLinkAttempt(attempt)) setBusy(null); } @@ -303,7 +316,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = const moveRole = (index: number, key: string) => { const next = key === "Home" ? 0 : key === "End" ? 1 : key === "ArrowRight" || key === "ArrowDown" ? (index + 1) % 2 : key === "ArrowLeft" || key === "ArrowUp" ? (index + 1) % 2 : index; - if (next === 1 && !isStandaloneRuntime()) return; + if (next === 1 && !childSelectable) return; if (next === index) return; roleRefs.current[next]?.focus(); setRole(next === 0 ? "home" : "child"); @@ -325,7 +338,7 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = // operator's choice so the heading does not contradict the action in front of them. const choseHome = role === "home" && (uiState === "adding-child" || uiState === "confirming-host" || uiState === "applying"); const roleLabel: TKey = status?.role === "home" || choseHome ? "remoteLink.role.home" : status?.role === "child" ? "remoteLink.role.child" : "remoteLink.role.standalone"; - const primaryActionDisabled = role === "child" && !standaloneRuntime; + const primaryActionDisabled = role === "child" && !childSelectable; if (!sessionReady) return

{t("link.title")}

{t("link.sessionRequired")}
; @@ -335,12 +348,12 @@ export default function RemoteLink({ apiBase, sessionReady, workspaceAvailable = {workspaceAvailable &&
{t("remoteLink.workspaceMoved.title")}

{t("remoteLink.workspaceMoved.body")}

} {statusError && {t(statusError)}} {statusRows.length === 0 && uiState === "off" &&
{t("link.switch")}

{t("link.switchOffHint")}

} - {uiState === "role-select" &&

{t("link.role.title")}

{t("link.role.hint")}

{!standaloneRuntime && {t("remoteLink.childDisabled")}}
} - {(uiState === "connected" || uiState === "reconnecting" || uiState === "failed" || uiState === "restart-waiting" || status?.role === "child" || statusRows.length > 0 || uiState === "adding-child" || uiState === "confirming-host" || uiState === "applying" || uiState === "joining") &&

{role === "child" && standaloneRuntime ? t("remoteLink.findHome.title") : t("link.children")}

{uiState === "restart-waiting" ? t("remoteLink.restart.waiting") : t(roleLabel)}

{uiState === "restart-waiting" ?
{t("remoteLink.restart.title")}

{t("remoteLink.restart.body")}

: status?.role === "child" ?
{status.child?.alias ?? t("remoteLink.role.child")}{status.child &&
{t(STATUS_LABEL[status.child.state])}
}
: statusRows.length > 0 ?
{statusRows.map(row =>
{row.alias}
{t(STATUS_LABEL[row.state])}{row.direction === "hub-initiated" ? t("remoteLink.direction.hub") : t("remoteLink.direction.client")}{row.reason && {row.reason in REASON_TKEY ? t(REASON_TKEY[row.reason]) : <>{t("remoteLink.reason.generic")} {row.reason}}}
)}
:

{t(role === "child" && standaloneRuntime ? "remoteLink.findHome.empty" : "link.noChildren")}

}{(uiState === "reconnecting" || (uiState === "failed" && ((failedAction !== null && actionError !== "remoteLink.error.join_restart_failed") || statusRows.some(row => row.state === "failed")))) &&
{t(STATUS_LABEL[uiState === "failed" ? "failed" : "reconnecting"])}
}{uiState === "joining" &&

{t("remoteLink.joining")}

}{actionError && {t(actionError)}}
} + {uiState === "role-select" &&

{t("link.role.title")}

{t("link.role.hint")}

{!standaloneRuntime && {t("remoteLink.childDisabled")}}{standaloneRuntime && status !== null && !status.joinAvailable && {t("remoteLink.childJoinUnavailable")}}
} + {(uiState === "connected" || uiState === "reconnecting" || uiState === "failed" || uiState === "restart-waiting" || status?.role === "child" || statusRows.length > 0 || uiState === "adding-child" || uiState === "confirming-host" || uiState === "applying" || uiState === "joining") &&

{role === "child" && standaloneRuntime ? t("remoteLink.findHome.title") : t("link.children")}

{uiState === "restart-waiting" ? t("remoteLink.restart.waiting") : t(roleLabel)}

{uiState === "restart-waiting" ?
{t("remoteLink.restart.title")}

{t("remoteLink.restart.body")}

: status?.role === "child" ?
{status.child?.alias ?? t("remoteLink.role.child")}{status.child &&
{t(STATUS_LABEL[status.child.state])}
}
: statusRows.length > 0 ?
{statusRows.map(row =>
{row.alias}
{t(STATUS_LABEL[row.state])}{row.direction === "hub-initiated" ? t("remoteLink.direction.hub") : t("remoteLink.direction.client")}{row.reason && {row.reason in REASON_TKEY ? t(REASON_TKEY[row.reason]) : <>{t("remoteLink.reason.generic")} {row.reason}}}
)}
:

{t(role === "child" && standaloneRuntime ? "remoteLink.findHome.empty" : "link.noChildren")}

}{(uiState === "reconnecting" || (uiState === "failed" && ((failedAction !== null && actionError?.key !== "remoteLink.error.join_restart_failed") || statusRows.some(row => row.state === "failed")))) &&
{t(STATUS_LABEL[uiState === "failed" ? "failed" : "reconnecting"])}
}{uiState === "joining" &&

{t("remoteLink.joining")}

}{actionError && }
} { event.preventDefault(); closeSheet(); }}>
-

{role === "child" && standaloneRuntime ? t("remoteLink.findHome.body") : t("link.candidates")}

{busy === "candidates" ?

{t("link.loading")}

: candidates.length > 0 ?
{candidates.map(candidate => )}
:

{t("link.noCandidates")}

}
setAlias(event.target.value)} placeholder={t("link.aliasPlaceholder")} autoComplete="off" />
{probe &&
{t("link.hostFingerprint")}

{probe.fingerprint}

{probe.keyType}
}{confirmation &&

{t("link.ocxVersion", { version: confirmation.ocxVersion })}

}{actionError && {t(actionError)}}
+

{role === "child" && standaloneRuntime ? t("remoteLink.findHome.body") : t("link.candidates")}

{busy === "candidates" ?

{t("link.loading")}

: candidates.length > 0 ?
{candidates.map(candidate => )}
:

{t("link.noCandidates")}

}
setAlias(event.target.value)} placeholder={t("link.aliasPlaceholder")} autoComplete="off" />
{probe &&
{t("link.hostFingerprint")}

{probe.fingerprint}

{probe.keyType}
}{confirmation &&

{t("link.ocxVersion", { version: confirmation.ocxVersion })}

}{actionError && }
{ event.preventDefault(); closeConfirmation(); }}> diff --git a/gui/src/pages/providers-shared.ts b/gui/src/pages/providers-shared.ts index 99c9418a355..3408690f335 100644 --- a/gui/src/pages/providers-shared.ts +++ b/gui/src/pages/providers-shared.ts @@ -26,6 +26,7 @@ export interface OAuthStatus { email?: string; error?: string; done?: boolean; + hint?: import("../components/login-url-block").LoginHintData; needsReauth?: boolean; activeAccountId?: string | null; } diff --git a/gui/src/pages/use-providers-oauth.ts b/gui/src/pages/use-providers-oauth.ts index d97d28dfc0c..4a68c8b9c1a 100644 --- a/gui/src/pages/use-providers-oauth.ts +++ b/gui/src/pages/use-providers-oauth.ts @@ -199,6 +199,9 @@ export function useProvidersOAuth({ finished = true; break; } + // A later provider step replaces the initial POST hint (including an + // absent device code); generation checks above keep old polls out. + if (s.hint) setLoginInfo({ provider, url: s.hint.url, instructions: s.hint.instructions, deviceCode: s.hint.deviceCode }); } if (!finished && oauthLoginGenerationRef.current!.get(provider) === generation && aliveRef.current) { await cancelServerLogin(provider); diff --git a/gui/src/remote-link-api.ts b/gui/src/remote-link-api.ts index 935a19032e7..cd4398dd709 100644 --- a/gui/src/remote-link-api.ts +++ b/gui/src/remote-link-api.ts @@ -28,6 +28,9 @@ export const LINK_ERROR_CODES = [ "probe_failed", "remote_connect_failed", "remote_disconnect_failed", + "remote_ocx_missing", + "remote_ocx_outdated", + "remote_ocx_unrecognized", "remote_port_failed", "standalone_required", "tailscale_session_refused", @@ -49,6 +52,11 @@ export interface RemoteLinkStatusWire { listener: { state: LinkListenerState; port: number | null }; links: LinkRowWire[]; child: null | { alias: string; state: LinkWireState; since: string; reason: string | null }; + /** + * Whether this dashboard session may join a Home as a Child. Only a paired session on a + * standalone runtime may; the server omits the field for non-dashboard callers, read as false. + */ + joinAvailable: boolean; } const LINK_STATES: readonly LinkWireState[] = ["connecting", "connected", "reconnecting", "failed", "idle"]; @@ -57,19 +65,42 @@ const LINK_ROLES = ["standalone", "home", "child"] as const; export class LinkApiError extends Error { readonly code: string; readonly status: number; + /** The server's bounded hint line (ssh stderr, the ssh runner's own failure, or the parsed remote version), shown under the translated message. */ + readonly hint: string | null; - constructor(code: string, status: number) { + constructor(code: string, status: number, hint: string | null = null) { super(code); this.name = "LinkApiError"; this.code = code; this.status = status; + this.hint = hint; } } +const HINT_MAX_CHARS = 160; + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } +/** C0/C1 controls plus the invisible and bidi formatting ranges, compared by code point. */ +function isHintControl(code: number): boolean { + return code < 0x20 || (code >= 0x7f && code <= 0x9f) || (code >= 0x200b && code <= 0x200f) + || (code >= 0x202a && code <= 0x202e) || (code >= 0x2060 && code <= 0x206f) || code === 0xfeff; +} + +/** + * The server already bounds hints; the dashboard re-bounds them so no response can grow the UI. + * The cap counts and cuts code points, so an astral character is never split into a lone surrogate. + */ +export function boundLinkHint(value: unknown): string | null { + if (typeof value !== "string") return null; + const clean = Array.from(value, char => isHintControl(char.codePointAt(0) ?? 0) ? " " : char).join("").replace(/\s+/g, " ").trim(); + if (!clean) return null; + const points = Array.from(clean); + return points.length > HINT_MAX_CHARS ? `${points.slice(0, HINT_MAX_CHARS - 1).join("")}\u2026` : clean; +} + function nonEmpty(value: unknown): value is string { return typeof value === "string" && value.length > 0; } function isLinkState(value: unknown): value is LinkWireState { return typeof value === "string" && LINK_STATES.includes(value as LinkWireState); } @@ -87,7 +118,7 @@ export function parseRemoteLinkStatus(value: unknown): RemoteLinkStatusWire { if (!isRecord(value.child) || !nonEmpty(value.child.alias) || !isLinkState(value.child.state) || !nonEmpty(value.child.since) || (value.child.reason !== null && typeof value.child.reason !== "string")) throw new Error("invalid child"); child = { alias: value.child.alias, state: value.child.state, since: value.child.since, reason: value.child.reason as string | null }; } - return { role: value.role as RemoteLinkStatusWire["role"], listener: { state: listener.state as LinkListenerState, port: listener.port as number | null }, links, child }; + return { role: value.role as RemoteLinkStatusWire["role"], listener: { state: listener.state as LinkListenerState, port: listener.port as number | null }, links, child, joinAvailable: value.joinAvailable === true }; } /** Read link-route JSON and preserve the server's machine-readable error code. */ @@ -100,7 +131,7 @@ export async function readLinkJson(response: Response): Promise { if (!response.ok) { const error = isRecord(body) && isRecord(body.error) ? body.error : null; const code = error && typeof error.code === "string" ? error.code : "unknown"; - throw new LinkApiError(code, response.status); + throw new LinkApiError(code, response.status, boundLinkHint(error?.hint)); } if (body === null || body === undefined) throw new LinkApiError("invalid_body", response.status); return body as T; diff --git a/gui/src/styles-remote-link.css b/gui/src/styles-remote-link.css index 14c53706fa4..c8cb45d7a2b 100644 --- a/gui/src/styles-remote-link.css +++ b/gui/src/styles-remote-link.css @@ -35,6 +35,7 @@ .remote-link-row-meta { display: flex; flex-wrap: wrap; gap: var(--space-3); color: var(--muted); font-size: var(--text-caption); } .remote-link-row .btn { min-height: var(--control-touch); } .remote-link-error { color: var(--red); } +.remote-link-hint { display: block; margin-top: var(--space-1); color: var(--muted); font-size: var(--text-label); overflow-wrap: anywhere; } .remote-link-info { color: var(--muted); font-size: var(--text-label); } .remote-link-restart { display: grid; gap: var(--space-2); padding: var(--space-4); border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--raised); } .remote-link-restart p { margin: 0; color: var(--muted); } diff --git a/gui/tests/add-codex-account-device-code.test.tsx b/gui/tests/add-codex-account-device-code.test.tsx index 64f139d7e9f..da8cde355ec 100644 --- a/gui/tests/add-codex-account-device-code.test.tsx +++ b/gui/tests/add-codex-account-device-code.test.tsx @@ -141,6 +141,7 @@ test("a device login renders the short code, not just the verification URL", asy expect(code).toBeTruthy(); expect(code?.textContent).toBe(DEVICE_CODE); expect(host.textContent).toContain(DEVICE_URL); + expect(host.querySelector(".login-hint-paste")).toBeNull(); }); test("the default browser flow does not ask for a device login", async () => { diff --git a/gui/tests/add-provider-oauth-url-leak.test.tsx b/gui/tests/add-provider-oauth-url-leak.test.tsx index 347e314b2de..8b558fe5f33 100644 --- a/gui/tests/add-provider-oauth-url-leak.test.tsx +++ b/gui/tests/add-provider-oauth-url-leak.test.tsx @@ -149,6 +149,8 @@ function ProvidersOAuthHarness({ provider = "orcarouter-oauth", apiBase = "", on {busy ?? "idle"} {loginInfo?.url ?? "no-login-info"} + {loginInfo?.deviceCode ?? ""} + {loginInfo?.instructions ?? ""}