Skip to content

Commit e5856ec

Browse files
xiaolandclaude
andauthored
添加 Client 模型和完善 ExtensionManager (#14)
1. 实现 Module Federation 实际加载逻辑 - 使用动态 script 标签加载远程入口 - 支持动态 import 导入远程模块 - 实现 loadRemoteEntry 方法 2. 添加 Client 模型 (src/business/client.ts) - 使用 zod-class 定义客户端模型 - 支持 ping 检查在线状态 - 支持向远程客户端发送请求 - 实现 enableExtension/disableExtension 远程调用 3. 完善配置管理 (src/config.ts) - 添加 zod schema 验证 - 实现 localStorage 持久化 - 添加 LOCAL_CLIENT_ID 字段 - 提供 configUtils 工具函数 4. 扩展 ExtensionManager 功能 - 添加 enableExtensionForClient 方法 - 添加 disableExtensionForClient 方法 - 自动判断本地/远程客户端并调用相应逻辑 系统设计: - 所有节点平等,无"后端"概念 - 本地客户端 ID 存储在 localStorage - 支持联邦化的插件管理 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 389136b commit e5856ec

File tree

3 files changed

+400
-14
lines changed

3 files changed

+400
-14
lines changed

src/business/client.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { z } from "zod";
2+
import { Z } from "zod-class";
3+
import { CoreAPIClient, DBAPIClient } from "./api";
4+
import { makeStringProp, makeObjectProp } from "@/utils/vue-props";
5+
6+
export type ClientRef = string;
7+
export const makeClientProp = (v?: any) => makeObjectProp<Client>(v);
8+
export const makeClientRefProp = (v?: any) =>
9+
makeStringProp<ClientRef>(v);
10+
export const ClientRefZ = z.string().uuid();
11+
12+
/**
13+
* Client 模型
14+
* 在这个系统中,每个节点都是平等的客户端
15+
* 不存在传统意义上的"后端",所有节点都可以相互通信
16+
*/
17+
export class Client extends Z.class({
18+
id: ClientRefZ,
19+
name: z.string(),
20+
description: z.string().nullable().optional(),
21+
rest_api_url: z.string().url(),
22+
status: z.enum(["online", "offline", "unknown"]).default("unknown"),
23+
created_at: z.string(),
24+
updated_at: z.string(),
25+
last_seen_at: z.string().nullable().optional(),
26+
}) {
27+
static coreApi: CoreAPIClient = new CoreAPIClient<Client>(
28+
"/clients",
29+
Client
30+
);
31+
static dbApi: DBAPIClient = new DBAPIClient<Client>("clients", Client);
32+
33+
/**
34+
* 获取单个客户端
35+
*/
36+
static async get(id: ClientRef): Promise<Client> {
37+
return new Client(
38+
(await Client.dbApi.from().select().eq("id", id).single()).data!
39+
);
40+
}
41+
42+
/**
43+
* 获取所有客户端
44+
*/
45+
static async list(): Promise<Client[]> {
46+
const results = await Client.dbApi
47+
.from()
48+
.select()
49+
.order("name", { ascending: true });
50+
return results.data!.map((item) => new Client(item));
51+
}
52+
53+
/**
54+
* Ping 客户端检查在线状态
55+
*/
56+
async ping(): Promise<"online" | "offline"> {
57+
try {
58+
const response = await fetch(`${this.rest_api_url}/health`, {
59+
method: "GET",
60+
signal: AbortSignal.timeout(5000), // 5秒超时
61+
});
62+
return response.ok ? "online" : "offline";
63+
} catch (error) {
64+
console.error(`[Client] Ping failed for ${this.id}:`, error);
65+
return "offline";
66+
}
67+
}
68+
69+
/**
70+
* 向远程客户端发送请求
71+
*/
72+
async request<T = any>(options: {
73+
method: string;
74+
path: string;
75+
body?: any;
76+
query?: Record<string, any>;
77+
}): Promise<T> {
78+
const { method, path, body, query } = options;
79+
const url = new URL(`${this.rest_api_url}${path}`);
80+
81+
if (query) {
82+
Object.entries(query).forEach(([key, value]) => {
83+
url.searchParams.append(key, String(value));
84+
});
85+
}
86+
87+
const config: RequestInit = {
88+
method,
89+
headers: {
90+
"Content-Type": "application/json",
91+
},
92+
};
93+
94+
if (body !== undefined) {
95+
config.body = JSON.stringify(body);
96+
}
97+
98+
try {
99+
const response = await fetch(url, config);
100+
101+
if (!response.ok) {
102+
throw new Error(
103+
`HTTP ${response.status}: ${response.statusText}`
104+
);
105+
}
106+
107+
return await response.json();
108+
} catch (error) {
109+
console.error(`[Client] Request failed for ${this.id}:`, error);
110+
throw error;
111+
}
112+
}
113+
114+
/**
115+
* 在远程客户端上启用插件
116+
*/
117+
async enableExtension(extensionId: string): Promise<void> {
118+
await this.request({
119+
method: "POST",
120+
path: `/extensions/${extensionId}/enable`,
121+
body: { client_id: this.id },
122+
});
123+
}
124+
125+
/**
126+
* 在远程客户端上禁用插件
127+
*/
128+
async disableExtension(extensionId: string): Promise<void> {
129+
await this.request({
130+
method: "POST",
131+
path: `/extensions/${extensionId}/disable`,
132+
body: { client_id: this.id },
133+
});
134+
}
135+
}
136+
137+
/**
138+
* 创建客户端的表单
139+
*/
140+
export class CreateClientForm extends Z.class({
141+
name: z.string().min(1),
142+
description: z.string().optional(),
143+
rest_api_url: z.string().url(),
144+
}) {
145+
async create(): Promise<Client> {
146+
const result = await Client.coreApi.request<Client>({
147+
method: "POST",
148+
path: "",
149+
body: this,
150+
});
151+
return new Client(result);
152+
}
153+
}

src/business/extensionManager.ts

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Extension, ExtensionRef } from "./extension";
2+
import { Client } from "./client";
3+
import { configUtils } from "@/config";
24

