@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.
Files changed (57) hide show
  1. package/.cursor/rules/i18n.mdc +1 -2
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +1 -1
  4. package/README.zh-CN.md +1 -1
  5. package/apps/desktop/electron-builder.js +22 -0
  6. package/apps/desktop/src/main/controllers/McpInstallCtr.ts +153 -0
  7. package/apps/desktop/src/main/controllers/index.ts +19 -0
  8. package/apps/desktop/src/main/core/App.ts +46 -0
  9. package/apps/desktop/src/main/core/infrastructure/IoCContainer.ts +4 -0
  10. package/apps/desktop/src/main/core/infrastructure/ProtocolManager.ts +256 -0
  11. package/apps/desktop/src/main/types/protocol.ts +60 -0
  12. package/apps/desktop/src/main/utils/__tests__/protocol.test.ts +203 -0
  13. package/apps/desktop/src/main/utils/protocol.ts +210 -0
  14. package/changelog/v1.json +18 -0
  15. package/locales/ar/plugin.json +196 -136
  16. package/locales/bg-BG/plugin.json +204 -144
  17. package/locales/de-DE/plugin.json +176 -116
  18. package/locales/en-US/plugin.json +192 -132
  19. package/locales/es-ES/plugin.json +203 -143
  20. package/locales/fa-IR/plugin.json +155 -95
  21. package/locales/fr-FR/plugin.json +161 -101
  22. package/locales/it-IT/plugin.json +193 -133
  23. package/locales/ja-JP/plugin.json +195 -135
  24. package/locales/ko-KR/plugin.json +163 -103
  25. package/locales/nl-NL/plugin.json +211 -151
  26. package/locales/pl-PL/plugin.json +171 -111
  27. package/locales/pt-BR/plugin.json +180 -120
  28. package/locales/ru-RU/plugin.json +191 -131
  29. package/locales/tr-TR/plugin.json +187 -127
  30. package/locales/vi-VN/plugin.json +152 -92
  31. package/locales/zh-CN/plugin.json +60 -0
  32. package/locales/zh-TW/plugin.json +157 -97
  33. package/package.json +2 -1
  34. package/packages/electron-client-ipc/src/events/index.ts +5 -2
  35. package/packages/electron-client-ipc/src/events/protocol.ts +29 -0
  36. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  37. package/packages/electron-client-ipc/src/types/mcpInstall.ts +19 -0
  38. package/packages/types/src/plugins/mcp.ts +38 -1
  39. package/packages/types/src/plugins/protocol.ts +166 -0
  40. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +4 -1
  41. package/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/ActionButton/index.tsx +1 -2
  42. package/src/components/KeyValueEditor/index.tsx +4 -2
  43. package/src/features/ChatItem/index.tsx +25 -2
  44. package/src/features/MCP/MCPInstallProgress/index.tsx +1 -1
  45. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +30 -36
  46. package/src/features/PluginStore/McpList/List/Item.tsx +1 -1
  47. package/src/features/ProtocolUrlHandler/InstallPlugin/ConfigDisplay.tsx +211 -0
  48. package/src/features/ProtocolUrlHandler/InstallPlugin/CustomPluginInstallModal.tsx +228 -0
  49. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/Detail.tsx +44 -0
  50. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/index.tsx +105 -0
  51. package/src/features/ProtocolUrlHandler/InstallPlugin/index.tsx +55 -0
  52. package/src/features/ProtocolUrlHandler/InstallPlugin/types.ts +45 -0
  53. package/src/features/ProtocolUrlHandler/index.tsx +30 -0
  54. package/src/locales/default/plugin.ts +60 -0
  55. package/src/store/tool/slices/mcpStore/action.ts +127 -1
  56. package/src/store/tool/slices/mcpStore/initialState.ts +8 -13
  57. package/src/store/tool/slices/mcpStore/selectors.ts +13 -0
@@ -1,6 +1,5 @@
1
1
  ---
2
- description: i18n workflow and troubleshooting
3
- globs:
2
+ globs: *.tsx
4
3
  alwaysApply: false
5
4
  ---
6
5
  # LobeChat 国际化指南
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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
+ }