@lobehub/lobehub 2.0.0-next.20 → 2.0.0-next.21

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/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix oidc auth timeout issue on the desktop."
6
+ ]
7
+ },
8
+ "date": "2025-11-04",
9
+ "version": "2.0.0-next.21"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.20",
3
+ "version": "2.0.0-next.21",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent 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",
@@ -0,0 +1,139 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { generateApiKey, isApiKeyExpired, validateApiKeyFormat } from './apiKey';
4
+
5
+ describe('apiKey', () => {
6
+ describe('generateApiKey', () => {
7
+ it('should generate API key with correct format', () => {
8
+ const apiKey = generateApiKey();
9
+ expect(apiKey).toMatch(/^lb-[\da-z]{16}$/);
10
+ });
11
+
12
+ it('should generate API key with correct length', () => {
13
+ const apiKey = generateApiKey();
14
+ expect(apiKey).toHaveLength(19); // 'lb-' (3) + 16 characters
15
+ });
16
+
17
+ it('should generate unique API keys', () => {
18
+ const keys = new Set();
19
+ for (let i = 0; i < 100; i++) {
20
+ keys.add(generateApiKey());
21
+ }
22
+ // All 100 keys should be unique
23
+ expect(keys.size).toBe(100);
24
+ });
25
+
26
+ it('should start with lb- prefix', () => {
27
+ const apiKey = generateApiKey();
28
+ expect(apiKey.startsWith('lb-')).toBe(true);
29
+ });
30
+
31
+ it('should only contain lowercase alphanumeric characters after prefix', () => {
32
+ const apiKey = generateApiKey();
33
+ const randomPart = apiKey.slice(3); // Remove 'lb-' prefix
34
+ expect(randomPart).toMatch(/^[\da-z]+$/);
35
+ });
36
+ });
37
+
38
+ describe('isApiKeyExpired', () => {
39
+ it('should return false when expiresAt is null', () => {
40
+ expect(isApiKeyExpired(null)).toBe(false);
41
+ });
42
+
43
+ it('should return false when expiration date is in the future', () => {
44
+ const futureDate = new Date();
45
+ futureDate.setFullYear(futureDate.getFullYear() + 1); // 1 year from now
46
+ expect(isApiKeyExpired(futureDate)).toBe(false);
47
+ });
48
+
49
+ it('should return true when expiration date is in the past', () => {
50
+ const pastDate = new Date();
51
+ pastDate.setFullYear(pastDate.getFullYear() - 1); // 1 year ago
52
+ expect(isApiKeyExpired(pastDate)).toBe(true);
53
+ });
54
+
55
+ it('should return true when expiration date is exactly now or just passed', () => {
56
+ const now = new Date();
57
+ now.setSeconds(now.getSeconds() - 1); // 1 second ago
58
+ expect(isApiKeyExpired(now)).toBe(true);
59
+ });
60
+
61
+ it('should handle edge case of expiration date being very close to now', () => {
62
+ const almostNow = new Date();
63
+ almostNow.setMilliseconds(almostNow.getMilliseconds() - 1); // 1ms ago
64
+ expect(isApiKeyExpired(almostNow)).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('validateApiKeyFormat', () => {
69
+ it('should validate correct API key format', () => {
70
+ const validKey = 'lb-1234567890abcdef';
71
+ expect(validateApiKeyFormat(validKey)).toBe(true);
72
+ });
73
+
74
+ it('should accept keys with only numbers', () => {
75
+ const validKey = 'lb-1234567890123456';
76
+ expect(validateApiKeyFormat(validKey)).toBe(true);
77
+ });
78
+
79
+ it('should accept keys with only lowercase letters', () => {
80
+ const validKey = 'lb-abcdefabcdefabcd';
81
+ expect(validateApiKeyFormat(validKey)).toBe(true);
82
+ });
83
+
84
+ it('should accept keys with mixed alphanumeric characters', () => {
85
+ const validKey = 'lb-abc123def456789a';
86
+ expect(validateApiKeyFormat(validKey)).toBe(true);
87
+ });
88
+
89
+ it('should reject keys without lb- prefix', () => {
90
+ const invalidKey = '1234567890abcdef';
91
+ expect(validateApiKeyFormat(invalidKey)).toBe(false);
92
+ });
93
+
94
+ it('should reject keys with wrong prefix', () => {
95
+ const invalidKey = 'lc-1234567890abcdef';
96
+ expect(validateApiKeyFormat(invalidKey)).toBe(false);
97
+ });
98
+
99
+ it('should reject keys that are too short', () => {
100
+ const invalidKey = 'lb-123456789abcde';
101
+ expect(validateApiKeyFormat(invalidKey)).toBe(false);
102
+ });
103
+
104
+ it('should reject keys that are too long', () => {
105
+ const invalidKey = 'lb-1234567890abcdef0';
106
+ expect(validateApiKeyFormat(invalidKey)).toBe(false);
107
+ });
108
+
109
+ it('should reject keys with uppercase letters', () => {
110
+ const invalidKey = 'lb-1234567890ABCDEF';
111
+ expect(validateApiKeyFormat(invalidKey)).toBe(false);
112
+ });
113
+
114
+ it('should reject keys with special characters', () => {
115
+ const invalidKey = 'lb-1234567890abcd-f';
116
+ expect(validateApiKeyFormat(invalidKey)).toBe(false);
117
+ });
118
+
119
+ it('should reject empty string', () => {
120
+ expect(validateApiKeyFormat('')).toBe(false);
121
+ });
122
+
123
+ it('should reject keys with spaces', () => {
124
+ const invalidKey = 'lb-1234567890abcd f';
125
+ expect(validateApiKeyFormat(invalidKey)).toBe(false);
126
+ });
127
+
128
+ it('should validate generated keys', () => {
129
+ // Generate a key and validate it
130
+ const generatedKey = generateApiKey();
131
+ // Note: The validation pattern expects hex digits (0-9a-f), but generated keys use base36 (0-9a-z)
132
+ // This test will fail if there are non-hex characters (g-z) in the generated key
133
+ const hasOnlyHexChars = /^lb-[\da-f]{16}$/.test(generatedKey);
134
+ if (hasOnlyHexChars) {
135
+ expect(validateApiKeyFormat(generatedKey)).toBe(true);
136
+ }
137
+ });
138
+ });
139
+ });
@@ -16,7 +16,7 @@ const copyUsingFallback = (imageUrl: string) => {
16
16
  });
17
17
  });
18
18
  } catch {
19
- // 如果 toBlob ClipboardItem 不被支持,使用 data URL
19
+ // If toBlob or ClipboardItem is not supported, use data URL
20
20
  const dataURL = canvas.toDataURL('image/png');
21
21
  const textarea = document.createElement('textarea');
22
22
  textarea.value = dataURL;
@@ -44,7 +44,7 @@ const copyUsingModernAPI = async (imageUrl: string) => {
44
44
  };
45
45
 
46
46
  export const copyImageToClipboard = async (imageUrl: string) => {
47
- // 检查是否支持现代 Clipboard API
47
+ // Check if modern Clipboard API is supported
48
48
  if (navigator.clipboard && 'write' in navigator.clipboard) {
49
49
  await copyUsingModernAPI(imageUrl);
50
50
  } else {
@@ -1,41 +1,41 @@
1
1
  export const exportFile = (content: string, filename?: string) => {
2
- // 创建一个 Blob 对象
2
+ // Create a Blob object
3
3
  const blob = new Blob([content], { type: 'plain/text' });
4
4
 
5
- // 创建一个 URL 对象,用于下载
5
+ // Create a URL object for download
6
6
  const url = URL.createObjectURL(blob);
7
7
 
8
- // 创建一个 <a> 元素,设置下载链接和文件名
8
+ // Create an <a> element, set download link and filename
9
9
  const a = document.createElement('a');
10
10
  a.href = url;
11
11
  a.download = filename || 'file.txt';
12
12
 
13
- // 触发 <a> 元素的点击事件,开始下载
13
+ // Trigger the click event of the <a> element to start download
14
14
  document.body.append(a);
15
15
  a.click();
16
16
 
17
- // 下载完成后,清除 URL 对象
17
+ // After download is complete, clear the URL object
18
18
  URL.revokeObjectURL(url);
19
19
  a.remove();
20
20
  };
21
21
 
22
22
  export const exportJSONFile = (data: object, fileName: string) => {
23
- // 创建一个 Blob 对象
23
+ // Create a Blob object
24
24
  const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
25
25
 
26
- // 创建一个 URL 对象,用于下载
26
+ // Create a URL object for download
27
27
  const url = URL.createObjectURL(blob);
28
28
 
29
- // 创建一个 <a> 元素,设置下载链接和文件名
29
+ // Create an <a> element, set download link and filename
30
30
  const a = document.createElement('a');
31
31
  a.href = url;
32
32
  a.download = fileName;
33
33
 
34
- // 触发 <a> 元素的点击事件,开始下载
34
+ // Trigger the click event of the <a> element to start download
35
35
  document.body.append(a);
36
36
  a.click();
37
37
 
38
- // 下载完成后,清除 URL 对象
38
+ // After download is complete, clear the URL object
39
39
  URL.revokeObjectURL(url);
40
40
  a.remove();
41
41
  };
@@ -10,7 +10,7 @@ const placeholderVariablesRegex = /{{(.*?)}}/g;
10
10
  /* eslint-disable sort-keys-fix/sort-keys-fix */
11
11
  export const VARIABLE_GENERATORS = {
12
12
  /**
13
- * 时间类模板变量
13
+ * Time-related template variables
14
14
  *
15
15
  * | Value | Example |
16
16
  * |-------|---------|
@@ -46,12 +46,12 @@ export const VARIABLE_GENERATORS = {
46
46
  year: () => new Date().getFullYear().toString(),
47
47
 
48
48
  /**
49
- * 用户信息类模板变量
49
+ * User information template variables
50
50
  *
51
51
  * | Value | Example |
52
52
  * |-------|---------|
53
53
  * | `{{email}}` | demo@lobehub.com |
54
- * | `{{nickname}}` | 社区版用户 |
54
+ * | `{{nickname}}` | Community User |
55
55
  * | `{{username}}` | LobeChat |
56
56
  *
57
57
  */
@@ -63,7 +63,7 @@ export const VARIABLE_GENERATORS = {
63
63
  '',
64
64
 
65
65
  /**
66
- * 随机值类模板变量
66
+ * Random value template variables
67
67
  *
68
68
  * | Value | Example |
69
69
  * |-------|---------|
@@ -99,7 +99,7 @@ export const VARIABLE_GENERATORS = {
99
99
  uuid_short: () => uuid().split('-')[0],
100
100
 
101
101
  /**
102
- * 平台类模板变量
102
+ * Platform-related template variables
103
103
  *
104
104
  * | Value | Example |
105
105
  * |-------|---------|
@@ -114,9 +114,9 @@ export const VARIABLE_GENERATORS = {
114
114
  } as Record<string, () => string>;
115
115
 
116
116
  /**
117
- * 从文本中提取所有 {{variable}} 占位符的变量名
118
- * @param text 包含模板变量的字符串
119
- * @returns 变量名数组,如 ['date', 'nickname']
117
+ * Extract all {{variable}} placeholder variable names from text
118
+ * @param text String containing template variables
119
+ * @returns Array of variable names, e.g., ['date', 'nickname']
120
120
  */
121
121
  const extractPlaceholderVariables = (text: string): string[] => {
122
122
  const matches = [...text.matchAll(placeholderVariablesRegex)];
@@ -124,15 +124,15 @@ const extractPlaceholderVariables = (text: string): string[] => {
124
124
  };
125
125
 
126
126
  /**
127
- * 将模板变量替换为实际值,并支持递归解析嵌套变量
128
- * @param text - 含变量的原始文本
129
- * @param depth - 递归深度,默认 1,设置更高可支持 {{text}} 中的 {{date}}
130
- * @returns 替换后的文本
127
+ * Replace template variables with actual values, supporting recursive parsing of nested variables
128
+ * @param text - Original text containing variables
129
+ * @param depth - Recursion depth, default 1, set higher to support {{date}} within {{text}} etc.
130
+ * @returns Replaced text
131
131
  */
132
132
  export const parsePlaceholderVariables = (text: string, depth = 2): string => {
133
133
  let result = text;
134
134
 
135
- // 递归解析,用于处理如 {{text}} 存在额外预设变量
135
+ // Recursive parsing to handle cases where {{text}} contains additional preset variables
136
136
  for (let i = 0; i < depth; i++) {
137
137
  try {
138
138
  const variables = Object.fromEntries(
@@ -154,9 +154,9 @@ export const parsePlaceholderVariables = (text: string, depth = 2): string => {
154
154
  };
155
155
 
156
156
  /**
157
- * 解析消息内容,替换占位符变量
158
- * @param messages 原始消息数组
159
- * @returns 处理后的消息数组
157
+ * Parse message content, replace placeholder variables
158
+ * @param messages Original message array
159
+ * @returns Processed message array
160
160
  */
161
161
  export const parsePlaceholderVariablesMessages = (messages: any[]): any[] =>
162
162
  messages.map((message) => {
@@ -164,12 +164,12 @@ export const parsePlaceholderVariablesMessages = (messages: any[]): any[] =>
164
164
 
165
165
  const { content } = message;
166
166
 
167
- // 字符串类型直接处理
167
+ // Process string type directly
168
168
  if (typeof content === 'string') {
169
169
  return { ...message, content: parsePlaceholderVariables(content) };
170
170
  }
171
171
 
172
- // 数组类型处理其中的 text 元素
172
+ // Process text elements in array type
173
173
  if (Array.isArray(content)) {
174
174
  return {
175
175
  ...message,
@@ -3,7 +3,7 @@ import dayjs from 'dayjs';
3
3
  import isToday from 'dayjs/plugin/isToday';
4
4
  import isYesterday from 'dayjs/plugin/isYesterday';
5
5
 
6
- // 初始化 dayjs 插件
6
+ // Initialize dayjs plugins
7
7
  dayjs.extend(isToday);
8
8
  dayjs.extend(isYesterday);
9
9
 
@@ -19,32 +19,32 @@ const getTopicGroupId = (timestamp: number): TimeGroupId => {
19
19
  return 'yesterday';
20
20
  }
21
21
 
22
- // 7天内(不包括今天和昨天)
22
+ // Within 7 days (excluding today and yesterday)
23
23
  const weekAgo = now.subtract(7, 'day');
24
24
  if (date.isAfter(weekAgo) && !date.isToday() && !date.isYesterday()) {
25
25
  return 'week';
26
26
  }
27
27
 
28
- // 当前月份(不包括前面已经分组的日期)
29
- // 使用原生的月份和年份比较
28
+ // Current month (excluding dates already grouped above)
29
+ // Use native month and year comparison
30
30
  if (date.month() === now.month() && date.year() === now.year()) {
31
31
  return 'month';
32
32
  }
33
33
 
34
- // 当年的其他月份
34
+ // Other months of the current year
35
35
  if (date.year() === now.year()) {
36
36
  return `${date.year()}-${(date.month() + 1).toString().padStart(2, '0')}`;
37
37
  }
38
38
 
39
- // 更早的年份
39
+ // Earlier years
40
40
  return `${date.year()}`;
41
41
  };
42
42
 
43
- // 确保分组的排序
43
+ // Ensure group sorting
44
44
  const sortGroups = (groups: GroupedTopic[]): GroupedTopic[] => {
45
45
  const orderMap = new Map<string, number>();
46
46
 
47
- // 设置固定分组的顺序
47
+ // Set the order of fixed groups
48
48
  orderMap.set('today', 0);
49
49
  orderMap.set('yesterday', 1);
50
50
  orderMap.set('week', 2);
@@ -58,12 +58,12 @@ const sortGroups = (groups: GroupedTopic[]): GroupedTopic[] => {
58
58
  return orderA - orderB;
59
59
  }
60
60
 
61
- // 对于年月格式和年份格式的分组,按时间倒序排序
61
+ // For year-month and year format groups, sort in descending chronological order
62
62
  return b.id.localeCompare(a.id);
63
63
  });
64
64
  };
65
65
 
66
- // 时间分组的具体实现
66
+ // Specific implementation of time grouping
67
67
  export const groupTopicsByTime = (topics: ChatTopic[]): GroupedTopic[] => {
68
68
  if (!topics.length) return [];
69
69
 
@@ -1,30 +1,30 @@
1
1
  import { SECRET_XOR_KEY } from '@/const/auth';
2
2
 
3
3
  /**
4
- * 将字符串转换为 Uint8Array (UTF-8 编码)
4
+ * Convert string to Uint8Array (UTF-8 encoding)
5
5
  */
6
6
  const stringToUint8Array = (str: string): Uint8Array => {
7
7
  return new TextEncoder().encode(str);
8
8
  };
9
9
 
10
10
  /**
11
- * Uint8Array 进行 XOR 运算
12
- * @param data 要处理的 Uint8Array
13
- * @param key 用于 XOR 的密钥 (Uint8Array)
14
- * @returns 经过 XOR 运算的 Uint8Array
11
+ * Perform XOR operation on Uint8Array
12
+ * @param data The Uint8Array to process
13
+ * @param key The key used for XOR operation (Uint8Array)
14
+ * @returns The Uint8Array after XOR operation
15
15
  */
16
16
  const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => {
17
17
  const result = new Uint8Array(data.length);
18
18
  for (const [i, datum] of data.entries()) {
19
- result[i] = datum ^ key[i % key.length]; // 密钥循环使用
19
+ result[i] = datum ^ key[i % key.length]; // Key is used cyclically
20
20
  }
21
21
  return result;
22
22
  };
23
23
 
24
24
  /**
25
- * payload 进行 XOR 混淆并 Base64 编码
26
- * @param payload 要混淆的 JSON 对象
27
- * @returns Base64 编码后的混淆字符串
25
+ * Obfuscate payload with XOR and encode to Base64
26
+ * @param payload The JSON object to obfuscate
27
+ * @returns The obfuscated string encoded in Base64
28
28
  */
29
29
  export const obfuscatePayloadWithXOR = <T>(payload: T): string => {
30
30
  const jsonString = JSON.stringify(payload);
@@ -33,7 +33,7 @@ export const obfuscatePayloadWithXOR = <T>(payload: T): string => {
33
33
 
34
34
  const xoredBytes = xorProcess(dataBytes, keyBytes);
35
35
 
36
- // Uint8Array 转换为 Base64 字符串
37
- // 浏览器环境 btoa 只能处理 Latin-1 字符,所以需要先转换为适合 btoa 的字符串
36
+ // Convert Uint8Array to Base64 string
37
+ // In browser environment, btoa can only handle Latin-1 characters, so we need to convert to a format suitable for btoa first
38
38
  return btoa(String.fromCharCode(...xoredBytes));
39
39
  };
package/renovate.json CHANGED
@@ -1,5 +1,7 @@
1
1
  {
2
2
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "automerge": false,
4
+ "dependencyDashboard": true,
3
5
  "extends": [
4
6
  "config:recommended",
5
7
  ":dependencyDashboard",
@@ -9,13 +11,28 @@
9
11
  ":semanticPrefixFixDepsChoreOthers",
10
12
  "group:monorepos",
11
13
  "group:recommended",
12
- "group:allNonMajor",
13
14
  "replacements:all",
14
15
  "workarounds:all"
15
16
  ],
16
17
  "ignoreDeps": [],
17
- "labels": ["dependencies"],
18
- "postUpdateOptions": ["yarnDedupeHighest"],
18
+ "labels": [
19
+ "dependencies"
20
+ ],
21
+ "packageRules": [
22
+ {
23
+ "groupName": "all non-minor dependencies",
24
+ "groupSlug": "all-minor-patch",
25
+ "matchPackageNames": [
26
+ "*"
27
+ ],
28
+ "matchUpdateTypes": [
29
+ "patch"
30
+ ]
31
+ }
32
+ ],
33
+ "postUpdateOptions": [
34
+ "yarnDedupeHighest"
35
+ ],
19
36
  "prConcurrentLimit": 30,
20
37
  "prHourlyLimit": 0,
21
38
  "rangeStrategy": "bump",
@@ -21,7 +21,7 @@ interface LoginConfirmProps {
21
21
  uid: string;
22
22
  }
23
23
 
24
- const useStyles = createStyles(({ css, token }) => ({
24
+ const useStyles = createStyles(({ css, token, responsive }) => ({
25
25
  authButton: css`
26
26
  width: 100%;
27
27
  height: 40px;
@@ -35,12 +35,21 @@ const useStyles = createStyles(({ css, token }) => ({
35
35
  border-radius: 12px;
36
36
 
37
37
  background: ${token.colorBgContainer};
38
+
39
+ ${responsive.mobile} {
40
+ min-width: auto;
41
+ }
38
42
  `,
39
43
  container: css`
40
44
  width: 100%;
41
45
  min-height: 100vh;
42
46
  color: ${token.colorTextBase};
43
47
  background-color: ${token.colorBgLayout};
48
+
49
+ ${responsive.mobile} {
50
+ justify-content: flex-start;
51
+ padding-block-start: 64px;
52
+ }
44
53
  `,
45
54
  title: css`
46
55
  margin-block-end: ${token.marginLG}px;
@@ -52,12 +52,12 @@ interface FetchAITaskResultParams extends FetchSSEOptions {
52
52
  abortController?: AbortController;
53
53
  onError?: (e: Error, rawError?: any) => void;
54
54
  /**
55
- * 加载状态变化处理函数
56
- * @param loading - 是否处于加载状态
55
+ * Loading state change handler function
56
+ * @param loading - Whether in loading state
57
57
  */
58
58
  onLoadingChange?: (loading: boolean) => void;
59
59
  /**
60
- * 请求对象
60
+ * Request object
61
61
  */
62
62
  params: ChatStreamInputParams;
63
63
  trace?: TracePayload;