語言繁體中文

遠程會議

將 URL 和另一個個人 AirCode Ø 實例的範圍代幣保存,然後在 新會話中選擇該連接 → 在上運行。 遠程實例呼籲其自己的連接 Claude Code, Codex 或其他可用的本地提供商。 您的當前客戶端在遠程代理工作時顯示對話和終端。

選擇項目居住的地方。 使用 項目位置 → 此實例,A 擁有項目 P1 和 B 使用 B 的提供商帳戶運行代理。 B 通過工作空間工具請求所需的文件; A 讀取、編輯和運行實際 P1 目錄中的命令。 沒有克隆或最終同步。 請求的代碼和命令輸出達到 B 及其 AI 提供商。 提供商憑證和使用會計留在B。 依賴,構建和測試保持在A。

A 打開對 B 的驗證的 WSS 連接,該連接在兩個方向上傳遞工具調用。 機器可以位於不同的網絡或數據中心。 A 不需要入口端口或公共地址; B 需要一個可訪問的 HTTPS 實例 URL,其代理必須支持 WebSocket 升級。 兩種實例都必須支持連接的工作區。 關閉瀏覽器或移動視圖將留下該服務器連接的位置。 必須繼續運行,以便代理人使用其項目。

項目位置 → 遠程實例 保留了另一個模式:代理人和代幣的授權項目都在 B。 這兩種模式是明確的;選擇B上的類似名稱項目並不意味着P1上的A。

連接您的兩個實例

  1. 在遠程實例中,連接您的本地提供商帳戶 AI 使用。
  2. 開啟 設定 → AI gateway,在手機上開啟 更多 → AI gateway。在 建立存取權 下啟用 允許私人連線。在 建立權杖 中保留預設的 另一個執行個體,為連線命名,再選擇 建立連線權杖。預設情況下,檔案和命令留在 A,代理程式使用 B 的 AI 帳戶。若要處理 B 的檔案,請開啟 要在哪裡處理檔案和執行命令?,選擇 此執行個體 與 B 上的專案。每個工作階段選擇 可在同一個 B 專案邊界內使用兩種位置。
  3. 複製其一次性披露的代幣和顯示的 Base URL。
  4. 在 A 開啟 設定 → AI gateway → 新增另一個伺服器。輸入連線名稱、B 的執行個體 URL 或 Base URL,以及權杖,然後選擇 儲存連線。也可以從 新工作階段 → 執行於 → 新增遠端執行個體… 新增。
  5. 在 新會話中,選擇在 上運行中保存的連接。 檢查一下 項目位置 默認爲 此實例允許代幣時 。 保持或選擇 P1 在 項目下,此實例中,選擇 → 模型 → 努力 → 許可證,輸入提示,然後選擇 開始會話。 目前的工作區適配器支持 Claude Code 和 Codex;可用 選項來自B的現場目錄。 其他發動機保持遠程項目模式。

該代幣被髮送一次到您的當前服務器進行加密存儲。 Web 和本地客戶端隨後使用連接標識符。 網站永遠不會收到 URL 或代幣。 使用 HTTPS 用於互聯網連接或您信任的私人網絡。 網關代幣是個人身份證,不是共享鏈接。

工作區橋和其 MCP 工具只啓動 項目位置 → 此實例 與遠程代理。 通常的會話不會開始任何這些服務。 關閉最後一個分佈式會話會阻止私人橋樑傾聽器;僅關閉其瀏覽器或移動視圖會讓該會話運行。

Codex 目前需要本地文件支持登錄 (auth.json) 在 B 這個模式;僅鍵入登錄被明確拒絕。 其私人配置使用鏈接到原始帳戶和對話文件,而不復制 OAuth 值。 CLI 兼容性和機器管理策略仍然適用。

使用遠程代理工作

嚮應用程序提示並問任何問題

