Skip to content
Merged
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
153 changes: 153 additions & 0 deletions src/business/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { z } from "zod";
import { Z } from "zod-class";
import { CoreAPIClient, DBAPIClient } from "./api";
import { makeStringProp, makeObjectProp } from "@/utils/vue-props";

export type ClientRef = string;
export const makeClientProp = (v?: any) => makeObjectProp<Client>(v);
export const makeClientRefProp = (v?: any) =>
makeStringProp<ClientRef>(v);
export const ClientRefZ = z.string().uuid();

/**
* Client 模型
* 在这个系统中,每个节点都是平等的客户端
* 不存在传统意义上的"后端",所有节点都可以相互通信
*/
export class Client extends Z.class({
id: ClientRefZ,
name: z.string(),
description: z.string().nullable().optional(),
rest_api_url: z.string().url(),
status: z.enum(["online", "offline", "unknown"]).default("unknown"),
created_at: z.string(),
updated_at: z.string(),
last_seen_at: z.string().nullable().optional(),
}) {
static coreApi: CoreAPIClient = new CoreAPIClient<Client>(
"/clients",
Client
);
static dbApi: DBAPIClient = new DBAPIClient<Client>("clients", Client);

/**
* 获取单个客户端
*/
static async get(id: ClientRef): Promise<Client> {
return new Client(
(await Client.dbApi.from().select().eq("id", id).single()).data!
);
}

/**
* 获取所有客户端
*/
static async list(): Promise<Client[]> {
const results = await Client.dbApi
.from()
.select()
.order("name", { ascending: true });
return results.data!.map((item) => new Client(item));
}

/**
* Ping 客户端检查在线状态
*/
async ping(): Promise<"online" | "offline"> {
try {
const response = await fetch(`${this.rest_api_url}/health`, {
method: "GET",
signal: AbortSignal.timeout(5000), // 5秒超时
});
return response.ok ? "online" : "offline";
} catch (error) {
console.error(`[Client] Ping failed for ${this.id}:`, error);
return "offline";
}
}

/**
* 向远程客户端发送请求
*/
async request<T = any>(options: {
method: string;
path: string;
body?: any;
query?: Record<string, any>;
}): Promise<T> {
const { method, path, body, query } = options;
const url = new URL(`${this.rest_api_url}${path}`);

if (query) {
Object.entries(query).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
}

const config: RequestInit = {
method,
headers: {
"Content-Type": "application/json",
},
};

if (body !== undefined) {
config.body = JSON.stringify(body);
}

try {
const response = await fetch(url, config);

if (!response.ok) {
throw new Error(
`HTTP ${response.status}: ${response.statusText}`
);
}

return await response.json();
} catch (error) {
console.error(`[Client] Request failed for ${this.id}:`, error);
throw error;
}
}

/**
* 在远程客户端上启用插件
*/
async enableExtension(extensionId: string): Promise<void> {
await this.request({
method: "POST",
path: `/extensions/${extensionId}/enable`,
body: { client_id: this.id },
});
}

/**
* 在远程客户端上禁用插件
*/
async disableExtension(extensionId: string): Promise<void> {
await this.request({
method: "POST",
path: `/extensions/${extensionId}/disable`,
body: { client_id: this.id },
});
}
}

/**
* 创建客户端的表单
*/
export class CreateClientForm extends Z.class({
name: z.string().min(1),
description: z.string().optional(),
rest_api_url: z.string().url(),
}) {
async create(): Promise<Client> {
const result = await Client.coreApi.request<Client>({
method: "POST",
path: "",
body: this,
});
return new Client(result);
}
}
129 changes: 124 additions & 5 deletions src/business/extensionManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Extension, ExtensionRef } from "./extension";
import { Client } from "./client";
import { configUtils } from "@/config";

