@lobehub/chat 1.99.6 → 1.100.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 (108) hide show
  1. package/.cursor/rules/testing-guide/testing-guide.mdc +173 -0
  2. package/.github/workflows/desktop-pr-build.yml +3 -3
  3. package/.github/workflows/release-desktop-beta.yml +3 -3
  4. package/CHANGELOG.md +50 -0
  5. package/apps/desktop/package.json +5 -2
  6. package/apps/desktop/src/main/controllers/AuthCtr.ts +310 -111
  7. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +1 -1
  8. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +50 -3
  9. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +188 -23
  10. package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +37 -18
  11. package/apps/desktop/src/main/types/store.ts +1 -0
  12. package/apps/desktop/src/preload/electronApi.ts +2 -1
  13. package/apps/desktop/src/preload/streamer.ts +58 -0
  14. package/changelog/v1.json +18 -0
  15. package/docs/development/database-schema.dbml +9 -0
  16. package/docs/self-hosting/environment-variables/model-provider.mdx +25 -0
  17. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +25 -0
  18. package/docs/self-hosting/faq/vercel-ai-image-timeout.mdx +65 -0
  19. package/docs/self-hosting/faq/vercel-ai-image-timeout.zh-CN.mdx +63 -0
  20. package/docs/usage/providers/fal.mdx +6 -6
  21. package/docs/usage/providers/fal.zh-CN.mdx +6 -6
  22. package/locales/ar/electron.json +3 -0
  23. package/locales/ar/oauth.json +8 -4
  24. package/locales/bg-BG/electron.json +3 -0
  25. package/locales/bg-BG/oauth.json +8 -4
  26. package/locales/de-DE/electron.json +3 -0
  27. package/locales/de-DE/oauth.json +9 -5
  28. package/locales/en-US/electron.json +3 -0
  29. package/locales/en-US/oauth.json +8 -4
  30. package/locales/es-ES/electron.json +3 -0
  31. package/locales/es-ES/oauth.json +9 -5
  32. package/locales/fa-IR/electron.json +3 -0
  33. package/locales/fa-IR/oauth.json +8 -4
  34. package/locales/fr-FR/electron.json +3 -0
  35. package/locales/fr-FR/oauth.json +8 -4
  36. package/locales/it-IT/electron.json +3 -0
  37. package/locales/it-IT/oauth.json +9 -5
  38. package/locales/ja-JP/electron.json +3 -0
  39. package/locales/ja-JP/oauth.json +8 -4
  40. package/locales/ko-KR/electron.json +3 -0
  41. package/locales/ko-KR/oauth.json +8 -4
  42. package/locales/nl-NL/electron.json +3 -0
  43. package/locales/nl-NL/oauth.json +9 -5
  44. package/locales/pl-PL/electron.json +3 -0
  45. package/locales/pl-PL/oauth.json +8 -4
  46. package/locales/pt-BR/electron.json +3 -0
  47. package/locales/pt-BR/oauth.json +8 -4
  48. package/locales/ru-RU/electron.json +3 -0
  49. package/locales/ru-RU/oauth.json +8 -4
  50. package/locales/tr-TR/electron.json +3 -0
  51. package/locales/tr-TR/oauth.json +8 -4
  52. package/locales/vi-VN/electron.json +3 -0
  53. package/locales/vi-VN/oauth.json +9 -5
  54. package/locales/zh-CN/electron.json +3 -0
  55. package/locales/zh-CN/oauth.json +8 -4
  56. package/locales/zh-TW/electron.json +3 -0
  57. package/locales/zh-TW/oauth.json +8 -4
  58. package/package.json +3 -3
  59. package/packages/electron-client-ipc/src/dispatch.ts +14 -2
  60. package/packages/electron-client-ipc/src/index.ts +1 -0
  61. package/packages/electron-client-ipc/src/streamInvoke.ts +62 -0
  62. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +5 -0
  63. package/packages/electron-client-ipc/src/utils/headers.ts +27 -0
  64. package/packages/electron-client-ipc/src/utils/request.ts +28 -0
  65. package/src/app/(backend)/oidc/callback/desktop/route.ts +58 -0
  66. package/src/app/(backend)/oidc/handoff/route.ts +46 -0
  67. package/src/app/[variants]/oauth/callback/error/page.tsx +55 -0
  68. package/src/app/[variants]/oauth/callback/layout.tsx +12 -0
  69. package/src/app/[variants]/oauth/callback/loading.tsx +3 -0
  70. package/src/app/[variants]/oauth/{consent/[uid] → callback}/success/page.tsx +10 -1
  71. package/src/app/[variants]/oauth/consent/[uid]/Consent.tsx +7 -1
  72. package/src/database/client/migrations.json +8 -0
  73. package/src/database/migrations/0028_oauth_handoffs.sql +8 -0
  74. package/src/database/migrations/meta/0028_snapshot.json +6055 -0
  75. package/src/database/migrations/meta/_journal.json +7 -0
  76. package/src/database/models/oauthHandoff.ts +94 -0
  77. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  78. package/src/database/schemas/oidc.ts +46 -0
  79. package/src/features/ElectronTitlebar/Connection/Waiting.tsx +59 -115
  80. package/src/features/ElectronTitlebar/Connection/WaitingAnim.tsx +114 -0
  81. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +1 -1
  82. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +2 -1
  83. package/src/libs/oidc-provider/config.ts +16 -17
  84. package/src/libs/oidc-provider/jwt.ts +135 -0
  85. package/src/libs/oidc-provider/provider.ts +22 -38
  86. package/src/libs/trpc/client/async.ts +1 -2
  87. package/src/libs/trpc/client/edge.ts +1 -2
  88. package/src/libs/trpc/client/lambda.ts +1 -1
  89. package/src/libs/trpc/client/tools.ts +1 -2
  90. package/src/libs/trpc/lambda/context.ts +9 -16
  91. package/src/locales/default/electron.ts +3 -0
  92. package/src/locales/default/oauth.ts +8 -4
  93. package/src/middleware.ts +10 -4
  94. package/src/server/globalConfig/genServerAiProviderConfig.test.ts +235 -0
  95. package/src/server/globalConfig/genServerAiProviderConfig.ts +9 -10
  96. package/src/server/services/oidc/index.ts +0 -71
  97. package/src/services/chat.ts +5 -1
  98. package/src/services/electron/remoteServer.ts +0 -7
  99. package/src/store/aiInfra/slices/aiProvider/action.ts +2 -1
  100. package/src/{libs/trpc/client/helpers → utils/electron}/desktopRemoteRPCFetch.ts +22 -7
  101. package/src/utils/getFallbackModelProperty.test.ts +193 -0
  102. package/src/utils/getFallbackModelProperty.ts +36 -0
  103. package/src/utils/parseModels.test.ts +150 -48
  104. package/src/utils/parseModels.ts +26 -11
  105. package/src/utils/server/auth.ts +22 -0
  106. package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +0 -36
  107. package/src/app/[variants]/oauth/handoff/Client.tsx +0 -98
  108. package/src/app/[variants]/oauth/handoff/page.tsx +0 -13