打開 A 上的任何一扇門,打開 模型 (編譯器中的代理設置芯片),然後在下方選擇保存的 B 連接 奔跑. 從 B 的目錄中選擇 → 模型 → 努力 → 許可證 。 您也可以使用 **添加遠程實例…**保存連接。 兩種實例都需要一個版本廣告 conversationWorkspace: 1;缺少範圍或不可用 B 塊啓動。

對於 嚮應用程序的提示,請描述應用程序,並如往常確認其應用程序/項目名稱。 項目、保存的應用程序、開發主管和預覽屬於A。 後續消息、視覺編輯和選擇的屏幕截圖區域將轉到B代理。 計劃首先 →單個代理 可以提交其 Plan 檢查點 A; 匯聚目前需要 此實例。

對於 問任何事,輸入您的問題和任何文件。 其私人對話文件夾和 文件 庫存屬於 A。 附加的圖像和PDF可以通過工作區工具讀取。 轉換爲項目 將 A 文件夾複製爲普通 A 項目。

這些門保持普通的對話身份和歷史。 打開結束的對話並選擇 簡歷 明確恢復了 B 的同一本地背景;重新連接網絡只會重新連接讀者。 附件、命令輸出和私人增量轉錄在 A 上仍然可用。 代理設置可以更改 A 的工作空間權限和 B 的模型/努力。 終端顯示 B 並且只讀;命令在 A 上運行並顯示在 Chat 中。 瀏覽器、規劃和 Knowledge 工具通過對話的現有 A 功能,可根據需求進行曝光。 沒有助手或橋樑通過此功能添加到普通的本地對話中。

聊天 顯示新的轉錄事件和當前的權限/問題對話。 發送後續消息,回答對話,或使用 停止 來中斷當前的響應。 在連接工作區模式中,權限由A執行:只讀模式拒絕寫作和命令;正常模式在這個對話中要求;編輯批准模式允許文件編輯,但要求殼命令;繞過允許所請求的操作。 單獨的新會話遠程工作區表單在啓動時修復允許模式;上面兩個對話門暴露了A的正常允許控制。 代理終端 (僅可讀) 顯示 B 的實際供應商輸出;項目命令通過工作區工具在 A 上運行,並在聊天中流出其輸出。 在遠程項目模式中, 終端 也接受本地輸入和調整大小。

終端通過WebSocket傳輸實時輸出。 聊天使用連續的 SSE 響應,並在服務提供商編寫其轉錄時發佈事件;一些服務提供商記錄一個完成的消息,而不是每個代幣。 這並不保證每個 CLI的代幣級別聊天更新。 單獨的單獨文本的 /chat/completions API 仍然使用 緩衝 SSE。

網絡重新連接從最後一個轉錄方程式中讀取。 它從來沒有自動啓動另一個代理或重新發送消息。 經過不確定啓動後,重新打開 新會話,選擇連接,然後在 遠程對話下查看。 同一請求的明確重複保持其意圖ID。 不要更改ID,僅僅是爲了繞過不確定的結果。

關閉視圖只會讓其讀者分離。 刪除已保存的連接會關閉其工作區訪問,並停止其在 A 上擁有的命令;它會保留 B 的對話。 閉合會話 停止代理和其所有命令;其轉錄仍然保留在遠程實例中,可重新打開讀取。 啓動一個新的會話,在關閉後再次運行代理。 已保存的遠程對話在新會話中列出他們的連接。 本段落描述了單獨的新會話遠程表單。 請求應用程序和問任何事,保持其正常的歷史和明確的複製行動在A。

範圍、撤銷和限制

為 應用程式 建立的權杖僅允許透過 AI API 處理文字與圖片。選擇 另一個執行個體 時,預設授予對啟動工作階段的執行個體工作區的存取權。建立權杖時選擇 此執行個體 與一個專案,則僅允許在該專案啟動工作階段,並存取使用該權杖建立的對話。它不授予擁有者登入、設定、其他對話、任意擁有者檔案 API 或權杖管理權限。代理程式工具使用啟動時選擇的權限;選擇專案並非作業系統層級的隔離。

