Skip to content

Commit d510ee3

Browse files
committed
feat(i18n): add Kafka internationalization support and remove unused Kafka keys
1 parent a594892 commit d510ee3

28 files changed

Lines changed: 1321 additions & 582 deletions

File tree

docs/PLUGINS_zh.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
3. GitHub Actions 只负责读取产物、校验一致性、发布和回写 catalog
1313
4. 本地验证优先走“构建插件 JAR -> 插件管理安装 -> 重启验证”这条路径
1414

15+
如果你当前更想看 runtime 内核实现,而不是开发/发布流程,可以直接先读:
16+
17+
- [PLUGIN_RUNTIME_ARCHITECTURE_zh.md](./PLUGIN_RUNTIME_ARCHITECTURE_zh.md)
18+
1519
## 1. 组件分层和原理
1620

1721
当前插件体系分成 6 层:
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# EasyPostman 插件 Runtime 架构说明
2+
3+
这份文档聚焦 `easy-postman-plugin-runtime` 在当前重构后的实现结构。
4+
5+
它不重复讲“怎么开发插件”或“怎么发布插件”,而是回答下面几个问题:
6+
7+
- runtime 现在分成了哪些职责块
8+
- 插件从安装到加载的真实链路是什么
9+
- 宿主如何消费插件贡献
10+
- 当前冲突策略和测试覆盖到了哪里
11+
- 后面继续演进时,优先改哪里
12+
13+
相关总览文档见:
14+
15+
- [PLUGINS_zh.md](./PLUGINS_zh.md)
16+
17+
## 1. 当前结构
18+
19+
当前 runtime 相关的主类可以按职责理解为:
20+
21+
```text
22+
easy-postman-plugin-runtime
23+
├── PluginRuntime
24+
├── PluginScanner
25+
├── PluginCandidateResolver
26+
├── PluginLoader
27+
├── PluginRegistry
28+
├── PluginStateStore
29+
├── PluginSettingsStore
30+
├── PluginRuntimePaths
31+
├── RuntimeVersionResolver
32+
├── PluginCompatibility
33+
├── PluginFileInfo
34+
└── PluginVersionComparator
35+
```
36+
37+
### 1.1 每个类负责什么
38+
39+
- `PluginRuntime`
40+
- 运行时总编排入口
41+
- 协调启动、关闭、候选选择、生命周期收尾
42+
- 对外暴露 `initialize()``shutdown()``getInstalledPlugins()` 等静态入口
43+
- `PluginScanner`
44+
- 扫描插件目录
45+
- 读取插件 jar 内的 descriptor
46+
- 生成磁盘层的 `PluginFileInfo`
47+
- `PluginCandidateResolver`
48+
- 从扫描结果里选出真正应该加载的插件
49+
- 处理禁用、待卸载、兼容性过滤、同 `plugin.id` 多版本择优
50+
- `PluginLoader`
51+
- 创建 `URLClassLoader`
52+
- 反射实例化插件入口类
53+
- 调用 `onLoad()` / `onStart()` / `onStop()`
54+
-`PluginContext` 注册动作接到 `PluginRegistry`
55+
- `PluginRegistry`
56+
- 保存插件注册出来的脚本 API、服务、Toolbox、补全、Snippet
57+
- 宿主运行时从这里消费插件能力
58+
- `PluginStateStore`
59+
- 维护禁用插件和待卸载插件状态
60+
- `PluginSettingsStore`
61+
- 底层 JSON 持久化
62+
- `PluginRuntimePaths`
63+
- 统一运行时数据目录、安装目录、包缓存目录
64+
- `RuntimeVersionResolver`
65+
- 解析 app version 和 plugin platform version
66+
- 打包态优先读资源,开发态回退读 `pom.xml`
67+
68+
## 2. 真实加载链路
69+
70+
当前插件加载链路可以简化成:
71+
72+
```text
73+
PluginManager / 本地放入 jar
74+
-> plugins/installed 和 plugins/packages
75+
-> StartupCoordinator.prepareMainFrame()
76+
-> PluginRuntime.initialize()
77+
-> cleanupPendingUninstallPlugins()
78+
-> PluginScanner.resolvePluginDirs()
79+
-> PluginScanner.listPluginsFromDirectory()
80+
-> PluginCandidateResolver.resolveLoadCandidates()
81+
-> PluginLoader.loadPluginJar()
82+
-> plugin.onLoad(context)
83+
-> PluginRegistry 收集贡献
84+
-> PluginLoader.startPlugins()
85+
-> 宿主 UI / 脚本 / bridge 层消费贡献
86+
```
87+
88+
这里有几个关键设计点:
89+
90+
- 安装和加载分离
91+
- `plugin-manager` 负责落盘和校验
92+
- `runtime` 只负责“当前进程应该加载谁”
93+
- 扫描和选择分离
94+
- `PluginScanner` 负责“看见什么”
95+
- `PluginCandidateResolver` 负责“最终选谁”
96+
- 注册和消费分离
97+
- 插件只在 `onLoad()` 里声明能力
98+
- 宿主稍后再从 `PluginRegistry` 统一读取
99+
100+
## 3. 宿主消费链路
101+
102+
宿主 app 层已经不再四处直连 `PluginRuntime.getRegistry()`,而是统一收口到:
103+
104+
- [PluginAccess.java](../easy-postman-app/src/main/java/com/laker/postman/plugin/bridge/PluginAccess.java)
105+
106+
当前消费关系大致是:
107+
108+
```text
109+
PluginRegistry
110+
-> PluginAccess
111+
-> PostmanApiContext.createScriptApis()
112+
-> ToolboxPanel.getToolboxContributions()
113+
-> ScriptSnippetManager.getScriptCompletionContributors()
114+
-> SnippetDialog.getSnippetDefinitions()
115+
-> ClientCertificatePluginServices.getService(...)
116+
```
117+
118+
这样做的价值是:
119+
120+
- app 层不用到处依赖 `PluginRuntime`
121+
- 未来如果 registry 或 runtime 实现变化,app 改动面会小很多
122+
123+
## 4. 插件入口模式
124+
125+
插件入口类当前建议只做“能力声明”,不做复杂业务。
126+
127+
官方插件已经开始统一走:
128+
129+
- `PluginContributionSupport`
130+
- `PluginAccess`
131+
- `RedisI18n` / `KafkaI18n`
132+
133+
例如 `KafkaPlugin` / `RedisPlugin``onLoad()` 现在更接近:
134+
135+
```text
136+
registerScriptApi
137+
registerToolbox
138+
registerScriptCompletionContributor
139+
registerExampleSnippet
140+
```
141+
142+
这比原来直接在入口类里手写大量 `ToolboxContribution` / `ShorthandCompletion` / `SnippetDefinition` 更容易 review。
143+
144+
## 5. 当前冲突策略
145+
146+
### 5.1 Script API alias / service type
147+
148+
`PluginRegistry` 目前对两类冲突做了基础保护:
149+
150+
- 脚本 API alias 冲突
151+
- service type 冲突
152+
153+
当前策略是:
154+
155+
- 保持兼容:后注册覆盖前注册
156+
- 不再静默:会打 `warn` 日志
157+
- 日志会带 owner 信息,指出“哪个插件覆盖了哪个插件”
158+
159+
也就是说,现在 registry 已经会记录:
160+
161+
- 某个 script API alias 属于哪个 pluginId
162+
- 某个 service type 属于哪个 pluginId
163+
164+
这一步还不是最终方案,但已经足够让冲突可观测。
165+
166+
### 5.2 还没做的部分
167+
168+
下面这些冲突目前还没有统一治理:
169+
170+
- `ToolboxContribution` 的重复 `id`
171+
- `SnippetDefinition` 的重复标题或语义冲突
172+
- `ScriptCompletionContributor` 的重复补全项
173+
174+
这些可以后续再做。
175+
176+
## 6. 当前测试覆盖
177+
178+
runtime 测试目前主要在:
179+
180+
- [PluginRuntimeTest.java](../easy-postman-plugin-runtime/src/test/java/com/laker/postman/plugin/runtime/PluginRuntimeTest.java)
181+
- [PluginRegistryTest.java](../easy-postman-plugin-runtime/src/test/java/com/laker/postman/plugin/runtime/PluginRegistryTest.java)
182+
183+
已经覆盖的关键行为包括:
184+
185+
- 重新启用插件时会清理 pending uninstall
186+
- 自定义数据目录会生效
187+
- plugin platform version 独立于 app version
188+
- 不兼容平台版本会被拒绝
189+
- pending uninstall 文件清理
190+
- 同一 `plugin.id` 多版本时只加载最高版本
191+
- 最新版本不兼容时,回退到次新兼容版本
192+
- 某个插件加载失败时,其他插件继续加载
193+
- `shutdown()` 会调用插件 `onStop()`
194+
- registry 对 alias/type 的重复注册遵循“后注册覆盖前注册”
195+
- registry 会保留最新注册者的 owner 信息
196+
197+
## 7. 当前仍然存在的限制
198+
199+
这轮重构之后,结构已经清楚很多,但 runtime 还不是终点版本。
200+
201+
目前仍然存在这些限制:
202+
203+
- `PluginRuntime` 仍然是静态全局入口
204+
- 测试虽然可控,但还不是真正的实例化 runtime
205+
- registry 对 UI 贡献没有 owner 级别追踪
206+
- 暂时只追踪了 script API 和 service
207+
- 不支持真正的热卸载
208+
- 当前启用/禁用/卸载仍以“下次启动生效”思路为主
209+
- `PluginSettingsStore` 还是通用 key-value JSON 包装
210+
- 还没有 typed state model
211+
212+
## 8. 后续演进建议
213+
214+
如果继续演进,建议优先级如下:
215+
216+
1.`ToolboxContribution` / `SnippetDefinition` / completion 也补 owner 追踪
217+
2.`PluginRuntime` 从静态入口再包一层实例化 facade
218+
3.`PluginStateStore` 引入 typed state model,而不只是字符串集合
219+
4. 评估是否支持无需重启的热启停
220+
5. 把当前冲突策略整理成对插件开发者可见的契约文档
221+
222+
## 9. 阅读顺序建议
223+
224+
如果要顺着代码理解现在这套 runtime,推荐顺序是:
225+
226+
1. [PluginRuntime.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginRuntime.java)
227+
2. [PluginScanner.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginScanner.java)
228+
3. [PluginCandidateResolver.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginCandidateResolver.java)
229+
4. [PluginLoader.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginLoader.java)
230+
5. [PluginRegistry.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginRegistry.java)
231+
6. [PluginRuntimeTest.java](../easy-postman-plugin-runtime/src/test/java/com/laker/postman/plugin/runtime/PluginRuntimeTest.java)
232+
7. [PluginRegistryTest.java](../easy-postman-plugin-runtime/src/test/java/com/laker/postman/plugin/runtime/PluginRegistryTest.java)

