Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/check-dts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ jobs:
local diff_output="$1"
local noise_filter='grep -v "^exports\[" | grep -v "^$" | grep -v "^[[:space:]]*$" | grep -v "^\`;" | grep -v "^[[:space:]]*}[;,]\?$" | grep -v "^export { }$" | grep -v "^[[:space:]]*static readonly version" | grep -v "^export { .*_[0-9]\+ as "'

# Extract deleted lines (strip leading -, normalize quotes and Object casing)
# Extract deleted lines (strip leading -, normalize non-semantic syntax differences)
local deleted
deleted=$(echo "$diff_output" | grep '^-' | grep -v '^---' | sed 's/^-//' \
| eval "$noise_filter" | sed "s/\"/'/g; s/\bObject\b/object/g" | sort -u || true)
| eval "$noise_filter" | sed "s/\"/'/g; s/\bObject\b/object/g; s/,[[:space:]]*$//" | sort -u || true)

# Extract added lines (strip leading +, normalize quotes and Object casing)
# Extract added lines (strip leading +, normalize non-semantic syntax differences)
local added
added=$(echo "$diff_output" | grep '^+' | grep -v '^+++' | sed 's/^+//' \
| eval "$noise_filter" | sed "s/\"/'/g; s/\bObject\b/object/g" | sort -u || true)
| eval "$noise_filter" | sed "s/\"/'/g; s/\bObject\b/object/g; s/,[[:space:]]*$//" | sort -u || true)

