news 2026/10/14 18:35:53

teams-for-linux Wayland 屏享拾取器 Spike(2534)技术方案解析:用应用内 StreamSelector 接管 getDisplayMedia

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
teams-for-linux Wayland 屏享拾取器 Spike(2534)技术方案解析:用应用内 StreamSelector 接管 getDisplayMedia
  • 桌面应用
  • 即时通讯

【免费下载链接】teams-for-linux

Unofficial Microsoft Teams for Linux client

项目地址:https://gitcode.com/gh_mirrors/te/teams-for-linux
点击查看免费下载

导读

本文围绕 teams-for-linux 仓库中的 Spike 方案文档 2026-05-13-spike-2534-wayland-picker.md 展开,深入剖析一个真实存在的问题:在 Wayland 会话下,由于 Chromium 的WebRTCPipeWireCapturer会绕过 Electron 的setDisplayMediaRequestHandler,应用内自绘的屏幕选择器(StreamSelector)与分享预览小窗口在 Wayland 上双双失效。该 Spike 提出了一条可行的工程路径——在注入脚本中拦截navigator.mediaDevices.getDisplayMedia,先通过新增 IPC 唤起应用内拾取器,再用getUserMedia的chromeMediaSource: 'desktop'约束合成媒体流。读完本文,你将掌握这一方案的整体架构、四个文件的具体改动方式、完整可复制的代码,以及验证假设是否成立的测试步骤与判定标准。

背景:Wayland 下 Chromium 原生拾取器绕过了应用内选择器

问题根源:WebRTCPipeWireCapturer与setDisplayMediaRequestHandler的旁路

Electron 应用可以通过session.setDisplayMediaRequestHandler()拦截getDisplayMedia,从而提供自绘的源选择界面。但在 Wayland 且启用WebRTCPipeWireCapturer(TFL 的默认配置)时,Chromium 的原生getDisplayMedia会直接走xdg-desktop-portal完成源选择,完全跳过Electron 注册的setDisplayMediaRequestHandler。这就造成了一个割裂的局面:

  • X11 / Windows / macOS:Chromium 调用应用的setDisplayMediaRequestHandler,进而打开应用内 StreamSelector;
  • Wayland + PipeWire:Chromium 直接调用 xdg-desktop-portal,应用内拾取器被旁路,预览小窗口也随之中断。

仓库中对这一行为有明确注释佐证,见 app/screenSharing/injectedScreenSharing.js 第 87–93 行:

Delegate picking to the platform's native flow: X11 / Win / macOS: Chromium calls oursetDisplayMediaRequestHandler, which opens the in-app StreamSelector. Wayland with WebRTCPipeWireCapturer: Chromium calls xdg-desktop-portal directly (setDisplayMediaRequestHandleris bypassed).

为什么不用 Chromium/Electron 的系统拾取器:ADR 008 的结论

仓库在 ADR 008: useSystemPicker Feature for Electron 38 中曾评估过 Electron 38 提供的useSystemPicker: true选项,结论是Rejected(拒绝):

  • Linux(Wayland + PipeWire)为 ❌ BLOCKER:useSystemPicker在 Linux 上并不会真正调用系统拾取器,应用仍会被迫走自定义 handler 路径(对应 Electron Issue #48223);
  • macOS(Sonoma+)存在applyConstraints()切换挂起(toggle-hang)的已知缺陷(Electron Issue #45306);
  • 维护双代码路径(自定义 + 系统拾取器)会带来平台检测、特性开关、配置持久化和成倍的测试矩阵;
  • 现有基于desktopCapturer的自定义拾取器在所有平台都可工作。

因此仓库维持"单一统一代码路径"的决策,而 #2534 的 Spike 正是为了补上 Wayland 这条唯一缺口:既然系统侧无法提供统一体验,那就把 Wayland 的源选择也强行拉回应用内拾取器。

预览窗口的触发路径:只有 in-app picker 路径才会打开

预览窗口(preview window)只在应用内拾取器路径下才会触发。Spike 文档明确指出:"The preview window only fires from the in-app picker path"。对应的主进程实现是 app/mainAppWindow/index.js 中bindDisplayMediaHandler()(第 81–98 行)回调 →handleScreenSourceSelection(source, callback)(第 100–130 行)→setupScreenSharing(source)(第 59–62 行)→createScreenSharePreviewWindow()(第 132 行起)这一调用链。Wayland 下这个链条根本不会被走到,预览窗口自然也就无法出现。

Spike 目标与技术假设

Spike #2534 的核心目标用一句话概括:

验证在 Wayland 上,将getDisplayMedia路由到 TFL 的应用内StreamSelector(替代 Chromium 原生 handler),能否在不破坏屏幕捕获本身的前提下,恢复分享预览窗口。

其技术依据来自一条关键事实:预览窗口在 Wayland 上已经能通过getUserMedia({ video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId } } })这一约束形态正常工作,对应实现见 app/screenSharing/previewWindow.html 第 68–69 行的注释(该文件现已演进为 ImageBitmap 快照管线,但约束形态的历史依据仍在文档中注明)。因此 Spike 的思路是:拦截渲染进程的getDisplayMedia,先调用新增 IPC 唤起应用内StreamSelector,拿到用户选择的源 ID 后,再用上述约束形态通过getUserMedia合成媒体流——与预览窗口已在使用的形态保持一致,复用同一套已验证可行的 WebRTC 管线。

