@lobehub/chat 1.109.1 → 1.110.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/i18n.mdc +1 -2
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/apps/desktop/electron-builder.js +22 -0
- package/apps/desktop/src/main/controllers/McpInstallCtr.ts +153 -0
- package/apps/desktop/src/main/controllers/index.ts +19 -0
- package/apps/desktop/src/main/core/App.ts +46 -0
- package/apps/desktop/src/main/core/infrastructure/IoCContainer.ts +4 -0
- package/apps/desktop/src/main/core/infrastructure/ProtocolManager.ts +256 -0
- package/apps/desktop/src/main/types/protocol.ts +60 -0
- package/apps/desktop/src/main/utils/__tests__/protocol.test.ts +203 -0
- package/apps/desktop/src/main/utils/protocol.ts +210 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/plugin.json +196 -136
- package/locales/bg-BG/plugin.json +204 -144
- package/locales/de-DE/plugin.json +176 -116
- package/locales/en-US/plugin.json +192 -132
- package/locales/es-ES/plugin.json +203 -143
- package/locales/fa-IR/plugin.json +155 -95
- package/locales/fr-FR/plugin.json +161 -101
- package/locales/it-IT/plugin.json +193 -133
- package/locales/ja-JP/plugin.json +195 -135
- package/locales/ko-KR/plugin.json +163 -103
- package/locales/nl-NL/plugin.json +211 -151
- package/locales/pl-PL/plugin.json +171 -111
- package/locales/pt-BR/plugin.json +180 -120
- package/locales/ru-RU/plugin.json +191 -131
- package/locales/tr-TR/plugin.json +187 -127
- package/locales/vi-VN/plugin.json +152 -92
- package/locales/zh-CN/plugin.json +60 -0
- package/locales/zh-TW/plugin.json +157 -97
- package/package.json +2 -1
- package/packages/electron-client-ipc/src/events/index.ts +5 -2
- package/packages/electron-client-ipc/src/events/protocol.ts +29 -0
- package/packages/electron-client-ipc/src/types/index.ts +1 -0
- package/packages/electron-client-ipc/src/types/mcpInstall.ts +19 -0
- package/packages/types/src/plugins/mcp.ts +38 -1
- package/packages/types/src/plugins/protocol.ts +166 -0
- package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +4 -1
- package/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/ActionButton/index.tsx +1 -2
- package/src/components/KeyValueEditor/index.tsx +4 -2
- package/src/features/ChatItem/index.tsx +25 -2
- package/src/features/MCP/MCPInstallProgress/index.tsx +1 -1
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +30 -36
- package/src/features/PluginStore/McpList/List/Item.tsx +1 -1
- package/src/features/ProtocolUrlHandler/InstallPlugin/ConfigDisplay.tsx +211 -0
- package/src/features/ProtocolUrlHandler/InstallPlugin/CustomPluginInstallModal.tsx +228 -0
- package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/Detail.tsx +44 -0
- package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/index.tsx +105 -0
- package/src/features/ProtocolUrlHandler/InstallPlugin/index.tsx +55 -0
- package/src/features/ProtocolUrlHandler/InstallPlugin/types.ts +45 -0
- package/src/features/ProtocolUrlHandler/index.tsx +30 -0
- package/src/locales/default/plugin.ts +60 -0
- package/src/store/tool/slices/mcpStore/action.ts +127 -1
- package/src/store/tool/slices/mcpStore/initialState.ts +8 -13
- package/src/store/tool/slices/mcpStore/selectors.ts +13 -0
package/.cursor/rules/i18n.mdc
CHANGED
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.110.1](https://github.com/lobehub/lobe-chat/compare/v1.110.0...v1.110.1)
|
6
|
+
|
7
|
+
<sup>Released on **2025-08-06**</sup>
|
8
|
+
|
9
|
+
#### 🐛 Bug Fixes
|
10
|
+
|
11
|
+
- **misc**: Fix remote avatar broken in desktop again.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's fixed
|
19
|
+
|
20
|
+
- **misc**: Fix remote avatar broken in desktop again, closes [#8688](https://github.com/lobehub/lobe-chat/issues/8688) ([41b4363](https://github.com/lobehub/lobe-chat/commit/41b4363))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
## [Version 1.110.0](https://github.com/lobehub/lobe-chat/compare/v1.109.1...v1.110.0)
|
31
|
+
|
32
|
+
<sup>Released on **2025-08-06**</sup>
|
33
|
+
|
34
|
+
#### ✨ Features
|
35
|
+
|
36
|
+
- **misc**: Support mcp plugin install from web.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### What's improved
|
44
|
+
|
45
|
+
- **misc**: Support mcp plugin install from web, closes [#8680](https://github.com/lobehub/lobe-chat/issues/8680) ([022d858](https://github.com/lobehub/lobe-chat/commit/022d858))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.109.1](https://github.com/lobehub/lobe-chat/compare/v1.109.0...v1.109.1)
|
6
56
|
|
7
57
|
<sup>Released on **2025-08-06**</sup>
|
package/README.md
CHANGED
@@ -283,7 +283,7 @@ We have implemented support for the following model service providers:
|
|
283
283
|
- **[Search1API](https://lobechat.com/discover/provider/search1api)**: Search1API provides access to the DeepSeek series of models that can connect to the internet as needed, including standard and fast versions, supporting a variety of model sizes.
|
284
284
|
- **[InfiniAI](https://lobechat.com/discover/provider/infiniai)**: Provides high-performance, easy-to-use, and secure large model services for application developers, covering the entire process from large model development to service deployment.
|
285
285
|
- **[Qiniu](https://lobechat.com/discover/provider/qiniu)**: Qiniu, as a long-established cloud service provider, delivers cost-effective and reliable AI inference services for both real-time and batch processing, with a simple and user-friendly experience.
|
286
|
-
- **[302.AI](https://lobechat.com/discover/provider/ai302)**:
|
286
|
+
- **[302.AI](https://lobechat.com/discover/provider/ai302)**: 302.AI is an on-demand AI application platform offering the most comprehensive AI APIs and online AI applications available on the market.
|
287
287
|
|
288
288
|
</details>
|
289
289
|
|
package/README.zh-CN.md
CHANGED
@@ -283,7 +283,7 @@ LobeChat 支持文件上传与知识库功能,你可以上传文件、图片
|
|
283
283
|
- **[Search1API](https://lobechat.com/discover/provider/search1api)**: Search1API 提供可根据需要自行联网的 DeepSeek 系列模型的访问,包括标准版和快速版本,支持多种参数规模的模型选择。
|
284
284
|
- **[InfiniAI](https://lobechat.com/discover/provider/infiniai)**: 为应用开发者提供高性能、易上手、安全可靠的大模型服务,覆盖从大模型开发到大模型服务化部署的全流程。
|
285
285
|
- **[Qiniu](https://lobechat.com/discover/provider/qiniu)**: 七牛作为老牌云服务厂商,提供高性价比稳定的实时、批量 AI 推理服务,简单易用。
|
286
|
-
- **[302.AI](https://lobechat.com/discover/provider/ai302)**:
|
286
|
+
- **[302.AI](https://lobechat.com/discover/provider/ai302)**: 302.AI 是一个按需付费的 AI 应用平台,提供市面上最全的 AI API 和 AI 在线应用
|
287
287
|
|
288
288
|
</details>
|
289
289
|
|
@@ -11,6 +11,16 @@ console.log(`🚄 Build Version ${packageJSON.version}, Channel: ${channel}`);
|
|
11
11
|
const isNightly = channel === 'nightly';
|
12
12
|
const isBeta = packageJSON.name.includes('beta');
|
13
13
|
|
14
|
+
// 根据版本类型确定协议 scheme
|
15
|
+
const getProtocolScheme = () => {
|
16
|
+
if (isNightly) return 'lobehub-nightly';
|
17
|
+
if (isBeta) return 'lobehub-beta';
|
18
|
+
|
19
|
+
return 'lobehub';
|
20
|
+
};
|
21
|
+
|
22
|
+
const protocolScheme = getProtocolScheme();
|
23
|
+
|
14
24
|
/**
|
15
25
|
* @type {import('electron-builder').Configuration}
|
16
26
|
* @see https://www.electron.build/configuration
|
@@ -60,6 +70,12 @@ const config = {
|
|
60
70
|
compression: 'maximum',
|
61
71
|
entitlementsInherit: 'build/entitlements.mac.plist',
|
62
72
|
extendInfo: {
|
73
|
+
CFBundleURLTypes: [
|
74
|
+
{
|
75
|
+
CFBundleURLName: 'LobeHub Protocol',
|
76
|
+
CFBundleURLSchemes: [protocolScheme],
|
77
|
+
},
|
78
|
+
],
|
63
79
|
NSCameraUsageDescription: "Application requests access to the device's camera.",
|
64
80
|
NSDocumentsFolderUsageDescription:
|
65
81
|
"Application requests access to the user's Documents folder.",
|
@@ -91,6 +107,12 @@ const config = {
|
|
91
107
|
uninstallDisplayName: '${productName}',
|
92
108
|
uninstallerSidebar: './build/nsis-sidebar.bmp',
|
93
109
|
},
|
110
|
+
protocols: [
|
111
|
+
{
|
112
|
+
name: 'LobeHub Protocol',
|
113
|
+
schemes: [protocolScheme],
|
114
|
+
},
|
115
|
+
],
|
94
116
|
publish: [
|
95
117
|
{
|
96
118
|
owner: 'lobehub',
|
@@ -0,0 +1,153 @@
|
|
1
|
+
import { createLogger } from '@/utils/logger';
|
2
|
+
|
3
|
+
import { ControllerModule, createProtocolHandler } from '.';
|
4
|
+
import { McpSchema } from '../types/protocol';
|
5
|
+
|
6
|
+
const logger = createLogger('controllers:McpInstallCtr');
|
7
|
+
|
8
|
+
const protocolHandler = createProtocolHandler('plugin');
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 验证 MCP Schema 对象结构
|
12
|
+
*/
|
13
|
+
function validateMcpSchema(schema: any): schema is McpSchema {
|
14
|
+
if (!schema || typeof schema !== 'object') return false;
|
15
|
+
|
16
|
+
// 必填字段验证
|
17
|
+
if (typeof schema.identifier !== 'string' || !schema.identifier) return false;
|
18
|
+
if (typeof schema.name !== 'string' || !schema.name) return false;
|
19
|
+
if (typeof schema.author !== 'string' || !schema.author) return false;
|
20
|
+
if (typeof schema.description !== 'string' || !schema.description) return false;
|
21
|
+
if (typeof schema.version !== 'string' || !schema.version) return false;
|
22
|
+
|
23
|
+
// 可选字段验证
|
24
|
+
if (schema.homepage !== undefined && typeof schema.homepage !== 'string') return false;
|
25
|
+
if (schema.icon !== undefined && typeof schema.icon !== 'string') return false;
|
26
|
+
|
27
|
+
// config 字段验证
|
28
|
+
if (!schema.config || typeof schema.config !== 'object') return false;
|
29
|
+
const config = schema.config;
|
30
|
+
|
31
|
+
if (config.type === 'stdio') {
|
32
|
+
if (typeof config.command !== 'string' || !config.command) return false;
|
33
|
+
if (config.args !== undefined && !Array.isArray(config.args)) return false;
|
34
|
+
if (config.env !== undefined && typeof config.env !== 'object') return false;
|
35
|
+
} else if (config.type === 'http') {
|
36
|
+
if (typeof config.url !== 'string' || !config.url) return false;
|
37
|
+
try {
|
38
|
+
new URL(config.url); // 验证URL格式
|
39
|
+
} catch {
|
40
|
+
return false;
|
41
|
+
}
|
42
|
+
if (config.headers !== undefined && typeof config.headers !== 'object') return false;
|
43
|
+
} else {
|
44
|
+
return false; // 未知的 config type
|
45
|
+
}
|
46
|
+
|
47
|
+
return true;
|
48
|
+
}
|
49
|
+
|
50
|
+
interface McpInstallParams {
|
51
|
+
id: string;
|
52
|
+
marketId?: string;
|
53
|
+
schema?: any;
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
* MCP 插件安装控制器
|
58
|
+
* 负责处理 MCP 插件安装流程
|
59
|
+
*/
|
60
|
+
export default class McpInstallController extends ControllerModule {
|
61
|
+
/**
|
62
|
+
* 处理 MCP 插件安装请求
|
63
|
+
* @param parsedData 解析后的协议数据
|
64
|
+
* @returns 是否处理成功
|
65
|
+
*/
|
66
|
+
@protocolHandler('install')
|
67
|
+
public async handleInstallRequest(parsedData: McpInstallParams): Promise<boolean> {
|
68
|
+
try {
|
69
|
+
// 从参数中提取必需字段
|
70
|
+
const { id, schema: schemaParam, marketId } = parsedData;
|
71
|
+
|
72
|
+
if (!id) {
|
73
|
+
logger.warn(`🔧 [McpInstall] Missing required MCP parameters:`, {
|
74
|
+
id: !!id,
|
75
|
+
});
|
76
|
+
return false;
|
77
|
+
}
|
78
|
+
|
79
|
+
// 映射协议来源
|
80
|
+
|
81
|
+
const isOfficialMarket = marketId === 'lobehub';
|
82
|
+
|
83
|
+
// 对于官方市场,schema 是可选的;对于第三方市场,schema 是必需的
|
84
|
+
if (!isOfficialMarket && !schemaParam) {
|
85
|
+
logger.warn(`🔧 [McpInstall] Schema is required for third-party marketplace:`, {
|
86
|
+
marketId,
|
87
|
+
});
|
88
|
+
return false;
|
89
|
+
}
|
90
|
+
|
91
|
+
let mcpSchema: McpSchema | undefined;
|
92
|
+
|
93
|
+
// 如果提供了 schema 参数,则解析和验证
|
94
|
+
if (schemaParam) {
|
95
|
+
try {
|
96
|
+
mcpSchema = JSON.parse(schemaParam);
|
97
|
+
} catch (error) {
|
98
|
+
logger.error(`🔧 [McpInstall] Failed to parse MCP schema:`, error);
|
99
|
+
return false;
|
100
|
+
}
|
101
|
+
|
102
|
+
if (!validateMcpSchema(mcpSchema)) {
|
103
|
+
logger.error(`🔧 [McpInstall] Invalid MCP Schema structure`);
|
104
|
+
return false;
|
105
|
+
}
|
106
|
+
|
107
|
+
// 验证 identifier 与 id 参数匹配
|
108
|
+
if (mcpSchema.identifier !== id) {
|
109
|
+
logger.error(`🔧 [McpInstall] Schema identifier does not match URL id parameter:`, {
|
110
|
+
schemaId: mcpSchema.identifier,
|
111
|
+
urlId: id,
|
112
|
+
});
|
113
|
+
return false;
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
logger.debug(`🔧 [McpInstall] MCP install request validated:`, {
|
118
|
+
hasSchema: !!mcpSchema,
|
119
|
+
marketId,
|
120
|
+
pluginId: id,
|
121
|
+
pluginName: mcpSchema?.name || 'Unknown',
|
122
|
+
pluginVersion: mcpSchema?.version || 'Unknown',
|
123
|
+
});
|
124
|
+
|
125
|
+
// 广播安装请求到前端
|
126
|
+
const installRequest = {
|
127
|
+
marketId,
|
128
|
+
pluginId: id,
|
129
|
+
schema: mcpSchema,
|
130
|
+
};
|
131
|
+
|
132
|
+
logger.debug(`🔧 [McpInstall] Broadcasting install request:`, {
|
133
|
+
hasSchema: !!installRequest.schema,
|
134
|
+
marketId: installRequest.marketId,
|
135
|
+
pluginId: installRequest.pluginId,
|
136
|
+
pluginName: installRequest.schema?.name || 'Unknown',
|
137
|
+
});
|
138
|
+
|
139
|
+
// 通过应用实例广播到前端
|
140
|
+
if (this.app?.browserManager) {
|
141
|
+
this.app.browserManager.broadcastToWindow('chat', 'mcpInstallRequest', installRequest);
|
142
|
+
logger.debug(`🔧 [McpInstall] Install request broadcasted successfully`);
|
143
|
+
return true;
|
144
|
+
} else {
|
145
|
+
logger.error(`🔧 [McpInstall] App or browserManager not available`);
|
146
|
+
return false;
|
147
|
+
}
|
148
|
+
} catch (error) {
|
149
|
+
logger.error(`🔧 [McpInstall] Error processing install request:`, error);
|
150
|
+
return false;
|
151
|
+
}
|
152
|
+
}
|
153
|
+
}
|
@@ -44,11 +44,30 @@ const shortcutDecorator = (name: string) => (target: any, methodName: string, de
|
|
44
44
|
*/
|
45
45
|
export const shortcut = (method: ShortcutActionType) => shortcutDecorator(method);
|
46
46
|
|
47
|
+
const protocolDecorator =
|
48
|
+
(urlType: string, action: string) => (target: any, methodName: string, descriptor?: any) => {
|
49
|
+
const handlers = IoCContainer.protocolHandlers.get(target.constructor) || [];
|
50
|
+
handlers.push({ action, methodName, urlType });
|
51
|
+
|
52
|
+
IoCContainer.protocolHandlers.set(target.constructor, handlers);
|
53
|
+
|
54
|
+
return descriptor;
|
55
|
+
};
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Protocol handler decorator
|
59
|
+
* @param urlType 协议URL类型 (如: 'plugin')
|
60
|
+
* @param action 操作类型 (如: 'install')
|
61
|
+
*/
|
62
|
+
export const createProtocolHandler = (urlType: string) => (action: string) =>
|
63
|
+
protocolDecorator(urlType, action);
|
64
|
+
|
47
65
|
interface IControllerModule {
|
48
66
|
afterAppReady?(): void;
|
49
67
|
app: App;
|
50
68
|
beforeAppReady?(): void;
|
51
69
|
}
|
70
|
+
|
52
71
|
export class ControllerModule implements IControllerModule {
|
53
72
|
constructor(public app: App) {
|
54
73
|
this.app = app;
|
@@ -16,6 +16,7 @@ import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
|
|
16
16
|
import { BrowserManager } from './browser/BrowserManager';
|
17
17
|
import { I18nManager } from './infrastructure/I18nManager';
|
18
18
|
import { IoCContainer } from './infrastructure/IoCContainer';
|
19
|
+
import { ProtocolManager } from './infrastructure/ProtocolManager';
|
19
20
|
import { StaticFileServerManager } from './infrastructure/StaticFileServerManager';
|
20
21
|
import { StoreManager } from './infrastructure/StoreManager';
|
21
22
|
import { UpdaterManager } from './infrastructure/UpdaterManager';
|
@@ -27,6 +28,7 @@ const logger = createLogger('core:App');
|
|
27
28
|
|
28
29
|
export type IPCEventMap = Map<string, { controller: any; methodName: string }>;
|
29
30
|
export type ShortcutMethodMap = Map<string, () => Promise<void>>;
|
31
|
+
export type ProtocolHandlerMap = Map<string, { controller: any; methodName: string }>;
|
30
32
|
|
31
33
|
type Class<T> = new (...args: any[]) => T;
|
32
34
|
|
@@ -43,6 +45,7 @@ export class App {
|
|
43
45
|
shortcutManager: ShortcutManager;
|
44
46
|
trayManager: TrayManager;
|
45
47
|
staticFileServerManager: StaticFileServerManager;
|
48
|
+
protocolManager: ProtocolManager;
|
46
49
|
chromeFlags: string[] = ['OverlayScrollbar', 'FluentOverlayScrollbar', 'FluentScrollbar'];
|
47
50
|
|
48
51
|
/**
|
@@ -100,11 +103,15 @@ export class App {
|
|
100
103
|
this.shortcutManager = new ShortcutManager(this);
|
101
104
|
this.trayManager = new TrayManager(this);
|
102
105
|
this.staticFileServerManager = new StaticFileServerManager(this);
|
106
|
+
this.protocolManager = new ProtocolManager(this);
|
103
107
|
|
104
108
|
// register the schema to interceptor url
|
105
109
|
// it should register before app ready
|
106
110
|
this.registerNextHandler();
|
107
111
|
|
112
|
+
// initialize protocol handlers
|
113
|
+
this.protocolManager.initialize();
|
114
|
+
|
108
115
|
// 统一处理 before-quit 事件
|
109
116
|
app.on('before-quit', this.handleBeforeQuit);
|
110
117
|
|
@@ -160,6 +167,10 @@ export class App {
|
|
160
167
|
});
|
161
168
|
|
162
169
|
app.on('activate', this.onActivate);
|
170
|
+
|
171
|
+
// Process any pending protocol URLs after everything is ready
|
172
|
+
await this.protocolManager.processPendingUrls();
|
173
|
+
|
163
174
|
logger.info('Application bootstrap completed');
|
164
175
|
};
|
165
176
|
|
@@ -171,6 +182,32 @@ export class App {
|
|
171
182
|
return this.controllers.get(controllerClass);
|
172
183
|
}
|
173
184
|
|
185
|
+
/**
|
186
|
+
* Handle protocol request by dispatching to registered handlers
|
187
|
+
* @param urlType 协议URL类型 (如: 'plugin')
|
188
|
+
* @param action 操作类型 (如: 'install')
|
189
|
+
* @param data 解析后的协议数据
|
190
|
+
* @returns 是否成功处理
|
191
|
+
*/
|
192
|
+
async handleProtocolRequest(urlType: string, action: string, data: any): Promise<boolean> {
|
193
|
+
const key = `${urlType}:${action}`;
|
194
|
+
const handler = this.protocolHandlerMap.get(key);
|
195
|
+
|
196
|
+
if (!handler) {
|
197
|
+
logger.warn(`No protocol handler found for ${key}`);
|
198
|
+
return false;
|
199
|
+
}
|
200
|
+
|
201
|
+
try {
|
202
|
+
logger.debug(`Dispatching protocol request ${key} to controller`);
|
203
|
+
const result = await handler.controller[handler.methodName](data);
|
204
|
+
return result !== false; // 假设控制器返回 false 表示处理失败
|
205
|
+
} catch (error) {
|
206
|
+
logger.error(`Error handling protocol request ${key}:`, error);
|
207
|
+
return false;
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
174
211
|
private onActivate = () => {
|
175
212
|
logger.debug('Application activated');
|
176
213
|
this.browserManager.showMainWindow();
|
@@ -233,6 +270,7 @@ export class App {
|
|
233
270
|
private ipcClientEventMap: IPCEventMap = new Map();
|
234
271
|
private ipcServerEventMap: IPCEventMap = new Map();
|
235
272
|
shortcutMethodMap: ShortcutMethodMap = new Map();
|
273
|
+
protocolHandlerMap: ProtocolHandlerMap = new Map();
|
236
274
|
|
237
275
|
/**
|
238
276
|
* use in next router interceptor in prod browser render
|
@@ -308,6 +346,14 @@ export class App {
|
|
308
346
|
controller[shortcut.methodName]();
|
309
347
|
});
|
310
348
|
});
|
349
|
+
|
350
|
+
IoCContainer.protocolHandlers.get(ControllerClass)?.forEach((handler) => {
|
351
|
+
const key = `${handler.urlType}:${handler.action}`;
|
352
|
+
this.protocolHandlerMap.set(key, {
|
353
|
+
controller,
|
354
|
+
methodName: handler.methodName,
|
355
|
+
});
|
356
|
+
});
|
311
357
|
};
|
312
358
|
|
313
359
|
private addService = (ServiceClass: IServiceModule) => {
|
@@ -8,5 +8,9 @@ export class IoCContainer {
|
|
8
8
|
> = new WeakMap();
|
9
9
|
|
10
10
|
static shortcuts: WeakMap<any, { methodName: string; name: string }[]> = new WeakMap();
|
11
|
+
|
12
|
+
static protocolHandlers: WeakMap<any, { action: string; methodName: string; urlType: string }[]> =
|
13
|
+
new WeakMap();
|
14
|
+
|
11
15
|
init() {}
|
12
16
|
}
|
@@ -0,0 +1,256 @@
|
|
1
|
+
import { app } from 'electron';
|
2
|
+
|
3
|
+
import { isDev } from '@/const/env';
|
4
|
+
import { createLogger } from '@/utils/logger';
|
5
|
+
import { getProtocolScheme, parseProtocolUrl } from '@/utils/protocol';
|
6
|
+
|
7
|
+
import { App } from '../App';
|
8
|
+
|
9
|
+
// Create logger
|
10
|
+
const logger = createLogger('core:ProtocolManager');
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Protocol handler manager for custom URI schemes
|
14
|
+
*/
|
15
|
+
export class ProtocolManager {
|
16
|
+
private app: App;
|
17
|
+
private protocolScheme: string;
|
18
|
+
private pendingUrls: string[] = [];
|
19
|
+
|
20
|
+
constructor(app: App) {
|
21
|
+
logger.debug('Initializing ProtocolManager');
|
22
|
+
this.app = app;
|
23
|
+
this.protocolScheme = getProtocolScheme();
|
24
|
+
logger.info(`ProtocolManager initialized for scheme: ${this.protocolScheme}://`);
|
25
|
+
}
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Register protocol handlers and set up event listeners
|
29
|
+
*/
|
30
|
+
public initialize(): void {
|
31
|
+
logger.debug('Setting up protocol handlers');
|
32
|
+
|
33
|
+
this.registerProtocolHandlers();
|
34
|
+
this.setupEventListeners();
|
35
|
+
|
36
|
+
logger.debug('Protocol initialization completed');
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Register the application as default protocol client
|
41
|
+
*/
|
42
|
+
private registerProtocolHandlers(): void {
|
43
|
+
logger.debug(`🔗 [Protocol] Registering protocol handlers for ${this.protocolScheme}://`);
|
44
|
+
|
45
|
+
// Debug info about current app
|
46
|
+
logger.debug(`🔗 [Protocol] App name: ${app.name}`);
|
47
|
+
logger.debug(`🔗 [Protocol] App path: ${app.getPath('exe')}`);
|
48
|
+
logger.debug(`🔗 [Protocol] Is development: ${isDev}`);
|
49
|
+
logger.debug(`🔗 [Protocol] Process argv[0]: ${process.argv[0]}`);
|
50
|
+
|
51
|
+
// Check if already registered
|
52
|
+
const isCurrentlyRegistered = app.isDefaultProtocolClient(this.protocolScheme);
|
53
|
+
logger.debug(`🔗 [Protocol] Is currently default protocol client: ${isCurrentlyRegistered}`);
|
54
|
+
|
55
|
+
// Register as default protocol client
|
56
|
+
let registrationResult: boolean;
|
57
|
+
|
58
|
+
if (isDev) {
|
59
|
+
// In development, use explicit parameters to ensure proper registration
|
60
|
+
const appPath = process.cwd(); // Current working directory (our app)
|
61
|
+
logger.debug(`🔗 [Protocol] Development mode: using explicit registration parameters`);
|
62
|
+
logger.debug(`🔗 [Protocol] Executable path: ${process.execPath}`);
|
63
|
+
logger.debug(`🔗 [Protocol] App path: ${appPath}`);
|
64
|
+
logger.debug(`🔗 [Protocol] Arguments: ${JSON.stringify([appPath])}`);
|
65
|
+
|
66
|
+
registrationResult = app.setAsDefaultProtocolClient(this.protocolScheme, process.execPath, [
|
67
|
+
appPath,
|
68
|
+
]);
|
69
|
+
} else {
|
70
|
+
// In production, use simple registration
|
71
|
+
registrationResult = app.setAsDefaultProtocolClient(this.protocolScheme);
|
72
|
+
}
|
73
|
+
|
74
|
+
logger.debug(`🔗 [Protocol] Registration result: ${registrationResult}`);
|
75
|
+
|
76
|
+
if (!registrationResult) {
|
77
|
+
logger.error(
|
78
|
+
`🔗 [Protocol] Failed to register as default protocol client for ${this.protocolScheme}://`,
|
79
|
+
);
|
80
|
+
} else {
|
81
|
+
logger.debug(`🔗 [Protocol] Successfully registered ${this.protocolScheme}:// protocol`);
|
82
|
+
}
|
83
|
+
|
84
|
+
// Verify registration
|
85
|
+
const isRegisteredAfter = app.isDefaultProtocolClient(this.protocolScheme);
|
86
|
+
logger.debug(`🔗 [Protocol] Final registration status: ${isRegisteredAfter}`);
|
87
|
+
}
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Set up protocol event listeners
|
91
|
+
*/
|
92
|
+
private setupEventListeners(): void {
|
93
|
+
// Handle protocol URL from cold start (Windows/Linux)
|
94
|
+
const protocolUrl = this.getProtocolUrlFromArgs(process.argv);
|
95
|
+
if (protocolUrl) {
|
96
|
+
logger.debug(`🔗 [Protocol] Found protocol URL from cold start: ${protocolUrl}`);
|
97
|
+
this.pendingUrls.push(protocolUrl);
|
98
|
+
}
|
99
|
+
|
100
|
+
// Handle protocol URL from macOS open-url event
|
101
|
+
app.on('open-url', (event, url) => {
|
102
|
+
event.preventDefault();
|
103
|
+
logger.debug(`🔗 [Protocol] Received URL from open-url event: ${url}`);
|
104
|
+
logger.debug(`🔗 [Protocol] App ready state: ${app.isReady()}`);
|
105
|
+
logger.debug(`🔗 [Protocol] Event prevented, processing URL...`);
|
106
|
+
this.handleProtocolUrl(url);
|
107
|
+
});
|
108
|
+
|
109
|
+
// Handle protocol URL from second instance (Windows/Linux)
|
110
|
+
app.on('second-instance', (event, commandLine) => {
|
111
|
+
const url = this.getProtocolUrlFromArgs(commandLine);
|
112
|
+
if (url) {
|
113
|
+
logger.debug(`🔗 [Protocol] Received protocol URL from second instance: ${url}`);
|
114
|
+
this.handleProtocolUrl(url);
|
115
|
+
}
|
116
|
+
// Show main window when second instance is triggered
|
117
|
+
this.app.browserManager.showMainWindow();
|
118
|
+
});
|
119
|
+
}
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Extract protocol URL from command line arguments
|
123
|
+
*/
|
124
|
+
private getProtocolUrlFromArgs(args: string[]): string | null {
|
125
|
+
const protocolPrefix = `${this.protocolScheme}://`;
|
126
|
+
|
127
|
+
logger.debug(`🔗 [Protocol] Searching for protocol URLs in args: ${JSON.stringify(args)}`);
|
128
|
+
logger.debug(`🔗 [Protocol] Looking for prefix: ${protocolPrefix}`);
|
129
|
+
|
130
|
+
for (const arg of args) {
|
131
|
+
if (arg.startsWith(protocolPrefix)) {
|
132
|
+
logger.debug(`🔗 [Protocol] Found protocol URL in args: ${arg}`);
|
133
|
+
return arg;
|
134
|
+
}
|
135
|
+
}
|
136
|
+
logger.debug(`🔗 [Protocol] No protocol URL found in args`);
|
137
|
+
return null;
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Handle protocol URL - either immediately or store for later processing
|
142
|
+
*/
|
143
|
+
private handleProtocolUrl(url: string): void {
|
144
|
+
try {
|
145
|
+
logger.debug(`🔗 [Protocol] handleProtocolUrl called with: ${url}`);
|
146
|
+
logger.debug(`🔗 [Protocol] App ready state: ${app.isReady()}`);
|
147
|
+
logger.debug(`🔗 [Protocol] Current pending URLs count: ${this.pendingUrls.length}`);
|
148
|
+
|
149
|
+
if (!app.isReady()) {
|
150
|
+
// App not ready yet, store for later processing
|
151
|
+
logger.debug('🔗 [Protocol] App not ready, storing protocol URL for later processing');
|
152
|
+
this.pendingUrls.push(url);
|
153
|
+
logger.debug(`🔗 [Protocol] Pending URLs after push: ${this.pendingUrls.length}`);
|
154
|
+
return;
|
155
|
+
}
|
156
|
+
|
157
|
+
// App is ready, process immediately
|
158
|
+
logger.debug('🔗 [Protocol] App is ready, processing URL immediately');
|
159
|
+
this.processProtocolUrl(url);
|
160
|
+
} catch (error) {
|
161
|
+
logger.error('🔗 [Protocol] Failed to handle protocol URL:', error);
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
/**
|
166
|
+
* Process protocol URL by showing main window and sending to renderer
|
167
|
+
*/
|
168
|
+
private async processProtocolUrl(url: string): Promise<void> {
|
169
|
+
try {
|
170
|
+
logger.debug(`🔗 [Protocol] processProtocolUrl called with: ${url}`);
|
171
|
+
|
172
|
+
// Basic URL validation - just check if it's our protocol
|
173
|
+
if (!url.startsWith(`${this.protocolScheme}://`)) {
|
174
|
+
logger.warn(`🔗 [Protocol] Invalid protocol scheme in URL: ${url}`);
|
175
|
+
return;
|
176
|
+
}
|
177
|
+
|
178
|
+
// Show main window
|
179
|
+
logger.debug('🔗 [Protocol] Showing main window...');
|
180
|
+
this.app.browserManager.showMainWindow();
|
181
|
+
|
182
|
+
// Parse protocol URL to extract urlType and action
|
183
|
+
const parsed = parseProtocolUrl(url);
|
184
|
+
|
185
|
+
if (!parsed) {
|
186
|
+
logger.warn(`🔗 [Protocol] Failed to parse protocol URL: ${url}`);
|
187
|
+
return;
|
188
|
+
}
|
189
|
+
|
190
|
+
logger.debug(
|
191
|
+
`🔗 [Protocol] Parsed URL - type: ${parsed.urlType}, action: ${parsed.action}, data: %s`,
|
192
|
+
parsed.params,
|
193
|
+
);
|
194
|
+
|
195
|
+
// Dispatch to registered protocol handlers via App with parsed data
|
196
|
+
logger.debug('🔗 [Protocol] Dispatching to protocol handlers...');
|
197
|
+
const handled = await this.app.handleProtocolRequest(
|
198
|
+
parsed.urlType,
|
199
|
+
parsed.action,
|
200
|
+
parsed.params,
|
201
|
+
);
|
202
|
+
|
203
|
+
if (handled) {
|
204
|
+
logger.debug('🔗 [Protocol] Protocol URL processed successfully by handler');
|
205
|
+
} else {
|
206
|
+
logger.warn(
|
207
|
+
`🔗 [Protocol] No handler found for protocol: ${parsed.urlType}:${parsed.action}`,
|
208
|
+
);
|
209
|
+
}
|
210
|
+
} catch (error) {
|
211
|
+
logger.error('🔗 [Protocol] Failed to process protocol URL:', error);
|
212
|
+
logger.error('🔗 [Protocol] Error details:', error);
|
213
|
+
}
|
214
|
+
}
|
215
|
+
|
216
|
+
/**
|
217
|
+
* Process any pending protocol URLs after app is ready
|
218
|
+
*/
|
219
|
+
public async processPendingUrls(): Promise<void> {
|
220
|
+
logger.debug(`🔗 [Protocol] processPendingUrls called`);
|
221
|
+
logger.debug(`🔗 [Protocol] Pending URLs count: ${this.pendingUrls.length}`);
|
222
|
+
|
223
|
+
if (this.pendingUrls.length === 0) {
|
224
|
+
logger.debug(`🔗 [Protocol] No pending URLs to process`);
|
225
|
+
return;
|
226
|
+
}
|
227
|
+
|
228
|
+
logger.debug(
|
229
|
+
`🔗 [Protocol] Processing ${this.pendingUrls.length} pending protocol URLs:`,
|
230
|
+
this.pendingUrls,
|
231
|
+
);
|
232
|
+
|
233
|
+
for (const url of this.pendingUrls) {
|
234
|
+
logger.debug(`🔗 [Protocol] Processing pending URL: ${url}`);
|
235
|
+
await this.processProtocolUrl(url);
|
236
|
+
}
|
237
|
+
|
238
|
+
// Clear pending URLs
|
239
|
+
this.pendingUrls = [];
|
240
|
+
logger.debug(`🔗 [Protocol] All pending URLs processed and cleared`);
|
241
|
+
}
|
242
|
+
|
243
|
+
/**
|
244
|
+
* Get current protocol scheme
|
245
|
+
*/
|
246
|
+
public getScheme(): string {
|
247
|
+
return this.protocolScheme;
|
248
|
+
}
|
249
|
+
|
250
|
+
/**
|
251
|
+
* Check if protocol is registered
|
252
|
+
*/
|
253
|
+
public isRegistered(): boolean {
|
254
|
+
return app.isDefaultProtocolClient(this.protocolScheme);
|
255
|
+
}
|
256
|
+
}
|