Skip to content

Commit cb0a76e

Browse files
committed
refactor(bundled-modules): redesign proposal with EF migration-based module seeding
- Update proposal: module loading from DB only, skip directory scanning - Add strongly-typed migration infrastructure (IModuleSeedData, IMenuSeedData) - Add MigrationBuilderExtensions for type-safe InsertModule/InsertMenu/DeleteModule - CLI command: modulus add-module-migration (auto-generated filename) - Add migration consolidation task as first step - CLI: auto-increment version (1.0.YYDDD.HHmm format)
1 parent 2bcc2c7 commit cb0a76e

6 files changed

Lines changed: 355 additions & 161 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
name: /openspec-modify
3+
id: openspec-modify
4+
category: OpenSpec
5+
description: Modify an existing OpenSpec change proposal.
6+
---
7+
<!-- OPENSPEC:START -->
8+
**Guardrails**
9+
- Favor straightforward, minimal modifications and add complexity only when it is requested or clearly required.
10+
- Keep changes tightly scoped to the requested outcome.
11+
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
12+
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
13+
- Do not write any code during the proposal modification stage. Only update design documents (proposal.md, tasks.md, design.md, and spec deltas).
14+
15+
**Steps**
16+
1. Determine the change ID to modify:
17+
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
18+
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
19+
- Otherwise, review the conversation, run `openspec list`, and ask the user which change to modify; wait for a confirmed change ID before proceeding.
20+
- If you still cannot identify a single change ID, stop and tell the user you cannot modify anything yet.
21+
2. Validate the change exists by running `openspec show <id>` and stop if the change is missing or already archived.
22+
3. Read the existing proposal files (`proposal.md`, `tasks.md`, `design.md` if present, and spec deltas under `specs/`) to understand current state.
23+
4. Review the user's requested modifications and identify which files need to be updated.
24+
5. Apply the requested changes to the appropriate files:
25+
- Update `proposal.md` for scope, rationale, or impact changes
26+
- Update `tasks.md` for implementation plan changes
27+
- Update `design.md` for architectural decision changes
28+
- Update spec deltas under `specs/<capability>/spec.md` for requirement changes
29+
6. Validate with `openspec validate <id> --strict` and resolve every issue before confirming completion.
30+
31+
**Reference**
32+
- Use `openspec show <id> --json --deltas-only` to inspect the current delta structure.
33+
- Use `openspec show <spec> --type spec` to review the base spec when modifying delta requirements.
34+
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` to avoid duplicates or conflicts.
35+
<!-- OPENSPEC:END -->
36+
Lines changed: 190 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,239 @@
11
## Context
22

3-
Modulus 框架支持模块化扩展,但当前构建流程将 Host 和模块分别输出到不同位置:
4-
- Host: `artifacts/`
5-
- Modules: `artifacts/Modules/`
3+
Modulus 框架当前启动流程:
4+
1. 扫描模块目录(`{AppBaseDir}/Modules/`、用户目录)
5+
2. 读取每个模块的 `extension.vsixmanifest`
6+
3. 逐个与数据库比对,不存在则安装
7+
4. 从数据库加载已启用模块
68

7-
在 DEBUG 模式下,Host 从解决方案根目录的 `artifacts/Modules/` 加载模块。但在 RELEASE 模式下,Host 期望从 `{AppBaseDir}/Modules/` 加载内置模块,该目录当前为空
9+
**问题**:每次启动都执行目录扫描和 manifest 解析,即使模块没有变化
810

9-
**目标**: 框架 Owner 可声明哪些模块作为内置模块发布,运行即包含
11+
**目标**:模块加载的唯一来源是数据库,内置模块通过 EF 迁移预置
1012

1113
## Goals / Non-Goals
1214

1315
**Goals**:
14-
- 配置文件声明内置模块列表
15-
- 构建时自动复制内置模块到 Host 输出目录
16-
- 内置模块标记为系统模块(不可卸载)
16+
- 内置模块数据通过 EF 迁移管理
17+
- 提供 CLI 工具自动生成模块数据迁移
18+
- 简化运行时启动流程
19+
- 合并现有迁移,提供干净起点
1720

1821
**Non-Goals**:
19-
- 模块版本锁定(使用当前构建版本
20-
- 内置模块的独立更新机制
21-
- 内置模块的签名验证
22+
- 用户安装模块仍使用现有流程(`InstallFromPathAsync`
23+
- 不修改模块打包格式(`.modpkg`
24+
- 开发模式(DEBUG)可保留目录扫描便于调试
2225

2326
## Decisions
2427

25-
### 1. 配置文件格式
28+
### 1. 合并现有迁移
2629

27-
**Decision**: 使用 `bundled-modules.json` 配置文件
30+
**Decision**: 将 8 个现有迁移合并为 `InitialCreate`,作为干净起点
2831

29-
```json
32+
**步骤**
33+
1. 删除所有现有迁移文件
34+
2. 删除本地数据库
35+
3. 重新生成 `InitialCreate` 迁移(包含当前完整 schema)
36+
4. 迁移中可包含 Host 内置模块(`HostModules`)的种子数据
37+
38+
### 2. EF 迁移管理模块数据
39+
40+
**Decision**: 使用 EF Core 迁移框架,但通过**强类型接口和扩展方法**避免硬编码。
41+
42+
#### 2.1 数据模型接口
43+
44+
```csharp
45+
// Modulus.Infrastructure.Data/Seeding/IModuleSeedData.cs
46+
public interface IModuleSeedData
3047
{
31-
"$schema": "./bundled-modules.schema.json",
32-
"modules": [
33-
"EchoPlugin",
34-
"ComponentsDemo"
35-
]
48+
string Id { get; }
49+
string Name { get; }
50+
string Version { get; }
51+
string? Description { get; }
52+
string? Author { get; }
53+
string Path { get; }
54+
bool IsSystem { get; }
55+
bool IsEnabled { get; }
56+
MenuLocation MenuLocation { get; }
57+
ModuleState State { get; }
58+
}
59+
60+
public interface IMenuSeedData
61+
{
62+
string Id { get; }
63+
string ModuleId { get; }
64+
string DisplayName { get; }
65+
string Icon { get; }
66+
string Route { get; }
67+
MenuLocation Location { get; }
68+
int Order { get; }
3669
}
3770
```
3871