# Lines in deleted but NOT in added = truly removed
if [ -n "$deleted" ]; then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8068,6 +8068,7 @@ export declare function get<T>(key: string, scope?: ConfigurationScope): Promise
export declare function get_2(): Promise<Project_2>;
export declare function getConfig(useDefault?: boolean): Promise<IEngineConfig>;
export declare function getConfigPath(scope?: ConfigurationScope): Promise<string>;
export declare function getCurrentMcpToolCallContext(): Readonly<McpToolCallContext> | undefined;
export declare function getInfo(): Promise<EngineInfo>;
export declare function getInfo_2(): Promise<ProjectInfo>;
export declare function getMetadata(): Promise<ICocosConfigurationNode[]>;
Expand Down Expand Up @@ -8514,6 +8515,8 @@ export declare function init_6(): Promise<void>;
export declare function init_7(projectPath: string): Promise<void>;
export declare function initEngine(enginePath: string, projectPath: string, serverURL?: string): Promise<void>;
export declare function initProgrammingFacet(): Promise<ProgrammingFacet>;
export declare function installPinkSceneCommandBackend(invoke: PinkSceneCommandInvoker, completeOperation: PinkSceneOperationCompleter): PinkSceneCommandBackend;
export declare function installSceneCommandBackend(backend: SceneCommandBackend): void;
export declare interface IPhysicsConfig {
gravity: IVec3Like;
allowSleep: boolean;
Expand Down Expand Up @@ -8779,6 +8782,15 @@ export declare namespace Mcp {
getStatus
}
}
export declare interface McpToolCallContext {
readonly operationId: string;
readonly routeToken?: string;
readonly origin: McpToolCallOrigin;
readonly lifecycleState: McpToolCallLifecycleState;
readonly hasDispatchedSceneRpc: boolean;
}
export declare type McpToolCallLifecycleState = 'running' | 'completing' | 'completed';
export declare type McpToolCallOrigin = 'pink' | 'standalone';
export declare interface MeshClusterOptions {
enable: boolean;
coneCluster?: boolean;
Expand Down Expand Up @@ -8891,6 +8903,31 @@ export declare interface ParticleAssetUserData {
rotatePerSVar: number;
spriteFrameUuid: string;
}
export declare type PinkMcpToolCallContext = Readonly<{
operationId: string;
origin: 'pink';
routeToken: string;
}>;
export declare class PinkSceneCommandBackend implements SceneCommandBackend {
private readonly invoke;
private readonly completeOperation;
constructor(invoke: PinkSceneCommandInvoker, completeOperation: PinkSceneOperationCompleter);
request(module: string, method: string, args?: any[], options?: SceneCommandRequestOptions): Promise<any>;
}
export declare class PinkSceneCommandContextError extends Error {
readonly code: PinkSceneCommandContextErrorCode;
readonly name = "PinkSceneCommandContextError";
constructor(code: PinkSceneCommandContextErrorCode, message: string);
}
export declare type PinkSceneCommandContextErrorCode = 'MCP_CONTEXT_UNAVAILABLE' | 'MCP_CONTEXT_EXPIRED' | 'PINK_ORIGIN_REQUIRED' | 'PINK_ROUTE_TOKEN_REQUIRED';
export declare interface PinkSceneCommandInvocation extends PinkMcpToolCallContext {
readonly module: string;
readonly method: string;
readonly args: any[];
readonly options?: Readonly<SceneCommandRequestOptions>;
}
export declare type PinkSceneCommandInvoker = (invocation: PinkSceneCommandInvocation) => Promise<any>;
export declare type PinkSceneOperationCompleter = (operation: PinkMcpToolCallContext) => Promise<void>;
export declare interface PluginScriptInfo {
file: string;
uuid: string;
Expand Down Expand Up @@ -9054,9 +9091,32 @@ export declare function saveSerializedData(uuidOrUrlOrPath: string, patch: Seria
export declare namespace Scene {
export {
init_6 as init,
startupWorker
startupWorker,
installSceneCommandBackend,
getCurrentMcpToolCallContext,
installPinkSceneCommandBackend,
SceneCommandBackend,
SceneCommandRequestOptions,
McpToolCallContext,
McpToolCallOrigin,
PinkSceneCommandBackend,
PinkSceneCommandContextError,
PinkMcpToolCallContext,
PinkSceneCommandContextErrorCode,
PinkSceneCommandInvocation,
PinkSceneCommandInvoker,
PinkSceneOperationCompleter
}
}
export declare interface SceneCommandBackend {
request(module: string, method: string, args?: any[], options?: SceneCommandRequestOptions): Promise<any>;
notify?(module: string, method: string, args?: any[]): void;
isConnect?(): boolean | undefined;
dispose?(): void;
}
export declare interface SceneCommandRequestOptions {
timeout?: number;
}
export declare namespace Scripting {
export {
init_7 as init,
Expand Down
26 changes: 16 additions & 10 deletions src/api/scene/scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import {
} from './schema';
import { description, param, result, title, tool } from '../decorator/decorator.js';
import { COMMON_STATUS, CommonResultType, getCommonErrorStatus } from '../base/schema-base';
import { Scene, TSceneTemplateType } from '../../core/scene';
import { Scene } from '../../core/scene';
import { assetManager } from '../../core/assets';
import { ComponentApi } from './component';
import { NodeApi } from './node';
import { PrefabApi } from './prefab';
import { options } from '../../core/builder/platforms/android/i18n/en';

export class SceneApi {
public component: ComponentApi;
Expand Down Expand Up @@ -126,16 +126,22 @@ export class SceneApi {
@result(SchemaCreateResult)
async createScene(@param(SchemaCreateOptions) options: TCreateOptions): Promise<CommonResultType<TCreateResult>> {
try {
const data = await Scene.create({
type: 'scene',
baseName: options.baseName,
targetDirectory: options.dbURL,
templateType: options.templateType as TSceneTemplateType,
});

const assetInfo = await assetManager.createAssetByType(
'scene',
options.dbURL,
options.baseName,
{ templateName: options.templateType ?? '2d' },
);
const data: TCreateResult = {
assetName: assetInfo.name,
assetUuid: assetInfo.uuid,
assetUrl: assetInfo.url,
assetType: assetInfo.type,
};

return {
code: COMMON_STATUS.SUCCESS,
data: data as TCreateResult,
data,
};
} catch (e) {
console.error(e);
Expand Down
183 changes: 147 additions & 36 deletions src/core/scene/main-process/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,168 @@
import type { ChildProcess } from 'child_process';
import type { IPublicServiceManager } from '../scene-process';
import { ProcessRPC } from '../process-rpc';
import { ChildProcess } from 'child_process';
import { assetManager } from '../../assets';
import scriptManager from '../../scripting';
import { sceneConfigInstance } from '../scene-configs';
import i18n from '../../base/i18n';
import {
SceneCommandBackend,
SceneCommandRequestOptions,
WorkerSceneCommandBackend,
} from './scene-command-backend';
import { SceneHostLocalExecutor } from './scene-host-local-executor';

import type { IPublicServiceManager } from '../scene-process';
type AnySceneMethod = (...args: any[]) => any;
type SceneServiceMethod<
K extends keyof IPublicServiceManager,
M extends keyof IPublicServiceManager[K],
> = Extract<IPublicServiceManager[K][M], AnySceneMethod>;

export { ProcessRPC };
export type { SceneCommandBackend, SceneCommandRequestOptions } from './scene-command-backend';
export { WorkerSceneCommandBackend } from './scene-command-backend';
export { SceneHostLocalExecutor } from './scene-host-local-executor';
export type { SceneHostModules } from './scene-host-local-executor';

/** `Rpc.getInstance()` 对调用方公开的最小接口。 */
export interface SceneRpcClient {
request<K extends keyof IPublicServiceManager, M extends keyof IPublicServiceManager[K]>(
module: K,
method: M,
...rest: Parameters<SceneServiceMethod<K, M>> extends []
? [args?: [], options?: SceneCommandRequestOptions]
: [args: Parameters<SceneServiceMethod<K, M>>, options?: SceneCommandRequestOptions]
): Promise<Awaited<ReturnType<SceneServiceMethod<K, M>>>>;

notify<K extends keyof IPublicServiceManager, M extends keyof IPublicServiceManager[K]>(
module: K,
method: M,
args?: Parameters<SceneServiceMethod<K, M>>,
): void;

export class RpcProxy {
private rpcInstance: ProcessRPC<IPublicServiceManager> | null = null;
executeLocal(module: string, method: string, args?: any[]): Promise<any>;
isConnect(): boolean | undefined;
}

export class RpcProxy implements SceneRpcClient {
private commandBackend: SceneCommandBackend | null = null;
private hostLocalExecutor: SceneHostLocalExecutor | null = null;

public getInstance() {
if (!this.rpcInstance) {
public getInstance(): SceneRpcClient {
if (!this.hostLocalExecutor) {
throw new Error('[Node] Rpc instance is not started!');
}
return this.rpcInstance;
return this;
}

public isConnect() {
return this.rpcInstance?.isConnect();
public isConnect(): boolean | undefined {
return this.commandBackend?.isConnect?.();
}

async startup(prc?: ChildProcess | NodeJS.Process) {
// 在创建新实例前,先清理旧实例,防止内存泄漏
/**
* 保持原有的启动约定:
* - 传入 process 时使用 `WorkerSceneCommandBackend` 连接 Scene Worker;
* - 未传入 process 时,仅初始化供 Scene Webview Runtime 使用的 `SceneHostLocalExecutor`。
*/
public async startup(process?: ChildProcess | NodeJS.Process): Promise<void> {
this.dispose();
this.rpcInstance = new ProcessRPC<IPublicServiceManager>();
if (prc) {
this.rpcInstance.attach(prc);
const hostLocalExecutor = this.ensureHostLocalExecutor();
if (process) {
this.commandBackend = new WorkerSceneCommandBackend(process, hostLocalExecutor);
}
this.rpcInstance.register({
assetManager: assetManager,
programming: scriptManager,
sceneConfigInstance: sceneConfigInstance,
i18n: i18n,
});
console.log(`[Node] Scene Process RPC ready ${prc ? '(Attached)' : '(Detached - Web Mode)'}`);
console.log(`[Node] Scene Process RPC ready ${process ? '(Attached)' : '(Detached - Web Mode)'}`);
}

/**
* 清理 RPC 实例
* 安装宿主指定的 `SceneCommandBackend`,例如桥接 PinK Scene Webview Runtime 的 backend。
* 安装前会释放当前 backend;新 backend 抛出的错误直接向上传播,不切换到其他 backend 重试。
*/
dispose(): void {
if (this.rpcInstance) {
console.log('[Node] Disposing RPC instance');
try {
this.rpcInstance.dispose();
} catch (error) {
console.warn('[Node] Error disposing RPC instance:', error);
} finally {
this.rpcInstance = null;
}
public installBackend(backend: SceneCommandBackend): void {
if (!backend || typeof backend.request !== 'function') {
throw new TypeError('[Node] Scene command backend must implement request()');
}
if (backend === this.commandBackend) {
return;
}

this.ensureHostLocalExecutor();
this.disposeCommandBackend();
this.commandBackend = backend;
console.log('[Node] Scene command backend installed');
}

public request<K extends keyof IPublicServiceManager, M extends keyof IPublicServiceManager[K]>(
module: K,
method: M,
...rest: Parameters<SceneServiceMethod<K, M>> extends []
? [args?: [], options?: SceneCommandRequestOptions]
: [args: Parameters<SceneServiceMethod<K, M>>, options?: SceneCommandRequestOptions]
): Promise<Awaited<ReturnType<SceneServiceMethod<K, M>>>> {
const backend = this.commandBackend;
if (!backend) {
return Promise.reject(new Error('RPC 尚未挂载进程且未安装 Scene command backend'));
}

const [args, options] = rest;
return backend.request(
String(module),
String(method),
(args ?? []) as any[],
options,
) as Promise<Awaited<ReturnType<SceneServiceMethod<K, M>>>>;
}

public notify<K extends keyof IPublicServiceManager, M extends keyof IPublicServiceManager[K]>(
module: K,
method: M,
args?: Parameters<SceneServiceMethod<K, M>>,
): void {
const backend = this.commandBackend;
if (!backend) {
throw new Error('RPC 尚未挂载进程且未安装 Scene command backend');
}
if (!backend.notify) {
throw new Error('[Node] Scene command backend does not support notify()');
}
backend.notify(String(module), String(method), (args ?? []) as any[]);
}

public executeLocal(module: string, method: string, args: any[] = []): Promise<any> {
const executor = this.hostLocalExecutor;
if (!executor) {
return Promise.reject(new Error('[Node] Scene host local executor is not started!'));
}
return executor.executeLocal(module, method, args);
}

/** 同时释放 `SceneCommandBackend` 和 `SceneHostLocalExecutor`。 */
public dispose(): void {
if (!this.commandBackend && !this.hostLocalExecutor) {
return;
}

console.log('[Node] Disposing RPC instance');
this.disposeCommandBackend();
try {
this.hostLocalExecutor?.dispose();
} catch (error) {
console.warn('[Node] Error disposing Scene host local executor:', error);
} finally {
this.hostLocalExecutor = null;
}
}

private ensureHostLocalExecutor(): SceneHostLocalExecutor {
this.hostLocalExecutor ??= new SceneHostLocalExecutor();
return this.hostLocalExecutor;
}

private disposeCommandBackend(): void {
const backend = this.commandBackend;
this.commandBackend = null;
if (!backend?.dispose) {
return;
}
try {
backend.dispose();
} catch (error) {
console.warn('[Node] Error disposing Scene command backend:', error);
}
}
}
Expand Down
Loading
Loading