
Odysseus Companion 配对桥详解让 LAN 客户端发现、配对自托管 AI 工作区【免费下载链接】odysseusSelf-hosted AI workspace.项目地址: https://gitcode.com/gh_mirrors/ody/odysseusOdysseusSelf-hosted AI workspace内置了一个名为Companion bridge的轻量附加层位于companion/目录。它允许局域网内的客户端例如手机在不复制任何 LLM 逻辑的前提下发现 Odysseus 服务器提供了哪些模型端点并配对获取一个一次性、限定chat权限的 API token。读完本文你将掌握该桥接层的全部 5 个 HTTP 端点、owner 作用域规则、配对 CSRF 防护原理、COMPANION_BASE_URL的严格校验逻辑以及 token 铸造mint到立即可用的完整调用链。一、设计定位薄薄一层、只做发现与配对Companion bridge 的模块文档 companion/README.md 将其定义为 A thin, additive layer——一个只做增量、不改变既有行为的附加层。其端点总览如下完整继承自 README 表格MethodPathAuthPurposeGET/api/companion/pingsession or token廉价的、带鉴权验证的健康检查GET/api/companion/infosession or token服务器身份信息 能力标志位GET/api/companion/modelssession or token调用者自己的模型端点列表GET/api/companion/pairadmin cookie配对页面一个表单从不铸造 tokenPOST/api/companion/pairadmin cookie铸造一次性配对 token?formatjson供应用内界面使用其中一条核心安全约束在 README 中明确给出/models按调用者真实 owner 作用域过滤外加遗留的 null-owner 共享行与系统其他地方的owner_filter规则一致且永远不返回 API-key 材料。实现分布在两个文件companion/routes.py —— 路由层暴露setup_companion_routes()与几个可独立测试的纯函数token_owner、owner_can_see、require_models_scope、mint_pairing_tokencompanion/pairing.py —— 配对辅助单元token 铸造、LAN IP 发现、地址校验与 QR 渲染。在应用启动时app.py#L886-L887 通过app.include_router(setup_companion_routes())挂载整组路由。鉴权本身不需要 bridge 自己实现模块 docstring 指出鉴权由全局AuthMiddleware统一强制执行能进入这些 handler 就意味着调用者已通过 cookie session 或ody_前缀的 Bearer API token 二选一认证。二、只读端点逐一拆解2.1/ping确认主机 凭证都有效ping的语义不是普通的存活探测而是一个鉴权验证过的健康检查返回 200 且oktrue说明 host/port 正确并且凭证有效否则由中间件返回 401。它返回四个字段见 companion/routes.py#L85-L95{ ok: true, name: odysseus, version: APP_VERSION, auth: token | session }其中auth字段告诉客户端本次调用走的是 token 还是 session 凭证便于移动端区分自己的配对状态。2.2/info身份 能力标志info返回服务器名称、版本、调用者自己的owner对 Bearer 调用者即 token 的真实 owner以及当前硬编码的能力标志{chat: true, streaming: true}companion/routes.py#L97-L107。这个能力列表就是 LAN 客户端发现服务器提供什么的入口。2.3/models按 owner 作用域过滤的模型清单这是只读端点中逻辑最重的一个它解决了一个 Bearer 调用者的关键问题标准的/api/models路由按get_current_user作用域过滤而 Bearer token 在中间件里以沙箱伪用户api身份运行什么都不拥有直接复用会返回空。Companion 版本改为按 token 的真实 owner过滤。其过滤规则companion/routes.py#L109-L166scope 门控require_models_scope对 Bearer 调用者要求 token scope 集合中包含chat即pairing.COMPANION_SCOPE否则 403 API token requires chat scopecookie 调用者不受此限。scope 解析兼容列表和逗号分隔字符串两种形态。SQL 过滤is_enabled True且model_type in {llm, None}null 类型是遗留端点若解析出 owner再追加owner caller OR owner IS NULL。Python 防御性复核每一行再过一遍纯谓词owner_can_see(row.owner, owner)—— 行 owner 为 None共享或等于调用者。这样 SQL 过滤与 Python 检查形成双保险且谓词可直接单测。单用户模式例外当AUTH_ENABLED关闭、无 token、且无法解析 owner 时single_user_mode保持与标准路由一致的单用户可见全部端点视图关闭鉴权并不会放大 ownerless Bearer token 的可见范围有专门测试验证。输出脱敏每个端点只输出endpoint_id、name、endpoint_url经src/endpoint_resolver.py的build_chat_url拼出的 chat 补全 URL异常时回退为原始base_url、modelscached_models扣除hidden_models两段 JSON 解析失败都静默降级为空、supports_tools。响应键集合被测试严格断言为这五个api_key、headers、base_url均不得出现tests/test_companion_readonly.py#L363-L403。owner 归因由纯函数token_owner完成companion/routes.py#L31-L41Bearer 请求取中间件盖在request.state.api_token_owner上的真实 ownercookie 请求走get_current_user都失败则返回 None此时只能看到共享行。tests/test_companion_readonly.py 用 mock 请求态完整覆盖了这些组合跨 owner 不可见、null-owner 共享行可见但不构成通向他人行的后门、cookie 用户与 token 用户看到的行集合均为自己的 共享的等。三、配对流程一次性 token 的铸造3.1 页面即表单铸造只发生在 POST配对端点是admin-cookie only由 core/middleware.py#L57 的require_admin把关非管理员 403bearer 伪用户api也不是管理员。GET /pair只渲染一个深色主题的极简表单页Generate pairing code 按钮表单methodPOST action/api/companion/pair。GET 绝不铸造凭证——因为 session cookie 是SameSiteLax见 routes/auth_routes.py#L188 的samesitelaxLax cookie 会随顶层 GET 导航发送若在 GET 上铸造 token恶意页面用a链接或img就能触发铸造。这正是 README Pairing CSRF posture 一节的核心论点。POST /pair才执行铸造require_admin→ 校验COMPANION_BASE_URL→get_current_user取 owner →mint_pairing_token(owner, invalidate)其中invalidate取自request.app.state.invalidate_token_cache。?formatjson返回 JSON供应用内配对界面消费否则渲染含 QR 码的结果页。3.2 token 铸造只显示一次落盘只有 bcrypt 哈希真正的铸造单元是 companion/pairing.py#L182-L205 的mint_tokenraw_token ody_ secrets.token_urlsafe(32) token_hash bcrypt.hashpw(raw_token.encode(), bcrypt.gensalt()).decode() token_id str(uuid.uuid4())[:8] # 写入 ApiToken 行ownerowner, namecompanion, scopeschat, is_activeTrue关键点原始 token 只返回一次页面/JSON 里展示数据库只存 bcrypt 哈希和 8 字符前缀token_prefix raw_token[:8]与routes/api_token_routes.py手工铸造的 token 完全同构因此鉴权中间件对两者一视同仁scope 固定为COMPANION_SCOPE chattoken 名为companion方便管理员事后识别测试 tests/test_companion_pairing.py#L82-L96 直接捕获写入的 ORM 行断言token_hash以$2bcrypt开头、token_prefix raw[:8]、scopes chat且明文不落盘。3.3 铸造后立即生效token 缓存失效机制README 中Minting invalidates the auth middlewares token cache, so a freshly minted token works on the next request without a restart这句的实现链路是app.py#L298-L312 在内存中维护一张prefix → [(token_id, token_hash, owner, scopes)]缓存避免每个 Bearer 请求都查库 线性扫 bcrypt缓存带脏标记_token_cache_dirtyapp.state.invalidate_token_cache就是置脏的回调app.py#L307-L310mint_pairing_tokencompanion/routes.py#L68-L79在mint_token成功后调用该回调若存在。它被设计为纯函数——invalidate由调用方注入所以单测里 test_mint_pairing_token_invalidates_cache 可以断言invalidate.assert_called_once()而test_mint_pairing_token_tolerates_no_invalidator则验证即使应用没暴露失效器也不会炸。3.4 配对载荷与 QR稳定契约客户端扫码拿到的载荷由pairing_payload生成键被明确要求保持稳定companion/pairing.py#L208-L210{v: 1, host: 192.168.1.9, port: 7000, token: ody_...}v对应常量PAIRING_VERSION 1。QR 渲染pairing_qr_png_data_uri将载荷 JSON 编码为 PNGdata:URIqrcode是可选依赖缺失时返回 None结果页退化为手动输入的提示文本JSON 响应中qr字段为 nulltests/test_companion_pairing.py#L411-L429。HTML 渲染路径还有一层 XSS 防护除已知的 PNG contenteditable="false">【免费下载链接】odysseusSelf-hosted AI workspace.项目地址: https://gitcode.com/gh_mirrors/ody/odysseus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考