技术栈

  • Electron(主进程 + 渲染进程双端);
  • WebRTCgetUserMedia,携带 legacy 约束chromeMediaSource: 'desktop';
  • 现成的StreamSelectorBrowserWindow(实际为挂载在主窗口上的WebContentsView全窗覆盖层,见 app/screenSharing/index.js 与 app/screenSharing/README.md)。

明确的范围界定(Out of scope)

作为 Spike,本次刻意不做以下事项,避免扩大验证面:

  • 配置开关(config flag):不做可切换的特性配置;
  • 按 profile 的会话处理(multi-account):多账户分区会话的场景不在本次范围;
  • 自动化测试:不引入自动化测试;
  • X11 fallback:应用内拾取器在 X11 上经由现有 handler 已可工作,但本次注入脚本的拦截逻辑在 X11 上同样会执行,需要顺带确认它不会破坏 X11 的分享流程。

总体设计与文件改动清单

Spike 的改动横跨 4 个文件,形成一条完整的调用链:

文件职责
app/security/ipcValidator.js将新 IPC 通道show-tfl-stream-picker加入屏幕共享白名单
app/browser/preload.js在electronAPI上暴露showTflStreamPicker()方法
app/mainAppWindow/index.js注册主进程ipcMain.handle("show-tfl-stream-picker", ...)
app/screenSharing/injectedScreenSharing.js重写getDisplayMedia包装器,接入新路径

数据流如下:

Teams 调用 getDisplayMedia │ ▼ 注入脚本拦截,调用 electronAPI.showTflStreamPicker() │ ▼(IPC: show-tfl-stream-picker) 主进程:streamSelector.show(callback) 显示应用内拾取器 │ ▼(用户选择 source) handleScreenSourceSelection(source, cb) → setupScreenSharing(创建预览窗口) ▼ 返回 source.id 渲染进程:getUserMedia({ video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId } } }) ▼ handleScreenShareStream(stream, "getDisplayMedia-via-tfl")(复用现有 UI/IPC 通知)

Task 1:将 IPC 通道加入安全白名单

文件:app/security/ipcValidator.js

由于仓库为补偿禁用 contextIsolation 与 sandbox 的场景实现了 IPC 通道白名单机制(见该文件头部注释 "Provides security validation for IPC channels as a compensating control"),任何新增 IPC 通道都必须显式登记,否则会被拒绝。

  • Step 1:插入通道名。在allowedChannels的屏幕共享区块(当前包含cancel-desktop-media、choose-desktop-media、desktop-capturer-get-sources、screen-sharing-started等,见 app/security/ipcValidator.js 第 24–42 行)中按字母序插入'show-tfl-stream-picker'。该区块注释明确要求 "sorted alphabetically"(按字母排序),插入后show-tfl-stream-picker应位于screen-sharing-stopped与stop-screen-sharing-from-thumbnail之间。

  • Step 2:提交:

git add app/security/ipcValidator.js git commit -m "chore(security): allowlist show-tfl-stream-picker IPC for spike #2534"

