feat(native): record encoding (#14188)

fix #13784 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Start/stop system or meeting recordings with Ogg/Opus artifacts and
native start/stop APIs; workspace backup recovery.

* **Refactor**
* Simplified recording lifecycle and UI flows; native runtime now
orchestrates recording/processing and reporting.

* **Bug Fixes**
* Stronger path validation, safer import/export dialogs, consistent
error handling/logging, and retry-safe recording processing.

* **Chores**
* Added cross-platform native audio capture and Ogg/Opus encoding
support.

* **Tests**
* New unit, integration, and e2e tests for recording, path guards,
dialogs, and workspace recovery.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-03-22 02:50:14 +08:00
committed by GitHub
parent 6a93566422
commit bcf2a51d41
44 changed files with 2921 additions and 1143 deletions
@@ -1,4 +1,5 @@
import { join } from 'node:path';
import { realpath } from 'node:fs/promises';
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import type { EventBasedChannel } from 'async-call-rpc';
@@ -47,6 +48,130 @@ export class MessageEventChannel implements EventBasedChannel {
export const resourcesPath = join(__dirname, `../resources`);
function normalizeComparedPath(path: string, caseInsensitive: boolean) {
return caseInsensitive ? path.toLowerCase() : path;
}
export function isPathInsideBase(
basePath: string,
targetPath: string,
options: { caseInsensitive?: boolean } = {}
) {
const { caseInsensitive = false } = options;
const normalizedBase = normalizeComparedPath(
resolve(basePath),
caseInsensitive
);
const normalizedTarget = normalizeComparedPath(
resolve(targetPath),
caseInsensitive
);
const rel = relative(normalizedBase, normalizedTarget);
return (
rel === '' ||
(!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`))
);
}
export function resolvePathInBase(
basePath: string,
targetPath: string,
options: { caseInsensitive?: boolean; label?: string } = {}
) {
const resolvedBase = resolve(basePath);
const resolvedTarget = resolve(resolvedBase, targetPath);
if (!isPathInsideBase(resolvedBase, resolvedTarget, options)) {
throw new Error(
options.label ? `Invalid ${options.label}` : 'Invalid path'
);
}
return resolvedTarget;
}
export async function resolveExistingPath(targetPath: string) {
try {
return await realpath(targetPath);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === 'ENOENT' || code === 'ENOTDIR') {
return resolve(targetPath);
}
throw error;
}
}
export async function resolveExistingPathInBase(
basePath: string,
targetPath: string,
options: { caseInsensitive?: boolean; label?: string } = {}
) {
const [resolvedBase, resolvedTarget] = await Promise.all([
resolveExistingPath(basePath),
resolveExistingPath(targetPath),
]);
if (!isPathInsideBase(resolvedBase, resolvedTarget, options)) {
throw new Error(
options.label ? `Invalid ${options.label}` : 'Invalid path'
);
}
return resolvedTarget;
}
export function assertPathComponent(
value: string,
label: string = 'path component'
) {
const hasControlChar = Array.from(value).some(
character => character.charCodeAt(0) < 0x20
);
if (
!value ||
value === '.' ||
value === '..' ||
/[/\\]/.test(value) ||
hasControlChar
) {
throw new Error(`Invalid ${label}`);
}
return value;
}
export function normalizeWorkspaceIdForPath(
value: string,
options: { windows?: boolean; label?: string } = {}
) {
const { windows = isWindows(), label = 'workspace id' } = options;
const safeValue = assertPathComponent(value, label);
if (!windows) {
return safeValue;
}
const windowsReservedChars = new Set(['<', '>', ':', '"', '|', '?', '*']);
let normalized = '';
for (const character of safeValue) {
normalized += windowsReservedChars.has(character) ? '_' : character;
}
while (normalized.endsWith('.') || normalized.endsWith(' ')) {
normalized = normalized.slice(0, -1);
}
if (!normalized || normalized === '.' || normalized === '..') {
throw new Error(`Invalid ${label}`);
}
return normalized;
}
// credit: https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js
export function shallowEqual<T>(objA: T, objB: T) {
if (Object.is(objA, objB)) {