新建立的 另一個執行個體 權杖預設包含連線工作區的存取權;舊權杖不會自動取得此權限。 每個在 A 上發射將一個項目目錄身份、連接、權限模式和意圖聯繫起來。 B 沒有 API 來改變這個根。 文件路徑是相對的,拒絕了外部符號鏈接,並且需要在讀取文件時觀察到的 SHA-256。 永久性或模糊的編輯被拒絕。 批准的殼命令具有A的用戶權限;項目 cwd不是操作系統沙盒。

工作空間效應具有持久的收入。 重新連接只重置相同的操作身份,並返回其記錄的結果。 在某個效果中發生服務器故障時,報告不確定性,而不是重新啓動它。 當 A 的服務器退出,訪問被撤銷或會話關閉時,所有命令會停止。 在請求新命令之前,檢查不確定命令的效果。 重新啓動任何一個實例永遠不會啓動另一個提供商或重新啓動提示。

代幣在90天后到期。 撤銷代幣或禁用遠程網關會刪除其會話權限並停止其代理。 每一次操作和邊緣背景檢查都檢查到過期,包括服務器重新啓動後。 刪除已保存的連接不會取消原始代幣:如果您想要取消權限,則在發行實例中取消該代幣。

每個發行實例最多有 20 個活躍的網關會話和每條代幣的 4 個直播讀者,在網關上有 20 個讀者。 每個打開的聊天使用一個讀者;打開的終端可以添加另一個。 這些是本地資源限制,而不是訂閱配額。 供應商配額和兼容性要求仍然適用;沒有提供商 CLI 通過連接安裝或更新。

連接的工作區在 B 上添加最多 20 個持久工具渠道,每代幣 4 個,每工作區有 4 個飛行調用和 4 個運行項目命令。 A 允許 20 個活躍的工作區和 20 個命令。 框架限制在 2 MiB,常規文件 / 編輯結果在 8 MiB,呼叫到每工作空間20,000,命令到十分鐘。 工具響應頁面文件內容,並明確標記剪切的命令尾巴。 閱讀頁面最多爲 128 KiB;寫 / 補丁文本參數有自己的方案限制,必須在一個框架內放置。 這些是本地資源的侷限性;沒有創建額外的公共來源。

會話 API 用於個人後端或 AI 工具

會話 OpenAPI 方案 描述了該協議。 在發行實例的 /api/ai-gateway/v1 Base URL的每個調用中使用 Authorization: Bearer TOKEN 。 拒絕重定向;永遠不要使用 URL/ Query 憑據。 不要將代幣放入提示,源代碼或瀏覽器存儲。

終端點 的含義
GET /session-catalog 協議版本,可選授權項目, workspaceAccess,以及現場引擎/模型/努力/許可目錄
POST /sessions 啓動本地代理;需要 UUID intention, engine, model, permissionMode, initialPrompt;可選 effort, name
GET /sessions 多達 100 次由此代幣創建的最近的對話
GET /sessions/{id} 該代幣所擁有的對話的當前總結
POST /sessions/{id}/resume UUID intention, initialPrompt,可選的 model/effort的對話工作區的明確延續;相同的本地 B 背景
POST /sessions/{id}/events 連續 SSE; JSON { "offset": null } 最初,然後在重新連接時返回的最後一個抵消
POST /sessions/{id}/transcript 閱讀一個已命名的頁面;發送 offset 用於更新的事件或 before 用於更早的消息
POST /sessions/{id}/input 追蹤: UUID intention 和非空的 text
POST /sessions/{id}/interaction UUID intention,當前對話對話 id,以及支持的選項/文本/拒絕答案
POST /sessions/{id}/command UUID intention 和 action: interrupt, close, model, effort 或 permission;選擇更改需要 arg 從目錄
GET /sessions/{id}/terminal 身份驗證 WebSocket 升級;原始終端輸出, JSON 輸入 { "t": "i", "d": "text" }, 更大 { "t": "r", "c": 100, "r": 30 }
GET /workspaces/{id} 通過原始工作區 UUID恢復 B 會話;從未在丟失的響應上創建替換
GET /workspaces/{id}/channel A 在啓動前打開已驗證的 WebSocket 到 B; B 發送工作空間工具調用,A 返回其結果