@@ -28,10 +28,14 @@
28
28
  },
29
29
  "title": "Autoryzacja {{clientName}}"
30
30
  },
31
- "failed": {
31
+ "error": {
32
32
  "backToHome": "Powrót do strony głównej",
33
- "subTitle": "Odmówiłeś autoryzacji aplikacji do dostępu do Twojego konta LobeChat",
34
- "title": "Autoryzacja odrzucona"
33
+ "desc": "Błąd autoryzacji OAuth, powód: {{reason}}",
34
+ "reason": {
35
+ "internal_error": "Błąd serwera",
36
+ "invalid_request": "Nieprawidłowe parametry żądania"
37
+ },
38
+ "title": "Autoryzacja nie powiodła się"
35
39
  },
36
40
  "handoff": {
37
41
  "desc": {
@@ -50,7 +54,7 @@
50
54
  "userWelcome": "Witaj z powrotem, "
51
55
  },
52
56
  "success": {
53
- "subTitle": "Pomyślnie autoryzowałeś aplikację do dostępu do Twojego konta LobeChat, możesz zamknąć tę stronę",
57
+ "subTitle": "Pomyślnie autoryzowano aplikację do dostępu do Twojego konta, możesz teraz zamknąć tę stronę",
54
58
  "title": "Autoryzacja zakończona sukcesem"
55
59
  }
56
60
  }
@@ -101,7 +101,10 @@
101
101
  "waitingOAuth": {
102
102
  "cancel": "Cancelar",
103
103
  "description": "A página de autorização foi aberta no navegador, por favor, complete a autorização no navegador",
104
+ "error": "Falha na autorização: {{error}}",
105
+ "errorTitle": "Falha na conexão de autorização",
104
106
  "helpText": "Se o navegador não abrir automaticamente, clique em cancelar e tente novamente",
107
+ "retry": "Tentar novamente",
105
108
  "title": "Aguardando conexão de autorização"
106
109
  }
107
110
  }
