feat(projects): add interactive AI app scaffold skill
This commit is contained in:
@@ -24,6 +24,7 @@ export interface InspectedDevicePackage {
|
||||
resolvedVersion: string | null;
|
||||
skillEntries: DevicePackageSkillEntry[];
|
||||
extensionEntries: string[];
|
||||
hasSkillScripts: boolean;
|
||||
ignoredLifecycleScripts: string[];
|
||||
}
|
||||
|
||||
@@ -143,6 +144,25 @@ async function collectSkillFiles(root: string): Promise<string[]> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function directoryContainsFile(directory: string): Promise<boolean> {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (entry.isFile() || entry.isSymbolicLink()) return true;
|
||||
if (entry.isDirectory() && await directoryContainsFile(path.join(directory, entry.name))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function skillHasScripts(skillPath: string): Promise<boolean> {
|
||||
const scriptsPath = path.join(path.dirname(skillPath), 'scripts');
|
||||
const metadata = await stat(scriptsPath).catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
});
|
||||
return metadata?.isDirectory() === true && await directoryContainsFile(scriptsPath);
|
||||
}
|
||||
|
||||
function manifestEntries(value: unknown, field: string): string[] {
|
||||
if (value === undefined) return [];
|
||||
const values = typeof value === 'string' ? [value] : value;
|
||||
@@ -195,7 +215,8 @@ export async function inspectDevicePackage(
|
||||
if (await existsFile(path.join(root, 'SKILL.md'))) skillFiles.push(path.join(root, 'SKILL.md'));
|
||||
|
||||
const uniqueSkills = new Map<string, DevicePackageSkillEntry>();
|
||||
for (const skillPath of [...new Set(skillFiles.map((entry) => path.resolve(entry)))]) {
|
||||
const uniqueSkillPaths = [...new Set(skillFiles.map((entry) => path.resolve(entry)))];
|
||||
for (const skillPath of uniqueSkillPaths) {
|
||||
const id = await skillId(skillPath);
|
||||
if (uniqueSkills.has(id)) throw new Error(`Duplicate Skill id: ${id}`);
|
||||
uniqueSkills.set(id, { id, entryPath: relativeContainedPath(root, skillPath) });
|
||||
@@ -214,6 +235,7 @@ export async function inspectDevicePackage(
|
||||
resolvedVersion: nonempty(packageJson?.version, 128) ?? nonempty(codexManifest?.version, 128),
|
||||
skillEntries: [...uniqueSkills.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
||||
extensionEntries: extensionEntries.sort(),
|
||||
hasSkillScripts: (await Promise.all(uniqueSkillPaths.map(skillHasScripts))).some(Boolean),
|
||||
ignoredLifecycleScripts: LIFECYCLE_SCRIPT_NAMES.filter((name) => typeof scripts[name] === 'string'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@ 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,将获得完整桌面权限,包括当前进程可用的文件、网络和进程权限。';
|
||||
const EXECUTABLE_WARNING = '此包包含可执行代码,将获得完整桌面权限,包括当前进程可用的文件、网络和进程权限。';
|
||||
const SKILL_SCRIPT_WARNING = '此包包含可执行 Skill 脚本;Skill 被调用时,脚本可能使用当前桌面用户可用的文件、网络和进程权限。';
|
||||
|
||||
function executableWarning(kind: InstallPreviewV1['kind'], includesExecutableCode: boolean): string | null {
|
||||
if (!includesExecutableCode) return null;
|
||||
return kind === 'skill-only' ? SKILL_SCRIPT_WARNING : EXECUTABLE_WARNING;
|
||||
}
|
||||
|
||||
export class DevicePackageError extends Error {
|
||||
constructor(
|
||||
@@ -375,7 +381,10 @@ export class DevicePackageManager {
|
||||
resolvedSource: string;
|
||||
inspected: Awaited<ReturnType<typeof inspectDevicePackage>>;
|
||||
}): Promise<InstallPreviewV1> {
|
||||
const includesExecutableCode = input.inspected.extensionEntries.length > 0;
|
||||
const kind = devicePackageKind(input.inspected);
|
||||
const includesExecutableCode = input.inspected.extensionEntries.length > 0
|
||||
|| input.inspected.hasSkillScripts;
|
||||
const warning = executableWarning(kind, includesExecutableCode);
|
||||
const resolvedVersion = input.inspected.resolvedVersion
|
||||
?? `local-${new Date(this.now()).toISOString().replace(/[-:.]/gu, '')}`;
|
||||
safeStorageSegment(resolvedVersion);
|
||||
@@ -388,12 +397,12 @@ export class DevicePackageManager {
|
||||
packageId: input.inspected.packageId,
|
||||
displayName: input.inspected.displayName,
|
||||
resolvedVersion,
|
||||
kind: devicePackageKind(input.inspected),
|
||||
kind,
|
||||
skillEntries: input.inspected.skillEntries,
|
||||
extensionEntries: input.inspected.extensionEntries,
|
||||
includesExecutableCode,
|
||||
ignoredLifecycleScripts: input.inspected.ignoredLifecycleScripts,
|
||||
warnings: includesExecutableCode ? [EXECUTABLE_WARNING] : [],
|
||||
warnings: warning ? [warning] : [],
|
||||
scope: 'device-parent-workers',
|
||||
};
|
||||
const record: DevicePackageRecordV1 = {
|
||||
@@ -435,7 +444,8 @@ export class DevicePackageManager {
|
||||
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)) {
|
||||
const warning = executableWarning(plan.preview.kind, plan.preview.includesExecutableCode);
|
||||
if (warning && !plan.preview.warnings.includes(warning)) {
|
||||
throw new DevicePackageError('local_package_confirmation_required', 'Executable permission warning is missing');
|
||||
}
|
||||
const stagedPackage = path.join(this.stagingDir, planId, 'package');
|
||||
|
||||
Reference in New Issue
Block a user