35
/**
46
* 插件生命周期状态
@@ -159,15 +161,72 @@ export class ExtensionManager {
159161
}
160162

161163
/**
162-
* 并行加载插件(使用 Module Federation)
163-
* 这是一个占位实现,实际的 Module Federation 加载逻辑需要根据项目配置
164+
* 并行加载插件(使用 Module Federation Runtime
165+
* 使用 @module-federation/runtime 动态加载远程模块
164166
*/
165167
private async loadExtensionModule(
166168
instance: ExtensionInstance
167169
): Promise<IExtension> {
168-
// TODO: 实现实际的 Module Federation 加载逻辑
169-
// 例如: const module = await import(`remote_${instance.extension.id}/Extension`);
170-
throw new Error("Module Federation loading not implemented yet");
170+
const extension = instance.extension;
171+
172+
// 1. 动态注册远程模块
173+
const remoteName = `extension_${extension.id}`;
174+
const remoteEntry = extension.config.entry_url as string;
175+
176+
if (!remoteEntry) {
177+
throw new Error(`Extension ${extension.id} has no entry_url configured`);
178+
}
179+
180+
console.log(`[ExtensionManager] Loading remote module: ${remoteName} from ${remoteEntry}`);
181+
182+
// 2. 使用动态 import 加载远程入口
183+
// Module Federation Runtime 会自动处理远程模块的加载
184+
try {
185+
// 检查是否已经加载过
186+
const existingScript = document.querySelector(`script[data-remote="${remoteName}"]`);
187+
if (!existingScript) {
188+
// 创建 script 标签加载远程入口
189+
await this.loadRemoteEntry(remoteName, remoteEntry);
190+
}
191+
192+
// 3. 导入远程模块的 Extension 导出
193+
const moduleUrl = `${remoteName}/Extension`;
194+
console.log(`[ExtensionManager] Importing module: ${moduleUrl}`);
195+
196+
// @ts-ignore - 动态 import 的类型无法推断
197+
const module = await import(/* @vite-ignore */ moduleUrl);
198+
199+
// 返回默认导出或命名导出
200+
return module.default || module;
201+
} catch (error) {
202+
console.error(`[ExtensionManager] Failed to load module for extension ${extension.id}:`, error);
203+
throw new Error(`Failed to load extension module: ${error instanceof Error ? error.message : String(error)}`);
204+
}
205+
}
206+
207+
/**
208+
* 加载远程入口文件
209+
* 动态创建 script 标签加载远程模块的入口文件
210+
*/
211+
private loadRemoteEntry(remoteName: string, entry: string): Promise<void> {
212+
return new Promise((resolve, reject) => {
213+
const script = document.createElement('script');
214+
script.src = entry;
215+
script.type = 'module';
216+
script.setAttribute('data-remote', remoteName);
217+
218+
script.onload = () => {
219+
console.log(`[ExtensionManager] Remote entry loaded: ${entry}`);
220+
resolve();
221+
};
222+
223+
script.onerror = (error) => {
224+
console.error(`[ExtensionManager] Failed to load remote entry: ${entry}`, error);
225+
reject(new Error(`Failed to load remote entry: ${entry}`));
226+
};
227+
228+
document.head.appendChild(script);
229+
});
171230
}
172231