39-
**位置**: 每个 Host 项目目录下(如 `src/Hosts/Modulus.Host.Avalonia/bundled-modules.json`
72+
#### 2.2 迁移辅助扩展方法
4073

41-
**Alternatives considered**:
42-
- MSBuild ItemGroup (`<BundledModule>`) - 需要复杂的 MSBuild 集成
43-
- appsettings.json 配置节 - 运行时配置,不适合构建时使用
74+
```csharp
75+
// Modulus.Infrastructure.Data/Seeding/MigrationBuilderExtensions.cs
76+
public static class MigrationBuilderExtensions
77+
{
78+
public static void InsertModule(this MigrationBuilder builder, IModuleSeedData module)
79+
{
80+
builder.InsertData(
81+
table: nameof(ModulusDbContext.Modules),
82+
columns: new[]
83+
{
84+
nameof(ModuleEntity.Id),
85+
nameof(ModuleEntity.Name),
86+
nameof(ModuleEntity.Version),
87+
// ... 使用 nameof() 确保编译时检查
88+
},
89+
values: new object?[] { module.Id, module.Name, module.Version, ... });
90+
}
91+
92+
public static void InsertMenu(this MigrationBuilder builder, IMenuSeedData menu) { ... }
93+
public static void DeleteModule(this MigrationBuilder builder, string moduleId) { ... }
94+
public static void UpdateModuleVersion(this MigrationBuilder builder, string moduleId, string newVersion) { ... }
95+
}
96+
```
4497

45-
### 2. 构建流程
98+
#### 2.3 生成的迁移示例
4699

47-
**Decision**: 新增 `BundleModules` Nuke 目标,在 `BuildModule` 后执行。
100+
```csharp
101+
// CLI 生成的迁移 - 强类型、无硬编码
102+
public partial class SeedModule_AddEchoPlugin : Migration
103+
{
104+
protected override void Up(MigrationBuilder migrationBuilder)
105+
{
106+
migrationBuilder.InsertModule(new ModuleSeed
107+
{
108+
Id = "EchoPlugin",
109+
Name = "Echo Plugin",
110+
Version = "1.0.0",
111+
Description = "Demo echo functionality",
112+
Author = "AGIBuild",
113+
Path = "Modules/EchoPlugin/extension.vsixmanifest",
114+
IsSystem = true,
115+
IsEnabled = true,
116+
MenuLocation = MenuLocation.Main,
117+
State = ModuleState.Ready
118+
});
119+
120+
migrationBuilder.InsertMenu(new MenuSeed
121+
{
122+
Id = "EchoPlugin.Main",
123+
ModuleId = "EchoPlugin",
124+
DisplayName = "Echo",
125+
Icon = "MessageCircle",
126+
Route = "echo",
127+
Location = MenuLocation.Main,
128+
Order = 100
129+
});
130+
}
131+
132+
protected override void Down(MigrationBuilder migrationBuilder)
133+
{
134+
migrationBuilder.DeleteModule("EchoPlugin");
135+
}
136+
}
48137

49-
```
50-
nuke build
51-
52-
├── Restore
53-
├── BuildApp → artifacts/*.dll
54-
├── BuildModule → artifacts/Modules/{ModuleName}/
55-
└── BundleModules → 复制到 artifacts/Modules/ (最终位置)
138+
// file-scoped 实现类
139+
file record ModuleSeed : IModuleSeedData { ... }
140+
file record MenuSeed : IMenuSeedData { ... }
56141
```
57142

58-
**BundleModules 逻辑**:
59-
1. 读取目标 Host 的 `bundled-modules.json`
60-
2. 对于每个声明的模块名:
61-
-`artifacts/Modules/{ModuleName}/` 复制到 `artifacts/Modules/`
62-
3. 模块已在正确位置,无需额外复制(当前构建输出已是 `artifacts/Modules/`
143+
**优势**
144+
- 复用 EF 迁移版本管理、回滚能力
145+
- **强类型接口** + `nameof()` 确保编译时检查
146+
- 避免硬编码表名/列名
147+
- 统一管理 Schema + 数据迁移
63148

64-
**注意**: 由于当前 `BuildModule` 已将模块输出到 `artifacts/Modules/``BundleModules` 主要用于:
65-
- 验证配置的模块确实存在
66-
- 未来支持选择性打包(仅打包声明的模块)
149+
### 3. CLI 命令
67150

68-
### 3. 发布包结构
151+
**Decision**: `modulus add-module-migration`
69152

70-
```
71-
artifacts/
72-
├── Modulus.Host.Avalonia.exe
73-
├── Modulus.Host.Avalonia.dll
74-
├── appsettings.json
75-
├── ... (Host 依赖)
76-
└── Modules/
77-
├── EchoPlugin/
78-
│ ├── extension.vsixmanifest
79-
│ ├── EchoPlugin.Core.dll
80-
│ └── EchoPlugin.UI.Avalonia.dll
81-
└── ComponentsDemo/
82-
├── extension.vsixmanifest
83-
└── *.dll
84-
```
153+
**自动生成逻辑**
154+
1. 扫描 `src/Modules/` 目录,解析所有 manifest
155+
2. 分析现有迁移,确定已种子的模块
156+
3. 检测差异(新增/版本更新/删除)
157+
4. 生成 EF 迁移类
158+
159+
**命名约定**
160+
- 文件名自动生成:`{timestamp}_SeedModule_{Action}.cs`
161+
- 例:`20251210143052_SeedModule_AddEchoPlugin.cs`
162+
163+
**示例输出**
164+
```bash
165+
$ modulus add-module-migration
166+
167+
Scanning modules...
168+
✓ EchoPlugin v1.0.0 (new)
169+
✓ ComponentsDemo v1.2.0 (updated)
85170

86-
### 4. 运行时行为
171+
Generating migration: 20251210143052_SeedModule_Update.cs
172+
+ INSERT EchoPlugin
173+
~ UPDATE ComponentsDemo
87174

88-
**现有代码已支持** (`App.axaml.cs`):
175+
Done!
176+
```
177+
178+
### 4. 启动流程简化
89179

180+
**Before**:
90181
```csharp
91-
#if !DEBUG
92-
// Production: Load from {AppBaseDir}/Modules/
93-
var appModules = Path.Combine(AppContext.BaseDirectory, "Modules");
94-
if (Directory.Exists(appModules))
182+
await db.Database.MigrateAsync();
183+
184+
var installer = scope.ServiceProvider.GetRequiredService<SystemModuleInstaller>();
185+
foreach (var dir in moduleDirectories)
95186
{
96-
moduleDirectories.Add(new ModuleDirectory(appModules, IsSystem: true));
187+
await installer.InstallFromDirectoryAsync(dir.Path, dir.IsSystem, hostType);
97188
}
98-
#endif
189+
190+
var enabledModules = await db.Modules.Where(m => m.IsEnabled).ToListAsync();
99191
```
100192

101-
- 内置模块自动标记为 `IsSystem: true`
102-
- 系统模块在 UI 中不显示卸载按钮
103-
- `SystemModuleInstaller` 自动注册到数据库
193+
**After**:
194+
```csharp
195+
await db.Database.MigrateAsync(); // EF 迁移自动应用模块数据
196+
197+
// 用户安装模块目录仍需扫描(非内置)
198+
if (userModulesDirectory != null && Directory.Exists(userModulesDirectory))
199+
{
200+
await installer.InstallFromDirectoryAsync(userModulesDirectory, isSystem: false, hostType);
201+
}
104202

105-
### 5. 模块冲突处理
203+
var enabledModules = await db.Modules.Where(m => m.IsEnabled).ToListAsync();
204+
```
205+
206+
**Release 模式**
207+
- 内置模块:通过 EF 迁移预置,无需扫描
208+
- 用户模块:保留目录扫描(`%APPDATA%/Modulus/Modules/`
106209

107-
**场景**: 用户安装了与内置模块同 ID 的模块。
210+
**Debug 模式**
211+
- 可保留 `artifacts/Modules/` 扫描,便于开发调试
108212

109-
**Decision**: 系统模块优先,用户模块被忽略。
213+
### 5. 迁移目录结构
110214

111-
**原理**:
112-
- 系统模块路径 (`{AppBaseDir}/Modules/`) 先于用户路径 (`%APPDATA%/Modulus/Modules/`) 加载
113-
- 相同 ID 的模块只加载第一个
215+
```
216+
src/Shared/Modulus.Infrastructure.Data/Migrations/
217+
├── 20251210000000_InitialCreate.cs # Schema + Host 模块种子
218+
├── 20251210000000_InitialCreate.Designer.cs
219+
├── 20251210143052_SeedModule_AddEchoPlugin.cs # 模块数据
220+
├── 20251210143052_SeedModule_AddEchoPlugin.Designer.cs
221+
├── 20251211092134_SeedModule_AddComponentsDemo.cs # 模块数据
222+
└── ModulusDbContextModelSnapshot.cs
223+
```
114224

115225
## Risks / Trade-offs
116226

117227
| 风险 | 影响 | Mitigation |
118228
|------|------|------------|
119-
| 配置模块名拼写错误 | 构建时无模块 | BundleModules 验证并报错 |
120-
| 内置模块与用户模块冲突 | 用户模块被忽略 | 文档说明优先级规则 |
121-
| 发布包体积增大 | 分发文件变大 | 可接受,按需配置 |
229+
| 迁移合并需要清除现有数据库 | 开发环境需重建 | 仅影响开发,提供清晰文档 |
230+
| CLI 生成迁移需要解析 manifest | 依赖 manifest 格式 | 复用现有 VsixManifestReader |
231+
| 开发模式仍需目录扫描 | DEBUG 和 RELEASE 行为不同 | 可接受,便于开发 |
122232

123233
## Open Questions
124234

125-
1. 是否需要支持条件打包(如仅 Release 配置打包)
126-
- 暂定:不需要,DEBUG 和 RELEASE 共用配置
235+
1. 是否在 `InitialCreate` 中包含默认内置模块种子
236+
- 建议:是,将 HostModules(Settings 等)作为初始种子
127237

238+
2. 用户安装模块是否也迁移到 EF 迁移方式?
239+
- 建议:否,保持运行时安装流程
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
# Change: 内置模块打包发布
22

33
## Why
4-
框架 Owner 需要将某些模块作为 Host 的内置部分发布,使用户运行 Host 时即包含指定扩展。当前构建流程无法将模块打包进 Host 发布包
4+
框架需要将某些模块作为 Host 的内置部分发布。当前启动流程每次都执行"目录扫描 → manifest 解析 → 数据库比对",即使模块没有变化。需要优化为:模块加载的唯一来源是数据库,内置模块通过 EF 迁移预置
55

66
## What Changes
7-
- 新增 `bundled-modules.json` 配置文件声明内置模块
8-
- 扩展 `nuke build` 流程,将指定模块复制到 Host 输出目录的 `Modules/` 子目录
9-
- 内置模块标记为 `IsSystem: true`,用户无法卸载
7+
- 合并并清理现有数据迁移脚本,作为干净的起点
8+
- 新增 CLI 命令 `modulus add-module-migration` 自动生成模块数据迁移
9+
- 内置模块数据通过 EF 迁移管理(使用 `InsertData`/`UpdateData`/`DeleteData`
10+
- 简化启动流程:移除目录扫描,直接从数据库加载模块
1011

1112
## Impact
1213
- Affected specs: 新增 `bundled-modules` capability
1314
- Affected code:
14-
- `build/BuildTasks.cs` - 新增 BundleModules 目标
15-
- `src/Hosts/Modulus.Host.Avalonia/bundled-modules.json` - 新增配置文件
16-
- `src/Hosts/Modulus.Host.Blazor/bundled-modules.json` - 新增配置文件
17-
15+
- `src/Shared/Modulus.Infrastructure.Data/Migrations/` - 合并现有迁移 + 新增模块数据迁移
16+
- `src/Modulus.Cli/` - 新增 `add-module-migration` 命令
17+
- `src/Modulus.Core/Runtime/ModulusApplicationFactory.cs` - 简化启动流程
18+
- `src/Modulus.Core/Installation/SystemModuleInstaller.cs` - 调整用途(仅用于用户安装)

0 commit comments

Comments
 (0)