值得留意的是,该文件同时维护了appPageOnlyChannels(仅限应用页面使用的通道集合,见 app/security/ipcValidator.js 第 231–239 行,包含close-view、desktop-capturer-get-sources等),它对应 GHSA-3vg9-cwq9-p773 的安全约束。Spike 文档只要求将新通道加入allowedChannels,是否同时纳入appPageOnlyChannels需要结合show-tfl-stream-picker的实际触发上下文(仅由注入到 Teams 页面中的脚本调用)评估——从代码结构看,注入脚本仅运行在受信任的 Teams 页面中,但这一点应在实现时按ipcValidator.js的既有规则确认。

Task 2:在 preload 中暴露 IPC 方法

文件:app/browser/preload.js

  • Step 1:在 electronAPI 对象中新增方法:
// Spike #2534: ask main to show the in-app StreamSelector and return the chosen source ID. showTflStreamPicker: () => ipcRenderer.invoke("show-tfl-stream-picker"),

该方法通过ipcRenderer.invoke发起调用,invoke返回的 Promise 会在主进程 handler 解析后收到选中的源 ID(或取消时的null)。

  • Step 2:提交:
git add app/browser/preload.js git commit -m "feat(preload): expose showTflStreamPicker for spike #2534"

从现有实现看,app/browser/preload.js 中已经有一批屏幕共享相关方法(sendScreenSharingStarted、sendScreenSharingStopped、stopSharing、sendSelectSource等,位于该文件第 95–109 行附近),showTflStreamPicker与它们并列放置即可,保持命名与注释风格一致。

Task 3:注册主进程 IPC handler

文件:app/mainAppWindow/index.js,靠近现有setDisplayMediaRequestHandler注册处(即bindDisplayMediaHandler附近)。

  • Step 1:注册 handler:
// Spike #2534: alternative entry point to the in-app StreamSelector for the // Wayland case, where setDisplayMediaRequestHandler is bypassed by Chromium's // native getDisplayMedia + WebRTCPipeWireCapturer. Returns the chosen source // ID so the renderer can synthesise the stream via getUserMedia. ipcMain.handle("show-tfl-stream-picker", () => new Promise((resolve) => { streamSelector.show((source) => { if (!source) { resolve(null); return; } handleScreenSourceSelection(source, () => resolve(source.id)); }); }) );

这段代码有三层关键逻辑:

  1. 返回一个 Promise,其解析值要么是用户选中的源 ID,要么在取消时解析为null;
  2. 包装streamSelector.show(callback):StreamSelector的show()方法会在用户选择或关闭视图后回调一个 source 对象(取消时回调null),实现见 app/screenSharing/index.js 第 25–65 行;
  3. 在拿到非空 source 时调用handleScreenSourceSelection(source, () => resolve(source.id)):这一步会触发setupScreenSharing(从而创建预览窗口),并在完成后把source.id解析给渲染进程。

handleScreenSourceSelection之所以存在,是因为仓库经历了 #2713 的教训:Wayland/PipeWire 下每次desktopCapturer.getSources()都会开启全新的 portal 会话并产生新的源 ID,拾取器枚举得到的 ID 永远无法匹配第二次枚举的结果。因此实现采用"直接用拾取器返回的 source,绝不重新查询"的策略(见 app/mainAppWindow/index.js 第 100–130 行注释,以及 app/screenSharing/README.md 的 "Single enumeration rule" 一节)。

  • Step 2:运行 lint:
npm run lint

预期结果:PASS。

  • Step 3:提交:
git add app/mainAppWindow/index.js git commit -m "feat(screenSharing): add show-tfl-stream-picker IPC handler for spike #2534"

Task 4:在渲染进程拦截 getDisplayMedia

文件:app/screenSharing/injectedScreenSharing.js(第 60–81 行的monitorScreenSharing包装区域)。

这是整个 Spike 的核心改动:把原有的getDisplayMedia包装器替换为"先走应用内拾取器、失败则回退原生"的新逻辑。

  • Step 1:替换 getDisplayMedia 包装器,新逻辑必须满足五个行为要求:
  1. 先调用electronAPI.showTflStreamPicker();
  2. 若拿到源 ID,则调用originalGetUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: id } } });
  3. 若拾取器返回null(用户取消),抛出DOMException('Permission denied', 'NotAllowedError');
  4. 若 IPC 本身出错(如electronAPI不存在),回退到originalGetDisplayMedia(constraints),保证分享仍可用;
  5. 任何成功的流都继续调用handleScreenShareStream(stream, "getDisplayMedia-via-tfl"),保持现有 UI 与 IPC 通知的一致性。

