feat(coding): add model tools and device packages

This commit is contained in:
2026-09-02 18:35:26 +08:00
parent 841273b433
commit 664b8823a0
60 changed files with 3847 additions and 2068 deletions

View File

@@ -0,0 +1,609 @@
import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { cp, mkdir, readFile, rename, rm, stat } from 'node:fs/promises';
import path from 'node:path';
import { DefaultPackageManager, SettingsManager } from '@earendil-works/pi-coding-agent';
import { atomicWriteJson } from '../coding-projects/atomic-json';
import {
devicePackageKind,
inspectDevicePackage,
parseDevicePackageSource,
relativeContainedPath,
safeStorageSegment,
type DevicePackageIndexV1,
type DevicePackageRecordV1,
type InstallPreviewV1,
} from './device-package-format';
export * from './device-package-format';
const PLAN_TTL_MS = 10 * 60 * 1000;
const INSTALL_OUTPUT_BYTES = 64 * 1024;
const INSTALL_TIMEOUT_MS = 2 * 60 * 1000;
const INDEX_SCHEMA_VERSION = 1;
const EXECUTABLE_WARNING = '此包包含可执行 Pi extension,将获得完整桌面权限,包括当前进程可用的文件、网络和进程权限。';
export class DevicePackageError extends Error {
constructor(
readonly code:
| 'local_package_source_unsupported'
| 'local_package_not_found'
| 'local_package_manifest_invalid'
| 'local_package_dependency_failed'
| 'local_package_confirmation_required'
| 'local_package_plan_expired'
| 'local_package_install_failed'
| 'local_package_not_installed'
| 'local_package_in_use',
message: string,
) {
super(message);
this.name = 'DevicePackageError';
}
}
export interface DevicePackageInstallInput {
source: string;
executablePath: string;
cliPath: string;
npmCliPath?: string;
agentDir: string;
cwd: string;
env: NodeJS.ProcessEnv;
}
export type DevicePackageInstallRunner = (input: DevicePackageInstallInput) => Promise<void>;
export interface DevicePackageManagerOptions {
rootDir: string;
executablePath?: string;
cliPath?: string;
npmCliPath?: string;
runInstall?: DevicePackageInstallRunner;
now?: () => number;
createId?: () => string;
writeIndex?: (filePath: string, value: DevicePackageIndexV1) => Promise<void>;
onGenerationChanged?(index: DevicePackageIndexV1): Promise<void> | void;
}
interface StagedPlanV1 {
schemaVersion: 1;
requestedByTurnId: string;
preview: InstallPreviewV1;
record: DevicePackageRecordV1;
}
export interface EnabledDevicePackageResources {
generation: number;
packageIds: string[];
packageRefs: Array<{ packageId: string; resolvedVersion: string }>;
skillEntries: Array<{ id: string; entryPath: string; packageRoot: string }>;
extensionPaths: string[];
}
function installEnvironment(agentDir: string): NodeJS.ProcessEnv {
return {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
PI_CODING_AGENT_DIR: agentDir,
PI_TELEMETRY: '0',
GIT_TERMINAL_PROMPT: '0',
GIT_SSH_COMMAND: 'ssh -o BatchMode=yes',
CI: '1',
npm_config_ignore_scripts: 'true',
npm_config_update_notifier: 'false',
npm_config_audit: 'false',
npm_config_fund: 'false',
};
}
export async function runPiPackageInstall(input: DevicePackageInstallInput): Promise<void> {
await mkdir(input.agentDir, { recursive: true });
await mkdir(input.cwd, { recursive: true });
if (input.npmCliPath) {
await atomicWriteJson(path.join(input.agentDir, 'settings.json'), {
npmCommand: [input.executablePath, input.npmCliPath],
});
}
await new Promise<void>((resolve, reject) => {
execFile(
input.executablePath,
[input.cliPath, 'install', input.source, '--no-approve'],
{
cwd: input.cwd,
env: input.env,
windowsHide: true,
timeout: INSTALL_TIMEOUT_MS,
maxBuffer: INSTALL_OUTPUT_BYTES,
},
(error) => error ? reject(error) : resolve(),
);
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function string(value: unknown, field: string, maximum = 2048): string {
if (typeof value !== 'string' || !value.trim() || value.length > maximum) {
throw new DevicePackageError('local_package_install_failed', `${field} is invalid`);
}
return value;
}
function parseRecord(value: unknown): DevicePackageRecordV1 {
if (!isRecord(value) || value.schemaVersion !== 1 || !isRecord(value.source)
|| !Array.isArray(value.skillEntries) || !Array.isArray(value.extensionEntries)
|| typeof value.enabled !== 'boolean' || typeof value.confirmedExecutableCode !== 'boolean') {
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
const sourceKind = value.source.kind;
if (sourceKind !== 'npm' && sourceKind !== 'git' && sourceKind !== 'file') {
throw new DevicePackageError('local_package_install_failed', 'Device package source is invalid');
}
const kind = value.kind;
if (kind !== 'skill-only' && kind !== 'pi-extension' && kind !== 'mixed') {
throw new DevicePackageError('local_package_install_failed', 'Device package kind is invalid');
}
const skillEntries = value.skillEntries.map((entry) => {
if (!isRecord(entry)) throw new DevicePackageError('local_package_install_failed', 'Skill entry is invalid');
return { id: string(entry.id, 'Skill id', 128), entryPath: string(entry.entryPath, 'Skill path') };
});
return {
schemaVersion: 1,
packageId: string(value.packageId, 'Package id', 128),
displayName: string(value.displayName, 'Display name', 256),
resolvedVersion: string(value.resolvedVersion, 'Resolved version', 128),
source: {
kind: sourceKind,
requested: string(value.source.requested, 'Requested source'),
resolved: string(value.source.resolved, 'Resolved source'),
},
kind,
skillEntries,
extensionEntries: value.extensionEntries.map((entry) => string(entry, 'Extension path')),
enabled: value.enabled,
confirmedExecutableCode: value.confirmedExecutableCode,
installedAt: string(value.installedAt, 'Installed at', 64),
};
}
function parseIndex(value: unknown): DevicePackageIndexV1 {
if (!isRecord(value) || value.schemaVersion !== INDEX_SCHEMA_VERSION
|| !Number.isSafeInteger(value.generation) || (value.generation as number) < 0
|| !Array.isArray(value.packages)) {
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
const packages = value.packages.map(parseRecord);
const ids = new Set<string>();
for (const record of packages) {
if (ids.has(record.packageId)) {
throw new DevicePackageError('local_package_install_failed', 'Device package index contains duplicate ids');
}
ids.add(record.packageId);
}
return { schemaVersion: 1, generation: value.generation as number, packages };
}
function cloneIndex(index: DevicePackageIndexV1): DevicePackageIndexV1 {
return structuredClone(index);
}
async function pathMetadata(target: string): Promise<'file' | 'directory' | null> {
return await stat(target).then((value) => value.isDirectory() ? 'directory' : value.isFile() ? 'file' : null)
.catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return null;
throw error;
});
}
async function gitHead(directory: string): Promise<string> {
return await new Promise<string>((resolve, reject) => {
execFile('git', ['rev-parse', 'HEAD'], {
cwd: directory,
windowsHide: true,
timeout: 10_000,
maxBuffer: 4096,
}, (error, stdout) => error ? reject(error) : resolve(stdout.trim()));
});
}
export class DevicePackageManager {
private readonly rootDir: string;
private readonly packagesDir: string;
private readonly stagingDir: string;
private readonly trashDir: string;
private readonly indexPath: string;
private readonly now: () => number;
private readonly createId: () => string;
private readonly runInstall: DevicePackageInstallRunner;
private readonly writeIndex: (filePath: string, value: DevicePackageIndexV1) => Promise<void>;
private readonly activePackages = new Map<string, number>();
private readonly pendingCleanup = new Set<string>();
private operation: Promise<void> = Promise.resolve();
constructor(private readonly options: DevicePackageManagerOptions) {
this.rootDir = path.resolve(options.rootDir);
this.packagesDir = path.join(this.rootDir, 'packages');
this.stagingDir = path.join(this.rootDir, 'staging');
this.trashDir = path.join(this.rootDir, 'trash');
this.indexPath = path.join(this.rootDir, 'index.json');
this.now = options.now ?? Date.now;
this.createId = options.createId ?? randomUUID;
this.runInstall = options.runInstall ?? runPiPackageInstall;
this.writeIndex = options.writeIndex ?? atomicWriteJson;
}
async prepare(sourceValue: string, requestedByTurnId: string): Promise<InstallPreviewV1> {
if (!requestedByTurnId.trim()) {
throw new DevicePackageError('local_package_confirmation_required', 'Request turn id is required');
}
let source;
try {
source = parseDevicePackageSource(sourceValue);
} catch (error) {
throw new DevicePackageError('local_package_source_unsupported', (error as Error).message);
}
const planId = this.createId();
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(planId)) {
throw new DevicePackageError('local_package_install_failed', 'Install plan id is invalid');
}
const planRoot = path.join(this.stagingDir, planId);
const packageRoot = path.join(planRoot, 'package');
await mkdir(planRoot, { recursive: true });
try {
let resolvedSource = sourceValue.trim();
if (source.kind === 'file') {
const metadata = await pathMetadata(source.absolutePath);
if (!metadata) throw new DevicePackageError('local_package_not_found', 'Local package source does not exist');
if (metadata === 'directory') await cp(source.absolutePath, packageRoot, { recursive: true, force: false });
else {
await mkdir(packageRoot, { recursive: true });
await cp(source.absolutePath, path.join(packageRoot, path.basename(source.absolutePath)), { force: false });
}
resolvedSource = source.absolutePath;
} else {
const executablePath = this.options.executablePath;
const cliPath = this.options.cliPath;
if (!executablePath || !cliPath) {
throw new DevicePackageError('local_package_dependency_failed', 'Bundled Pi installer is unavailable');
}
const agentDir = path.join(planRoot, 'pi-agent');
const cwd = path.join(planRoot, 'project');
await this.runInstall({
source: source.spec,
executablePath,
cliPath,
...(this.options.npmCliPath ? { npmCliPath: this.options.npmCliPath } : {}),
agentDir,
cwd,
env: installEnvironment(agentDir),
});
const manager = new DefaultPackageManager({
cwd,
agentDir,
settingsManager: SettingsManager.inMemory({}, { projectTrusted: false }),
});
const installedPath = manager.getInstalledPath(source.spec, 'user');
if (!installedPath) throw new DevicePackageError('local_package_dependency_failed', 'Pi did not install the package');
if (source.kind === 'npm') {
const installRoot = path.join(agentDir, 'npm');
await cp(installRoot, packageRoot, { recursive: true, force: false });
const relativePackage = relativeContainedPath(installRoot, installedPath);
const nested = path.join(packageRoot, ...relativePackage.split('/'));
const inspected = await inspectDevicePackage(nested);
const prefix = relativeContainedPath(packageRoot, nested);
const remapped = {
...inspected,
skillEntries: inspected.skillEntries.map((entry) => ({
...entry, entryPath: `${prefix}/${entry.entryPath}`,
})),
extensionEntries: inspected.extensionEntries.map((entry) => `${prefix}/${entry}`),
};
return await this.finishPrepare({
planId, planRoot, packageRoot, sourceValue, source, requestedByTurnId,
resolvedSource: `npm:${inspected.displayName}@${inspected.resolvedVersion ?? 'unknown'}`,
inspected: remapped,
});
}
await cp(installedPath, packageRoot, { recursive: true, force: false });
const commit = await gitHead(installedPath).catch(() => {
throw new DevicePackageError('local_package_dependency_failed', 'Git package commit cannot be resolved');
});
resolvedSource = `${source.spec.replace(/@[^/@]+$/u, '')}@${commit}`;
}
return await this.finishPrepare({
planId, planRoot, packageRoot, sourceValue, source, requestedByTurnId, resolvedSource,
inspected: await inspectDevicePackage(
packageRoot,
source.kind === 'file'
? path.basename(source.absolutePath, path.extname(source.absolutePath))
: path.basename(packageRoot),
),
});
} catch (error) {
await rm(planRoot, { recursive: true, force: true }).catch(() => undefined);
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_manifest_invalid', (error as Error).message);
}
}
private async finishPrepare(input: {
planId: string;
planRoot: string;
packageRoot: string;
sourceValue: string;
source: ReturnType<typeof parseDevicePackageSource>;
requestedByTurnId: string;
resolvedSource: string;
inspected: Awaited<ReturnType<typeof inspectDevicePackage>>;
}): Promise<InstallPreviewV1> {
const includesExecutableCode = input.inspected.extensionEntries.length > 0;
const resolvedVersion = input.inspected.resolvedVersion
?? `local-${new Date(this.now()).toISOString().replace(/[-:.]/gu, '')}`;
safeStorageSegment(resolvedVersion);
const preview: InstallPreviewV1 = {
schemaVersion: 1,
planId: input.planId,
expiresAt: new Date(this.now() + PLAN_TTL_MS).toISOString(),
requestedSource: input.sourceValue.trim(),
resolvedSource: input.resolvedSource,
packageId: input.inspected.packageId,
displayName: input.inspected.displayName,
resolvedVersion,
kind: devicePackageKind(input.inspected),
skillEntries: input.inspected.skillEntries,
extensionEntries: input.inspected.extensionEntries,
includesExecutableCode,
ignoredLifecycleScripts: input.inspected.ignoredLifecycleScripts,
warnings: includesExecutableCode ? [EXECUTABLE_WARNING] : [],
scope: 'device-parent-workers',
};
const record: DevicePackageRecordV1 = {
schemaVersion: 1,
packageId: preview.packageId,
displayName: preview.displayName,
resolvedVersion,
source: {
kind: input.source.kind,
requested: preview.requestedSource,
resolved: preview.resolvedSource,
},
kind: preview.kind,
skillEntries: preview.skillEntries,
extensionEntries: preview.extensionEntries,
enabled: true,
confirmedExecutableCode: includesExecutableCode,
installedAt: new Date(this.now()).toISOString(),
};
const plan: StagedPlanV1 = {
schemaVersion: 1,
requestedByTurnId: input.requestedByTurnId,
preview,
record,
};
await atomicWriteJson(path.join(input.planRoot, 'plan.json'), plan);
return structuredClone(preview);
}
async commit(planId: string, confirmed: true, confirmationTurnId: string): Promise<DevicePackageIndexV1> {
if (confirmed !== true || !confirmationTurnId.trim()) {
throw new DevicePackageError('local_package_confirmation_required', 'Literal confirmation is required');
}
return await this.withOperation(async () => {
const plan = await this.readPlan(planId);
if (plan.requestedByTurnId === confirmationTurnId) {
throw new DevicePackageError('local_package_confirmation_required', 'Confirm installation in a new user turn');
}
if (Date.parse(plan.preview.expiresAt) <= this.now()) {
throw new DevicePackageError('local_package_plan_expired', 'Install preview has expired');
}
if (plan.preview.includesExecutableCode && !plan.preview.warnings.includes(EXECUTABLE_WARNING)) {
throw new DevicePackageError('local_package_confirmation_required', 'Executable permission warning is missing');
}
const stagedPackage = path.join(this.stagingDir, planId, 'package');
await this.assertRecordEntries(stagedPackage, plan.record);
const index = await this.readIndex();
const existing = index.packages.find(({ packageId }) => packageId === plan.record.packageId);
if (existing && existing.source.requested !== plan.record.source.requested) {
throw new DevicePackageError('local_package_install_failed', 'Package id belongs to another source');
}
const destination = this.packageDirectory(plan.record);
const activeKey = this.packageKey(plan.record);
if (await pathMetadata(destination) && (this.activePackages.get(activeKey) ?? 0) > 0) {
throw new DevicePackageError('local_package_in_use', 'This package version is active in a worker');
}
await mkdir(path.dirname(destination), { recursive: true });
await mkdir(this.trashDir, { recursive: true });
const displaced = await pathMetadata(destination)
? path.join(this.trashDir, `${plan.record.packageId}-${this.createId()}`)
: null;
if (displaced) await rename(destination, displaced);
let moved = false;
try {
await rename(stagedPackage, destination);
moved = true;
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: [...index.packages.filter(({ packageId }) => packageId !== plan.record.packageId), plan.record]
.sort((left, right) => left.packageId.localeCompare(right.packageId)),
};
await this.writeIndex(this.indexPath, next);
await rm(path.join(this.stagingDir, planId), { recursive: true, force: true });
if (existing && existing.resolvedVersion !== plan.record.resolvedVersion) {
await this.cleanupPackage(existing);
}
if (displaced) await rm(displaced, { recursive: true, force: true }).catch(() => undefined);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
} catch (error) {
if (moved) await rm(destination, { recursive: true, force: true }).catch(() => undefined);
if (displaced) await rename(displaced, destination).catch(() => undefined);
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_install_failed', 'Device package commit failed');
}
});
}
async list(): Promise<DevicePackageIndexV1> {
return cloneIndex(await this.readIndex());
}
async setEnabled(packageId: string, enabled: boolean): Promise<DevicePackageIndexV1> {
return await this.withOperation(async () => {
const index = await this.readIndex();
const existing = index.packages.find((record) => record.packageId === packageId);
if (!existing) throw new DevicePackageError('local_package_not_installed', 'Device package is not installed');
if (existing.enabled === enabled) return cloneIndex(index);
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: index.packages.map((record) => record.packageId === packageId
? { ...record, enabled }
: record),
};
await this.writeIndex(this.indexPath, next);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
});
}
async uninstall(packageId: string): Promise<DevicePackageIndexV1> {
return await this.withOperation(async () => {
const index = await this.readIndex();
const existing = index.packages.find((record) => record.packageId === packageId);
if (!existing) return cloneIndex(index);
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: index.packages.filter((record) => record.packageId !== packageId),
};
await this.writeIndex(this.indexPath, next);
await this.cleanupPackage(existing);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
});
}
async resolveEnabledResources(): Promise<EnabledDevicePackageResources> {
const index = await this.readIndex();
const enabled = index.packages.filter(({ enabled }) => enabled);
const skillEntries: EnabledDevicePackageResources['skillEntries'] = [];
const extensionPaths: string[] = [];
for (const record of enabled) {
const packageRoot = this.packageDirectory(record);
await this.assertRecordEntries(packageRoot, record);
for (const entry of record.skillEntries) skillEntries.push({ ...entry, packageRoot });
for (const entry of record.extensionEntries) {
extensionPaths.push(path.join(packageRoot, ...entry.split('/')));
}
}
return {
generation: index.generation,
packageIds: enabled.map(({ packageId }) => packageId),
packageRefs: enabled.map(({ packageId, resolvedVersion }) => ({ packageId, resolvedVersion })),
skillEntries,
extensionPaths,
};
}
registerActiveWorker(refs: readonly { packageId: string; resolvedVersion: string }[]): () => Promise<void> {
const keys = [...new Set(refs.map((record) => this.packageKey(record)))];
for (const key of keys) this.activePackages.set(key, (this.activePackages.get(key) ?? 0) + 1);
let released = false;
return async () => {
if (released) return;
released = true;
for (const key of keys) {
const count = this.activePackages.get(key) ?? 0;
if (count <= 1) this.activePackages.delete(key);
else this.activePackages.set(key, count - 1);
}
for (const target of [...this.pendingCleanup]) {
const [key, packagePath] = target.split('\u0000', 2);
if (!key || !packagePath || (this.activePackages.get(key) ?? 0) > 0) continue;
this.pendingCleanup.delete(target);
await rm(packagePath, { recursive: true, force: true }).catch(() => undefined);
}
};
}
private async readPlan(planId: string): Promise<StagedPlanV1> {
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(planId)) {
throw new DevicePackageError('local_package_plan_expired', 'Install preview is unavailable');
}
try {
const value = JSON.parse(await readFile(path.join(this.stagingDir, planId, 'plan.json'), 'utf8')) as unknown;
if (!isRecord(value) || value.schemaVersion !== 1 || !isRecord(value.preview)) throw new Error();
const record = parseRecord(value.record);
const preview = value.preview as unknown as InstallPreviewV1;
if (preview.schemaVersion !== 1 || preview.planId !== planId
|| !Array.isArray(preview.warnings) || !Array.isArray(preview.skillEntries)
|| !Array.isArray(preview.extensionEntries)) throw new Error();
return {
schemaVersion: 1,
requestedByTurnId: string(value.requestedByTurnId, 'Request turn id', 256),
preview,
record,
};
} catch (error) {
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_plan_expired', 'Install preview is unavailable');
}
}
private async readIndex(): Promise<DevicePackageIndexV1> {
try {
return parseIndex(JSON.parse(await readFile(this.indexPath, 'utf8')) as unknown);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return { schemaVersion: 1, generation: 0, packages: [] };
}
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
}
private packageDirectory(record: Pick<DevicePackageRecordV1, 'packageId' | 'resolvedVersion'>): string {
return path.join(this.packagesDir, record.packageId, safeStorageSegment(record.resolvedVersion));
}
private packageKey(record: Pick<DevicePackageRecordV1, 'packageId' | 'resolvedVersion'>): string {
return `${record.packageId}@${record.resolvedVersion}`;
}
private async assertRecordEntries(packageRoot: string, record: DevicePackageRecordV1): Promise<void> {
if (await pathMetadata(packageRoot) !== 'directory') {
throw new DevicePackageError('local_package_install_failed', 'Device package bytes are unavailable');
}
for (const entry of [...record.skillEntries.map(({ entryPath }) => entryPath), ...record.extensionEntries]) {
const candidate = path.resolve(packageRoot, ...entry.split('/'));
try {
relativeContainedPath(packageRoot, candidate);
} catch {
throw new DevicePackageError('local_package_manifest_invalid', 'Device package entry escapes its root');
}
if (await pathMetadata(candidate) !== 'file') {
throw new DevicePackageError('local_package_manifest_invalid', 'Device package entry is unavailable');
}
}
}
private async cleanupPackage(record: DevicePackageRecordV1): Promise<void> {
const packagePath = this.packageDirectory(record);
const key = this.packageKey(record);
if ((this.activePackages.get(key) ?? 0) > 0) {
this.pendingCleanup.add(`${key}\u0000${packagePath}`);
return;
}
await rm(packagePath, { recursive: true, force: true }).catch(() => undefined);
}
private withOperation<T>(operation: () => Promise<T>): Promise<T> {
const result = this.operation.then(operation, operation);
this.operation = result.then(() => undefined, () => undefined);
return result;
}
}