easy-postman-app/src/main/java/com/laker/postman/common/component/dialog/SnippetDialog.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import com.laker.postman.common.component.SearchTextField;
55
import com.laker.postman.frame.MainFrame;
66
import com.laker.postman.model.Snippet;
7-
import com.laker.postman.plugin.runtime.PluginRuntime;
87
import com.laker.postman.model.SnippetType;
8+
import com.laker.postman.plugin.bridge.PluginAccess;
99
import com.laker.postman.util.FontsUtil;
1010
import com.laker.postman.util.I18nUtil;
1111
import com.laker.postman.util.IconUtil;
@@ -43,7 +43,7 @@ public class SnippetDialog extends JDialog {
4343
private static List<Snippet> getI18nSnippets() {
4444
return Stream.concat(
4545
Arrays.stream(SnippetType.values()).map(Snippet::new),
46-
PluginRuntime.getRegistry().getSnippetDefinitions().stream().map(Snippet::new)
46+
PluginAccess.getSnippetDefinitions().stream().map(Snippet::new)
4747
)
4848
.toList();
4949
}

easy-postman-app/src/main/java/com/laker/postman/common/component/editor/ScriptSnippetManager.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.laker.postman.common.component.editor;
22

3-
import com.laker.postman.plugin.runtime.PluginRuntime;
3+
import com.laker.postman.plugin.bridge.PluginAccess;
44
import com.laker.postman.util.I18nUtil;
55
import com.laker.postman.util.MessageKeys;
66
import lombok.experimental.UtilityClass;
@@ -137,7 +137,9 @@ private static void addAllCompletions(DefaultCompletionProvider provider) {
137137
addCheerio(provider);
138138
addXml2Json(provider);
139139

140-
for (var contributor : PluginRuntime.getRegistry().getScriptCompletionContributors()) {
140+
for (var contributor : PluginAccess.getScriptCompletionContributors()) {
141+
// 内建补全先注册,插件补全后追加。
142+
// 这样宿主负责“基础语言 + 核心 pm”,插件只补自己的增量能力。
141143
contributor.contribute(provider);
142144
}
143145
}

easy-postman-app/src/main/java/com/laker/postman/model/script/PostmanApiContext.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.laker.postman.model.script;
22

33
import com.laker.postman.model.*;
4-
import com.laker.postman.plugin.runtime.PluginRuntime;
4+
import com.laker.postman.plugin.bridge.PluginAccess;
55
import com.laker.postman.service.EnvironmentService;
66
import com.laker.postman.service.http.HttpService;
77
import lombok.extern.slf4j.Slf4j;
@@ -125,14 +125,18 @@ public PostmanApiContext(Environment environment) {
125125
this.es = this.elasticsearch;
126126
this.influxdb = new ScriptInfluxDbApi();
127127
this.influx = this.influxdb;
128-
PluginRuntime.getRegistry().createScriptApis().forEach(this::registerPluginApi);
128+
// 核心 pm 能力先由宿主内建,再把插件注册表里的扩展 API 动态挂进来。
129+
// 这样脚本层看到的是一个统一的 pm 对象,而不是“宿主 API + 插件 API”两套入口。
130+
PluginAccess.createScriptApis().forEach(this::registerPluginApi);
129131
}
130132

131133
private void registerPluginApi(String alias, Object api) {
132134
if (alias == null || api == null) {
133135
return;
134136
}
135137
pluginApis.put(alias, api);
138+
// 对 kafka / redis 保留字段别名,是为了兼容已有脚本里直接写 pm.kafka / pm.redis 的习惯。
139+
// 新能力统一建议走 pm.plugin(alias),这样未来不会因为字段膨胀把 pm 顶层塞满。
136140
if ("kafka".equals(alias)) {
137141
this.kafka = api;
138142
}
@@ -142,6 +146,7 @@ private void registerPluginApi(String alias, Object api) {
142146
}
143147

144148
public Object plugin(String alias) {
149+
// 统一的插件 API 访问入口,脚本里可以通过 pm.plugin("kafka") 这种方式按需取能力。
145150
return pluginApis.get(alias);
146151
}
147152

easy-postman-app/src/main/java/com/laker/postman/panel/toolbox/ToolboxPanel.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import com.laker.postman.common.component.SearchTextField;
77
import com.laker.postman.common.constants.ModernColors;
88
import com.laker.postman.plugin.api.ToolboxContribution;
9-
import com.laker.postman.plugin.runtime.PluginRuntime;
9+
import com.laker.postman.plugin.bridge.PluginAccess;
1010
import com.laker.postman.util.I18nUtil;
1111
import com.laker.postman.util.IconUtil;
1212
import com.laker.postman.util.MessageKeys;
@@ -142,8 +142,10 @@ private void registerAllTools() {
142142
}
143143

144144
private void registerPluginTools() {
145-
for (ToolboxContribution contribution : PluginRuntime.getRegistry().getToolboxContributions()) {
145+
for (ToolboxContribution contribution : PluginAccess.getToolboxContributions()) {
146146
try {
147+
// 插件在 onLoad 里只注册“面板工厂”,真正创建 Swing 面板放到宿主这里完成。
148+
// 这样可以把插件声明和 UI 实例化分开,降低启动期创建大量面板的成本。
147149
JPanel panel = contribution.panelSupplier().get();
148150
regPlugin(contribution.id(), contribution.displayName(), contribution.iconPath(),
149151
contribution.groupId(), contribution.groupDisplayName(), panel, contribution.iconClassLoader());

easy-postman-app/src/main/java/com/laker/postman/panel/topmenu/plugin/PluginManagerDialog.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1360,16 +1360,28 @@ private StatusPalette resolveStatusPalette(String text) {
13601360
if (text.contains(I18nUtil.getMessage(MessageKeys.PLUGIN_MANAGER_STATUS_UNINSTALL_PENDING))
13611361
|| text.contains(I18nUtil.getMessage(MessageKeys.PLUGIN_MANAGER_STATUS_DISABLE_PENDING))
13621362
|| text.contains(I18nUtil.getMessage(MessageKeys.PLUGIN_MANAGER_STATUS_RESTART_REQUIRED))
1363-
|| text.contains(I18nUtil.getMessage(MessageKeys.PLUGIN_MANAGER_MARKET_UPDATE_AVAILABLE).split("\\{")[0])) {
1363+
|| matchesStatusPrefix(text, MessageKeys.PLUGIN_MANAGER_MARKET_UPDATE_AVAILABLE)) {
13641364
return new StatusPalette(adaptStatusBackground(ModernColors.WARNING), adaptStatusForeground(ModernColors.WARNING));
13651365
}
13661366
if (text.contains(I18nUtil.getMessage(MessageKeys.PLUGIN_MANAGER_STATUS_DISABLED))
13671367
|| text.contains(I18nUtil.getMessage(MessageKeys.PLUGIN_MANAGER_STATUS_INCOMPATIBLE))) {
13681368
return new StatusPalette(adaptStatusBackground(ModernColors.ERROR), adaptStatusForeground(ModernColors.ERROR));
13691369
}
1370+
if (text.contains(I18nUtil.getMessage(MessageKeys.PLUGIN_MANAGER_MARKET_AVAILABLE))) {
1371+
return new StatusPalette(adaptStatusBackground(ModernColors.PRIMARY), adaptStatusForeground(ModernColors.PRIMARY));
1372+
}
13701373
return new StatusPalette(adaptStatusBackground(ModernColors.SUCCESS), adaptStatusForeground(ModernColors.SUCCESS));
13711374
}
13721375

1376+
private boolean matchesStatusPrefix(String text, String messageKey) {
1377+
String pattern = I18nUtil.getMessage(messageKey);
1378+
int placeholderIndex = pattern.indexOf('{');
1379+
if (placeholderIndex >= 0) {
1380+
return text.contains(pattern.substring(0, placeholderIndex));
1381+
}
1382+
return text.contains(pattern);
1383+
}
1384+
13731385
private Color adaptStatusBackground(Color color) {
13741386
if (ModernColors.isDarkTheme()) {
13751387
return new Color(color.getRed(), color.getGreen(), color.getBlue(), 90);

easy-postman-app/src/main/java/com/laker/postman/plugin/bridge/ClientCertificatePluginServices.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.laker.postman.plugin.bridge;
22

3-
import com.laker.postman.plugin.runtime.PluginRuntime;
4-
53
public final class ClientCertificatePluginServices {
64

75
private static final String MISSING_MESSAGE =
@@ -11,11 +9,11 @@ private ClientCertificatePluginServices() {
119
}
1210

1311
public static boolean isClientCertificatePluginInstalled() {
14-
return PluginRuntime.getRegistry().getService(ClientCertificatePluginService.class) != null;
12+
return PluginAccess.getService(ClientCertificatePluginService.class) != null;
1513
}
1614

1715
public static ClientCertificatePluginService getClientCertificateService() {
18-
return PluginRuntime.getRegistry().getService(ClientCertificatePluginService.class);
16+
return PluginAccess.getService(ClientCertificatePluginService.class);
1917
}
2018

2119
public static ClientCertificatePluginService requireClientCertificateService() {

0 commit comments

Comments
 (0)