對於內置的 A 工作區橋,使用 A 的所有者身份驗證 API 從當前客戶端: 保存 B 與 POST /api/ai-gateway/connections發現選擇與 GET /api/engines?remoteConnectionId=ID&workspaceLocation=local然後打電話 POST /api/ai-gateway/connections/ID/sessions 與 localProject: "P1" 以及目錄選擇。 A 提供工作區身份和逆轉頻道。 返回的 agremote_… 名稱打開 A 的 /api/ai-gateway/remote-sessions/NAME/events 和 /interaction,包括 A 的批准。 B的網關代幣不能授權這些所有者終端點。 下面的例子顯示了這一完整的區別。

對於 嚮應用程序 和 請問任何東西,使用正常的所有者認證的 POST /api/sessions 在 A 與 remoteConnectionId: "SAVED_CONNECTION_ID" 和 B 的目錄選擇。 使用 askAnything: true 來提問,或 project 和 createMode: true 來提問現有應用程序項目。 創建一個新的應用程序首先使用 POST /api/create-mode/projects/confirmed 與 prompt, appName 和 projectName。 在不確定發射時,將 idempotencyKey 保留下來。 答案有 A 的普通 tmuxName, sessionId 和 remoteAgent 元數據。 跟蹤,轉錄,工作空間和簡歷使用A的正常對話API;永遠不要替代A的所有者憑證的網關代碼。

如果要實現單獨的連接服務器,請參閱 工作區協議和工具方案。 在 workspace: { id: UUID, project: "P1" } 上運行 B 時,只有在實現頻道、粘貼本地根、權限和持久操作收據後才能啓動。 標籤本身不提供文件訪問權限。 B 的終端輸入和原生權限更改在這些會話中被禁用。

一個 SSE update 包含 { id, project, session, transcript }。 transcript.events 是新事件頁面, offset 是其不透明整數路徑, interaction 或 terminalAttention 描述當前輸入需求。 保持完整的 SSE 框架在網絡碎片上。 忽略評論 保守 在 error 事件中,保留對話並重新連接以便閱讀。 永遠不要推斷一個操作並非來自失去的網絡響應。

許可模式、模型ID和努力來自 /session-catalog,而不是文本 API的複合模型ID。 會話創建和操作拒絕不受支持的字段。 初始提示和後續內容僅限於64 KiB。 發射意圖被分爲代幣;行動意圖被分爲會話。 不同內容的意圖重複使用被拒絕。 一旦嘗試運行時交付,未知的結果仍然是 AI_GATEWAY_SESSION_UNCERTAIN ,不能自動重播。

對於配額牆,等待供應商的實際恢復;未知的重置時間仍然未知。 對於 AI_GATEWAY_SESSION_ATTENTION,回答當前對話框。 對於被撤銷/過期訪問,在發行實例上創建一個新的範圍代幣。 缺少的模型,更新目錄。 現有所有者對話不能通過新代幣。

請點擊 app 並在 A 上詢問任何東西

相同的所有者 API 將文件、歷史和預覽保存在 A 上,而保存的 B 連接運行代理。

