@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
@@ -10,6 +10,7 @@
10
10
  import { DESKTOP_USER_ID } from '@/const/desktop';
11
11
  import { isDesktop } from '@/const/version';
12
12
 
13
+ import { openTelemetry } from '../middleware/openTelemetry';
13
14
  import { userAuth } from '../middleware/userAuth';
14
15
  import { trpc } from './init';
15
16
  import { oidcAuth } from './middleware/oidcAuth';
@@ -24,14 +25,16 @@ export const router = trpc.router;
24
25
  * Create an unprotected procedure
25
26
  * @link https://trpc.io/docs/v11/procedures
26
27
  **/
27
- export const publicProcedure = trpc.procedure.use(({ next, ctx }) => {
28
+ const baseProcedure = trpc.procedure.use(openTelemetry);
29
+
30
+ export const publicProcedure = baseProcedure.use(({ next, ctx }) => {
28
31
  return next({
29
32
  ctx: { ...ctx, userId: isDesktop ? DESKTOP_USER_ID : ctx.userId },
30
33
  });
31
34
  });
32
35
 
33
36
  // procedure that asserts that the user is logged in
34
- export const authedProcedure = trpc.procedure.use(oidcAuth).use(userAuth);
37
+ export const authedProcedure = baseProcedure.use(oidcAuth).use(userAuth);
35
38
 
36
39
  /**
37
40
  * Create a server-side caller
@@ -0,0 +1,141 @@
1
+ import type { Attributes, Span } from '@lobechat/observability-otel/api';
2
+ import { SpanKind, SpanStatusCode, diag, trace } from '@lobechat/observability-otel/api';
3
+ import {
4
+ ATTR_ERROR_TYPE,
5
+ ATTR_EXCEPTION_MESSAGE,
6
+ ATTR_EXCEPTION_STACKTRACE,
7
+ DEFAULT_ERROR_CODE,
8
+ DEFAULT_SUCCESS_STATUS,
9
+ TRPCAttribute,
10
+ createAttributesForMetrics,
11
+ getPayloadSize,
12
+ serverDurationHistogram,
13
+ serverRequestSizeHistogram,
14
+ serverRequestsPerRpcHistogram,
15
+ serverResponseSizeHistogram,
16
+ serverResponsesPerRpcHistogram,
17
+ tRPCConventionFromPathAndType,
18
+ } from '@lobechat/observability-otel/trpc';
19
+ import { TRPCError } from '@trpc/server';
20
+ import { env } from 'node:process';
21
+
22
+ import { name } from '../../../../package.json';
23
+ import { trpc } from '../lambda/init';
24
+
25
+ const tracer = trace.getTracer('trpc-server');
26
+
27
+ const recordRpcServerMetrics = ({
28
+ attributes,
29
+ durationMs,
30
+ requestSize,
31
+ responseSize,
32
+ }: {
33
+ attributes: Attributes;
34
+ durationMs: number;
35
+ requestSize?: number;
36
+ responseSize?: number;
37
+ }) => {
38
+ serverDurationHistogram.record(durationMs, attributes);
39
+ serverRequestsPerRpcHistogram.record(1, attributes);
40
+ serverResponsesPerRpcHistogram.record(1, attributes);
41
+
42
+ if (typeof requestSize === 'number') {
43
+ serverRequestSizeHistogram.record(requestSize, attributes);
44
+ }
45
+
46
+ if (typeof responseSize === 'number') {
47
+ serverResponseSizeHistogram.record(responseSize, attributes);
48
+ }
49
+ };
50
+
51
+ const finalizeSpanWithError = (span: Span, error: unknown) => {
52
+ span.setStatus({
53
+ code: SpanStatusCode.ERROR,
54
+ message: error instanceof Error ? error.message : 'Unknown error',
55
+ });
56
+
57
+ if (error instanceof Error) {
58
+ span.recordException(error);
59
+ span.setAttribute(ATTR_ERROR_TYPE, error.constructor.name);
60
+ span.setAttribute(ATTR_EXCEPTION_MESSAGE, error.message);
61
+ span.setAttribute(ATTR_EXCEPTION_STACKTRACE, error.stack || '');
62
+ }
63
+ };
64
+
65
+ export const openTelemetry = trpc.middleware(async ({ path, type, next, getRawInput }) => {
66
+ if (!env.ENABLE_TELEMETRY) {
67
+ diag.debug(name, 'telemetry disabled', env.ENABLE_TELEMETRY);
68
+
69
+ return next();
70
+ }
71
+
72
+ diag.debug(name, 'tRPC instrumentation', 'incomingRequest');
73
+
74
+ const spanName = `tRPC ${type.toUpperCase()} ${path}`;
75
+ const baseAttributes = tRPCConventionFromPathAndType(path, type);
76
+ const input = getRawInput();
77
+ const requestSize = getPayloadSize(input);
78
+
79
+ const span = tracer.startSpan(spanName, {
80
+ attributes: baseAttributes,
81
+ kind: SpanKind.SERVER,
82
+ });
83
+
84
+ const startTimestamp = Date.now();
85
+
86
+ try {
87
+ const result = await next();
88
+ diag.debug(name, 'tRPC instrumentation', 'requestHandled');
89
+
90
+ const responseSize = getPayloadSize(result.ok ? result.data : result.error);
91
+
92
+ const durationMs = Date.now() - startTimestamp;
93
+ const statusCode = result.ok ? DEFAULT_SUCCESS_STATUS : result.error.code;
94
+ span.setAttribute(TRPCAttribute.RPC_TRPC_STATUS_CODE, statusCode);
95
+
96
+ if (result.ok) {
97
+ span.setStatus({ code: SpanStatusCode.OK });
98
+ } else {
99
+ finalizeSpanWithError(span, result.error);
100
+ }
101
+
102
+ recordRpcServerMetrics({
103
+ attributes: createAttributesForMetrics(baseAttributes, statusCode, {
104
+ [TRPCAttribute.RPC_TRPC_SUCCESS]: result.ok,
105
+ ...(result.ok ? undefined : { [ATTR_ERROR_TYPE]: result.error.code }),
106
+ }),
107
+ durationMs,
108
+ requestSize,
109
+ responseSize,
110
+ });
111
+
112
+ diag.debug(name, 'tRPC instrumentation', 'metrics recorded');
113
+
114
+ return result;
115
+ } catch (error) {
116
+ diag.error(name, 'tRPC instrumentation', 'requestError', error);
117
+
118
+ const durationMs = Date.now() - startTimestamp;
119
+ const trpcError = error instanceof TRPCError ? error : undefined;
120
+ const statusCode = trpcError ? trpcError.code : DEFAULT_ERROR_CODE;
121
+
122
+ span.setAttribute(TRPCAttribute.RPC_TRPC_STATUS_CODE, statusCode);
123
+ finalizeSpanWithError(span, error);
124
+
125
+ recordRpcServerMetrics({
126
+ attributes: createAttributesForMetrics(baseAttributes, statusCode, {
127
+ [TRPCAttribute.RPC_TRPC_SUCCESS]: false,
128
+ ...(trpcError ? { [ATTR_ERROR_TYPE]: trpcError.code } : undefined),
129
+ }),
130
+ durationMs,
131
+ requestSize,
132
+ responseSize: getPayloadSize(trpcError ? trpcError : error),
133
+ });
134
+
135
+ diag.error(name, 'tRPC instrumentation', 'metrics recorded with error', error);
136
+
137
+ throw error;
138
+ } finally {
139
+ span.end();
140
+ }
141
+ });
@@ -147,5 +147,49 @@ export default {
147
147
  profile: '个人资料',
148
148
  security: '安全',
149
149
  stats: '数据统计',
150
+ usage: '用量统计',
151
+ },
152
+ usage: {
153
+ activeModels: {
154
+ modelTable: '模型列表',
155
+ models: '活跃模型',
156
+ providerTable: '提供商列表',
157
+ providers: '活跃提供商',
158
+ table: {
159
+ calls: '调用次数',
160
+ model: '模型',
161
+ provider: '提供商',
162
+ spend: '花费',
163
+ },
164
+ },
165
+ cards: {
166
+ month: {
167
+ modelCalls: '模型调用',
168
+ title: '本月花费',
169
+ },
170
+ today: {
171
+ title: '今日花费',
172
+ yesterday: '昨日',
173
+ },
174
+ },
175
+ table: {
176
+ actions: '操作',
177
+ createdAt: '使用时间',
178
+ inputTokens: '输入 Token',
179
+ model: '模型',
180
+ outputTokens: '输出 Token',
181
+ spend: '花费',
182
+ tps: 'TPS',
183
+ ttft: 'TTFT',
184
+ type: '调用类型',
185
+ },
186
+ trends: {
187
+ spend: '金额',
188
+ tokens: 'Token',
189
+ },
190
+ welcome: {
191
+ model: '模型',
192
+ provider: '提供商',
193
+ },
150
194
  },
151
195
  };
@@ -188,6 +188,7 @@ export default {
188
188
  },
189
189
 
190
190
  messageAction: {
191
+ continueGeneration: '继续生成',
191
192
  delAndRegenerate: '删除并重新生成',
192
193
  deleteDisabledByThreads: '存在子话题,不能删除',
193
194
  regenerate: '重新生成',
@@ -87,13 +87,11 @@ export const mcpRouter = router({
87
87
  )
88
88
  .mutation(async ({ input }) => {
89
89
  // Pass the validated params, toolName, and args to the service
90
- const data = await mcpService.callTool(
90
+ return await mcpService.callTool(
91
91
  { ...input.params, env: input.env },
92
92
  input.toolName,
93
93
  input.args,
94
94
  );
95
-
96
- return JSON.stringify(data);
97
95
  }),
98
96
 
99
97
  validMcpServerInstallable: mcpProcedure
@@ -30,6 +30,7 @@ import { sessionGroupRouter } from './sessionGroup';
30
30
  import { threadRouter } from './thread';
31
31
  import { topicRouter } from './topic';
32
32
  import { uploadRouter } from './upload';
33
+ import { usageRouter } from './usage';
33
34
  import { userRouter } from './user';
34
35
 
35
36
  export const lambdaRouter = router({
@@ -61,6 +62,7 @@ export const lambdaRouter = router({
61
62
  thread: threadRouter,
62
63
  topic: topicRouter,
63
64
  upload: uploadRouter,
65
+ usage: usageRouter,
64
66
  user: userRouter,
65
67
  });
66
68
 
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+
3
+ import { authedProcedure, router } from '@/libs/trpc/lambda';
4
+ import { serverDatabase } from '@/libs/trpc/lambda/middleware';
5
+ import { UsageRecordService } from '@/server/services/usage';
6
+
7
+ const usageProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
8
+ const { ctx } = opts;
9
+ return opts.next({
10
+ ctx: {
11
+ usageRecordService: new UsageRecordService(ctx.serverDB, ctx.userId),
12
+ },
13
+ });
14
+ });
15
+
16
+ export const usageRouter = router({
17
+ findAndGroupByDay: usageProcedure
18
+ .input(
19
+ z.object({
20
+ mo: z.string().optional(),
21
+ }),
22
+ )
23
+ .query(async ({ ctx, input }) => {
24
+ return await ctx.usageRecordService.findAndGroupByDay(input.mo);
25
+ }),
26
+
27
+ findByMonth: usageProcedure
28
+ .input(
29
+ z.object({
30
+ mo: z.string().optional(),
31
+ }),
32
+ )
33
+ .query(async ({ ctx, input }) => {
34
+ return await ctx.usageRecordService.findByMonth(input.mo);
35
+ }),
36
+ });
@@ -100,8 +100,6 @@ export const mcpRouter = router({
100
100
  checkStdioEnvironment(input.params);
101
101
 
102
102
  // Pass the validated params, toolName, and args to the service
103
- const data = await mcpService.callTool(input.params, input.toolName, input.args);
104
-
105
- return JSON.stringify(data);
103
+ return await mcpService.callTool(input.params, input.toolName, input.args);
106
104
  }),
107
105
  });
@@ -33,7 +33,7 @@ describe('MCPService', () => {
33
33
  args: ['--test'],
34
34
  };
35
35
 
36
- it('should return original data when content array is empty', async () => {
36
+ it('should return MCPToolCallResult when content array is empty', async () => {
37
37
  mockClient.callTool.mockResolvedValue({
38
38
  content: [],
39
39
  isError: false,
@@ -41,10 +41,12 @@ describe('MCPService', () => {
41
41
 
42
42
  const result = await mcpService.callTool(mockParams, 'testTool', '{}');
43
43
 
44
- expect(result).toEqual([]);
44
+ expect(result.content).toBe('');
45
+ expect(result.success).toBe(true);
46
+ expect(result.state).toEqual({ content: [], isError: false });
45
47
  });
46
48
 
47
- it('should return original data when content is null or undefined', async () => {
49
+ it('should handle null content', async () => {
48
50
  mockClient.callTool.mockResolvedValue({
49
51
  content: null,
50
52
  isError: false,
@@ -52,10 +54,11 @@ describe('MCPService', () => {
52
54
 
53
55
  const result = await mcpService.callTool(mockParams, 'testTool', '{}');
54
56
 
55
- expect(result).toBeNull();
57
+ expect(result.content).toBe('');
58
+ expect(result.success).toBe(true);
56
59
  });
57
60
 
58
- it('should return parsed JSON when single element contains valid JSON', async () => {
61
+ it('should return JSON string when single element contains valid JSON', async () => {
59
62
  const jsonData = { message: 'Hello World', status: 'success' };
60
63
  mockClient.callTool.mockResolvedValue({
61
64
  content: [{ type: 'text', text: JSON.stringify(jsonData) }],
@@ -64,7 +67,8 @@ describe('MCPService', () => {
64
67
 
65
68
  const result = await mcpService.callTool(mockParams, 'testTool', '{}');
66
69
 
67
- expect(result).toEqual(jsonData);
70
+ expect(result.content).toBe(JSON.stringify(jsonData));
71
+ expect(result.success).toBe(true);
68
72
  });
69
73
 
70
74
  it('should return plain text when single element contains non-JSON text', async () => {
@@ -76,10 +80,12 @@ describe('MCPService', () => {
76
80
 
77
81
  const result = await mcpService.callTool(mockParams, 'testTool', '{}');
78
82
 
79
- expect(result).toBe(textData);
83
+ expect(result.content).toBe(textData);
84
+ expect(result.success).toBe(true);
85
+ expect(result.state.content).toEqual([{ type: 'text', text: textData }]);
80
86
  });
81
87
 
82
- it('should return original data when single element has no text', async () => {
88
+ it('should return empty content when single element has no text', async () => {
83
89
  const contentData = [{ type: 'text', text: '' }];
84
90
  mockClient.callTool.mockResolvedValue({
85
91
  content: contentData,
@@ -88,10 +94,12 @@ describe('MCPService', () => {
88
94
 
89
95
  const result = await mcpService.callTool(mockParams, 'testTool', '{}');
90
96
 
91
- expect(result).toEqual(contentData);
97
+ expect(result.content).toBe('');
98
+ expect(result.success).toBe(true);
99
+ expect(result.state.content).toEqual(contentData);
92
100
  });
93
101
 
94
- it('should return complete array when content has multiple elements', async () => {
102
+ it('should join multiple text elements with double newlines', async () => {
95
103
  const multipleContent = [
96
104
  { type: 'text', text: 'First message' },
97
105
  { type: 'text', text: 'Second message' },
@@ -105,11 +113,12 @@ describe('MCPService', () => {
105
113
 
106
114
  const result = await mcpService.callTool(mockParams, 'testTool', '{}');
107
115
 
108
- // 应该直接返回完整的数组,不进行任何处理
109
- expect(result).toEqual(multipleContent);
116
+ expect(result.content).toBe('First message\n\nSecond message\n\n{"json": "data"}');
117
+ expect(result.success).toBe(true);
118
+ expect(result.state.content).toEqual(multipleContent);
110
119
  });
111
120
 
112
- it('should return complete array when content has two elements', async () => {
121
+ it('should join two text elements with double newline', async () => {
113
122
  const twoContent = [
114
123
  { type: 'text', text: 'First message' },
115
124
  { type: 'text', text: 'Second message' },
@@ -122,7 +131,9 @@ describe('MCPService', () => {
122
131
 
123
132
  const result = await mcpService.callTool(mockParams, 'testTool', '{}');
124
133
 
125
- expect(result).toEqual(twoContent);
134
+ expect(result.content).toBe('First message\n\nSecond message');
135
+ expect(result.success).toBe(true);
136
+ expect(result.state.content).toEqual(twoContent);
126
137
  });
127
138
 
128
139
  it('should return error result when isError is true', async () => {
@@ -135,7 +146,9 @@ describe('MCPService', () => {
135
146
 
136
147
  const result = await mcpService.callTool(mockParams, 'testTool', '{}');
137
148
 
138
- expect(result).toEqual(errorResult);
149
+ expect(result.content).toBe('Error occurred');
150
+ expect(result.success).toBe(true);
151
+ expect(result.state).toEqual(errorResult);
139
152
  });
140
153
 
141
154
  it('should throw TRPCError when client throws error', async () => {
@@ -175,29 +175,40 @@ export class MCPService {
175
175
  loggableParams,
176
176
  result,
177
177
  );
178
- const { content, isError } = result;
179
178
 
180
- if (isError) return result;
181
-
182
- const data = content as { text: string; type: 'text' }[];
183
-
184
- if (!data || data.length === 0) return data;
185
-
186
- if (data.length > 1) return data;
187
-
188
- const text = data[0]?.text;
189
- if (!text) return data;
190
-
191
- // try to get json object, which will be stringify in the client
192
- const json = safeParseJSON(text);
193
- if (json) return json;
194
-
195
- return text;
179
+ // TODO: map more type
180
+ const content = result.content
181
+ ? result.content
182
+ .map((item) => {
183
+ switch (item.type) {
184
+ case 'text': {
185
+ return item.text;
186
+ }
187
+ default: {
188
+ return '';
189
+ }
190
+ }
191
+ })
192
+ .filter(Boolean)
193
+ .join('\n\n')
194
+ : '';
195
+
196
+ if (result.isError) return { content, state: result, success: true };
197
+
198
+ return { content, state: result, success: true };
196
199
  } catch (error) {
197
200
  if (error instanceof McpError) {
198
201
  const mcpError = error as McpError;
199
202
 
200
- return mcpError.message;
203
+ return {
204
+ content: mcpError.message,
205
+ error: error,
206
+ state: {
207
+ content: [{ text: mcpError.message, type: 'text' }],
208
+ isError: true,
209
+ },
210
+ success: false,
211
+ };
201
212
  }
202
213
 
203
214
  console.error(