@@ -28,10 +28,14 @@
28
28
  },
29
29
  "title": "Autorizar {{clientName}}"
30
30
  },
31
- "failed": {
31
+ "error": {
32
32
  "backToHome": "Voltar para a página inicial",
33
- "subTitle": "Você negou a autorização do aplicativo para acessar sua conta LobeChat",
34
- "title": "Autorização negada"
33
+ "desc": "Falha na autorização OAuth, motivo da falha: {{reason}}",
34
+ "reason": {
35
+ "internal_error": "Erro no servidor",
36
+ "invalid_request": "Parâmetros de solicitação inválidos"
37
+ },
38
+ "title": "Falha na autorização"
35
39
  },
36
40
  "handoff": {
37
41
  "desc": {
@@ -50,7 +54,7 @@
50
54
  "userWelcome": "Bem-vindo de volta, "
51
55
  },
52
56
  "success": {
53
- "subTitle": "Você autorizou com sucesso o aplicativo a acessar sua conta LobeChat, pode fechar esta página agora",
57
+ "subTitle": "Você autorizou com sucesso o aplicativo a acessar sua conta, pode fechar esta página.",
54
58
  "title": "Autorização bem-sucedida"
55
59
  }
56
60
  }
@@ -101,7 +101,10 @@
101
101
  "waitingOAuth": {
102
102
  "cancel": "Отмена",
103
103
  "description": "Браузер открыл страницу авторизации, пожалуйста, завершите авторизацию в браузере",
104
+ "error": "Ошибка авторизации: {{error}}",
105
+ "errorTitle": "Не удалось подключиться для авторизации",
104
106
  "helpText": "Если браузер не открылся автоматически, нажмите отмену и попробуйте снова",
107
+ "retry": "Повторить попытку",
105
108
  "title": "Ожидание авторизации"
106
109
  }
107
110
  }
@@ -28,10 +28,14 @@
28
28
  },
29
29
  "title": "Авторизация {{clientName}}"
30
30
  },
31
- "failed": {
31
+ "error": {
32
32
  "backToHome": "Вернуться на главную",
33
- "subTitle": "Вы отказали в доступе приложению к вашей учетной записи LobeChat",
34
- "title": "Авторизация отклонена"
33
+ "desc": "Ошибка авторизации OAuth, причина: {{reason}}",
34
+ "reason": {
35
+ "internal_error": "Ошибка сервера",
36
+ "invalid_request": "Недопустимые параметры запроса"
37
+ },
38
+ "title": "Ошибка авторизации"
35
39
  },
36
40
  "handoff": {
37
41
  "desc": {
@@ -50,7 +54,7 @@
50
54
  "userWelcome": "С возвращением, "
51
55
  },
52
56
  "success": {
53
- "subTitle": "Вы успешно разрешили приложению доступ к вашей учетной записи LobeChat, теперь вы можете закрыть эту страницу",
57
+ "subTitle": "Вы успешно авторизовали приложение для доступа к вашему аккаунту, теперь можно закрыть эту страницу",
54
58
  "title": "Авторизация успешна"
55
59
  }
56
60
  }
@@ -101,7 +101,10 @@
101
101
  "waitingOAuth": {
102
102
  "cancel": "İptal",
103
103
  "description": "Tarayıcıda yetkilendirme sayfası açıldı, lütfen tarayıcıda yetkilendirmeyi tamamlayın",
104
+ "error": "Yetkilendirme başarısız: {{error}}",
105
+ "errorTitle": "Yetkilendirme bağlantısı başarısız",
104
106
  "helpText": "Tarayıcı otomatik olarak açılmadıysa, lütfen iptal'e tıklayıp yeniden deneyin",
107
+ "retry": "Tekrar dene",
105
108
  "title": "Yetkilendirme bağlantısını bekliyor"
106
109
  }
107
110
  }
