fix(voice): bundle native audio addon into standalone archives - #5628
Conversation
Standalone archives shipped only curated dist/ entries, so the esbuild-external @qwen-code/audio-capture addon couldn't be resolved at runtime — streaming voice was unavailable in standalone installs (batch only, and only with SoX on PATH). create-standalone-package.js now bundles the addon into lib/node_modules (where the bundled lib/cli.js resolves bare specifiers): the trimmed package.json (install hook removed; type/exports kept for ESM resolution) + dist + only this target's prebuild (win-x64 -> win32-x64) + its zero-dep runtime dependency node-gyp-build. Targets without a matching prebuild (e.g. local builds) ship without it and degrade to SoX/arecord as before (warns, doesn't fail). The release pipeline already downloads prebuilds before packaging. Refs: QwenLM#5502, QwenLM#5590. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| dereference: true, | ||
| verbatimSymlinks: false, | ||
| }; | ||
| fs.cpSync( |
There was a problem hiding this comment.
[Suggestion] fs.cpSync copies the entire packages/audio-capture/dist/ verbatim, including platform.test.js, platform.test.d.ts, and platform.test.js.map. The addon's own package.json files field explicitly excludes test artifacts ("!dist/**/*.test.*"), but this raw filesystem copy bypasses that filter.
| fs.cpSync( | |
| fs.cpSync( | |
| path.join(addonSrc, 'dist'), | |
| path.join(addonDest, 'dist'), | |
| { ...copyOpts, filter: (src) => !/\.test\.[mc]?[jt]s(\.map)?$/.test(src) }, | |
| ); |
— qwen3.7-max via Qwen Code /review
Verification — native audio addon bundled into standalone archives ✅This is a packaging change, so rather than driving the interactive TUI I verified by running the real standalone packaging locally (built from HEAD Results
Key evidenceStandalone load — probe run from inside the extracted archive's Bundled layout (extracted Strict / degrade / mapping: Notes
No issues found end-to-end — the fix bundles and loads the native addon in a standalone install, with correct strict/degrade and platform mapping. LGTM. 🚀 中文版(合并参考)验证 —— 将原生音频 addon 打包进 standalone 归档 ✅这是一个打包改动,因此我没有去驱动交互式 TUI,而是在本地 真实运行 standalone 打包流程(基于 HEAD 结论
关键证据standalone 加载 —— 在解开后的归档 打包布局(解开 严格 / 降级 / 映射: 说明
端到端未发现问题 —— 该修复确实把原生 addon 打进并能在 standalone 安装中加载,严格/降级与平台映射均正确。LGTM 🚀 |
| return; | ||
| } | ||
|
|
||
| const nodeRequire = createRequire(import.meta.url); |
There was a problem hiding this comment.
[Suggestion] copyNativeAddon can throw raw ENOENT (if packages/audio-capture/dist/ is missing) or MODULE_NOT_FOUND (if node-gyp-build isn't resolvable) without a contextual error message. The rest of this file uses fail() with descriptive messages (e.g., "Required dist asset missing: ..."). Wrapping these calls in try/catch with fail(...) would match the existing pattern and make build failures self-diagnosing — especially important in CI where a raw ENOENT from cpSync doesn't mention the addon-bundling step.
| const nodeRequire = createRequire(import.meta.url); | |
| let nodeGypBuildSrc; | |
| try { | |
| const nodeRequire = createRequire(import.meta.url); | |
| nodeGypBuildSrc = path.dirname( | |
| nodeRequire.resolve('node-gyp-build/package.json'), | |
| ); | |
| } catch { | |
| fail('node-gyp-build (runtime dep of @qwen-code/audio-capture) is not resolvable from the repo root. Run npm install before packaging.'); | |
| } |
Similarly, add an existence check before the dist/ copy:
const addonDist = path.join(addonSrc, 'dist');
if (!fs.existsSync(addonDist)) {
fail(`audio-capture compiled output is missing: ${addonDist}. Run 'npm run build' first.`);
}— qwen3.7-max via Qwen Code /review
| 'audio-capture', | ||
| 'dist', | ||
| ); | ||
| expect(existsSync(path.join(addonDist, 'index.js'))).toBe(true); |
There was a problem hiding this comment.
[Suggestion] This test verifies that dist/index.js is copied and test files are filtered, but never asserts the .node prebuild binary or node-gyp-build were bundled — the two core reasons copyNativeAddon exists. If the prebuild copy or node-gyp-build copy were accidentally removed, this test would still pass while streaming voice silently breaks in every standalone release.
Consider adding:
// Verify .node prebuild is bundled
const addonPrebuild = path.join(
extractDir, 'qwen-code', 'lib', 'node_modules',
'@qwen-code', 'audio-capture', 'prebuilds', 'linux-x64',
);
expect(existsSync(path.join(addonPrebuild, '@qwen-code+audio-capture.node'))).toBe(true);
// Verify node-gyp-build is bundled
const nodeGypBuildDir = path.join(
extractDir, 'qwen-code', 'lib', 'node_modules', 'node-gyp-build',
);
expect(existsSync(path.join(nodeGypBuildDir, 'package.json'))).toBe(true);— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
CI failure root cause: two new tests build a Linux
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Hi @qqqys — thanks for the PR! The change itself looks well-thought-out (and @wenshao already ran thorough verification).
However, the PR body doesn't follow our PR template. It uses custom headings (Problem / Fix / Validation) instead of the required template sections:
## What this PR does## Why it's needed## Reviewer Test Plan(with How to verify, Evidence, Tested on)## Risk & Scope## Linked Issues<details>中文说明</details>
Could you restructure the PR description to match the template? The content is all there — it just needs to be organized into the expected sections so reviewers can quickly find what they need (especially the Tested-on matrix and risk/scope callouts for a cross-platform packaging change like this).
Also, @wenshao identified a CI failure in the two new tests: they use bare it() instead of itOnUnix(), causing the Linux tar fixture to fail on Windows runners. That'll need fixing too.
中文说明
@qqqys 你好 — 感谢 PR!改动本身考虑得很周全(@wenshao 也已经做了详细验证)。
但 PR 正文没有按照我们的 PR 模板 来写。用的是自定义标题(Problem / Fix / Validation),缺少模板要求的章节:
## What this PR does## Why it's needed## Reviewer Test Plan(含 How to verify、Evidence、Tested on)## Risk & Scope## Linked Issues<details>中文说明</details>
能否按模板重新组织一下 PR 描述?内容其实都有了,只是需要按预期章节整理,方便 reviewer 快速找到关键信息(尤其是这种跨平台打包改动的 Tested-on 矩阵和风险/范围说明)。
另外,@wenshao 发现了 CI 失败的原因:两个新测试用了裸 it() 而不是 itOnUnix(),导致 Linux 的 tar fixture 在 Windows runner 上失败。这个也需要一起修。
— Qwen Code · qwen3.7-max
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Hi @qqqys — re-triage after your latest commit (26d8120 — Windows test fix).
Template: the PR body still uses custom headings (Problem / Fix / Validation) instead of the PR template sections (What this PR does, Why it's needed, Reviewer Test Plan, Risk & Scope, Linked Issues). Would be great to align for consistency — but noting this isn't blocking given the technical quality and maintainer sign-off.
Direction: clearly aligned — standalone archives can't resolve @qwen-code/audio-capture without this, making streaming voice unavailable. Direct follow-up to #5502/#5590 (P0). Fills a real gap.
Approach: minimal and focused. One new function (copyNativeAddon) slotted into the existing packaging pipeline, graceful degradation when prebuilds are absent, strict mode for official releases via QWEN_STANDALONE_REQUIRE_AUDIO_CAPTURE_PREBUILD. Three focused files, no scope creep.
Code review: clean implementation — follows the existing code style in create-standalone-package.js, reuses assertNoSymlinks/fail patterns already established. The TARGET_PREBUILD_DIR map and hasNativePrebuild helper are straightforward. Test artifact filtering (*.test.*) is correct. The node-gyp-build runtime dep bundling is the right call. No issues found.
CI: all green — macOS ✓, Windows ✓ (after your latest fix), Ubuntu ✓, Lint ✓, CodeQL ✓.
Prior verification from @wenshao confirmed the addon bundles and loads correctly in a real standalone layout, strict/degrade/mapping all work as designed. LGTM.
中文说明
Hi @qqqys —— 在你最新提交 (26d8120 — Windows 测试修复) 之后重新过了一遍 triage。
模板: PR 描述仍然使用自定义标题(Problem / Fix / Validation),而非 PR 模板 要求的章节(What this PR does、Why it's needed、Reviewer Test Plan、Risk & Scope、Linked Issues)。建议后续对齐以保持一致性——但鉴于技术质量和 maintainer 签核,这不是阻塞项。
方向: 明确对齐 —— standalone 归档无法解析 @qwen-code/audio-capture,导致流式语音不可用。是 #5502/#5590(P0)的直接后续。填补了实际缺口。
方案: 最小且聚焦。一个新函数(copyNativeAddon)嵌入现有打包流水线,prebuild 缺失时优雅降级,官方发布通过 QWEN_STANDALONE_REQUIRE_AUDIO_CAPTURE_PREBUILD 启用严格模式。3 个文件,无范围蔓延。
代码审查: 实现干净 —— 遵循 create-standalone-package.js 现有代码风格,复用已有的 assertNoSymlinks/fail 模式。TARGET_PREBUILD_DIR 映射和 hasNativePrebuild 辅助函数都很简洁。测试产物过滤(*.test.*)正确。node-gyp-build 运行时依赖一并打入是正确的做法。未发现问题。
CI: 全绿 —— macOS ✓、Windows ✓(最新修复后)、Ubuntu ✓、Lint ✓、CodeQL ✓。
@wenshao 之前的验证已确认 addon 在真实 standalone 布局中正确打包并加载,严格/降级/映射均按预期工作。LGTM。
— Qwen Code · qwen3.7-max
Follow-up to #5502 (voice dictation) — P0 standalone streaming (tracked in #5590).
Problem: standalone release archives ship only curated
dist/entries, notnode_modules. The native addon@qwen-code/audio-captureis esbuild-external and resolved at runtime viaimport('@qwen-code/audio-capture'), so in a standalone install it can't be found → streaming voice is unavailable (batch only, and only if SoX is on PATH).Fix:
create-standalone-package.jsnow bundles the addon intolib/node_modules/(where the bundledlib/cli.jsresolves bare specifiers):@qwen-code/audio-capture/: trimmedpackage.json(install hook removed;type/exportskept for ESM resolution) +dist/+ only this archive target'sprebuilds/<platform>-<arch>/(win-x64→win32-x64).node-gyp-build/— the addon's only (zero-dep) runtime dependency.The release pipeline already downloads prebuilds before packaging.
Validation: locally simulated the bundled layout and confirmed ESM
import('@qwen-code/audio-capture')fromlib/resolves → loads the darwin-arm64 prebuild →getPlatformBackendName()=coreaudio,microphoneAuthorizationStatus()reads OK. Other targets use the same code path with their downloaded prebuild; full per-archive load is exercised by the release build.Optional follow-up (not in this PR): extend
verify:installation-releaseto assert the addon loads in each built archive, closing the per-platform loop in CI.Refs #5502, #5590.