feat(iteration): implement sdk v1 miniapp runtime loop

This commit is contained in:
2026-08-05 18:30:29 +08:00
parent 486280bec0
commit d2a2fd0aff
37 changed files with 2135 additions and 60 deletions
@@ -10,7 +10,12 @@
* or a marketplace namespace. The initial registry is static, but the same
* record shape is the boundary that the later App Registry/Market will own.
*/
import { validateMiniAppManifest, type MiniAppManifestV1 } from "@/runtime/app-management/miniapp-manifest";
import { BUNDLED_REFERENCE_MANIFESTS } from "@/runtime/app-management/reference-miniapps";
import type { JsonObject } from "@/runtime/protocol/lineup-v1";
export type AppScope = "chat" | "app-registry" | "settings" | "runtime" | (string & {});
export type MiniAppKind = "system" | "bundled";
export type AppToolDefinition = {
name: string;
@@ -18,6 +23,13 @@ export type AppToolDefinition = {
/** JSON-object property names accepted by this tool. */
parameters: readonly string[];
requires_permission?: string;
input_schema?: JsonObject;
output_schema?: JsonObject;
target?: {
app_scope: AppScope;
requires_foreground: boolean;
restore_previous_focus: boolean;
};
};
/**
@@ -33,16 +45,19 @@ export type AppManifest = {
export type CoreAppRecord = {
app_scope: AppScope;
kind: "core" | "extension";
kind: MiniAppKind;
enabled: boolean;
default_eligible: boolean;
recovery: boolean;
manifest: AppManifest;
};
/** Transitional input accepted while older host adapters are upgraded. */
export type CoreAppRecordInput = Omit<CoreAppRecord, "kind"> & { kind: MiniAppKind | "core" | "extension" };
const INITIAL_CORE_APPS: readonly CoreAppRecord[] = [
{
app_scope: "chat", kind: "core", enabled: true, default_eligible: true, recovery: true,
app_scope: "chat", kind: "system", enabled: true, default_eligible: true, recovery: true,
manifest: {
app_scope: "chat", version: "1.0.0", permissions: [],
tools: [
@@ -61,8 +76,11 @@ const INITIAL_CORE_APPS: readonly CoreAppRecord[] = [
export class CoreAppRegistry {
private readonly apps = new Map<AppScope, CoreAppRecord>();
public constructor(records: readonly CoreAppRecord[] = INITIAL_CORE_APPS) {
public constructor(records: readonly CoreAppRecordInput[] = INITIAL_CORE_APPS) {
for (const record of records) this.register(record);
if (records === INITIAL_CORE_APPS) {
for (const manifest of BUNDLED_REFERENCE_MANIFESTS) this.installMiniAppManifest(manifest);
}
}
public list(): readonly CoreAppRecord[] {
@@ -83,13 +101,39 @@ export class CoreAppRegistry {
}
/** Runtime-only installation path; extension Apps never receive this object. */
public install(record: CoreAppRecord): void { this.register(record); }
public install(record: CoreAppRecordInput): void { this.register(record); }
private register(record: CoreAppRecord): void {
/** Admits a validated SDK v1 Manifest without exposing the Manifest object to Apps. */
public installMiniAppManifest(manifest: MiniAppManifestV1, options: Pick<CoreAppRecord, "enabled" | "default_eligible" | "recovery"> = { enabled: true, default_eligible: false, recovery: false }): void {
const validation = validateMiniAppManifest(manifest);
if (!validation.accepted) throw new Error(`MiniApp Manifest denied: ${validation.detail}`);
this.install({
app_scope: manifest.app_scope,
kind: manifest.kind,
...options,
manifest: {
app_scope: manifest.app_scope,
version: manifest.version,
permissions: [...manifest.requested_capabilities],
tools: manifest.tools.map(tool => ({
name: tool.id,
handling: tool.handling,
parameters: [],
input_schema: tool.input_schema,
output_schema: tool.output_schema,
target: tool.target,
...(tool.permissions?.[0] ? { requires_permission: tool.permissions[0] } : {}),
})),
},
});
}
private register(record: CoreAppRecordInput): void {
if (!isAppScope(record.app_scope) || record.manifest.app_scope !== record.app_scope) throw new Error("Core App Registry received an invalid app_scope.");
if (this.apps.has(record.app_scope)) throw new Error(`Core App Registry has duplicate app_scope: ${record.app_scope}`);
if (!isManifest(record.manifest)) throw new Error(`Core App Registry has an invalid manifest: ${record.app_scope}`);
this.apps.set(record.app_scope, copyRecord(record));
const kind: MiniAppKind = record.kind === "core" ? "system" : record.kind === "extension" ? "bundled" : record.kind;
this.apps.set(record.app_scope, copyRecord({ ...record, kind }));
}
}
@@ -99,7 +143,7 @@ function copyRecord(record: CoreAppRecord): CoreAppRecord {
manifest: {
...record.manifest,
permissions: [...record.manifest.permissions],
tools: record.manifest.tools.map(tool => ({ ...tool, parameters: [...tool.parameters] })),
tools: record.manifest.tools.map(tool => ({ ...tool, parameters: [...tool.parameters], ...(tool.input_schema ? { input_schema: clone(tool.input_schema) } : {}), ...(tool.output_schema ? { output_schema: clone(tool.output_schema) } : {}) })),
},
};
}
@@ -113,12 +157,17 @@ function isManifest(manifest: AppManifest): boolean {
&& !toolNames.has(tool.name)
&& ["direct", "interactive", "launch", "foreground", "operation"].includes(tool.handling)
&& tool.parameters.every(parameter => /^[a-z][a-z0-9_]{0,63}$/.test(parameter))
&& (!tool.input_schema || isJsonObject(tool.input_schema))
&& (!tool.output_schema || isJsonObject(tool.output_schema))
&& (!tool.requires_permission || manifest.permissions.includes(tool.requires_permission));
toolNames.add(tool.name);
return valid;
});
}
function clone<T>(value: T): T { return JSON.parse(JSON.stringify(value)) as T; }
function isJsonObject(value: unknown): value is JsonObject { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }
export function isAppScope(value: unknown): value is AppScope {
return typeof value === "string" && /^[a-z][a-z0-9-]{0,63}$/.test(value);
}