// Node.js 22+. Private backend only; never send these credentials to the website.
const origin = new URL(process.env.AIRCODE_LOCAL_ORIGIN);
const ownerToken = process.env.AIRCODE_LOCAL_AUTH_TOKEN;
const connectionId = process.env.AIRCODE_REMOTE_CONNECTION_ID;
const idempotencyKey = process.env.AIRCODE_REMOTE_INTENTION;
if (!['https:', 'http:'].includes(origin.protocol) || origin.username || origin.password
    || origin.pathname !== '/' || origin.search || origin.hash
    || !ownerToken || !connectionId || !idempotencyKey) throw new Error('Configure A, its saved B connection and one retained intention UUID');
async function call(path, body) {
  const response = await fetch(origin.origin + '/api' + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error',
    headers: { authorization: 'Bearer ' + ownerToken, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('A refused the operation: HTTP ' + response.status);
  return response.json();
}
const catalog = await call('/engines?remoteConnectionId=' + encodeURIComponent(connectionId) + '&workspaceLocation=local');
const engine = process.env.AIRCODE_REMOTE_ENGINE, model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from B’s catalogue');
}
const selection = { remoteConnectionId: connectionId, engine, model, permissionMode,
  ...(process.env.AIRCODE_REMOTE_EFFORT ? { effort: process.env.AIRCODE_REMOTE_EFFORT } : {}) };
// Set AIRCODE_APP_PROJECT only for an existing A application project.
// To create one first, POST /create-mode/projects/confirmed with confirmed
// { projectName, appName, prompt }, then retain its returned name.
const project = process.env.AIRCODE_APP_PROJECT;
const session = await call('/sessions', { ...selection, idempotencyKey,
  ...(project ? { project, createMode: true } : { askAnything: true }),
  initialPrompt: process.env.AIRCODE_PROMPT || 'Explain how this distributed conversation works.',
});
console.log('Conversation on A:', session.tmuxName, 'Agent:', session.remoteAgent.instanceName);
// Read-only observation; never replay the launch or prompt to reconnect.
const transcript = await call('/sessions/' + encodeURIComponent(session.tmuxName) + '/transcript');
for (const event of transcript.events) if (event.text) console.log(event.text);
// Poll the same route with ?offset=LAST_OFFSET for incremental updates.
// Send an explicit follow-up to POST /sessions/NAME/input with { text }.
// Resume explicitly with POST /sessions { project: session.project,
//   resumeSessionId: session.sessionId, initialPrompt: 'Continue' }.

項目在A,代理在B

在私人後端中使用A的所有者會話憑證。 B 的網關代幣已經存儲在 A 上,不能取代這個所有者憑證。 選擇目錄值,並保留一個啓動意圖 UUID。

import { randomUUID } from 'node:crypto';

// A owns P1. B supplies the agent/account. Never send these values to the website.
// Save B once in A's Settings → AI gateway → Remote instances.
const origin = process.env.AIRCODE_LOCAL_ORIGIN;
const ownerToken = process.env.AIRCODE_LOCAL_AUTH_TOKEN;
const connectionId = process.env.AIRCODE_REMOTE_CONNECTION_ID;
if (!origin || !ownerToken || !connectionId) throw new Error('Configure A and its saved connection to B');
const url = new URL(origin);
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password
    || url.search || url.hash || url.pathname !== '/') throw new Error('Use A’s instance origin');