/**
* 插件生命周期状态
Expand Down Expand Up @@ -159,15 +161,72 @@ export class ExtensionManager {
}

/**
* 并行加载插件(使用 Module Federation)
* 这是一个占位实现,实际的 Module Federation 加载逻辑需要根据项目配置
* 并行加载插件(使用 Module Federation Runtime
* 使用 @module-federation/runtime 动态加载远程模块
*/
private async loadExtensionModule(
instance: ExtensionInstance
): Promise<IExtension> {
// TODO: 实现实际的 Module Federation 加载逻辑
// 例如: const module = await import(`remote_${instance.extension.id}/Extension`);
throw new Error("Module Federation loading not implemented yet");
const extension = instance.extension;

// 1. 动态注册远程模块
const remoteName = `extension_${extension.id}`;
const remoteEntry = extension.config.entry_url as string;

if (!remoteEntry) {
throw new Error(`Extension ${extension.id} has no entry_url configured`);
}

console.log(`[ExtensionManager] Loading remote module: ${remoteName} from ${remoteEntry}`);

// 2. 使用动态 import 加载远程入口
// Module Federation Runtime 会自动处理远程模块的加载
try {
// 检查是否已经加载过
const existingScript = document.querySelector(`script[data-remote="${remoteName}"]`);
if (!existingScript) {
// 创建 script 标签加载远程入口
await this.loadRemoteEntry(remoteName, remoteEntry);
}

// 3. 导入远程模块的 Extension 导出
const moduleUrl = `${remoteName}/Extension`;
console.log(`[ExtensionManager] Importing module: ${moduleUrl}`);

// @ts-ignore - 动态 import 的类型无法推断
const module = await import(/* @vite-ignore */ moduleUrl);

// 返回默认导出或命名导出
return module.default || module;
} catch (error) {
console.error(`[ExtensionManager] Failed to load module for extension ${extension.id}:`, error);
throw new Error(`Failed to load extension module: ${error instanceof Error ? error.message : String(error)}`);
}
}

/**
* 加载远程入口文件
* 动态创建 script 标签加载远程模块的入口文件
*/
private loadRemoteEntry(remoteName: string, entry: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = entry;
script.type = 'module';
script.setAttribute('data-remote', remoteName);

script.onload = () => {
console.log(`[ExtensionManager] Remote entry loaded: ${entry}`);
resolve();
};

script.onerror = (error) => {
console.error(`[ExtensionManager] Failed to load remote entry: ${entry}`, error);
reject(new Error(`Failed to load remote entry: ${entry}`));
};

document.head.appendChild(script);
});
}

/**
Expand Down Expand Up @@ -459,6 +518,66 @@ export class ExtensionManager {
instance.extension = updatedExtension;
}

/**
* 为指定客户端启用插件
* 如果是本地客户端,直接调用 enableExtension
* 如果是远程客户端,调用远程客户端的 REST API
*/
async enableExtensionForClient(id: ExtensionRef, clientId: string): Promise<void> {
console.log(`[ExtensionManager] 为客户端 ${clientId} 启用插件 ${id}`);

const instance = this.instances.get(id);
if (!instance) {
throw new Error(`Extension ${id} not found`);
}

// 检查是否为本地客户端
if (configUtils.isLocalClient(clientId)) {
// 本地客户端,直接激活
await this.enableExtension(id);
} else {
// 远程客户端,调用远程 API
const client = await Client.get(clientId);
await client.enableExtension(id);

// 更新本地实例的 extension 对象
const updatedExtension = await instance.extension.enable(clientId);
instance.extension = updatedExtension;
}

console.log(`[ExtensionManager] 插件 ${id} 已为客户端 ${clientId} 启用`);
}

/**
* 为指定客户端禁用插件
* 如果是本地客户端,直接调用 disableExtension
* 如果是远程客户端,调用远程客户端的 REST API
*/
async disableExtensionForClient(id: ExtensionRef, clientId: string): Promise<void> {
console.log(`[ExtensionManager] 为客户端 ${clientId} 禁用插件 ${id}`);

const instance = this.instances.get(id);
if (!instance) {
throw new Error(`Extension ${id} not found`);
}

// 检查是否为本地客户端
if (configUtils.isLocalClient(clientId)) {
// 本地客户端,直接停用
await this.disableExtension(id);
} else {
// 远程客户端,调用远程 API
const client = await Client.get(clientId);
await client.disableExtension(id);

// 更新本地实例的 extension 对象
const updatedExtension = await instance.extension.disable(clientId);
instance.extension = updatedExtension;
}

console.log(`[ExtensionManager] 插件 ${id} 已为客户端 ${clientId} 禁用`);
}

/**
* 完整的启动流程
*/
Expand Down
Loading