@@ -28,10 +28,14 @@
28
28
  },
29
29
  "title": "İzin Ver {{clientName}}"
30
30
  },
31
- "failed": {
31
+ "error": {
32
32
  "backToHome": "Ana sayfaya dön",
33
- "subTitle": "Uygulamaya LobeChat hesabınıza erişim izni vermeyi reddettiniz",
34
- "title": "Yetkilendirme reddedildi"
33
+ "desc": "OAuth yetkilendirmesi başarısız oldu, hata nedeni: {{reason}}",
34
+ "reason": {
35
+ "internal_error": "Sunucu hatası",
36
+ "invalid_request": "Geçersiz istek parametresi"
37
+ },
38
+ "title": "Yetkilendirme başarısız"
35
39
  },
36
40
  "handoff": {
37
41
  "desc": {
@@ -50,7 +54,7 @@
50
54
  "userWelcome": "Hoş geldiniz, "
51
55
  },
52
56
  "success": {
53
- "subTitle": "Uygulamaya LobeChat hesabınıza erişim izni verdiniz, bu sayfayı kapatabilirsiniz",
57
+ "subTitle": "Uygulamanın hesabınıza erişim izni başarıyla verildi, bu sayfayı kapatabilirsiniz",
54
58
  "title": "Yetkilendirme başarılı"
55
59
  }
56
60
  }
@@ -101,7 +101,10 @@
101
101
  "waitingOAuth": {
102
102
  "cancel": "Hủy",
103
103
  "description": "Trình duyệt đã mở trang ủy quyền, vui lòng hoàn tất ủy quyền trong trình duyệt",
104
+ "error": "Ủy quyền thất bại: {{error}}",
105
+ "errorTitle": "Kết nối ủy quyền thất bại",
104
106
  "helpText": "Nếu trình duyệt không tự động mở, vui lòng nhấp vào hủy và thử lại",
107
+ "retry": "Thử lại",
105
108
  "title": "Đang chờ kết nối ủy quyền"
106
109
  }
107
110
  }
@@ -28,10 +28,14 @@
28
28
  },
29
29
  "title": "Ủy quyền cho {{clientName}}"
30
30
  },
31
- "failed": {
32
- "backToHome": "Quay lại trang chính",
33
- "subTitle": "Bạn đã từ chối cho phép ứng dụng truy cập vào tài khoản LobeChat của bạn",
34
- "title": "Ủy quyền bị từ chối"
31
+ "error": {
32
+ "backToHome": "Quay lại trang chủ",
33
+ "desc": "Ủy quyền OAuth thất bại, do thất bại: {{reason}}",
34
+ "reason": {
35
+ "internal_error": "Lỗi máy chủ",
36
+ "invalid_request": "Tham số yêu cầu không hợp lệ"
37
+ },
38
+ "title": "Ủy quyền thất bại"
35
39
  },
36
40
  "handoff": {
37
41
  "desc": {
@@ -50,7 +54,7 @@
50
54
  "userWelcome": "Chào mừng bạn trở lại, "
51
55
  },
52
56
  "success": {
53
- "subTitle": "Bạn đã thành công trong việc cho phép ứng dụng truy cập vào tài khoản LobeChat của bạn, bạn có thể đóng trang này.",
57
+ "subTitle": "Bạn đã cấp quyền thành công cho ứng dụng truy cập tài khoản của mình, có thể đóng trang này.",
54
58
  "title": "Ủy quyền thành công"
55
59
  }
56
60
  }
@@ -101,7 +101,10 @@
101
101
  "waitingOAuth": {
102
102
  "cancel": "取消",
103
103
  "description": "浏览器已打开授权页面,请在浏览器中完成授权",
104
+ "error": "授权失败: {{error}}",
105
+ "errorTitle": "授权连接失败",
104
106
  "helpText": "如果浏览器没有自动打开,请点击取消后重新尝试",
107
+ "retry": "重试",
105
108
  "title": "等待授权连接"
106
109
  }
107
110
  }