async function call(path, body, signal) {
  const response = await fetch(url.origin + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error', signal,
    headers: { authorization: 'Bearer ' + ownerToken, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('A refused the operation: HTTP ' + response.status);
  return response;
}
const id = encodeURIComponent(connectionId);
const catalog = await (await call('/api/engines?remoteConnectionId=' + id + '&workspaceLocation=local')).json();
const engine = process.env.AIRCODE_REMOTE_ENGINE;
const model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from B’s catalogue');
}
// Persist the intention before launch and reuse it if its response is lost.
const intention = process.env.AIRCODE_REMOTE_INTENTION;
if (!intention) throw new Error('Set AIRCODE_REMOTE_INTENTION to a new UUID once, then retain it');
const session = await (await call('/api/ai-gateway/connections/' + id + '/sessions', {
  intention, localProject: process.env.AIRCODE_LOCAL_PROJECT || 'P1',
  engine, model, permissionMode, effort: process.env.AIRCODE_REMOTE_EFFORT || '',
  initialPrompt: 'Read the project instructions, then explain which machine owns its files.',
})).json();
console.log('Conversation on A:', session.tmuxName);
const path = '/api/ai-gateway/remote-sessions/' + encodeURIComponent(session.tmuxName);
// Approvals from A appear as transcript.interaction with id "workspace:…".
// Only after the owner chooses an answer, send it to A:
async function answerCurrentInteraction(interactionId, option) {
  return call(path + '/interaction', { intention: randomUUID(), id: interactionId, option });
}
// This bounded reader displays complete events and pending owner decisions.
// It never approves an operation automatically.
const signal = AbortSignal.timeout(30_000);
const stream = await call(path + '/events', { offset: null }, signal);
const decoder = new TextDecoder(); let buffer = '';
try {
  for await (const chunk of stream.body) {
    buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, '\n');
    let boundary;
    while ((boundary = buffer.indexOf('\n\n')) >= 0) {
      const frame = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2);
      const data = frame.split('\n').filter(line => line.startsWith('data:'))
        .map(line => line.slice(5).trimStart()).join('\n');
      if (!data) continue;
      const update = JSON.parse(data);
      console.log(JSON.stringify(update));
      // Display update.transcript.interaction and let the owner choose an answer.
    }
  }
} catch (error) { if (!signal.aborted) throw error; }
// Disconnecting the reader leaves the agent and workspace connection running.
// An explicit POST path + '/command' with {intention:UUID,action:'close'} stops them.

在B的項目現場會議

Node.js 22 或更高版本 將 AIRCODE_REMOTE_BASE_URL 和 AIRCODE_REMOTE_TOKEN 設置爲私人服務器配置。 第一個運行將打印目錄,直到您還選擇 AIRCODE_REMOTE_ENGINE、AIRCODE_REMOTE_MODEL 和 AIRCODE_REMOTE_PERMISSION。 AIRCODE_REMOTE_EFFORT 是可選的。 讀者在30秒後關閉;遠程代理仍在運行。

import { randomUUID } from 'node:crypto';

// Node.js 22+. Configure these privately on your application's server.
const base = process.env.AIRCODE_REMOTE_BASE_URL;
const token = process.env.AIRCODE_REMOTE_TOKEN;
if (!base || !token) throw new Error('Configure the remote Base URL and token');
const url = new URL(base);
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password
    || url.search || url.hash || url.pathname !== '/api/ai-gateway/v1') {
  throw new Error('Use the instance Base URL without a trailing slash');
}
async function call(path, body, signal) {
  const response = await fetch(base + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error', signal,
    headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('Remote request refused: HTTP ' + response.status);
  return response;
}
const catalog = await (await call('/session-catalog')).json();
const engine = process.env.AIRCODE_REMOTE_ENGINE;
const model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from this catalogue');
}
// Keep this intention for an explicit retry of this exact launch request.
const intention = process.env.AIRCODE_REMOTE_INTENTION || randomUUID();
const session = await (await call('/sessions', {
  intention, engine, model, permissionMode,
  effort: process.env.AIRCODE_REMOTE_EFFORT || '',
  initialPrompt: 'Describe the project without changing its files.',
})).json();
console.log('Remote conversation:', session.id);
// Demonstrate a bounded live reader. Disconnecting leaves the agent running.
const signal = AbortSignal.timeout(30_000);
const stream = await call('/sessions/' + session.id + '/events', { offset: null }, signal);
const decoder = new TextDecoder();
try {
  for await (const chunk of stream.body) process.stdout.write(decoder.decode(chunk, { stream: true }));
} catch (error) { if (!signal.aborted) throw error; }