@lobehub/lobehub 2.0.0-next.37 → 2.0.0-next.39

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 (127) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/modules/networkProxy/__tests__/dispatcher.test.ts +401 -0
  3. package/apps/desktop/src/main/modules/networkProxy/__tests__/tester.test.ts +531 -0
  4. package/apps/desktop/src/main/modules/networkProxy/__tests__/urlBuilder.test.ts +349 -0
  5. package/apps/desktop/src/main/modules/networkProxy/__tests__/validator.test.ts +492 -0
  6. package/changelog/v1.json +14 -0
  7. package/locales/ar/auth.json +45 -1
  8. package/locales/ar/modelProvider.json +13 -1
  9. package/locales/bg-BG/auth.json +45 -1
  10. package/locales/bg-BG/modelProvider.json +13 -1
  11. package/locales/de-DE/auth.json +45 -1
  12. package/locales/de-DE/modelProvider.json +13 -1
  13. package/locales/en-US/auth.json +45 -1
  14. package/locales/en-US/modelProvider.json +13 -1
  15. package/locales/es-ES/auth.json +45 -1
  16. package/locales/es-ES/modelProvider.json +13 -1
  17. package/locales/fa-IR/auth.json +45 -1
  18. package/locales/fa-IR/modelProvider.json +13 -1
  19. package/locales/fr-FR/auth.json +45 -1
  20. package/locales/fr-FR/modelProvider.json +13 -1
  21. package/locales/it-IT/auth.json +45 -1
  22. package/locales/it-IT/modelProvider.json +13 -1
  23. package/locales/ja-JP/auth.json +45 -1
  24. package/locales/ja-JP/modelProvider.json +13 -1
  25. package/locales/ko-KR/auth.json +45 -1
  26. package/locales/ko-KR/modelProvider.json +13 -1
  27. package/locales/nl-NL/auth.json +45 -1
  28. package/locales/nl-NL/modelProvider.json +13 -1
  29. package/locales/pl-PL/auth.json +45 -1
  30. package/locales/pl-PL/modelProvider.json +13 -1
  31. package/locales/pt-BR/auth.json +45 -1
  32. package/locales/pt-BR/modelProvider.json +13 -1
  33. package/locales/ru-RU/auth.json +45 -1
  34. package/locales/ru-RU/modelProvider.json +13 -1
  35. package/locales/tr-TR/auth.json +45 -1
  36. package/locales/tr-TR/modelProvider.json +13 -1
  37. package/locales/vi-VN/auth.json +45 -1
  38. package/locales/vi-VN/modelProvider.json +13 -1
  39. package/locales/zh-CN/auth.json +45 -1
  40. package/locales/zh-CN/modelProvider.json +13 -1
  41. package/locales/zh-TW/auth.json +45 -1
  42. package/locales/zh-TW/modelProvider.json +13 -1
  43. package/package.json +1 -1
  44. package/packages/context-engine/src/processors/MessageCleanup.ts +1 -0
  45. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +28 -0
  46. package/packages/obervability-otel/package.json +3 -1
  47. package/packages/obervability-otel/src/api.ts +2 -0
  48. package/packages/obervability-otel/src/trpc/convention.ts +16 -0
  49. package/packages/obervability-otel/src/trpc/index.test.ts +38 -0
  50. package/packages/obervability-otel/src/trpc/index.ts +62 -0
  51. package/packages/obervability-otel/src/trpc/metrics.ts +31 -0
  52. package/packages/types/src/usage/usageRecord.ts +54 -0
  53. package/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
  54. package/packages/web-crawler/src/crawImpl/naive.ts +9 -9
  55. package/packages/web-crawler/src/crawler.ts +5 -5
  56. package/packages/web-crawler/src/urlRules.ts +13 -13
  57. package/packages/web-crawler/src/utils/appUrlRules.ts +5 -5
  58. package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +10 -1
  59. package/src/app/[variants]/(main)/profile/usage/Client.tsx +114 -0
  60. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx +175 -0
  61. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/index.tsx +126 -0
  62. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/MonthSpend.tsx +53 -0
  63. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/TodaySpend.tsx +67 -0
  64. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/index.tsx +19 -0
  65. package/src/app/[variants]/(main)/profile/usage/features/UsageTable.tsx +145 -0
  66. package/src/app/[variants]/(main)/profile/usage/features/UsageTrends.tsx +107 -0
  67. package/src/app/[variants]/(main)/profile/usage/features/components/UsageBarChart.tsx +48 -0
  68. package/src/app/[variants]/(main)/profile/usage/page.tsx +23 -0
  69. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +3 -3
  70. package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +37 -14
  71. package/src/features/Conversation/Messages/Group/Error/index.tsx +1 -1
  72. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +13 -35
  73. package/src/features/Conversation/Messages/Group/GroupItem.tsx +43 -0
  74. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -2
  75. package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +1 -1
  76. package/src/features/Conversation/Messages/Group/Tool/index.tsx +0 -2
  77. package/src/features/Conversation/Messages/Group/index.tsx +7 -2
  78. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +3 -0
  79. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +21 -7
  80. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
  81. package/src/features/PluginsUI/Render/MCPType/index.tsx +52 -0
  82. package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +2 -2
  83. package/src/features/PluginsUI/Render/index.tsx +17 -0
  84. package/src/libs/mcp/client.ts +3 -2
  85. package/src/libs/mcp/types.ts +71 -0
  86. package/src/libs/trpc/lambda/index.ts +5 -2
  87. package/src/libs/trpc/middleware/openTelemetry.ts +141 -0
  88. package/src/locales/default/auth.ts +44 -0
  89. package/src/locales/default/chat.ts +1 -0
  90. package/src/server/routers/desktop/mcp.ts +1 -3
  91. package/src/server/routers/lambda/index.ts +2 -0
  92. package/src/server/routers/lambda/usage.ts +36 -0
  93. package/src/server/routers/tools/mcp.ts +1 -3
  94. package/src/server/services/mcp/index.test.ts +28 -15
  95. package/src/server/services/mcp/index.ts +29 -18
  96. package/src/server/services/usage/index.test.ts +310 -0
  97. package/src/server/services/usage/index.ts +164 -0
  98. package/src/services/chat/contextEngineering.test.ts +4 -0
  99. package/src/services/mcp.test.ts +7 -1
  100. package/src/services/mcp.ts +13 -12
  101. package/src/services/usage.ts +13 -0
  102. package/src/store/chat/agents/createAgentExecutors.ts +2 -3
  103. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +40 -1
  104. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +13 -5
  105. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +3 -3
  106. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +6 -6
  107. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +2 -2
  108. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  109. package/src/store/chat/slices/builtinTool/actions/search.ts +6 -6
  110. package/src/store/chat/slices/message/actions/publicApi.ts +19 -1
  111. package/src/store/chat/slices/message/initialState.ts +5 -0
  112. package/src/store/chat/slices/message/selectors/chat.test.ts +22 -602
  113. package/src/store/chat/slices/message/selectors/chat.ts +0 -2
  114. package/src/store/chat/slices/message/selectors/dbMessage.test.ts +51 -0
  115. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +818 -0
  116. package/src/store/chat/slices/message/selectors/displayMessage.ts +52 -1
  117. package/src/store/chat/slices/message/selectors/messageState.ts +2 -0
  118. package/src/store/chat/slices/plugin/action.test.ts +4 -4
  119. package/src/store/chat/slices/plugin/actions/index.ts +39 -0
  120. package/src/store/chat/slices/plugin/actions/internals.ts +83 -0
  121. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +188 -0
  122. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +213 -0
  123. package/src/store/chat/slices/plugin/actions/publicApi.ts +115 -0
  124. package/src/store/chat/slices/plugin/actions/workflow.ts +121 -0
  125. package/src/store/chat/store.ts +1 -1
  126. package/src/store/global/initialState.ts +1 -0
  127. package/src/store/chat/slices/plugin/action.ts +0 -539