@@ -28,10 +28,14 @@
28
28
  },
29
29
  "title": "授权 {{clientName}}"
30
30
  },
31
- "failed": {
31
+ "error": {
32
32
  "backToHome": "返回首页",
33
- "subTitle": "您已拒绝授权应用访问您的 LobeChat 账户",
34
- "title": "授权被拒绝"
33
+ "desc": "OAuth 授权失败,失败原因:{{reason}}",
34
+ "reason": {
35
+ "internal_error": "服务端错误",
36
+ "invalid_request": "无效的请求参数"
37
+ },
38
+ "title": "授权失败"
35
39
  },
36
40
  "handoff": {
37
41
  "desc": {
@@ -50,7 +54,7 @@
50
54
  "userWelcome": "欢迎回来,"
51
55
  },
52
56
  "success": {
53
- "subTitle": "您已成功授权应用访问您的 LobeChat 账户,可以关闭该页面了",
57
+ "subTitle": "您已成功授权应用访问您的账户,可以关闭该页面了",
54
58
  "title": "授权成功"
55
59
  }
56
60
  }
@@ -101,7 +101,10 @@
101
101
  "waitingOAuth": {
102
102
  "cancel": "取消",
103
103
  "description": "瀏覽器已打開授權頁面,請在瀏覽器中完成授權",
104
+ "error": "授權失敗: {{error}}",
105
+ "errorTitle": "授權連接失敗",
104
106
  "helpText": "如果瀏覽器沒有自動打開,請點擊取消後重新嘗試",
107
+ "retry": "重試",
105
108
  "title": "等待授權連接"
106
109
  }
107
110
  }
@@ -28,10 +28,14 @@
28
28
  },
29
29
  "title": "授權 {{clientName}}"
30
30
  },
