From 979d45bca4f307f44c0b63f922c296de38f02421 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Mon, 13 Apr 2026 22:06:05 +0800 Subject: [PATCH] docs(websocket): add integration guides --- docs/configuration/websocket.md | 659 ++++++++++++++++++ .../current/configuration/websocket.md | 659 ++++++++++++++++++ .../current/configuration/websocket.md | 659 ++++++++++++++++++ 3 files changed, 1977 insertions(+) create mode 100644 docs/configuration/websocket.md create mode 100644 i18n/pt-BR/docusaurus-plugin-content-docs/current/configuration/websocket.md create mode 100644 i18n/zh-Hans/docusaurus-plugin-content-docs/current/configuration/websocket.md diff --git a/docs/configuration/websocket.md b/docs/configuration/websocket.md new file mode 100644 index 0000000..bff9042 --- /dev/null +++ b/docs/configuration/websocket.md @@ -0,0 +1,659 @@ +--- +id: websocket +title: Connect WebSocket +--- +## PicoClaw WebSocket Integration Guide + +> Scope: current repository behavior as of 2026-04-13. +> This document is for third-party developers integrating with PicoClaw WebSocket through Launcher, including browser extensions, frontend pages running on the Launcher origin, desktop clients, and server-side clients. + +Since v0.2.5, PicoClaw WebSocket connections require two layers of validation. External clients should connect to Launcher on port `18800`, not directly to Gateway on port `18790`. + +## Three Things To Remember First + +1. The public entry point is `ws(s):///pico/ws`. +2. The handshake must pass Launcher auth and also provide a Pico token. +3. Browsers are the easiest way to test because they automatically send the logged-in session cookie. + +## Version Strategy + +- Build and test third-party integrations against release tags. +- Do not build production integrations against `main`; it changes frequently and compatibility is not guaranteed. +- When upgrading PicoClaw, pin both the PicoClaw version and the version of the documentation you rely on. + +We recommend recording these explicitly in your own project: + +1. The PicoClaw tag or release version +2. The version of this integration document +3. The minimum and maximum PicoClaw versions you support + +## Architecture And Auth Model + +In Launcher mode, the connection path is: + +- Client -> `ws(s):///pico/ws` +- Launcher -> reverse proxy to the Gateway Pico channel + +There are currently two auth layers: + +1. Launcher auth layer + Uses a logged-in session or `Authorization: Bearer ` on server-side requests. +2. Pico WebSocket auth layer + Uses `Sec-WebSocket-Protocol: token.`. + +Key points: + +- `/pico/ws` is not an anonymous WebSocket endpoint; it is protected by Launcher. +- After a successful Launcher login, Launcher writes the `picoclaw_launcher_auth` cookie. +- `picoclaw_launcher_auth` proves that this browser has already logged in to Launcher, which satisfies the first auth layer. +- This cookie is a session credential. You do not manually inject it into browser code; the browser sends it automatically on same-origin requests. +- Launcher converts `token.` into the internal format before forwarding to Gateway, so clients do not need to care about PID token composition. + +### Auth Architecture + +```text +┌─────────────────────────────────────────────────────────────────┐ +│ Launcher (18800) │ +├─────────────────────────────────────────────────────────────────┤ +│ First auth layer: Launcher login state │ +│ - Browser session cookie: picoclaw_launcher_auth │ +│ - Or Authorization: Bearer │ +├─────────────────────────────────────────────────────────────────┤ +│ Second auth layer: Pico WebSocket auth │ +│ - Sec-WebSocket-Protocol: token. │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Gateway (18790) │ +│ Launcher handles internal token conversion and forwarding │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Standard Integration Flow + +### Step 1: Log In To Launcher First + +For browser users, the simplest way is to open: + +```text +http://your-host:18800?token= +``` + +You can also call the login endpoint explicitly: + +```http +POST /api/auth/login +Content-Type: application/json + +{ + "token": "" +} +``` + +After login succeeds, Launcher sets the session cookie `picoclaw_launcher_auth`. After that, this browser will automatically send the login state when accessing `/api/pico/token` and `/pico/ws`. + +> You can obtain `dashboard_token` in these ways: +> - Check Launcher startup logs +> - Set `PICOCLAW_LAUNCHER_TOKEN` +> - Configure it in `~/.picoclaw/launcher.json` + +### Step 2: Get The Pico Token And `ws_url` + +This is the part that tends to confuse non-developers. What you actually do is: log in to Launcher in the browser first, then run a small `fetch` snippet in that same browser context to get `token` and `ws_url`. + +Important: the browser examples below assume your code is running on the Launcher origin, for example directly on the Launcher page. If your frontend is hosted on a different origin, the relative path `/api/pico/token` will target your own site, not Launcher. + +#### Method A: Verify In The Browser Address Bar + +Open this in the browser: + +```text +http://127.0.0.1:18800/api/pico/token +``` + +You may see one of these results: + +- A JSON response directly, which means you are already logged in and can continue. +- A redirect to the login page, which means Launcher is not logged in yet or the login session has expired. +- A `401`, which means the current login state is invalid. + +Expected response: + +```json +{ + "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ws_url": "ws://127.0.0.1:18800/pico/ws", + "enabled": true +} +``` + +#### Method B: Run It In Browser DevTools Console + +If you are following the code samples, they are usually meant to be run in the browser console, not in your system terminal. + +Also note that browser `fetch()` usually follows redirects automatically. So if the login state is invalid, you may not see a raw `302` in JavaScript. Instead, the request may end up on the login page and then fail when you try to parse HTML as JSON. + +Steps: + +1. Open the Launcher page, for example `http://127.0.0.1:18800/`. +2. Press `F12` to open DevTools. +3. Switch to the `Console` tab. +4. If Chrome blocks the first paste, type `allow pasting` manually and press Enter. +5. Then paste and run the following code. + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`Request was redirected to ${response.url}. Log in to Launcher / Dashboard first.`); +} + +if (response.status === 401) { + throw new Error('You need to log in to Launcher / Dashboard first'); +} + +if (!response.ok) { + throw new Error(`Failed to get Pico token: ${response.status}`); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`Expected JSON from /api/pico/token, got ${contentType || 'unknown content type'}`); +} + +const data = await response.json(); +console.log(data); +``` + +If the console prints something like this, the step succeeded: + +```json +{ + "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ws_url": "ws://127.0.0.1:18800/pico/ws", + "enabled": true +} +``` + +If `enabled` is `false`, the Pico WebSocket channel has not been enabled yet. + +### Step 3: Open The WebSocket Connection + +Connection URL: + +```text +?session_id= +``` + +Subprotocol: + +```text +token. +``` + +We recommend generating your own stable and traceable `session_id`, such as `browser-demo` or `node-test-001`. + +## Browser Integration Examples + +Native browser `WebSocket` usually cannot customize the `Authorization` header, so browser clients should log in to Launcher first and then rely on the same-origin cookie during the handshake. + +That same-origin requirement matters. The browser examples in this section are valid when your code runs on the Launcher page origin. They are not drop-in examples for a third-party web app hosted on another origin. + +### Chrome Extensions Need Extra Care + +If you are developing a Chrome extension, distinguish between these two execution contexts: + +- A script injected into the Launcher page +- The extension's own `popup`, `side panel`, or `service worker` + +The first case behaves much more like a normal page script and is the easiest way to reuse the Launcher page login state. + +The second case comes from `chrome-extension://...`, which is not the same origin as Launcher. That means even if you already logged in to Launcher in the browser, you should not assume that `fetch('/api/pico/token')` inside the extension popup will work the same way as in a normal page. + +If your goal is protocol debugging or quick testing, prefer one of these approaches: + +1. Open DevTools directly on the Launcher page and run the sample code there. +2. Let the extension inject a test script into the Launcher page instead of trying to connect directly from the extension popup. + +### Minimal Connect Example + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`Request was redirected to ${response.url}. Log in to Launcher first.`); +} + +if (response.status === 401) { + throw new Error('Launcher login state is invalid'); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`Expected JSON from /api/pico/token, got ${contentType || 'unknown content type'}`); +} + +const { token, ws_url, enabled } = await response.json(); + +if (!enabled) { + throw new Error('Pico WebSocket channel is not enabled'); +} + +const sessionId = 'browser-demo'; +const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], +); + +ws.onopen = () => { + console.log('WebSocket connected'); +}; + +ws.onmessage = (event) => { + console.log('Received message:', JSON.parse(event.data)); +}; + +ws.onerror = (error) => { + console.error('WebSocket error:', error); +}; + +ws.onclose = () => { + console.log('WebSocket closed'); +}; +``` + +### What To Do After Connecting + +Explaining only how to connect is not enough. After the handshake succeeds, you should at least know how to send one message and where to look for the response. + +This is a minimal interactive flow: + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`Request was redirected to ${response.url}. Log in to Launcher first.`); +} + +if (response.status === 401) { + throw new Error('Launcher login state is invalid'); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`Expected JSON from /api/pico/token, got ${contentType || 'unknown content type'}`); +} + +const { token, ws_url, enabled } = await response.json(); + +if (!enabled) { + throw new Error('Pico WebSocket channel is not enabled'); +} + +const sessionId = 'browser-demo'; +const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], +); + +ws.onopen = () => { + console.log('connected'); + + ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: sessionId, + timestamp: Date.now(), + payload: { + content: 'Hello PicoClaw!', + }, + })); +}; + +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + console.log('server -> client', message); +}; +``` + +Watch for two things: + +1. Whether `connected` appears in the Console first. +2. Whether the server sends back messages such as `typing.start`, `message.create`, `message.update`, `error`, or `pong`. + +To test heartbeat, you can also send: + +```javascript +ws.send(JSON.stringify({ + type: 'ping', + id: `ping-${Date.now()}`, + timestamp: Date.now(), + payload: {}, +})); +``` + +Normally you should receive `pong`. + +### A More Complete Browser Example + +```javascript +class PicoClawClient { + constructor(sessionId) { + this.sessionId = sessionId; + this.ws = null; + } + + async connect() { + const response = await fetch('/api/pico/token'); + + if (response.redirected) { + throw new Error(`Request was redirected to ${response.url}. Log in to Launcher / Dashboard first.`); + } + + if (response.status === 401) { + throw new Error('You need to log in to Launcher / Dashboard first'); + } + + if (!response.ok) { + throw new Error(`Failed to get token: ${response.status}`); + } + + const contentType = response.headers.get('content-type') || ''; + if (!contentType.includes('application/json')) { + throw new Error(`Expected JSON from /api/pico/token, got ${contentType || 'unknown content type'}`); + } + + const { token, ws_url, enabled } = await response.json(); + + if (!enabled) { + throw new Error('Pico WebSocket channel is not enabled'); + } + + this.ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(this.sessionId)}`, + [`token.${token}`], + ); + + this.ws.onmessage = (event) => { + console.log('Received message:', JSON.parse(event.data)); + }; + + return new Promise((resolve, reject) => { + this.ws.onopen = () => resolve(); + this.ws.onerror = () => reject(new Error('WebSocket connection failed')); + }); + } + + sendText(content) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket is not connected'); + } + + this.ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: this.sessionId, + timestamp: Date.now(), + payload: { content }, + })); + } + + ping() { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket is not connected'); + } + + this.ws.send(JSON.stringify({ + type: 'ping', + id: `ping-${Date.now()}`, + timestamp: Date.now(), + payload: {}, + })); + } + + close() { + if (this.ws) { + this.ws.close(); + } + } +} + +const client = new PicoClawClient('browser-demo'); +await client.connect(); +client.sendText('Hello PicoClaw!'); +client.ping(); +``` + +## Non-Browser Client Notes + +For Node.js, Python, desktop apps, or tools like Postman that do not automatically carry browser cookies, the current integration experience is much rougher and needs an explicit note. + +### Current Observed Behavior + +- If you request `/api/pico/token` without a valid Launcher login state, you will usually get a `302` redirect to the login page. +- In the "password-based startup login" case, the older documentation path that used `dashboardToken` directly is not a good general solution. +- In current testing, the handshake flow uses `session_id`; it is not a simple "get dashboardToken and connect blindly" model. +- In other words, non-browser clients are not yet a smooth standalone integration path in the current version. This still needs more design and cleanup. + +### What To Do If You Get 302 + +`302` usually means Launcher thinks you are not logged in, or this client does not actually have a usable login session. + +Do not just retry WebSocket immediately. Check these three things first: + +1. You are using `18800`, not `18790`. +2. You have already logged in to Launcher in the browser. +3. Your non-browser client is actually sending the login state. + +With the current implementation, the easiest thing to miss is the session cookie. The browser may already have `picoclaw_launcher_auth`, but your Node.js process does not inherit that cookie automatically. + +### A Node.js Example That Better Matches Current Reality + +This example demonstrates the "reuse the browser session cookie" idea instead of pretending `dashboardToken` alone covers all cases. + +This is intentionally not presented as a supported end-to-end workflow, because the current docs do not define a clean, official way to export and reuse the browser session in a separate Node.js process. Treat the snippet below as a debugging sketch that explains why the request still gets `302`, not as a polished integration recipe. + +```javascript +const fetch = require('node-fetch'); +const WebSocket = require('ws'); + +async function connectPico() { + const launcherBaseUrl = 'http://127.0.0.1:18800'; + const sessionId = 'node-demo'; + + // Replace this with a valid Launcher session cookie only if you already have one + // through your own debugging setup. + // The current docs do not define an official cookie export workflow. + const launcherCookie = 'picoclaw_launcher_auth=replace-with-real-cookie'; + + const tokenResponse = await fetch(`${launcherBaseUrl}/api/pico/token`, { + headers: { + Cookie: launcherCookie, + }, + redirect: 'manual', + }); + + if (tokenResponse.status === 302 || tokenResponse.status === 401) { + throw new Error('Launcher auth failed: this client does not have a valid login session'); + } + + if (!tokenResponse.ok) { + throw new Error(`Failed to get Pico token: ${tokenResponse.status}`); + } + + const { token, ws_url, enabled } = await tokenResponse.json(); + + if (!enabled) { + throw new Error('Pico WebSocket channel is not enabled'); + } + + const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], + { + headers: { + Cookie: launcherCookie, + }, + }, + ); + + ws.on('open', () => { + console.log('connected'); + ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: sessionId, + timestamp: Date.now(), + payload: { + content: 'Hello from Node.js', + }, + })); + }); + + ws.on('message', (data) => { + console.log('server -> client', JSON.parse(data.toString())); + }); + + ws.on('error', (err) => { + console.error('WebSocket error:', err); + }); +} + +connectPico().catch(console.error); +``` + +### Why This Section Is Intentionally Conservative + +Because in the current version, the non-browser client path still has several real problems: + +- Login state is much more natural inside the browser than outside it. +- The relationship between `session_id` and Launcher login state is not exposed clearly enough. +- When users hit `302`, they may understand they need to log in to Dashboard, but without a follow-up explanation of how to carry that login state into Node.js, they still get stuck. + +So this document intentionally describes the current limitation and the failure mode, while also making it clear that the non-browser flow still needs follow-up design work. + +## Development And Debugging Advice + +If you are building an integration, do not start directly with Node.js or a desktop app. Debug in this order instead: + +1. Log in to Launcher in the browser. +2. Confirm `GET /api/pico/token` returns JSON in the browser. +3. Complete one WebSocket connection in the browser DevTools `Console`. +4. Send one `message.send` and confirm you receive a response. +5. Only after the browser path works should you move on to non-browser session reuse. + +The reason is simple: the browser already carries `picoclaw_launcher_auth`, which makes it much easier to separate auth issues from message protocol issues. + +## Message Protocol + +Common inbound message types (client -> server): + +- `message.send` +- `media.send` +- `ping` + +Common outbound message types (server -> client): + +- `message.create` +- `message.update` +- `typing.start` +- `typing.stop` +- `error` +- `pong` + +General message structure: + +```json +{ + "type": "message.send", + "id": "optional-id", + "session_id": "optional-session-id", + "timestamp": 0, + "payload": {} +} +``` + +Minimal send example: + +```json +{ + "type": "message.send", + "id": "req-1", + "payload": { + "content": "hello" + } +} +``` + +## Common Errors And Troubleshooting + +| Problem | Common Cause | What To Do | +|------|----------|----------| +| `302` redirect to the login page or `/launcher-login` | Launcher auth failed; not logged in or login state was not carried over | First confirm the browser is logged in, then confirm the current client really sends the login state | +| `401 Unauthorized` | Wrong port, or current auth method is not accepted | Use `18800`, do not connect directly to `18790` | +| `403 Invalid Pico token` | `token.<...>` in `Sec-WebSocket-Protocol` is wrong | Call `GET /api/pico/token` again and use the latest token | +| `503 Gateway not available` | Gateway is not running or Launcher did not attach successfully | Check Gateway state and retry | +| Connection succeeds but nothing happens | Only the handshake completed; no `message.send` or `ping` was sent | Follow the interaction examples and send a message explicitly | +| Origin validation failed | `channels.pico.allow_origins` does not match the request origin | Configure allowed origins explicitly and avoid `*` | + +## Ports And Tokens + +### Ports + +| Port | Purpose | Allow External Direct Access | +|------|------|------------------| +| `18800` | Launcher (Web UI + API + WebSocket proxy) | Yes, recommended | +| `18790` | Gateway (internal service) | No, not supported | + +### Tokens / Session + +| Name | Purpose | Notes | +|------|------|------| +| Dashboard Token | Log in to Launcher and access protected APIs | Useful as a Launcher login entry point, but should no longer be treated as the single credential for all client scenarios | +| `picoclaw_launcher_auth` | Launcher session cookie in the browser | Set by Launcher after login; browsers send it automatically on same-origin requests | +| Pico Token | WebSocket subprotocol authentication | Obtained from `GET /api/pico/token` | +| `session_id` | Session identifier for the current connection | Passed in the URL query, for example `?session_id=browser-demo` | + +## Security Recommendations + +1. Use HTTPS/WSS in production. +2. Do not expose Gateway port `18790`. +3. Do not enable `allow_token_query` by default. +4. Keep `allow_origins` as narrow as possible; do not use `*`. +5. Rotate Pico tokens regularly; you can regenerate them through `POST /api/pico/token`. +6. Keep Launcher bound locally whenever possible. If you need LAN or public exposure, pair it with `allowed_cidrs`. +7. Do not write tokens or session cookies into frontend persistence, browser logs, or plaintext server logs. + +## Configuration Example + +```json +{ + "channels": { + "pico": { + "enabled": true, + "token": "replace-with-strong-random-token", + "allow_token_query": false, + "allow_origins": ["https://your-app.example.com"], + "max_connections": 100 + } + } +} +``` + +## Recommendations For Third-Party Developers + +At a minimum, cover this flow in automated tests: + +1. Log in to Launcher +2. Get a Pico token +3. Open a WebSocket connection using `token.` +4. Send `message.send` +5. Verify that you receive either a response or an error message + +Once that flow is stable, move on to UI, session management, reconnection, and recovery strategies. + +## Documentation Maintenance Advice + +- Keep this WebSocket integration guidance as one primary document so that the general integration flow and the developer-focused caveats do not drift apart. +- If there are future changes around auth design discussion, Node.js-specific examples, or tutorial improvements, split them into separate PRs instead of mixing them together. diff --git a/i18n/pt-BR/docusaurus-plugin-content-docs/current/configuration/websocket.md b/i18n/pt-BR/docusaurus-plugin-content-docs/current/configuration/websocket.md new file mode 100644 index 0000000..e500bfa --- /dev/null +++ b/i18n/pt-BR/docusaurus-plugin-content-docs/current/configuration/websocket.md @@ -0,0 +1,659 @@ +--- +id: websocket +title: Conectar WebSocket +--- +## Guia de Integracao do WebSocket do PicoClaw + +> Escopo: comportamento atual do repositorio em 2026-04-13. +> Este documento e voltado para desenvolvedores terceiros que integrem com o WebSocket do PicoClaw via Launcher, incluindo extensoes de navegador, paginas frontend executadas na mesma origem do Launcher, clientes desktop e clientes server-side. + +Desde a v0.2.5, as conexoes WebSocket do PicoClaw exigem duas camadas de validacao. Clientes externos devem se conectar ao Launcher na porta `18800`, e nao diretamente ao Gateway na porta `18790`. + +## Tres Pontos Para Lembrar Primeiro + +1. O ponto de entrada publico e `ws(s):///pico/ws`. +2. O handshake precisa passar pela autenticacao do Launcher e tambem fornecer um token Pico. +3. O navegador e a forma mais facil de testar porque envia automaticamente o cookie de sessao do login. + +## Estrategia de Versao + +- Desenvolva e teste integracoes de terceiros com base em release tags. +- Nao use `main` para integracoes de producao; ela muda com frequencia e a compatibilidade nao e garantida. +- Ao atualizar o PicoClaw, fixe tanto a versao do PicoClaw quanto a versao da documentacao em que voce se apoia. + +Recomendamos registrar explicitamente no seu projeto: + +1. A tag ou versao de release do PicoClaw +2. A versao deste documento de integracao +3. As versoes minima e maxima do PicoClaw que voce suporta + +## Arquitetura E Modelo De Autenticacao + +No modo Launcher, o caminho da conexao e: + +- Cliente -> `ws(s):///pico/ws` +- Launcher -> proxy reverso para o canal Pico do Gateway + +Atualmente existem duas camadas de autenticacao: + +1. Camada de autenticacao do Launcher + Usa uma sessao autenticada ou `Authorization: Bearer ` em requisicoes server-side. +2. Camada de autenticacao do Pico WebSocket + Usa `Sec-WebSocket-Protocol: token.`. + +Pontos importantes: + +- `/pico/ws` nao e um endpoint WebSocket anonimo; ele e protegido pelo Launcher. +- Depois de um login bem-sucedido no Launcher, o Launcher grava o cookie `picoclaw_launcher_auth`. +- `picoclaw_launcher_auth` prova que esse navegador ja fez login no Launcher, satisfazendo a primeira camada de autenticacao. +- Esse cookie e uma credencial de sessao. Voce nao precisa injeta-lo manualmente no codigo do navegador; o navegador o envia automaticamente em requisicoes same-origin. +- O Launcher converte `token.` para o formato interno antes de encaminhar ao Gateway, entao o cliente nao precisa se preocupar com a composicao do PID token. + +### Arquitetura De Autenticacao + +```text +┌─────────────────────────────────────────────────────────────────┐ +│ Launcher (18800) │ +├─────────────────────────────────────────────────────────────────┤ +│ Primeira camada: estado de login do Launcher │ +│ - Cookie de sessao do navegador: picoclaw_launcher_auth │ +│ - Ou Authorization: Bearer │ +├─────────────────────────────────────────────────────────────────┤ +│ Segunda camada: autenticacao do Pico WebSocket │ +│ - Sec-WebSocket-Protocol: token. │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Gateway (18790) │ +│ O Launcher cuida da conversao interna do token e do proxy │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Fluxo Padrao De Integracao + +### Passo 1: Faca Login No Launcher Primeiro + +Para usuarios de navegador, a forma mais simples e abrir: + +```text +http://your-host:18800?token= +``` + +Voce tambem pode chamar explicitamente o endpoint de login: + +```http +POST /api/auth/login +Content-Type: application/json + +{ + "token": "" +} +``` + +Depois que o login funcionar, o Launcher define o cookie de sessao `picoclaw_launcher_auth`. A partir dai, esse navegador enviara automaticamente o estado de login ao acessar `/api/pico/token` e `/pico/ws`. + +> Voce pode obter `dashboard_token` destas formas: +> - Verificando os logs de inicializacao do Launcher +> - Definindo `PICOCLAW_LAUNCHER_TOKEN` +> - Configurando em `~/.picoclaw/launcher.json` + +### Passo 2: Obter O Token Pico E O `ws_url` + +Esta e a parte que costuma confundir usuarios nao tecnicos. O que voce realmente faz e: primeiro faz login no Launcher no navegador, depois executa um pequeno snippet `fetch` nesse mesmo contexto do navegador para obter `token` e `ws_url`. + +Ha um pre-requisito importante aqui: os exemplos de navegador abaixo assumem que seu codigo esta rodando na mesma origem do Launcher, por exemplo diretamente na pagina do Launcher. Se o seu frontend estiver hospedado em outro dominio, o caminho relativo `/api/pico/token` vai apontar para o seu proprio site, e nao para o Launcher. + +#### Metodo A: Verificar Na Barra De Endereco Do Navegador + +Abra isto no navegador: + +```text +http://127.0.0.1:18800/api/pico/token +``` + +Voce pode ver um destes resultados: + +- Uma resposta JSON diretamente, o que significa que voce ja esta logado e pode continuar. +- Um redirecionamento para a pagina de login, o que significa que o Launcher ainda nao esta logado ou a sessao expirou. +- Um `401`, o que significa que o estado atual de login e invalido. + +Resposta esperada: + +```json +{ + "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ws_url": "ws://127.0.0.1:18800/pico/ws", + "enabled": true +} +``` + +#### Metodo B: Executar No Console Do DevTools + +Se voce estiver seguindo os exemplos de codigo, eles normalmente devem ser executados no console do navegador, e nao no terminal do sistema. + +Observe tambem que o `fetch()` do navegador normalmente segue redirecionamentos automaticamente. Portanto, se o estado de login estiver invalido, voce pode nao ver o `302` bruto no JavaScript. O caso mais comum e a requisicao acabar na pagina de login e depois falhar em `response.json()` porque recebeu HTML em vez de JSON. + +Passos: + +1. Abra a pagina do Launcher, por exemplo `http://127.0.0.1:18800/`. +2. Pressione `F12` para abrir o DevTools. +3. Va para a aba `Console`. +4. Se o Chrome bloquear a primeira colagem, digite `allow pasting` manualmente e pressione Enter. +5. Depois cole e execute o codigo abaixo. + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`A requisicao foi redirecionada para ${response.url}. Faca login no Launcher / Dashboard primeiro.`); +} + +if (response.status === 401) { + throw new Error('Voce precisa fazer login no Launcher / Dashboard primeiro'); +} + +if (!response.ok) { + throw new Error(`Falha ao obter o token Pico: ${response.status}`); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`/api/pico/token deveria retornar JSON, mas retornou ${contentType || 'tipo de conteudo desconhecido'}`); +} + +const data = await response.json(); +console.log(data); +``` + +Se o console mostrar algo como isto, o passo funcionou: + +```json +{ + "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ws_url": "ws://127.0.0.1:18800/pico/ws", + "enabled": true +} +``` + +Se `enabled` for `false`, o canal Pico WebSocket ainda nao foi habilitado. + +### Passo 3: Abrir A Conexao WebSocket + +URL de conexao: + +```text +?session_id= +``` + +Subprotocolo: + +```text +token. +``` + +Recomendamos gerar seu proprio `session_id` estavel e rastreavel, como `browser-demo` ou `node-test-001`. + +## Exemplos De Integracao No Navegador + +O `WebSocket` nativo do navegador normalmente nao permite customizar o header `Authorization`, entao clientes de navegador devem primeiro fazer login no Launcher e depois depender do cookie same-origin durante o handshake. + +Esse requisito de same-origin importa. Os exemplos de navegador desta secao sao validos quando o codigo roda na mesma origem da pagina do Launcher. Eles nao sao exemplos prontos para copiar e colar em uma aplicacao web third-party hospedada em outra origem. + +### Extensoes Do Chrome Exigem Cuidado Extra + +Se voce estiver desenvolvendo uma extensao do Chrome, diferencie estes dois contextos de execucao: + +- Um script injetado na pagina do Launcher +- O `popup`, `side panel` ou `service worker` da propria extensao + +O primeiro caso se comporta muito mais como um script normal de pagina e e a forma mais simples de reutilizar o estado de login da pagina do Launcher. + +O segundo caso vem de `chrome-extension://...`, que nao e a mesma origem do Launcher. Isso significa que, mesmo que voce ja tenha feito login no Launcher no navegador, nao deve presumir que `fetch('/api/pico/token')` dentro do popup da extensao funcionara da mesma forma que em uma pagina normal. + +Se seu objetivo for depurar protocolo ou fazer um teste rapido, prefira uma destas abordagens: + +1. Abra o DevTools diretamente na pagina do Launcher e execute o codigo de exemplo ali. +2. Faca a extensao injetar um script de teste na pagina do Launcher em vez de tentar se conectar diretamente do popup da extensao. + +### Exemplo Minimo De Conexao + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`A requisicao foi redirecionada para ${response.url}. Faca login no Launcher primeiro.`); +} + +if (response.status === 401) { + throw new Error('O estado de login do Launcher e invalido'); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`/api/pico/token deveria retornar JSON, mas retornou ${contentType || 'tipo de conteudo desconhecido'}`); +} + +const { token, ws_url, enabled } = await response.json(); + +if (!enabled) { + throw new Error('O canal Pico WebSocket nao esta habilitado'); +} + +const sessionId = 'browser-demo'; +const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], +); + +ws.onopen = () => { + console.log('WebSocket conectado'); +}; + +ws.onmessage = (event) => { + console.log('Mensagem recebida:', JSON.parse(event.data)); +}; + +ws.onerror = (error) => { + console.error('Erro do WebSocket:', error); +}; + +ws.onclose = () => { + console.log('WebSocket fechado'); +}; +``` + +### O Que Fazer Depois De Conectar + +Explicar apenas como conectar nao basta. Depois que o handshake funcionar, voce deve pelo menos saber como enviar uma mensagem e onde olhar a resposta. + +Este e um fluxo interativo minimo: + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`A requisicao foi redirecionada para ${response.url}. Faca login no Launcher primeiro.`); +} + +if (response.status === 401) { + throw new Error('O estado de login do Launcher e invalido'); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`/api/pico/token deveria retornar JSON, mas retornou ${contentType || 'tipo de conteudo desconhecido'}`); +} + +const { token, ws_url, enabled } = await response.json(); + +if (!enabled) { + throw new Error('O canal Pico WebSocket nao esta habilitado'); +} + +const sessionId = 'browser-demo'; +const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], +); + +ws.onopen = () => { + console.log('connected'); + + ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: sessionId, + timestamp: Date.now(), + payload: { + content: 'Hello PicoClaw!', + }, + })); +}; + +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + console.log('server -> client', message); +}; +``` + +Observe duas coisas: + +1. Se `connected` aparece primeiro no Console. +2. Se o servidor responde com mensagens como `typing.start`, `message.create`, `message.update`, `error` ou `pong`. + +Para testar heartbeat, voce tambem pode enviar: + +```javascript +ws.send(JSON.stringify({ + type: 'ping', + id: `ping-${Date.now()}`, + timestamp: Date.now(), + payload: {}, +})); +``` + +Normalmente voce deve receber `pong`. + +### Exemplo Mais Completo No Navegador + +```javascript +class PicoClawClient { + constructor(sessionId) { + this.sessionId = sessionId; + this.ws = null; + } + + async connect() { + const response = await fetch('/api/pico/token'); + + if (response.redirected) { + throw new Error(`A requisicao foi redirecionada para ${response.url}. Faca login no Launcher / Dashboard primeiro.`); + } + + if (response.status === 401) { + throw new Error('Voce precisa fazer login no Launcher / Dashboard primeiro'); + } + + if (!response.ok) { + throw new Error(`Falha ao obter o token: ${response.status}`); + } + + const contentType = response.headers.get('content-type') || ''; + if (!contentType.includes('application/json')) { + throw new Error(`/api/pico/token deveria retornar JSON, mas retornou ${contentType || 'tipo de conteudo desconhecido'}`); + } + + const { token, ws_url, enabled } = await response.json(); + + if (!enabled) { + throw new Error('O canal Pico WebSocket nao esta habilitado'); + } + + this.ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(this.sessionId)}`, + [`token.${token}`], + ); + + this.ws.onmessage = (event) => { + console.log('Mensagem recebida:', JSON.parse(event.data)); + }; + + return new Promise((resolve, reject) => { + this.ws.onopen = () => resolve(); + this.ws.onerror = () => reject(new Error('Falha na conexao WebSocket')); + }); + } + + sendText(content) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('O WebSocket nao esta conectado'); + } + + this.ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: this.sessionId, + timestamp: Date.now(), + payload: { content }, + })); + } + + ping() { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('O WebSocket nao esta conectado'); + } + + this.ws.send(JSON.stringify({ + type: 'ping', + id: `ping-${Date.now()}`, + timestamp: Date.now(), + payload: {}, + })); + } + + close() { + if (this.ws) { + this.ws.close(); + } + } +} + +const client = new PicoClawClient('browser-demo'); +await client.connect(); +client.sendText('Hello PicoClaw!'); +client.ping(); +``` + +## Notas Sobre Clientes Nao Navegador + +Para Node.js, Python, apps desktop ou ferramentas como Postman, que nao carregam automaticamente cookies do navegador, a experiencia atual de integracao ainda e bem mais dificil e precisa ser explicada de forma explicita. + +### Comportamento Observado Atualmente + +- Se voce requisitar `/api/pico/token` sem um estado de login valido do Launcher, normalmente recebera um `302` redirecionando para a pagina de login. +- No caso de "login por senha no startup", o fluxo mais antigo da documentacao que usava `dashboardToken` diretamente nao e uma boa solucao geral. +- Nos testes atuais, o handshake usa `session_id`; nao e um modelo simples de "pegue dashboardToken e conecte cegamente". +- Em outras palavras, clientes nao navegador ainda nao sao um caminho standalone suave na versao atual. Isso ainda precisa de mais design e limpeza. + +### O Que Fazer Se Voce Receber 302 + +`302` normalmente significa que o Launcher acha que voce nao esta logado, ou que esse cliente nao possui de fato uma sessao de login utilizavel. + +Nao apenas repita a tentativa do WebSocket imediatamente. Verifique estas tres coisas antes: + +1. Voce esta usando `18800`, e nao `18790`. +2. Voce ja fez login no Launcher no navegador. +3. Seu cliente nao navegador esta realmente enviando o estado de login. + +Na implementacao atual, a coisa mais facil de esquecer e o cookie de sessao. O navegador pode ja ter `picoclaw_launcher_auth`, mas o processo Node.js nao herda esse cookie automaticamente. + +### Um Exemplo Node.js Mais Proximo Da Realidade Atual + +Este exemplo demonstra a ideia de "reutilizar o cookie de sessao do navegador" em vez de fingir que `dashboardToken` sozinho cobre todos os casos. + +Isto e intencionalmente nao apresentado como um fluxo oficial, completo e suportado de ponta a ponta. A documentacao atual nao define uma maneira limpa e oficial de exportar e reutilizar a sessao do navegador em um processo Node.js separado. Portanto, o snippet abaixo deve ser tratado como um esboco de depuracao para explicar por que a requisicao continua recebendo `302`, e nao como uma receita madura de integracao. + +```javascript +const fetch = require('node-fetch'); +const WebSocket = require('ws'); + +async function connectPico() { + const launcherBaseUrl = 'http://127.0.0.1:18800'; + const sessionId = 'node-demo'; + + // Substitua isto apenas se voce ja tiver um cookie de sessao valido do Launcher + // obtido pelo seu proprio setup de depuracao. + // A documentacao atual nao define um fluxo oficial de exportacao de cookie. + const launcherCookie = 'picoclaw_launcher_auth=replace-with-real-cookie'; + + const tokenResponse = await fetch(`${launcherBaseUrl}/api/pico/token`, { + headers: { + Cookie: launcherCookie, + }, + redirect: 'manual', + }); + + if (tokenResponse.status === 302 || tokenResponse.status === 401) { + throw new Error('Falha na autenticacao do Launcher: este cliente nao possui uma sessao de login valida'); + } + + if (!tokenResponse.ok) { + throw new Error(`Falha ao obter o token Pico: ${tokenResponse.status}`); + } + + const { token, ws_url, enabled } = await tokenResponse.json(); + + if (!enabled) { + throw new Error('O canal Pico WebSocket nao esta habilitado'); + } + + const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], + { + headers: { + Cookie: launcherCookie, + }, + }, + ); + + ws.on('open', () => { + console.log('connected'); + ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: sessionId, + timestamp: Date.now(), + payload: { + content: 'Hello from Node.js', + }, + })); + }); + + ws.on('message', (data) => { + console.log('server -> client', JSON.parse(data.toString())); + }); + + ws.on('error', (err) => { + console.error('Erro do WebSocket:', err); + }); +} + +connectPico().catch(console.error); +``` + +### Por Que Esta Secao E Intencionalmente Conservadora + +Porque, na versao atual, o caminho para clientes nao navegador ainda tem varios problemas reais: + +- O estado de login e muito mais natural dentro do navegador do que fora dele. +- A relacao entre `session_id` e o estado de login do Launcher nao esta exposta com clareza suficiente. +- Quando usuarios encontram `302`, eles podem entender que precisam fazer login no Dashboard, mas sem uma explicacao seguinte de como carregar esse estado de login no Node.js, eles continuam travados. + +Por isso, esta documentacao descreve intencionalmente a limitacao atual e a forma como a falha acontece, enquanto deixa claro que o fluxo para clientes nao navegador ainda precisa de trabalho de design posterior. + +## Recomendacoes De Desenvolvimento E Depuracao + +Se voce estiver construindo uma integracao, nao comecar diretamente com Node.js ou um app desktop. Depure nesta ordem: + +1. Faca login no Launcher no navegador. +2. Confirme que `GET /api/pico/token` retorna JSON no navegador. +3. Complete uma conexao WebSocket no `Console` do DevTools do navegador. +4. Envie um `message.send` e confirme que recebe resposta. +5. Somente depois que o fluxo do navegador funcionar, avance para a reutilizacao de sessao fora do navegador. + +O motivo e simples: o navegador ja carrega `picoclaw_launcher_auth`, o que facilita separar problemas de autenticacao de problemas do protocolo de mensagem. + +## Protocolo De Mensagens + +Tipos comuns de mensagens de entrada (cliente -> servidor): + +- `message.send` +- `media.send` +- `ping` + +Tipos comuns de mensagens de saida (servidor -> cliente): + +- `message.create` +- `message.update` +- `typing.start` +- `typing.stop` +- `error` +- `pong` + +Estrutura geral da mensagem: + +```json +{ + "type": "message.send", + "id": "optional-id", + "session_id": "optional-session-id", + "timestamp": 0, + "payload": {} +} +``` + +Exemplo minimo de envio: + +```json +{ + "type": "message.send", + "id": "req-1", + "payload": { + "content": "hello" + } +} +``` + +## Erros Comuns E Solucao De Problemas + +| Problema | Causa Comum | O Que Fazer | +|------|----------|----------| +| Redirecionamento `302` para a pagina de login ou `/launcher-login` | Falha na autenticacao do Launcher; sem login ou sem transportar o estado de login | Primeiro confirme que o navegador esta logado, depois confirme que o cliente atual realmente envia o estado de login | +| `401 Unauthorized` | Porta errada, ou o metodo atual de autenticacao nao e aceito | Use `18800`; nao conecte diretamente a `18790` | +| `403 Invalid Pico token` | `token.<...>` em `Sec-WebSocket-Protocol` esta incorreto | Chame `GET /api/pico/token` novamente e use o token mais recente | +| `503 Gateway not available` | Gateway nao esta rodando ou Launcher nao se anexou com sucesso | Verifique o estado do Gateway e tente novamente | +| A conexao funciona mas nada acontece | So o handshake foi concluido; nenhum `message.send` ou `ping` foi enviado | Siga os exemplos de interacao e envie uma mensagem explicitamente | +| Validacao de origin falhou | `channels.pico.allow_origins` nao corresponde a origin da requisicao | Configure origins permitidas explicitamente e evite `*` | + +## Portas E Tokens + +### Portas + +| Porta | Finalidade | Permite Acesso Externo Direto | +|------|------|------------------| +| `18800` | Launcher (Web UI + API + proxy WebSocket) | Sim, recomendado | +| `18790` | Gateway (servico interno) | Nao, nao suportado | + +### Tokens / Sessao + +| Nome | Finalidade | Observacoes | +|------|------|------| +| Dashboard Token | Fazer login no Launcher e acessar APIs protegidas | Util para entrada de login no Launcher, mas nao deve mais ser tratado como a unica credencial para todos os cenarios de cliente | +| `picoclaw_launcher_auth` | Cookie de sessao do Launcher no navegador | Definido pelo Launcher apos o login; o navegador o envia automaticamente em requisicoes same-origin | +| Pico Token | Autenticacao do subprotocolo WebSocket | Obtido por `GET /api/pico/token` | +| `session_id` | Identificador de sessao da conexao atual | Passado na query da URL, por exemplo `?session_id=browser-demo` | + +## Recomendacoes De Seguranca + +1. Use HTTPS/WSS em producao. +2. Nao exponha a porta `18790` do Gateway. +3. Nao habilite `allow_token_query` por padrao. +4. Mantenha `allow_origins` o mais restrito possivel; nao use `*`. +5. Rotacione tokens Pico regularmente; voce pode regenera-los com `POST /api/pico/token`. +6. Sempre que possivel, mantenha o Launcher ligado localmente. Se precisar expor em LAN ou internet, combine com `allowed_cidrs`. +7. Nao grave tokens ou cookies de sessao em armazenamento persistente frontend, logs do navegador ou logs plaintext do servidor. + +## Exemplo De Configuracao + +```json +{ + "channels": { + "pico": { + "enabled": true, + "token": "replace-with-strong-random-token", + "allow_token_query": false, + "allow_origins": ["https://your-app.example.com"], + "max_connections": 100 + } + } +} +``` + +## Recomendacoes Para Desenvolvedores Terceiros + +No minimo, cubra este fluxo em testes automatizados: + +1. Fazer login no Launcher +2. Obter um token Pico +3. Abrir uma conexao WebSocket usando `token.` +4. Enviar `message.send` +5. Verificar que voce recebe uma resposta ou uma mensagem de erro + +Depois que esse fluxo estiver estavel, avance para UI, gerenciamento de sessao, reconexao e estrategias de recuperacao. + +## Recomendacoes De Manutencao Da Documentacao + +- Mantenha esta orientacao de integracao WebSocket como um documento principal unico, para que o fluxo geral e os avisos voltados para desenvolvedores nao se desalinhem. +- Se houver mudancas futuras sobre discussao de design de autenticacao, exemplos especificos de Node.js ou melhorias de tutorial, divida em PRs separados em vez de misturar tudo. diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/configuration/websocket.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/configuration/websocket.md new file mode 100644 index 0000000..ca0423a --- /dev/null +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/configuration/websocket.md @@ -0,0 +1,659 @@ +--- +id: websocket +title: 连接 WebSocket +--- +## PicoClaw WebSocket 接入指南 + +> 适用范围:当前仓库实现(截至 2026-04-13)。 +> 本文面向通过 Launcher 接入 PicoClaw WebSocket 的第三方开发者,包括浏览器插件、运行在 Launcher 同源下的前端页面、桌面客户端和服务端客户端。 + +从 v0.2.5 开始,PicoClaw WebSocket 连接需要双层校验。外部客户端应连接 Launcher 端口 `18800`,不要直接连 Gateway 端口 `18790`。 + +## 先记住三件事 + +1. 对外入口是 `ws(s):///pico/ws`。 +2. 握手时既要通过 Launcher 登录态校验,也要提供 Pico token。 +3. 浏览器最容易跑通,因为浏览器会自动带上登录后的会话 cookie。 + +## 版本策略 + +- 第三方集成请基于 release/tag 开发和测试。 +- 不要基于 `main` 做生产集成,主线改动频繁,不保证兼容。 +- 升级 PicoClaw 时,请同时固定 PicoClaw 版本和你依赖的文档版本。 + +建议在自己的项目中明确记录: + +1. PicoClaw 的 tag 或发布版本 +2. 当前接入文档对应的版本 +3. 你支持的最小和最大 PicoClaw 版本区间 + +## 架构与鉴权模型 + +Launcher 模式下,连接路径为: + +- 客户端 -> `ws(s):///pico/ws` +- Launcher -> 反向代理到 Gateway 的 Pico channel + +当前是两层鉴权: + +1. Launcher 鉴权层 + 使用已登录会话,或服务端请求头中的 `Authorization: Bearer `。 +2. Pico WebSocket 鉴权层 + 使用 `Sec-WebSocket-Protocol: token.`。 + +关键点: + +- `/pico/ws` 不是匿名 WebSocket,而是 Launcher 保护下的接口。 +- Launcher 登录成功后会写入 `picoclaw_launcher_auth` cookie。 +- `picoclaw_launcher_auth` 的作用是证明“这个浏览器已经登录过 Launcher”,从而通过第一层鉴权。 +- 这个 cookie 是会话凭证,不需要你手动拼到浏览器代码里;浏览器会在同源请求时自动携带。 +- Launcher 会把 `token.` 转换成内部格式再转发到 Gateway,客户端无需关心 PID token 的拼装细节。 + +### 认证架构 + +```text +┌─────────────────────────────────────────────────────────────────┐ +│ Launcher (18800) │ +├─────────────────────────────────────────────────────────────────┤ +│ 第一层认证:Launcher 登录态 │ +│ - 浏览器会话 cookie: picoclaw_launcher_auth │ +│ - 或 Authorization: Bearer │ +├─────────────────────────────────────────────────────────────────┤ +│ 第二层认证:Pico WebSocket 认证 │ +│ - Sec-WebSocket-Protocol: token. │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Gateway (18790) │ +│ Launcher 负责内部 token 转换与转发 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 标准接入流程 + +### 步骤 1:先登录 Launcher + +浏览器用户最简单的方式是直接访问: + +```text +http://your-host:18800?token= +``` + +也可以显式调用登录接口: + +```http +POST /api/auth/login +Content-Type: application/json + +{ + "token": "" +} +``` + +登录成功后,Launcher 会设置会话 cookie `picoclaw_launcher_auth`。之后这个浏览器访问 `/api/pico/token` 和 `/pico/ws` 时,就会自动带上登录态。 + +> `dashboard_token` 可通过以下方式获得: +> - 查看 Launcher 启动时的控制台输出 +> - 设置环境变量 `PICOCLAW_LAUNCHER_TOKEN` +> - 在 `~/.picoclaw/launcher.json` 中配置 + +### 步骤 2:获取 Pico token 与 `ws_url` + +这一步对普通用户最容易迷糊。实际要做的是:先在浏览器里登录 Launcher,然后在同一个浏览器页面里执行一段 `fetch` 代码,拿到 `token` 和 `ws_url`。 + +这里有个前提:下面这些浏览器示例默认你的代码运行在 Launcher 同源下,比如直接运行在 Launcher 页面里。如果你的前端页面部署在别的域名上,`/api/pico/token` 这个相对路径请求到的会是你自己的站点,而不是 Launcher。 + +#### 做法 A:直接在浏览器地址栏验证自己是否已登录 + +在浏览器打开: + +```text +http://127.0.0.1:18800/api/pico/token +``` + +可能出现三种情况: + +- 直接看到一段 JSON,说明你已经登录,继续下一步。 +- 被重定向到登录页,说明还没有登录 Launcher,或者当前登录态已经失效。 +- 返回 `401`,说明当前登录态无效或已经过期。 + +正常返回示例: + +```json +{ + "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ws_url": "ws://127.0.0.1:18800/pico/ws", + "enabled": true +} +``` + +#### 做法 B:在浏览器开发者工具的 Console 里执行 + +如果你是按示例代码操作,通常是在浏览器控制台执行,而不是在系统终端执行。 + +还要注意一点:浏览器里的 `fetch()` 默认通常会自动跟随重定向。所以如果当前登录态无效,你在 JavaScript 里不一定能直接看到原始 `302`,更常见的是请求已经跳到了登录页,随后在 `response.json()` 这里因为拿到的是 HTML 而失败。 + +操作顺序: + +1. 打开 Launcher 页面,例如 `http://127.0.0.1:18800/`。 +2. 按 `F12` 打开开发者工具。 +3. 切到 `Console` 标签。 +4. 第一次粘贴代码时,如果浏览器阻止粘贴,先手动输入 `allow pasting` 并回车。 +5. 再粘贴下面这段代码并执行。 + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`请求被重定向到了 ${response.url},需要先登录 Launcher / Dashboard`); +} + +if (response.status === 401) { + throw new Error('需要先登录 Launcher / Dashboard'); +} + +if (!response.ok) { + throw new Error(`获取 Pico token 失败: ${response.status}`); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`/api/pico/token 预期返回 JSON,实际是 ${contentType || '未知内容类型'}`); +} + +const data = await response.json(); +console.log(data); +``` + +控制台看到类似下面的输出,就说明这一步成功了: + +```json +{ + "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ws_url": "ws://127.0.0.1:18800/pico/ws", + "enabled": true +} +``` + +如果 `enabled` 是 `false`,说明 Pico WebSocket 通道还没有启用。 + +### 步骤 3:建立 WebSocket 连接 + +连接地址: + +```text +?session_id= +``` + +子协议: + +```text +token. +``` + +这里的 `session_id` 建议由你的客户端自己生成一个稳定、可追踪的值,例如 `browser-demo`、`node-test-001`。 + +## 浏览器接入示例 + +浏览器原生 `WebSocket` 通常不能自定义 `Authorization` 头,所以浏览器场景应先登录 Launcher,再依赖同源 cookie 完成握手。 + +这个“同源”前提很重要。本节里的浏览器示例适用于代码运行在 Launcher 页面同源下的情况,不是给任意第三方站点直接复制粘贴就能工作的通用跨域示例。 + +### Chrome 扩展要额外注意 + +如果你开发的是 Chrome 扩展,需要区分两类运行环境: + +- 扩展注入到 Launcher 页面里的脚本 +- 扩展自己的 `popup`、`side panel`、`service worker` + +前者更接近普通网页脚本,最容易直接复用 Launcher 页面的登录态。 + +后者的来源是 `chrome-extension://...`,不等同于 Launcher 同源页面。也就是说,即使你已经在浏览器里登录了 Launcher,也不能想当然地认为扩展 `popup` 里的 `fetch('/api/pico/token')` 会像普通网页一样自然工作。 + +如果你的目标只是做联调或抓协议,推荐优先使用两种方式: + +1. 直接在 Launcher 页面里打开开发者工具执行示例代码。 +2. 让扩展把测试脚本注入到 Launcher 页面里运行,而不是直接在扩展 `popup` 中建连。 + + +### 最小连接示例 + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`请求被重定向到了 ${response.url},需要先登录 Launcher`); +} + +if (response.status === 401) { + throw new Error('Launcher 登录态无效'); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`/api/pico/token 预期返回 JSON,实际是 ${contentType || '未知内容类型'}`); +} + +const { token, ws_url, enabled } = await response.json(); + +if (!enabled) { + throw new Error('Pico WebSocket 通道未启用'); +} + +const sessionId = 'browser-demo'; +const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], +); + +ws.onopen = () => { + console.log('WebSocket 已连接'); +}; + +ws.onmessage = (event) => { + console.log('收到消息:', JSON.parse(event.data)); +}; + +ws.onerror = (error) => { + console.error('WebSocket 错误:', error); +}; + +ws.onclose = () => { + console.log('WebSocket 已关闭'); +}; +``` + +### 建连后怎么交互 + +前面只讲“怎么连上”是不够的。连上后,你至少还需要会发送一条消息,并知道去哪里看返回内容。 + +下面这段代码包含了一个最小可交互流程: + +```javascript +const response = await fetch('/api/pico/token'); + +if (response.redirected) { + throw new Error(`请求被重定向到了 ${response.url},需要先登录 Launcher`); +} + +if (response.status === 401) { + throw new Error('Launcher 登录态无效'); +} + +const contentType = response.headers.get('content-type') || ''; +if (!contentType.includes('application/json')) { + throw new Error(`/api/pico/token 预期返回 JSON,实际是 ${contentType || '未知内容类型'}`); +} + +const { token, ws_url, enabled } = await response.json(); + +if (!enabled) { + throw new Error('Pico WebSocket 通道未启用'); +} + +const sessionId = 'browser-demo'; +const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], +); + +ws.onopen = () => { + console.log('connected'); + + ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: sessionId, + timestamp: Date.now(), + payload: { + content: 'Hello PicoClaw!', + }, + })); +}; + +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + console.log('server -> client', message); +}; +``` + +你需要观察两个地方: + +1. Console 中是否先出现 `connected`。 +2. 发送后是否收到服务端返回的消息,例如`typing.start`、`message.create`、`message.update`、`error` 或 `pong`。 + +如果要测试心跳,也可以发送: + +```javascript +ws.send(JSON.stringify({ + type: 'ping', + id: `ping-${Date.now()}`, + timestamp: Date.now(), + payload: {}, +})); +``` + +正常情况下你会收到 `pong`。 + +### 一个更完整的浏览器示例 + +```javascript +class PicoClawClient { + constructor(sessionId) { + this.sessionId = sessionId; + this.ws = null; + } + + async connect() { + const response = await fetch('/api/pico/token'); + + if (response.redirected) { + throw new Error(`请求被重定向到了 ${response.url},需要先登录 Launcher / Dashboard`); + } + + if (response.status === 401) { + throw new Error('需要先登录 Launcher / Dashboard'); + } + + if (!response.ok) { + throw new Error(`获取 token 失败: ${response.status}`); + } + + const contentType = response.headers.get('content-type') || ''; + if (!contentType.includes('application/json')) { + throw new Error(`/api/pico/token 预期返回 JSON,实际是 ${contentType || '未知内容类型'}`); + } + + const { token, ws_url, enabled } = await response.json(); + + if (!enabled) { + throw new Error('Pico WebSocket 通道未启用'); + } + + this.ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(this.sessionId)}`, + [`token.${token}`], + ); + + this.ws.onmessage = (event) => { + console.log('收到消息:', JSON.parse(event.data)); + }; + + return new Promise((resolve, reject) => { + this.ws.onopen = () => resolve(); + this.ws.onerror = () => reject(new Error('WebSocket 连接失败')); + }); + } + + sendText(content) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket 未连接'); + } + + this.ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: this.sessionId, + timestamp: Date.now(), + payload: { content }, + })); + } + + ping() { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket 未连接'); + } + + this.ws.send(JSON.stringify({ + type: 'ping', + id: `ping-${Date.now()}`, + timestamp: Date.now(), + payload: {}, + })); + } + + close() { + if (this.ws) { + this.ws.close(); + } + } +} + +const client = new PicoClawClient('browser-demo'); +await client.connect(); +client.sendText('Hello PicoClaw!'); +client.ping(); +``` + +## 非浏览器客户端说明 + +对于 Node.js、Python、桌面端或 Postman 这类不能自动携带浏览器 cookie 的客户端,当前接入体验明显更差,这里需要特别说明。 + +### 当前实测现状 + +- 直接请求 `/api/pico/token` 时,如果没有 Launcher 登录态,通常会收到 `302` 并跳转到登录页。 +- 在“启动时使用密码登录”的场景下,旧文档中直接使用 `dashboardToken` 的方式并不适合作为通用方案。 +- 当前实测流程里,握手参数使用的是 `session_id`,不是“拿到 dashboardToken 就能无脑建连”的模型。 +- 也就是说,非浏览器客户端在当前版本下并不是一个足够顺手的独立接入路径,这部分后续还需要继续梳理和改进。 + +### 如果你拿到了 302,下一步该做什么 + +`302` 的含义通常是:Launcher 认为你还没有登录,或者你当前这个客户端没有可用的登录会话。 + +这时不要直接继续重试 WebSocket。应先确认下面三件事: + +1. 你访问的是 `18800`,不是 `18790`。 +2. 你已经在浏览器里成功登录 Launcher。 +3. 你这个非浏览器客户端是否真的把登录态带上了。 + +当前实现下,如果你是在浏览器里登录成功,再去做 Node.js 调试,最容易遗漏的就是会话 cookie。浏览器里已经有 `picoclaw_launcher_auth`,但你的 Node.js 进程默认并不会自动继承这个 cookie。 + +### 当前更接近真实情况的 Node.js 示例 + +下面的示例演示的是“复用浏览器里已有的会话 cookie”这一思路,而不是继续假设只靠 `dashboardToken` 就能覆盖所有情况。 + +这里要说清楚:下面这段并不是一个官方支持、可直接照抄落地的完整流程。当前文档并没有定义“如何把浏览器登录态规范地导出并复用到另一个 Node.js 进程”这件事,所以这段代码更适合作为解释当前失败模式的调试草图,而不是成熟的接入方案。 + +```javascript +const fetch = require('node-fetch'); +const WebSocket = require('ws'); + +async function connectPico() { + const launcherBaseUrl = 'http://127.0.0.1:18800'; + const sessionId = 'node-demo'; + + // 这里只能在你已经通过自己的调试手段拿到有效 Launcher 会话 cookie 时替换。 + // 当前文档并没有定义官方的 cookie 导出工作流。 + const launcherCookie = 'picoclaw_launcher_auth=replace-with-real-cookie'; + + const tokenResponse = await fetch(`${launcherBaseUrl}/api/pico/token`, { + headers: { + Cookie: launcherCookie, + }, + redirect: 'manual', + }); + + if (tokenResponse.status === 302 || tokenResponse.status === 401) { + throw new Error('Launcher 鉴权失败:当前客户端没有有效登录会话'); + } + + if (!tokenResponse.ok) { + throw new Error(`获取 Pico token 失败: ${tokenResponse.status}`); + } + + const { token, ws_url, enabled } = await tokenResponse.json(); + + if (!enabled) { + throw new Error('Pico WebSocket 通道未启用'); + } + + const ws = new WebSocket( + `${ws_url}?session_id=${encodeURIComponent(sessionId)}`, + [`token.${token}`], + { + headers: { + Cookie: launcherCookie, + }, + }, + ); + + ws.on('open', () => { + console.log('connected'); + ws.send(JSON.stringify({ + type: 'message.send', + id: `req-${Date.now()}`, + session_id: sessionId, + timestamp: Date.now(), + payload: { + content: 'Hello from Node.js', + }, + })); + }); + + ws.on('message', (data) => { + console.log('server -> client', JSON.parse(data.toString())); + }); + + ws.on('error', (err) => { + console.error('WebSocket 错误:', err); + }); +} + +connectPico().catch(console.error); +``` + +### 这部分文档为什么写得这么保守 + +因为当前版本下,非浏览器客户端这一段仍然存在几个现实问题: + +- 登录态建立在浏览器里更自然,脱离浏览器后复用会话不够直观。 +- `session_id` 和 Launcher 登录态的关系对外暴露得不够清晰。 +- 用户遇到 `302` 后,虽然知道要去登录 Dashboard,但文档若不继续说明“如何把登录态带到 Node.js 里”,就会卡住。 + +因此这里先把当前限制和实际失败方式讲清楚,同时明确这块仍需后续讨论和改进。 + +## 开发与调试建议 + +如果你正在做接入开发,建议先按下面顺序调试,而不是一上来就直接写 Node.js 或桌面端代码: + +1. 先用浏览器登录 Launcher。 +2. 在浏览器里确认 `GET /api/pico/token` 能返回 JSON。 +3. 在浏览器开发者工具的 `Console` 中完成一次 WebSocket 建连。 +4. 主动发送一条 `message.send`,确认能收到回包。 +5. 浏览器链路跑通后,再去处理非浏览器客户端如何复用登录态。 + +这样做的原因很直接:浏览器会自动携带 `picoclaw_launcher_auth`,最容易把“是鉴权问题,还是消息协议问题”分开排查。 + +## 消息协议 + +常见入站消息类型(客户端 -> 服务端): + +- `message.send` +- `media.send` +- `ping` + +常见出站消息类型(服务端 -> 客户端): + +- `message.create` +- `message.update` +- `typing.start` +- `typing.stop` +- `error` +- `pong` + +通用消息结构: + +```json +{ + "type": "message.send", + "id": "optional-id", + "session_id": "optional-session-id", + "timestamp": 0, + "payload": {} +} +``` + +最小发送示例: + +```json +{ + "type": "message.send", + "id": "req-1", + "payload": { + "content": "hello" + } +} +``` + +## 常见错误与排查 + +| 问题 | 常见原因 | 处理方式 | +|------|----------|----------| +| `302` 跳转到登录页或 `/launcher-login` | Launcher 鉴权失败,未登录或会话未带上 | 先确认浏览器已登录,再确认当前客户端确实带上了登录态 | +| `401 Unauthorized` | 连错端口,或当前鉴权方式不被接受 | 改连 `18800`,不要直连 `18790` | +| `403 Invalid Pico token` | `Sec-WebSocket-Protocol` 中的 `token.<...>` 不正确 | 重新调用 `GET /api/pico/token` 获取最新 token | +| `503 Gateway not available` | Gateway 未运行或 Launcher 未成功附着 | 检查 Gateway 状态后再重试 | +| 建连成功但不会交互 | 只完成握手,没有发送 `message.send` 或 `ping` | 按本文交互示例主动发送消息,观察回包 | +| Origin 校验失败 | `channels.pico.allow_origins` 与请求来源不匹配 | 显式配置允许的 origin,避免使用 `*` | + +## 端口与 Token 说明 + +### 端口 + +| 端口 | 用途 | 是否允许外部直连 | +|------|------|------------------| +| `18800` | Launcher(Web UI + API + WebSocket 代理) | 是,推荐 | +| `18790` | Gateway(内部服务) | 否,不支持 | + +### Token / 会话 + +| 名称 | 用途 | 说明 | +|------|------|------| +| Dashboard Token | 登录 Launcher、访问受保护 API | 适合做 Launcher 登录入口,但不应再被理解成“覆盖所有客户端场景的唯一凭证” | +| `picoclaw_launcher_auth` | 浏览器中的 Launcher 会话 cookie | 登录成功后由 Launcher 设置,浏览器同源请求会自动带上 | +| Pico Token | WebSocket 子协议认证 | 通过 `GET /api/pico/token` 获取 | +| `session_id` | 当前连接的会话标识 | 建连时放在 URL 查询参数中,例如 `?session_id=browser-demo` | + +## 安全建议 + +1. 生产环境使用 HTTPS/WSS。 +2. 不要暴露 Gateway 端口 `18790`。 +3. 不要默认开启 `allow_token_query`。 +4. `allow_origins` 应使用最小允许集合,不要直接使用 `*`。 +5. 定期轮换 Pico token,可通过 `POST /api/pico/token` 重新生成。 +6. Launcher 尽量保持本地监听;如需公网或局域网暴露,请配合 `allowed_cidrs`。 +7. 不要把 token 或会话 cookie 写入前端持久化存储、浏览器日志或服务端明文日志。 + +## 配置示例 + +```json +{ + "channels": { + "pico": { + "enabled": true, + "token": "replace-with-strong-random-token", + "allow_token_query": false, + "allow_origins": ["https://your-app.example.com"], + "max_connections": 100 + } + } +} +``` + +## 对第三方开发者的建议 + +在自动化测试中,至少覆盖以下流程: + +1. 登录 Launcher +2. 获取 Pico token +3. 使用 `token.` 建立 WebSocket 连接 +4. 主动发送 `message.send` +5. 验证收到回包或错误消息 + +当以上流程稳定后,再继续实现 UI、会话管理、重连和错误恢复策略。 + +## 文档维护建议 + +- 这一类 websocket 接入说明,最好继续维持为一篇主文档,避免“普通接入流程”和“开发者补充说明”分散后互相漂移。 +- 如果后续还有鉴权设计讨论、Node.js 专项示例修正、接入教程补充,建议分别拆成不同 PR,不要混在同一个 PR 里。