完整代码:

navigator.mediaDevices.getDisplayMedia = function (constraints) { console.debug("[SCREEN_SHARE_DIAG] getDisplayMedia intercepted (spike #2534 path)"); disableAudioInConstraints(constraints, "getDisplayMedia"); const fallback = () => originalGetDisplayMedia(constraints) .then((stream) => { console.debug(`[SCREEN_SHARE_DIAG] Screen sharing started via getDisplayMedia native (${stream.getAudioTracks().length}a/${stream.getVideoTracks().length}v)`); handleScreenShareStream(stream, "getDisplayMedia"); return stream; }); const electronAPI = globalThis.electronAPI; if (!electronAPI?.showTflStreamPicker) { console.debug("[SCREEN_SHARE_DIAG] showTflStreamPicker unavailable, falling back to native"); return fallback(); } return electronAPI.showTflStreamPicker() .then((sourceId) => { if (!sourceId) { throw new DOMException("User cancelled screen share", "NotAllowedError"); } console.debug(`[SCREEN_SHARE_DIAG] TFL picker returned source ${sourceId}, calling getUserMedia`); return originalGetUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: sourceId, }, }, }); }) .then((stream) => { console.debug(`[SCREEN_SHARE_DIAG] Screen sharing started via TFL picker (${stream.getAudioTracks().length}a/${stream.getVideoTracks().length}v)`); handleScreenShareStream(stream, "getDisplayMedia-via-tfl"); return stream; }) .catch((error) => { if (error?.name === "NotAllowedError") { throw error; } console.error(`[SCREEN_SHARE_DIAG] TFL picker path failed (${error?.name}/${error?.message}), falling back to native`); return fallback(); }); };
  • Step 2:运行 lint:
npm run lint

预期结果:PASS。

  • Step 3:提交:
git add app/screenSharing/injectedScreenSharing.js git commit -m "spike(screenSharing): route getDisplayMedia through in-app picker for #2534"

与现有注入脚本的衔接:防重复注入与流处理一致性

injectedScreenSharing.js并非从零开始,它已经有完整的运行框架,Spike 只是替换其中的getDisplayMedia包装器。理解现有实现有助于把握改动的边界:

  • 防重复注入守卫:脚本顶部通过globalThis.__tflScreenShareInjected标记防止重复注入。mainAppWindow的injectScreenSharingLogic在每次did-finish-load时都会执行(登录跳转、内部导航、iframe 加载都会触发多次),若没有这个守卫,每轮注入都会在旧的getDisplayMedia包装器上再叠一层,导致一次分享调用扇出成 N 个流处理,预览端端口竞争(见 app/screenSharing/injectedScreenSharing.js 第 1–13 行);
  • disableAudioInConstraints:强制关闭分享流的音频,防止回声(第 55–63 行);
  • handleScreenShareStream:注册流、发送screen-sharing-started事件(主进程据此打开预览窗口并建立 MessagePort 连接)、启动快照中继与停止按钮监控(第 148–204 行)。Spike 路径复用它,保证"经应用内拾取器"与"原生路径"产出的流行为完全一致;
  • 快照中继(startVideoFrameRelay):以 5fps 的频率将分享轨道抽帧为 ImageBitmap,通过 MessagePort 传给预览窗口(第 214–354 行)。这个机制本身就是 #2534 相关工作的产物——预览不再二次调用getUserMedia/portal(Wayland 下无法复用 PipeWire token),而是"一次捕获,两端消费"。

预览窗口侧的配合机制