31
- "failed": {
31
+ "error": {
32
32
  "backToHome": "返回首頁",
33
- "subTitle": "您已拒絕授權應用訪問您的 LobeChat 帳戶",
34
- "title": "授權被拒絕"
33
+ "desc": "OAuth 授權失敗,失敗原因:{{reason}}",
34
+ "reason": {
35
+ "internal_error": "服務端錯誤",
36
+ "invalid_request": "無效的請求參數"
37
+ },
38
+ "title": "授權失敗"
35
39
  },
36
40
  "handoff": {
37
41
  "desc": {
@@ -50,7 +54,7 @@
50
54
  "userWelcome": "歡迎回來,"
51
55
  },
52
56
  "success": {
53
- "subTitle": "您已成功授權應用訪問您的 LobeChat 帳戶,可以關閉該頁面了",
57
+ "subTitle": "您已成功授權應用訪問您的帳戶,可以關閉該頁面了",
54
58
  "title": "授權成功"
55
59
  }
56
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.99.6",
3
+ "version": "1.100.1",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -218,7 +218,7 @@
218
218
  "numeral": "^2.0.6",
219
219
  "nuqs": "^2.4.3",
220
220
  "officeparser": "5.1.1",
221
- "oidc-provider": "^8.8.1",
221
+ "oidc-provider": "^9.2.0",
222
222
  "ollama": "^0.5.16",
223
223
  "openai": "^4.104.0",
224
224
  "openapi-fetch": "^0.9.8",
@@ -306,7 +306,7 @@
306
306
  "@types/lodash-es": "^4.17.12",
307
307
  "@types/node": "^22.15.29",
308
308
  "@types/numeral": "^2.0.5",
309
- "@types/oidc-provider": "^8.8.1",
309
+ "@types/oidc-provider": "^9.1.1",
310
310
  "@types/pg": "^8.15.4",
311
311
  "@types/react": "^19.1.6",
312
312
  "@types/react-dom": "^19.1.5",
@@ -1,7 +1,19 @@
1
- import { DispatchInvoke } from './types';
1
+ import { DispatchInvoke, type ProxyTRPCRequestParams } from './types';
2
+
3
+ interface StreamerCallbacks {
4
+ onData: (chunk: Uint8Array) => void;
5
+ onEnd: () => void;
6
+ onError: (error: Error) => void;
7
+ onResponse: (response: {
8
+ headers: Record<string, string>;
9
+ status: number;
10
+ statusText: string;
11
+ }) => void;
12
+ }
2
13
 
3
14
  interface IElectronAPI {
4
15
  invoke: DispatchInvoke;
16
+ onStreamInvoke: (params: ProxyTRPCRequestParams, callbacks: StreamerCallbacks) => () => void;
5
17
  }
6
18
 
7
19
  declare global {
@@ -11,7 +23,7 @@ declare global {
11
23
  }
12
24
 
13
25
  /**
14
- * client 端请求 sketch 端 event 数据的方法
26
+ * client 端请求 main 端 event 数据的方法
15
27
  */
16
28
  export const dispatch: DispatchInvoke = async (event, ...data) => {
17
29
  if (!window.electronAPI || !window.electronAPI.invoke)
@@ -1,4 +1,5 @@
1
1
  export * from './dispatch';
2
2
  export * from './events';
3
+ export * from './streamInvoke';
3
4
  export * from './types';
4
5
  export * from './useWatchBroadcast';
@@ -0,0 +1,62 @@
1
+ import { ProxyTRPCRequestParams } from './types';
2
+ import { headersToRecord } from './utils/headers';
3
+ import { getRequestBody } from './utils/request';
4
+
5
+ // eslint-disable-next-line no-undef
6
+ export const streamInvoke = async (input: RequestInfo | URL, init?: RequestInit) => {
7
+ const url = input.toString();
8
+ const parsedUrl = new URL(url, window.location.origin);
9
+ const urlPath = parsedUrl.pathname + parsedUrl.search;
10
+ const method = init?.method?.toUpperCase() || 'GET';
11
+ const headers = headersToRecord(init?.headers);
12
+ const body = await getRequestBody(init?.body);
13
+
14
+ const params: ProxyTRPCRequestParams = {
15
+ body,
16
+ headers,
17
+ method,
18
+ urlPath,
19
+ };
20
+
21
+ return new Promise<Response>((resolve, reject) => {
22
+ let streamController: ReadableStreamDefaultController<any>;
23
+ let responseResolved = false;
24
+
25
+ const stream = new ReadableStream({
26
+ cancel() {
27
+ // This will be called if the consumer of the stream calls .cancel()
28
+ // We should clean up the IPC listeners
29
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
30
+ cleanup?.();
31
+ },
32
+ start(controller) {
33
+ streamController = controller;
34
+ },
35
+ });
36
+
37
+ const cleanup = window.electronAPI.onStreamInvoke(params, {
38
+ onData: (chunk) => {
39
+ if (streamController) streamController.enqueue(chunk);
40
+ },
41
+ onEnd: () => {
42
+ if (streamController) streamController.close();
43
+ },
44
+ onError: (error) => {
45
+ console.error('[streamInvoke] Error during IPC stream proxy call:', error);
46
+ if (!responseResolved) {
47
+ responseResolved = true;
48
+ reject(error); // Reject the main promise if response not yet sent
49
+ } else if (streamController) {
50
+ streamController.error(error); // Otherwise, propagate error through the stream
51
+ }
52
+ },
53
+ onResponse: (meta) => {
54
+ if (responseResolved) return;
55
+ responseResolved = true;
56
+
57
+ const response = new Response(stream, meta);
58
+ resolve(response);
59
+ },
60
+ });
61
+ });
62
+ };
@@ -9,6 +9,11 @@ export type ProxyTRPCRequestParams = {
9
9
  urlPath: string;
10
10
  };
11
11
 
12
+ export interface ProxyTRPCStreamRequestParams extends Omit<ProxyTRPCRequestParams, 'body'> {
13
+ body?: ArrayBuffer;
14
+ requestId: string;
15
+ }
16
+
12
17
  export interface ProxyTRPCRequestResult {
13
18
  /** Response body (likely as ArrayBuffer or string) */
14
19
  body: ArrayBuffer | string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 将 HeadersInit 转换为 Record<string, string>
3
+ * @param headersInit - Headers 初始化对象
4
+ * @returns 转换后的记录对象
5
+ */
6
+ // eslint-disable-next-line no-undef
7
+ export const headersToRecord = (headersInit?: HeadersInit): Record<string, string> => {
8
+ const record: Record<string, string> = {};
9
+ if (!headersInit) {
10
+ return record;
11
+ }
12
+ if (headersInit instanceof Headers) {
13
+ headersInit.forEach((value, key) => {
14
+ record[key] = value;
15
+ });
16
+ } else if (Array.isArray(headersInit)) {
17
+ headersInit.forEach(([key, value]) => {
18
+ record[key] = value;
19
+ });
20
+ } else {
21
+ Object.assign(record, headersInit);
22
+ }
23
+ delete record['host'];
24
+ delete record['connection'];
25
+ delete record['content-length'];
26
+ return record;
27
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 从请求体中获取数据
3
+ * @param body - 请求体
4
+ * @returns 转换后的请求体数据
5
+ */
6
+ export const getRequestBody = async (
7
+ // eslint-disable-next-line no-undef
8
+ body?: BodyInit | null,
9
+ ): Promise<string | ArrayBuffer | undefined> => {
10
+ if (!body) {
11
+ return undefined;
12
+ }
13
+ if (typeof body === 'string') {
14
+ return body;
15
+ }
16
+ if (body instanceof ArrayBuffer) {
17
+ return body;
18
+ }
19
+ if (ArrayBuffer.isView(body)) {
20
+ return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer;
21
+ }
22
+ if (body instanceof Blob) {
23
+ return await body.arrayBuffer();
24
+ }
25
+
26
+ console.warn('不支持的 IPC 代理请求体类型:', typeof body);
27
+ throw new Error('不支持的 IPC 代理请求体类型');
28
+ };
@@ -0,0 +1,58 @@
1
+ import debug from 'debug';
2
+ import { NextRequest, NextResponse, after } from 'next/server';
3
+
4
+ import { OAuthHandoffModel } from '@/database/models/oauthHandoff';
5
+ import { serverDB } from '@/database/server';
6
+
7
+ const log = debug('lobe-oidc:callback:desktop');
8
+
9
+ export const GET = async (req: NextRequest) => {
10
+ try {
11
+ const searchParams = req.nextUrl.searchParams;
12
+ const code = searchParams.get('code');
13
+ const state = searchParams.get('state'); // This `state` is the handoff ID
14
+
15
+ if (!code || !state || typeof code !== 'string' || typeof state !== 'string') {
16
+ log('Missing code or state in form data');
17
+ const errorUrl = req.nextUrl.clone();
18
+ errorUrl.pathname = '/oauth/callback/error';
19
+ errorUrl.searchParams.set('reason', 'invalid_request');
20
+ return NextResponse.redirect(errorUrl);
21
+ }
22
+
23
+ log('Received OIDC callback. state(handoffId): %s', state);
24
+
25
+ // The 'client' is 'desktop' because this redirect_uri is for the desktop client.
26
+ const client = 'desktop';
27
+ const payload = { code, state };
28
+ const id = state;
29
+
30
+ const authHandoffModel = new OAuthHandoffModel(serverDB);
31
+ await authHandoffModel.create({ client, id, payload });
32
+ log('Handoff record created successfully for id: %s', id);
33
+
34
+ // Redirect to a generic success page. The desktop app will poll for the result.
35
+ const successUrl = req.nextUrl.clone();
36
+ successUrl.pathname = '/oauth/callback/success';
37
+
38
+ // cleanup expired
39
+ after(async () => {
40
+ const cleanedCount = await authHandoffModel.cleanupExpired();
41
+
42
+ log('Cleaned up %d expired handoff records', cleanedCount);
43
+ });
44
+
45
+ return NextResponse.redirect(successUrl);
46
+ } catch (error) {
47
+ log('Error in OIDC callback: %O', error);
48
+ const errorUrl = req.nextUrl.clone();
49
+ errorUrl.pathname = '/oauth/callback/error';
50
+ errorUrl.searchParams.set('reason', 'internal_error');
51
+
52
+ if (error instanceof Error) {
53
+ errorUrl.searchParams.set('errorMessage', error.message);
54
+ }
55
+
56
+ return NextResponse.redirect(errorUrl);
57
+ }
58
+ };
@@ -0,0 +1,46 @@
1
+ import debug from 'debug';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+
4
+ import { OAuthHandoffModel } from '@/database/models/oauthHandoff';
5
+ import { serverDB } from '@/database/server';
6
+
7
+ const log = debug('lobe-oidc:handoff');
8
+
9
+ /**
10
+ * GET /oidc/handoff?id=xxx&client=xxx
11
+ * 轮询获取并消费认证凭证
12
+ */
13
+ export async function GET(request: NextRequest) {
14
+ log('Received GET request for /oidc/handoff');
15
+
16
+ try {
17
+ const { searchParams } = new URL(request.url);
18
+ const id = searchParams.get('id');
19
+ const client = searchParams.get('client');
20
+
21
+ if (!id || !client) {
22
+ return NextResponse.json(
23
+ { error: 'Missing required parameters: id and client' },
24
+ { status: 400 },
25
+ );
26
+ }
27
+
28
+ log('Fetching handoff record - id=%s, client=%s', id, client);
29
+
30
+ const authHandoffModel = new OAuthHandoffModel(serverDB);
31
+ const result = await authHandoffModel.fetchAndConsume(id, client);
32
+
33
+ if (!result) {
34
+ log('Handoff record not found or expired - id=%s', id);
35
+ return NextResponse.json({ error: 'Handoff record not found or expired' }, { status: 404 });
36
+ }
37
+
38
+ log('Handoff record found and consumed - id=%s', id);
39
+
40
+ return NextResponse.json({ data: result, success: true });
41
+ } catch (error) {
42
+ log('Error fetching handoff record: %O', error);
43
+
44
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
45
+ }
46
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { Button, Highlighter, Icon } from '@lobehub/ui';
4
+ import { Card, Result } from 'antd';
5
+ import { ShieldX } from 'lucide-react';
6
+ import Link from 'next/link';
7
+ import { parseAsString, useQueryState } from 'nuqs';
8
+ import React, { memo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { Center, Flexbox } from 'react-layout-kit';
11
+
12
+ const FailedPage = memo(() => {
13
+ const { t } = useTranslation('oauth');
14
+ const [reason] = useQueryState('reason');
15
+ const [errorMessage] = useQueryState<string>('errorMessage', parseAsString);
16
+
17
+ return (
18
+ <Center height="100vh">
19
+ <Card
20
+ style={{
21
+ alignItems: 'center',
22
+ display: 'flex',
23
+ justifyContent: 'center',
24
+ minHeight: 280,
25
+ minWidth: 500,
26
+ width: '100%',
27
+ }}
28
+ >
29
+ <Result
30
+ extra={
31
+ <Link href="/">
32
+ <Button type="primary">{t('error.backToHome')}</Button>
33
+ </Link>
34
+ }
35
+ icon={<Icon icon={ShieldX} />}
36
+ status="error"
37
+ subTitle={
38
+ <Flexbox gap={8}>
39
+ {t('error.desc', {
40
+ reason: t(`error.reason.${reason}` as any, { defaultValue: reason }),
41
+ })}
42
+
43
+ {!!errorMessage && <Highlighter language={'log'}>{errorMessage}</Highlighter>}
44
+ </Flexbox>
45
+ }
46
+ title={t('error.title')}
47
+ />
48
+ </Card>
49
+ </Center>
50
+ );
51
+ });
52
+
53
+ FailedPage.displayName = 'FailedPage';
54
+
55
+ export default FailedPage;
@@ -0,0 +1,12 @@
1
+ import { notFound } from 'next/navigation';
2
+ import { PropsWithChildren } from 'react';
3
+
4
+ import { oidcEnv } from '@/envs/oidc';
5
+
6
+ const Layout = ({ children }: PropsWithChildren) => {
7
+ if (!oidcEnv.ENABLE_OIDC) return notFound();
8
+
9
+ return children;
10
+ };
11
+
12
+ export default Layout;
@@ -0,0 +1,3 @@
1
+ 'use client';
2
+
3
+ export { default } from '@/components/Loading/BrandTextLoading';