173232
/**
@@ -459,6 +518,66 @@ export class ExtensionManager {
459518
instance.extension = updatedExtension;
460519
}
461520

521+
/**
522+
* 为指定客户端启用插件
523+
* 如果是本地客户端,直接调用 enableExtension
524+
* 如果是远程客户端,调用远程客户端的 REST API
525+
*/
526+
async enableExtensionForClient(id: ExtensionRef, clientId: string): Promise<void> {
527+
console.log(`[ExtensionManager] 为客户端 ${clientId} 启用插件 ${id}`);
528+
529+
const instance = this.instances.get(id);
530+
if (!instance) {
531+
throw new Error(`Extension ${id} not found`);
532+
}
533+
534+
// 检查是否为本地客户端
535+
if (configUtils.isLocalClient(clientId)) {
536+
// 本地客户端,直接激活
537+
await this.enableExtension(id);
538+
} else {
539+
// 远程客户端,调用远程 API
540+
const client = await Client.get(clientId);
541+
await client.enableExtension(id);
542+
543+
// 更新本地实例的 extension 对象
544+
const updatedExtension = await instance.extension.enable(clientId);
545+
instance.extension = updatedExtension;
546+
}
547+
548+
console.log(`[ExtensionManager] 插件 ${id} 已为客户端 ${clientId} 启用`);
549+
}
550+
551+
/**
552+
* 为指定客户端禁用插件
553+
* 如果是本地客户端,直接调用 disableExtension
554+
* 如果是远程客户端,调用远程客户端的 REST API
555+
*/
556+
async disableExtensionForClient(id: ExtensionRef, clientId: string): Promise<void> {
557+
console.log(`[ExtensionManager] 为客户端 ${clientId} 禁用插件 ${id}`);
558+
559+
const instance = this.instances.get(id);
560+
if (!instance) {
561+
throw new Error(`Extension ${id} not found`);
562+
}
563+
564+
// 检查是否为本地客户端
565+
if (configUtils.isLocalClient(clientId)) {
566+
// 本地客户端,直接停用
567+
await this.disableExtension(id);
568+
} else {
569+
// 远程客户端,调用远程 API
570+
const client = await Client.get(clientId);
571+
await client.disableExtension(id);
572+
573+
// 更新本地实例的 extension 对象
574+
const updatedExtension = await instance.extension.disable(clientId);
575+
instance.extension = updatedExtension;
576+
}
577+
578+
console.log(`[ExtensionManager] 插件 ${id} 已为客户端 ${clientId} 禁用`);
579+
}
580+
462581
/**
463582
* 完整的启动流程
464583
*/

0 commit comments

Comments
 (0)