预览窗口侧(app/screenSharing/previewWindow.html)接收主进程经screen-share-port通道投递的 MessagePort,用bitmaprenderer上下文将 ImageBitmap 逐帧绘制到 canvas 上,并随首帧自适应窗口尺寸(最大 400×300、最小 200×120,见该文件第 71–97 行)。端口转发由 app/screenSharing/previewWindowPreload.js 完成——MessagePort无法通过 contextBridge 函数参数序列化,只能经window.postMessage配合 transfer 跨越 contextIsolation 边界,并限定目标为window.location.origin以满足跨源安全检查。主进程侧的端口接线逻辑位于 app/mainAppWindow/index.js 第 766–801 行的ipcMain.on("screen-sharing-started")广播监听器中:它同时向分享方渲染进程(sender)与预览窗口投递一对MessageChannelMain端口。Spike 路径由于同样调用了handleScreenShareStream,会自动走通这条已建好的预览管线。

相关配置项

预览窗口是否显示、是否置顶,由配置项screenSharing.thumbnail控制,定义于 app/config/options.js 第 50–77 行:

  • screenSharing.thumbnail.enabled(boolean,默认true):分享开始时自动显示预览小窗;
  • screenSharing.thumbnail.alwaysOnTop(boolean,默认true):预览小窗保持置顶;
  • screenSharing.lockInhibitionMethod("Electron"/"WakeLockSentinel",默认"Electron"):分享期间的锁屏抑制方式。

验证 Spike 时若不想被预览窗口干扰,或想专门确认预览窗口行为,可以通过关闭thumbnail.enabled隔离变量。

Task 5 & 6:推送分支、打开草稿 PR 并在 issue 上留评

  • Step 1:推送分支:
git push -u origin spike/2534-wayland-in-app-picker
  • Step 2:创建草稿 PR:
gh pr create --draft --base main --title "spike(#2534): route getDisplayMedia through in-app picker" --body "<body with test plan>"

PR 描述必须包含:

  • 指向 #2534 的链接(注意:由于这是 Spike,不能用closes关键字自动关 issue);

  • 假设陈述(hypothesis);

  • 构建产物(build artifact)的指引;

  • 给 piotrleszczynski 的测试计划(test plan)。

  • Step 3:在 #2534 上评论,请 piotrleszczynski 拉取 PR 构建产物进行测试,并给出精确的操作路径与日志中应观察的关键信息。

验证计划:Wayland 会话上的判定性测试

Spike 的成败取决于一套明确的验证步骤。报告人(reporter)在 Wayland 会话上进行测试,环境参考为KDE Plasma + xdg-desktop-portal-kde(TUXEDO OS):

  1. 安装 PR 构建的 deb 包;
  2. 正常启动(无需--ozone-platform=x11参数);
  3. 开始或加入一个会议;
  4. 点击 Share → Screen(分享 → 屏幕);
  5. 确认出现 TFL 应用内拾取器网格(而不是 KDE/portal 原生拾取器);
  6. 选择一个屏幕;
  7. 确认右下角出现小的缩略预览窗口;
  8. 确认会议中的实际分享正常(其他与会者能看到你的屏幕);
  9. 在预览窗或 Teams 中点击 "Stop sharing",确认预览窗口关闭。

判定标准(如何解读结果)

  • 若步骤 5 与 7 均通过→ 假设成立,可以将该方案从 Spike 升级为完整特性(届时需要补上:配置开关、多账户会话处理、X11 验证、自动化测试);
  • 若步骤 5 失败→ 需要做日志分析(说明应用内拾取器在 Wayland 上没有被唤起);
  • 若步骤 8 失败→ 说明getUserMedia+chromeMediaSource: 'desktop'在 Wayland 上对 Teams 的 WebRTC 管线并不等价于原生getDisplayMedia,Spike 将证伪该方案。

这条判定链条设计得非常清晰:每一步失败都对应一个明确的结论方向,避免"部分成功"带来的模糊判断。

从 Spike 到正式特性的后续路线

Spike 文档明确给出了升级为完整特性的路线图。当验证假设成立后,需要补齐以下内容:

  • 配置开关:为"Wayland 走应用内拾取器"的行为增加 config flag(参照 app/config/options.js 的screenSharing配置块模式,为type: "object"+fields结构,并可参照 docs-site/docs/configuration.md 的文档约定);
  • 多账户会话处理:现有代码中,setDisplayMediaRequestHandler是 per-session 的,多账户 profile 视图(运行在各自分区会话中)需要单独的绑定,这已在 #2529 中处理(见 app/mainAppWindow/profileViewManager.js 第 526–529 行附近)。新的show-tfl-stream-picker路径同样需要评估多账户场景下streamSelector与handleScreenSourceSelection的绑定范围;
  • X11 验证:确认拦截逻辑在 X11 上不破坏现有 handler 路径;
  • 自动化测试:参照 tests/unit 下的既有模式(如screenSharingInjection.test.js、meetingStartDetector.test.js等)为拦截逻辑补充单元测试。