@@ -131,6 +131,34 @@ describe('MessageCleanupProcessor', () => {
131
131
  });
132
132
  });
133
133
 
134
+ it('should preserve reasoning in assistant messages', async () => {
135
+ const processor = new MessageCleanupProcessor();
136
+ const reasoning = {
137
+ content: 'Let me think about this...',
138
+ signature: 'sha256:abc123',
139
+ };
140
+
141
+ const context = createContext([
142
+ {
143
+ content: 'Here is the answer',
144
+ extraField: 'remove',
145
+ id: 'msg5',
146
+ reasoning: reasoning,
147
+ role: 'assistant',
148
+ timestamp: Date.now(),
149
+ },
150
+ ]);
151
+
152
+ const result = await processor.process(context);
153
+
154
+ expect(result.messages).toHaveLength(1);
155
+ expect(result.messages[0]).toEqual({
156
+ content: 'Here is the answer',
157
+ reasoning: reasoning,
158
+ role: 'assistant',
159
+ });
160
+ });
161
+
134
162
  it('should clean tool messages with name', async () => {
135
163
  const processor = new MessageCleanupProcessor();
136
164
  const context = createContext([
@@ -3,7 +3,9 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "exports": {
6
- "./node": "./src/node.ts"
6
+ "./node": "./src/node.ts",
7
+ "./api": "./src/api.ts",
8
+ "./trpc": "./src/trpc/index.ts"
7
9
  },
8
10
  "dependencies": {
9
11
  "@opentelemetry/api": "^1.9.0",
@@ -0,0 +1,2 @@
1
+ export type { Attributes, Span } from '@opentelemetry/api';
2
+ export { diag, metrics, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
@@ -0,0 +1,16 @@
1
+ export enum TRPCAttribute {
2
+ RPC_METHOD = 'rpc.method',
3
+ RPC_SERVICE = 'rpc.service',
4
+ RPC_SYSTEM = 'rpc.system',
5
+ RPC_TRPC_PATH = 'rpc.trpc.path',
6
+ RPC_TRPC_STATUS_CODE = 'rpc.trpc.status_code',
7
+ RPC_TRPC_SUCCESS = 'rpc.trpc.success',
8
+ RPC_TRPC_TYPE = 'rpc.trpc.type',
9
+ }
10
+
11
+ export {
12
+ ATTR_ERROR_TYPE,
13
+ ATTR_EXCEPTION_MESSAGE,
14
+ ATTR_EXCEPTION_STACKTRACE,
15
+ ATTR_EXCEPTION_TYPE,
16
+ } from '@opentelemetry/semantic-conventions';
@@ -0,0 +1,38 @@
1
+ import { parseTRPCPath, tRPCConventionFromPathAndType } from './';
2
+
3
+ describe('splitRpcPath', () => {
4
+ it('should split path into service and method with url', () => {
5
+ const { method, service } = parseTRPCPath('/trpc/lambda/someService.someMethod');
6
+ expect(method).toBe('someMethod');
7
+ expect(service).toBe('someService');
8
+ });
9
+
10
+ it('should split path into service and method without url', () => {
11
+ const { method, service } = parseTRPCPath('someService.someMethod');
12
+ expect(method).toBe('someMethod');
13
+ expect(service).toBe('someService');
14
+ });
15
+ });
16
+
17
+ describe('createBaseAttributes', () => {
18
+ it('should create base attributes with service and method', () => {
19
+ const attributes = tRPCConventionFromPathAndType('someService.someMethod', 'query');
20
+ expect(attributes).toEqual({
21
+ 'rpc.system': 'trpc',
22
+ 'rpc.trpc.path': 'someService.someMethod',
23
+ 'rpc.trpc.type': 'query',
24
+ 'rpc.service': 'someService',
25
+ 'rpc.method': 'someMethod',
26
+ });
27
+ });
28
+
29
+ it('should create base attributes without service', () => {
30
+ const attributes = tRPCConventionFromPathAndType('someMethod', 'mutation');
31
+ expect(attributes).toEqual({
32
+ 'rpc.system': 'trpc',
33
+ 'rpc.trpc.path': 'someMethod',
34
+ 'rpc.trpc.type': 'mutation',
35
+ 'rpc.method': 'someMethod',
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,62 @@
1
+ import { Attributes } from '@opentelemetry/api';
2
+
3
+ import { TRPCAttribute } from './convention';
4
+
5
+ export const DEFAULT_ERROR_CODE = 'UNKNOWN_ERROR';
6
+ export const DEFAULT_SUCCESS_STATUS = 'OK';
7
+
8
+ const textEncoder = new TextEncoder();
9
+
10
+ export function parseTRPCPath(path: string) {
11
+ const parts = path?.split('.') ?? [];
12
+ if (parts.length <= 1) {
13
+ return { method: path, service: undefined };
14
+ }
15
+
16
+ const method = parts.at(-1);
17
+ const service = parts.slice(0, -1).join('.').split('/').pop();
18
+ return { method, service };
19
+ }
20
+
21
+ export function tRPCConventionFromPathAndType(path: string, type: string): Attributes {
22
+ const { method, service } = parseTRPCPath(path);
23
+
24
+ const attributes: Attributes = {
25
+ [TRPCAttribute.RPC_SYSTEM]: 'trpc',
26
+ [TRPCAttribute.RPC_TRPC_PATH]: path,
27
+ [TRPCAttribute.RPC_TRPC_TYPE]: type,
28
+ };
29
+
30
+ if (service) {
31
+ attributes[TRPCAttribute.RPC_SERVICE] = service;
32
+ }
33
+ if (method) {
34
+ attributes[TRPCAttribute.RPC_METHOD] = method;
35
+ }
36
+
37
+ return attributes;
38
+ }
39
+
40
+ export const getPayloadSize = (payload: unknown): number | undefined => {
41
+ if (payload === undefined || payload === null) return undefined;
42
+
43
+ try {
44
+ const serialized = JSON.stringify(payload);
45
+ return textEncoder.encode(serialized).length;
46
+ } catch {
47
+ return undefined;
48
+ }
49
+ };
50
+
51
+ export const createAttributesForMetrics = (
52
+ baseAttributes: Attributes,
53
+ statusCode: string,
54
+ extraAttributes?: Attributes,
55
+ ): Attributes => ({
56
+ ...baseAttributes,
57
+ [TRPCAttribute.RPC_TRPC_STATUS_CODE]: statusCode,
58
+ ...extraAttributes,
59
+ });
60
+
61
+ export * from './convention';
62
+ export * from './metrics';
@@ -0,0 +1,31 @@
1
+ import { metrics } from '@opentelemetry/api';
2
+
3
+ const meter = metrics.getMeter('trpc-server');
4
+
5
+ export const serverDurationHistogram = meter.createHistogram('rpc.server.duration', {
6
+ description: 'Measures the duration of inbound RPC.',
7
+ unit: 'ms',
8
+ });
9
+
10
+ export const serverRequestSizeHistogram = meter.createHistogram('rpc.server.request.size', {
11
+ description: 'Measures the size of RPC request messages (uncompressed).',
12
+ unit: 'By',
13
+ });
14
+
15
+ export const serverResponseSizeHistogram = meter.createHistogram('rpc.server.response.size', {
16
+ description: 'Measures the size of RPC response messages (uncompressed).',
17
+ unit: 'By',
18
+ });
19
+
20
+ export const serverRequestsPerRpcHistogram = meter.createHistogram('rpc.server.requests_per_rpc', {
21
+ description: 'Measures the number of messages received per RPC.',
22
+ unit: '{count}',
23
+ });
24
+
25
+ export const serverResponsesPerRpcHistogram = meter.createHistogram(
26
+ 'rpc.server.responses_per_rpc',
27
+ {
28
+ description: 'Measures the number of messages sent per RPC.',
29
+ unit: '{count}',
30
+ },
31
+ );
@@ -0,0 +1,54 @@
1
+ import { MessageMetadata } from '../message';
2
+
3
+ export interface UsageRecordItem {
4
+ createdAt: Date;
5
+ /**
6
+ * ID
7
+ **/
8
+ id: string;
9
+ inputStartAt?: Date | null;
10
+ /**
11
+ * Meta information
12
+ **/
13
+ metadata?: MessageMetadata | null;
14
+ /**
15
+ * Model id
16
+ */
17
+ model: string;
18
+ outputFinishAt?: Date | null;
19
+ outputStartAt?: Date | null;
20
+ /**
21
+ * Provider id
22
+ */
23
+ provider: string;
24
+ /**
25
+ * Spend
26
+ **/
27
+ spend: number;
28
+ /**
29
+ * Usage details
30
+ **/
31
+ totalInputTokens?: number | null;
32
+ totalOutputTokens?: number | null;
33
+ totalTokens?: number | null;
34
+ /**
35
+ * Performance details
36
+ **/
37
+ tps?: number | null;
38
+ ttft?: number | null;
39
+ /**
40
+ * Call types
41
+ **/
42
+ type: string;
43
+ updatedAt: Date;
44
+ userId: string;
45
+ }
46
+
47
+ export type UsageLog = {
48
+ date: number;
49
+ day: string;
50
+ records: UsageRecordItem[];
51
+ totalRequests: number;
52
+ totalSpend: number;
53
+ totalTokens: number;
54
+ };
@@ -56,7 +56,7 @@ export const browserless: CrawlImpl = async (url, { filterOptions }) => {
56
56
  if (
57
57
  !!result.content &&
58
58
  result.title &&
59
- // Just a moment... 说明被 CF 拦截了
59
+ // "Just a moment..." indicates being blocked by CloudFlare
60
60
  result.title.trim() !== 'Just a moment...'
61
61
  ) {
62
62
  return {
@@ -6,25 +6,25 @@ import { htmlToMarkdown } from '../utils/htmlToMarkdown';
6
6
  import { DEFAULT_TIMEOUT, withTimeout } from '../utils/withTimeout';
7
7
 
8
8
  const mixinHeaders = {
9
- // 接受的内容类型
9
+ // Accepted content types
10
10
  'Accept':
11
11
  'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
12
- // 接受的编码方式
12
+ // Accepted encoding methods
13
13
  'Accept-Encoding': 'gzip, deflate, br',
14
- // 接受的语言
14
+ // Accepted languages
15
15
  'Accept-Language': 'en-US,en;q=0.9,zh;q=0.8',
16
- // 缓存控制
16
+ // Cache control
17
17
  'Cache-Control': 'max-age=0',
18
- // 连接类型
18
+ // Connection type
19
19
  'Connection': 'keep-alive',
20
- // 表明请求来自哪个站点
20
+ // Indicates which site the request is from
21
21
  'Referer': 'https://www.google.com/',
22
- // 升级不安全请求
22
+ // Upgrade insecure requests
23
23
  'Upgrade-Insecure-Requests': '1',
24
- // 模拟真实浏览器的 User-Agent
24
+ // Simulate real browser User-Agent
25
25
  'User-Agent':
26
26
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
27
- // 防止跨站请求伪造
27
+ // Prevent cross-site request forgery
28
28
  'sec-ch-ua': '"Google Chrome";v="121", "Not A(Brand";v="99", "Chromium";v="121"',
29
29
  'sec-ch-ua-mobile': '?0',
30
30
  'sec-ch-ua-platform': '"Windows"',
@@ -19,8 +19,8 @@ export class Crawler {
19
19
  }
20
20
 
21
21
  /**
22
- * 爬取网页内容
23
- * @param options 爬取选项
22
+ * Crawl webpage content
23
+ * @param options Crawl options
24
24
  */
25
25
  async crawl({
26
26
  url,
@@ -31,14 +31,14 @@ export class Crawler {
31
31
  impls?: CrawlImplType[];
32
32
  url: string;
33
33
  }): Promise<CrawlUniformResult> {
34
- // 应用URL规则
34
+ // Apply URL rules
35
35
  const {
36
36
  transformedUrl,
37
37
  filterOptions: ruleFilterOptions,
38
38
  impls: ruleImpls,
39
39
  } = applyUrlRules(url, crawUrlRules);
40
40
 
41
- // 合并用户提供的过滤选项和规则中的过滤选项,用户选项优先
41
+ // Merge user-provided filter options and rule filter options, user options take priority
42
42
  const mergedFilterOptions = {
43
43
  ...ruleFilterOptions,
44
44
  ...userFilterOptions,
@@ -53,7 +53,7 @@ export class Crawler {
53
53
  ? (userImpls.filter((impl) => Object.keys(crawlImpls).includes(impl)) as CrawlImplType[])
54
54
  : systemImpls;
55
55
 
56
- // 按照内置的实现顺序依次尝试
56
+ // Try each implementation in the built-in order
57
57
  for (const impl of finalImpls) {
58
58
  try {
59
59
  const res = await crawlImpls[impl](transformedUrl, { filterOptions: mergedFilterOptions });
@@ -1,32 +1,32 @@
1
1
  import { CrawlUrlRule } from './type';
2
2
 
3
3
  export const crawUrlRules: CrawlUrlRule[] = [
4
- // 搜狗微信链接,使用 search1api
4
+ // Sogou WeChat links, use search1api
5
5
  {
6
6
  impls: ['search1api'],
7
7
  urlPattern: 'https://weixin.sogou.com/link(.*)',
8
8
  },
9
- // 搜狗链接,使用 search1api
9
+ // Sogou links, use search1api
10
10
  {
11
11
  impls: ['search1api'],
12
12
  urlPattern: 'https://sogou.com/link(.*)',
13
13
  },
14
- // YouTube 链接,使用 search1api,格式化 markdown,且可以返回字幕内容
14
+ // YouTube links, use search1api, formatted as markdown, can return subtitle content
15
15
  {
16
16
  impls: ['search1api'],
17
17
  urlPattern: 'https://www.youtube.com/watch(.*)',
18
18
  },
19
- // Reddit 链接,使用 search1api,格式化 markdown,包含标题、作者、互动数量、具体评论内容等
19
+ // Reddit links, use search1api, formatted as markdown, includes title, author, interaction count, specific comment content, etc.
20
20
  {
21
21
  impls: ['search1api'],
22
22
  urlPattern: 'https://www.reddit.com/r/(.*)/comments/(.*)',
23
23
  },
24
- // 微信公众号有爬虫防护,优先使用 search1apijina 作为兜底(目前 jina 爬取会被风控)
24
+ // WeChat official accounts have crawler protection, use search1api first, jina as fallback (currently jina crawling may be blocked)
25
25
  {
26
26
  impls: ['search1api', 'jina'],
27
27
  urlPattern: 'https://mp.weixin.qq.com(.*)',
28
28
  },
29
- // github 源码解析
29
+ // GitHub source code parsing
30
30
  {
31
31
  filterOptions: {
32
32
  enableReadability: false,
@@ -43,7 +43,7 @@ export const crawUrlRules: CrawlUrlRule[] = [
43
43
  // GitHub discussion
44
44
  urlPattern: 'https://github.com/(.*)/discussions/(.*)',
45
45
  },
46
- // 所有 PDF 都用 jina
46
+ // All PDFs use jina
47
47
  {
48
48
  impls: ['jina'],
49
49
  urlPattern: 'https://(.*).pdf',
@@ -53,7 +53,7 @@ export const crawUrlRules: CrawlUrlRule[] = [
53
53
  impls: ['jina'],
54
54
  urlPattern: 'https://arxiv.org/pdf/(.*)',
55
55
  },
56
- // 知乎有爬虫防护,使用 jina
56
+ // Zhihu has crawler protection, use jina
57
57
  {
58
58
  impls: ['jina'],
59
59
  urlPattern: 'https://zhuanlan.zhihu.com(.*)',
@@ -63,7 +63,7 @@ export const crawUrlRules: CrawlUrlRule[] = [
63
63
  urlPattern: 'https://zhihu.com(.*)',
64
64
  },
65
65
  {
66
- // Medium 文章转换为 Scribe.rip
66
+ // Convert Medium articles to Scribe.rip
67
67
  urlPattern: 'https://medium.com/(.*)',
68
68
  urlTransform: 'https://scribe.rip/$1',
69
69
  },
@@ -74,10 +74,10 @@ export const crawUrlRules: CrawlUrlRule[] = [
74
74
  impls: ['jina', 'browserless'],
75
75
  urlPattern: 'https://(twitter.com|x.com)/(.*)',
76
76
  },
77
- // 体育数据网站规则
77
+ // Sports data website rules
78
78
  {
79
79
  filterOptions: {
80
- // 对体育数据表格禁用 Readability 并且转换为纯文本
80
+ // Disable Readability for sports data tables and convert to plain text
81
81
  enableReadability: false,
82
82
  pureText: true,
83
83
  },
@@ -94,13 +94,13 @@ export const crawUrlRules: CrawlUrlRule[] = [
94
94
  impls: ['jina'],
95
95
  urlPattern: 'https://cvpr.thecvf.com(.*)',
96
96
  },
97
- // 飞书用 jina
97
+ // Feishu use jina
98
98
  // https://github.com/lobehub/lobe-chat/issues/6879
99
99
  {
100
100
  impls: ['jina'],
101
101
  urlPattern: 'https://(.*).feishu.cn/(.*)',
102
102
  },
103
- // 小红书存在爬虫防护,使用 Search1API Jina (备用)
103
+ // Xiaohongshu has crawler protection, use Search1API or Jina (fallback)
104
104
  {
105
105
  impls: ['search1api', 'jina'],
106
106
  urlPattern: 'https://(.*).xiaohongshu.com/(.*)',
@@ -9,14 +9,14 @@ export const applyUrlRules = (
9
9
  transformedUrl: string;
10
10
  } => {
11
11
  for (const rule of urlRules) {
12
- // 转换为正则表达式
12
+ // Convert to regular expression
13
13
  const regex = new RegExp(rule.urlPattern);
14
14
  const match = url.match(regex);
15
15
 
16
16
  if (match) {
17
17
  if (rule.urlTransform) {
18
- // 如果有转换规则,进行 URL 转换
19
- // 替换 $1, $2 等占位符为捕获组内容
18
+ // If there is a transformation rule, perform URL transformation
19
+ // Replace placeholders like $1, $2 with capture group content
20
20
  const transformedUrl = rule.urlTransform.replaceAll(
21
21
  /\$(\d+)/g,
22
22
  (_, index) => match[parseInt(index)] || '',
@@ -28,7 +28,7 @@ export const applyUrlRules = (
28
28
  transformedUrl,
29
29
  };
30
30
  } else {
31
- // 没有转换规则但匹配了模式,只返回过滤选项
31
+ // No transformation rule but pattern matched, only return filter options
32
32
  return {
33
33
  filterOptions: rule.filterOptions,
34
34
  impls: rule.impls,
@@ -38,6 +38,6 @@ export const applyUrlRules = (
38
38
  }
39
39
  }
40
40
 
41
- // 没有匹配任何规则,返回原始 URL
41
+ // No rule matched, return original URL
42
42
  return { transformedUrl: url };
43
43
  };
@@ -1,5 +1,5 @@
1
1
  import { Icon } from '@lobehub/ui';
2
- import { ChartColumnBigIcon, KeyIcon, ShieldCheck, UserCircle } from 'lucide-react';
2
+ import { BadgeCentIcon, ChartColumnBigIcon, KeyIcon, ShieldCheck, UserCircle } from 'lucide-react';
3
3
  import Link from 'next/link';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
@@ -54,6 +54,15 @@ export const useCategory = () => {
54
54
  </Link>
55
55
  ),
56
56
  },
57
+ {
58
+ icon: <Icon icon={BadgeCentIcon} />,
59
+ key: ProfileTabs.Usage,
60
+ label: (
61
+ <Link href={'/profile/usage'} onClick={(e) => e.preventDefault()}>
62
+ {t('tab.usage')}
63
+ </Link>
64
+ ),
65
+ },
57
66
  ].filter(Boolean) as MenuProps['items'];
58
67
 
59
68
  return cateItems;
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { Icon, Segmented } from '@lobehub/ui';
4
+ import { Col, DatePicker, DatePickerProps, Row } from 'antd';
5
+ import dayjs from 'dayjs';
6
+ import { Brain, Codesandbox } from 'lucide-react';
7
+ import { memo, useEffect, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Flexbox } from 'react-layout-kit';
10
+
11
+ import { useClientDataSWR } from '@/libs/swr';
12
+ import { usageService } from '@/services/usage';
13
+ import { UsageLog } from '@/types/usage/usageRecord';
14
+
15
+ import Welcome from '../stats/features/Welcome';
16
+ import UsageCards from './features/UsageCards';
17
+ import UsageTable from './features/UsageTable';
18
+ import UsageTrends from './features/UsageTrends';
19
+
20
+ export interface UsageChartProps {
21
+ data?: UsageLog[];
22
+ dateStrings?: string;
23
+ groupBy?: GroupBy;
24
+ inShare?: boolean;
25
+ isLoading?: boolean;
26
+ mobile?: boolean;
27
+ }
28
+
29
+ export enum GroupBy {
30
+ Model = 'model',
31
+ Provider = 'provider',
32
+ }
33
+
34
+ const Client = memo<{ mobile?: boolean }>(({ mobile }) => {
35
+ const { t, i18n } = useTranslation('auth');
36
+ dayjs.locale(i18n.language);
37
+
38
+ const [groupBy, setGroupBy] = useState<GroupBy>(GroupBy.Model);
39
+ const [dateRange, setDateRange] = useState<dayjs.Dayjs>(dayjs(new Date()));
40
+ const [dateStrings, setDateStrings] = useState<string>();
41
+
42
+ const { data, isLoading, mutate } = useClientDataSWR('usage-stat', async () =>
43
+ usageService.findAndGroupByDay(dateStrings),
44
+ );
45
+
46
+ useEffect(() => {
47
+ if (dateStrings) {
48
+ mutate();
49
+ }
50
+ }, [dateStrings]);
51
+
52
+ const handleDateChange: DatePickerProps['onChange'] = (dates, dateStrings) => {
53
+ setDateRange(dates);
54
+ if (typeof dateStrings === 'string') {
55
+ setDateStrings(dateStrings);
56
+ }
57
+ };
58
+
59
+ return (
60
+ <Flexbox gap={mobile ? 0 : 24}>
61
+ <Flexbox>
62
+ <Row>
63
+ <Col span={16}>
64
+ {mobile ? (
65
+ <Welcome mobile />
66
+ ) : (
67
+ <Flexbox align={'flex-start'} gap={16} horizontal justify={'space-between'}>
68
+ <Welcome />
69
+ </Flexbox>
70
+ )}
71
+ </Col>
72
+ <Col span={8}>
73
+ <Flexbox gap={16} horizontal>
74
+ <Segmented
75
+ onChange={(v) => setGroupBy(v as GroupBy)}
76
+ options={[
77
+ {
78
+ icon: <Icon icon={Codesandbox} />,
79
+ label: t('usage.welcome.model'),
80
+ value: GroupBy.Model,
81
+ },
82
+ {
83
+ icon: <Icon icon={Brain} />,
84
+ label: t('usage.welcome.provider'),
85
+ value: GroupBy.Provider,
86
+ },
87
+ ]}
88
+ value={groupBy}
89
+ />
90
+ <DatePicker onChange={handleDateChange} picker="month" value={dateRange} />
91
+ </Flexbox>
92
+ </Col>
93
+ </Row>
94
+ </Flexbox>
95
+ <Flexbox>
96
+ <UsageCards data={data} groupBy={groupBy} isLoading={isLoading} />
97
+ </Flexbox>
98
+ <Flexbox>
99
+ <Row gutter={[16, 16]}>
100
+ <Col span={24}>
101
+ <UsageTrends data={data} groupBy={groupBy} isLoading={isLoading} />
102
+ </Col>
103
+ </Row>
104
+ </Flexbox>
105
+ <Row>
106
+ <Col span={24}>
107
+ <UsageTable dateStrings={dateStrings} />
108
+ </Col>
109
+ </Row>
110
+ </Flexbox>
111
+ );
112
+ });
113
+
114
+ export default Client;