Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

199 lines
6.9 KiB
JavaScript

#!/usr/bin/env node
// Patches the Capacitor-generated iOS Info.plist for continuous-chat
// support (R10 §6.1).
//
// Capacitor regenerates ios/App/App/Info.plist on `cap sync`; this script
// runs after `cap sync ios` and idempotently inserts the keys the voice
// stack needs:
//
// UIBackgroundModes = ["audio"]
// NSMicrophoneUsageDescription
// NSSpeechRecognitionUsageDescription
//
// Without `UIBackgroundModes = audio` the AVAudioSession the TalkMode
// plugin configures (.playAndRecord / .voiceChat with .mixWithOthers +
// .duckOthers) is paused when the screen locks. With it, the session
// survives lock and continuous-chat keeps working end-to-end.
//
// The script is intentionally a small XML-aware text patcher rather than
// pulling in a plist parser. The schema is well-defined and Capacitor
// generates the same `<dict>` layout every sync; we look for the keys we
// own, add them if missing, leave anything else untouched.
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { ensurePlistUrlScheme } from "../../app-core/scripts/lib/ios-plist-url-scheme.mjs";
import { readAppIdentity } from "../../app-core/scripts/lib/read-app-identity.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const APP_DIR = resolve(__dirname, "..");
const MIC_PURPOSE = "Eliza listens when you talk to your agent.";
const SPEECH_PURPOSE =
"Eliza transcribes your speech so you can talk to the agent.";
const KEYS =
/** @type {Array<{ key: string; value: string | string[] | boolean }>} */ ([
{ key: "UIBackgroundModes", value: ["audio"] },
{ key: "NSMicrophoneUsageDescription", value: MIC_PURPOSE },
{ key: "NSSpeechRecognitionUsageDescription", value: SPEECH_PURPOSE },
// Live Activities for the voice/dictation session (#12185 D10). Kept in
// sync with the app-core merger (scripts/mobile/ios-plist.mjs) so the two
// plist patchers agree.
{ key: "NSSupportsLiveActivities", value: true },
]);
const TARGET_PATH = resolve(__dirname, "..", "ios", "App", "App", "Info.plist");
function findInsertionPoint(xml) {
// Insert before the closing `</dict>` of the top-level `<plist>`. The
// generated file always has exactly one top-level dict; we anchor on
// the last `</dict>` followed by `</plist>`.
const m = xml.match(/<\/dict>\s*<\/plist>/);
if (!m || typeof m.index !== "number") {
throw new Error("Info.plist: could not locate top-level </dict>");
}
return m.index;
}
function hasKey(xml, key) {
return new RegExp(`<key>${escapeKey(key)}</key>`).test(xml);
}
function escapeKey(key) {
return key.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
}
function renderEntry({ key, value }) {
if (Array.isArray(value)) {
const items = value
.map((v) => `\t\t<string>${escapeXml(v)}</string>`)
.join("\n");
return `\t<key>${key}</key>\n\t<array>\n${items}\n\t</array>\n`;
}
if (typeof value === "boolean") {
return `\t<key>${key}</key>\n\t<${value ? "true" : "false"}/>\n`;
}
return `\t<key>${key}</key>\n\t<string>${escapeXml(value)}</string>\n`;
}
function escapeXml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
function ensureArrayStringValue(xml, key, value) {
const keyPattern = `<key>${escapeKey(key)}</key>`;
const blockPattern = new RegExp(
`(${keyPattern}\\s*<array>)([\\s\\S]*?)(\\s*</array>)`,
);
const match = xml.match(blockPattern);
if (!match) {
return { next: xml, changed: false };
}
const [, prefix, body, suffix] = match;
if (
new RegExp(`<string>${escapeKey(escapeXml(value))}</string>`).test(body)
) {
return { next: xml, changed: false };
}
const indent = body.match(/\n(\s*)<string>/)?.[1] ?? "\t\t";
const insertedBody = `${body}\n${indent}<string>${escapeXml(value)}</string>`;
return {
next: xml.replace(blockPattern, `${prefix}${insertedBody}${suffix}`),
changed: true,
};
}
function patchPlist(xml, urlScheme) {
let changed = false;
let next = xml;
for (const entry of KEYS) {
if (hasKey(next, entry.key)) {
if (Array.isArray(entry.value)) {
for (const value of entry.value) {
const result = ensureArrayStringValue(next, entry.key, value);
next = result.next;
changed = changed || result.changed;
}
}
continue;
}
const insertAt = findInsertionPoint(next);
next = next.slice(0, insertAt) + renderEntry(entry) + next.slice(insertAt);
changed = true;
}
const nextWithScheme = ensurePlistUrlScheme(next, urlScheme);
if (nextWithScheme !== next) {
next = nextWithScheme;
changed = true;
}
return { next, changed };
}
function main() {
if (!existsSync(TARGET_PATH)) {
// Not a failure: this script is wired into `cap sync ios`; on
// workspaces that haven't generated the iOS platform yet (CI Linux
// workers, fresh checkouts that only target web/desktop), there's
// nothing to patch and we exit cleanly.
console.log(
`[patch-ios-plist] no Info.plist found at ${TARGET_PATH} — skipping.`,
);
return;
}
const original = readFileSync(TARGET_PATH, "utf8");
const { urlScheme } = readAppIdentity(APP_DIR);
const { next, changed } = patchPlist(original, urlScheme);
if (!changed) {
console.log("[patch-ios-plist] all keys already present — no changes.");
return;
}
writeFileSync(TARGET_PATH, next);
console.log(
"[patch-ios-plist] patched UIBackgroundModes, microphone/speech usage descriptions, and URL scheme.",
);
}
// Allow `node patch-ios-plist.mjs --check` to verify without writing.
const checkOnly = process.argv.includes("--check");
if (checkOnly) {
if (!existsSync(TARGET_PATH)) {
console.log("[patch-ios-plist] no Info.plist; nothing to check.");
process.exit(0);
}
const xml = readFileSync(TARGET_PATH, "utf8");
const { urlScheme } = readAppIdentity(APP_DIR);
const patched = patchPlist(xml, urlScheme);
const missingUrlScheme = ensurePlistUrlScheme(xml, urlScheme) !== xml;
if (!patched.changed && !missingUrlScheme) {
console.log("[patch-ios-plist] OK — all required keys present.");
process.exit(0);
}
const missing = KEYS.filter((k) => !hasKey(xml, k.key));
const incompleteArrays = KEYS.flatMap((entry) => {
if (!Array.isArray(entry.value) || !hasKey(xml, entry.key)) return [];
return entry.value
.filter((value) => ensureArrayStringValue(xml, entry.key, value).changed)
.map((value) => `${entry.key}:${value}`);
});
console.error(
`[patch-ios-plist] missing keys: ${[
...missing.map((k) => k.key),
...incompleteArrays,
...(missingUrlScheme ? [`CFBundleURLTypes:${urlScheme}`] : []),
].join(", ")}`,
);
process.exit(1);
}
main();
// Exports for tests.
export { findInsertionPoint, hasKey, KEYS, patchPlist };