风险与注意事项汇总

综合 Spike 文档与仓库源码,实施本方案时有几个关键风险点需要牢记:

  1. 单次枚举规则(#2713):拾取器返回的源 ID 必须原样传递,绝不能重新调用desktopCapturer.getSources()"刷新",否则 Wayland/PipeWire 的新 portal 会话会产生新 ID 导致分享失败;
  2. 源 ID 格式约束(ADR 001):Wayland 下需要screen:x:y或window:x:y格式的 desktopCapturer 源 ID,MediaStream UUID 会失败,详见 ADR 001: DesktopCapturer Source ID Format。同时 app/screenSharing/service.js 第 131–146 行也体现了这一约束:screen-sharing-started事件只接受screen:/window:前缀的源 ID;
  3. 取消与错误语义:用户取消必须以NotAllowedError抛出(与getDisplayMedia的规范语义一致),其他错误则静默回退原生路径,保证功能韧性;
  4. 音频剥离:新路径的getUserMedia约束中audio: false必须保留,同时disableAudioInConstraints依旧会作用于getDisplayMedia的原始约束,避免回声(相关历史问题见 app/screenSharing/injectedScreenSharing.js 第 119–122 行注释中提及的 #1871、#1896);
  5. 重复注入与重复调用:__tflScreenShareInjected守卫和activeFrameRelay单例守卫都是防止"一份分享、N 条管线"的关键,改动时不能破坏。

结语

#2534 Spike 是 teams-for-linux 在 Wayland 屏幕分享问题上的一次精准工程尝试:它不试图与 Chromium 的原生 portal 路径对抗,而是通过注入脚本 + 新 IPC + 复用已验证的getUserMedia约束形态,把源选择权"接回"应用内拾取器,从而让预览窗口这条唯一缺失的功能链在 Wayland 上重新贯通。本文梳理的 4 个文件改动、完整代码与 9 步验证计划均直接继承自仓库中的 Spike 文档,并结合 app/screenSharing 模块、app/mainAppWindow/index.js、app/security/ipcValidator.js 与 ADR 008 等源码与决策记录做了纵深展开,读者可以沿着这些路径继续深入阅读仓库实现,验证每一步结论。

  • 桌面应用
  • 即时通讯

【免费下载链接】teams-for-linux

Unofficial Microsoft Teams for Linux client

项目地址:https://gitcode.com/gh_mirrors/te/teams-for-linux
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 18:35:19

Map 的 putIfAbsent(k, v) 详解

承接前两篇早会分享&#xff1a;getOrDefault&#xff08;借你用一下不存&#xff09;→ computeIfAbsent&#xff08;没有就现场造一个&#xff0c;并真的存进去&#xff09;。 这篇讲第三兄弟 putIfAbsent&#xff1a;没有才放进去——而且放的是你提前准备好的值&#xff0c…

作者头像 李华
网站建设 2026/10/14 18:33:50

死锁的四个必要条件:从多线程事故到系统化排查

1. 死锁不是玄学&#xff1a;从一次真实的多线程事故说起三年前我在某团队维护一个订单处理服务&#xff0c;有一阵子线上频繁报警&#xff0c;线程池被占满&#xff0c;请求大量超时。翻日志发现一段很诡异的循环&#xff1a;A任务在等B任务释放锁&#xff0c;B任务又在等A任务…

作者头像 李华
网站建设 2026/10/14 18:33:29

2026年全球AI大模型API聚合平台深度评测:企业级与开发者各自的首选

选大模型 API 聚合平台&#xff0c;2026 年公认的四个硬维度是&#xff1a;延迟、价格、模型覆盖、合规支付。本篇基于这四个维度对全球主流聚合服务做一轮深度评测&#xff0c;覆盖 OpenRouter、硅基流动、词元之河(TokenRiver.ai)等代表选手&#xff0c;并给出企业与开发者各…

作者头像 李华