@opensumi/ide-ai-native 3.8.1-next-1740571693.0 → 3.8.1-next-1740625266.0

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 (71) hide show
  1. package/lib/browser/ai-core.contribution.d.ts +4 -1
  2. package/lib/browser/ai-core.contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-core.contribution.js +20 -1
  4. package/lib/browser/ai-core.contribution.js.map +1 -1
  5. package/lib/browser/chat/chat-manager.service.d.ts +1 -0
  6. package/lib/browser/chat/chat-manager.service.d.ts.map +1 -1
  7. package/lib/browser/chat/chat-manager.service.js +13 -0
  8. package/lib/browser/chat/chat-manager.service.js.map +1 -1
  9. package/lib/browser/chat/chat.internal.service.d.ts +1 -0
  10. package/lib/browser/chat/chat.internal.service.d.ts.map +1 -1
  11. package/lib/browser/chat/chat.internal.service.js +3 -0
  12. package/lib/browser/chat/chat.internal.service.js.map +1 -1
  13. package/lib/browser/components/ChatHistory.d.ts +0 -1
  14. package/lib/browser/components/ChatHistory.d.ts.map +1 -1
  15. package/lib/browser/components/ChatHistory.js +14 -14
  16. package/lib/browser/components/ChatHistory.js.map +1 -1
  17. package/lib/browser/mcp/base-apply.service.d.ts +31 -40
  18. package/lib/browser/mcp/base-apply.service.d.ts.map +1 -1
  19. package/lib/browser/mcp/base-apply.service.js +233 -167
  20. package/lib/browser/mcp/base-apply.service.js.map +1 -1
  21. package/lib/browser/mcp/tools/components/EditFile.d.ts.map +1 -1
  22. package/lib/browser/mcp/tools/components/EditFile.js +55 -41
  23. package/lib/browser/mcp/tools/components/EditFile.js.map +1 -1
  24. package/lib/browser/mcp/tools/components/index.module.less +22 -3
  25. package/lib/browser/mcp/tools/editFile.js +1 -1
  26. package/lib/browser/mcp/tools/editFile.js.map +1 -1
  27. package/lib/browser/mcp/tools/handlers/EditFile.d.ts +5 -1
  28. package/lib/browser/mcp/tools/handlers/EditFile.d.ts.map +1 -1
  29. package/lib/browser/mcp/tools/handlers/EditFile.js +4 -4
  30. package/lib/browser/mcp/tools/handlers/EditFile.js.map +1 -1
  31. package/lib/browser/model/msg-history-manager.d.ts +1 -0
  32. package/lib/browser/model/msg-history-manager.d.ts.map +1 -1
  33. package/lib/browser/model/msg-history-manager.js +12 -2
  34. package/lib/browser/model/msg-history-manager.js.map +1 -1
  35. package/lib/browser/types.d.ts +1 -1
  36. package/lib/browser/types.d.ts.map +1 -1
  37. package/lib/browser/widget/inline-diff/inline-diff-manager.d.ts +6 -0
  38. package/lib/browser/widget/inline-diff/inline-diff-manager.d.ts.map +1 -0
  39. package/lib/browser/widget/inline-diff/inline-diff-manager.js +27 -0
  40. package/lib/browser/widget/inline-diff/inline-diff-manager.js.map +1 -0
  41. package/lib/browser/widget/inline-diff/inline-diff-widget.module.less +12 -0
  42. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.d.ts +2 -0
  43. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.d.ts.map +1 -1
  44. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.js +11 -4
  45. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.js.map +1 -1
  46. package/lib/common/types.d.ts +17 -0
  47. package/lib/common/types.d.ts.map +1 -1
  48. package/lib/common/types.js.map +1 -1
  49. package/lib/node/base-language-model.d.ts +1 -1
  50. package/lib/node/base-language-model.d.ts.map +1 -1
  51. package/lib/node/base-language-model.js +54 -3
  52. package/lib/node/base-language-model.js.map +1 -1
  53. package/package.json +23 -23
  54. package/src/browser/ai-core.contribution.ts +25 -1
  55. package/src/browser/chat/chat-manager.service.ts +12 -0
  56. package/src/browser/chat/chat.internal.service.ts +4 -0
  57. package/src/browser/components/ChatHistory.tsx +21 -15
  58. package/src/browser/mcp/base-apply.service.ts +266 -213
  59. package/src/browser/mcp/tools/components/EditFile.tsx +82 -60
  60. package/src/browser/mcp/tools/components/index.module.less +22 -3
  61. package/src/browser/mcp/tools/editFile.ts +2 -2
  62. package/src/browser/mcp/tools/handlers/EditFile.ts +4 -4
  63. package/src/browser/model/msg-history-manager.ts +12 -2
  64. package/src/browser/types.ts +1 -1
  65. package/src/browser/widget/inline-diff/inline-diff-manager.tsx +38 -0
  66. package/src/browser/widget/inline-diff/inline-diff-widget.module.less +12 -0
  67. package/src/browser/widget/inline-stream-diff/inline-stream-diff.handler.tsx +13 -4
  68. package/src/common/types.ts +20 -0
  69. package/src/node/base-language-model.ts +63 -1
  70. /package/lib/browser/components/{chat-history.css → chat-history.module.less} +0 -0
  71. /package/src/browser/components/{chat-history.css → chat-history.module.less} +0 -0
@@ -1,5 +1,5 @@
1
1
  import cls from 'classnames';
2
- import React, { useEffect, useMemo } from 'react';
2
+ import React, { useEffect, useMemo, useState } from 'react';
3
3
 
4
4
  import { Icon, Popover } from '@opensumi/ide-components';
5
5
  import {
@@ -10,7 +10,6 @@ import {
10
10
  Uri,
11
11
  detectModeId,
12
12
  path,
13
- useAutorun,
14
13
  useInjectable,
15
14
  } from '@opensumi/ide-core-browser';
16
15
  import { Loading } from '@opensumi/ide-core-browser/lib/components/ai-native';
@@ -18,61 +17,24 @@ import { ILanguageService } from '@opensumi/monaco-editor-core/esm/vs/editor/com
18
17
  import { IModelService } from '@opensumi/monaco-editor-core/esm/vs/editor/common/services/model';
19
18
  import { StandaloneServices } from '@opensumi/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
20
19
 
20
+ import { CodeBlockData } from '../../../../common/types';
21
21
  import { ChatMarkdown } from '../../../components/ChatMarkdown';
22
22
  import { IMCPServerToolComponentProps } from '../../../types';
23
- import { BaseApplyService, CodeBlockData } from '../../base-apply.service';
23
+ import { BaseApplyService } from '../../base-apply.service';
24
24
 
25
25
  import styles from './index.module.less';
26
26
 
27
- const renderStatus = (codeBlockData: CodeBlockData) => {
28
- const status = codeBlockData.status;
29
- switch (status) {
30
- case 'generating':
31
- return <Loading />;
32
- case 'pending':
33
- return (
34
- <Popover title={status} id={'edit-file-tool-status-pending'}>
35
- <Icon iconClass='codicon codicon-circle-large' />
36
- </Popover>
37
- );
38
- case 'success':
39
- return (
40
- <Popover title={status} id={'edit-file-tool-status-success'}>
41
- <Icon iconClass='codicon codicon-check-all' />
42
- </Popover>
43
- );
44
- case 'failed':
45
- return (
46
- <Popover title={status} id={'edit-file-tool-status-failed'}>
47
- <Icon iconClass='codicon codicon-error' color='var(--vscode-input-errorForeground)' />
48
- </Popover>
49
- );
50
- case 'cancelled':
51
- return (
52
- <Popover title={status} id={'edit-file-tool-status-cancelled'}>
53
- <Icon iconClass='codicon codicon-close' color='var(--vscode-input-placeholderForeground)' />
54
- </Popover>
55
- );
56
- default:
57
- return null;
58
- }
59
- };
60
-
61
27
  export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
62
28
  const { args, messageId, toolCallId } = props;
29
+ const [mode, setMode] = useState<'code' | 'diff'>('code');
63
30
  const labelService = useInjectable(LabelService);
64
31
  const appConfig = useInjectable<AppConfig>(AppConfig);
65
32
  const applyService = useInjectable<BaseApplyService>(BaseApplyService);
66
33
  const { target_file = '', code_edit, instructions } = args || {};
67
34
  const absolutePath = path.join(appConfig.workspaceDir, target_file);
68
-
69
- const codeBlockData = applyService.getCodeBlock(absolutePath, messageId);
70
-
71
- useAutorun(applyService.codeBlockMapObservable);
72
-
73
- if (toolCallId && codeBlockData) {
74
- applyService.initToolCallId(codeBlockData.id, toolCallId);
75
- }
35
+ const [codeBlockData, setCodeBlockData] = useState<CodeBlockData | undefined>(
36
+ applyService.getCodeBlock(toolCallId, messageId),
37
+ );
76
38
 
77
39
  const icon = useMemo(() => {
78
40
  if (!target_file) {
@@ -91,40 +53,66 @@ export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
91
53
  return detectedModeId;
92
54
  }, [target_file, absolutePath]);
93
55
 
56
+ useEffect(() => {
57
+ const disposable = applyService.onCodeBlockUpdate((codeBlockData) => {
58
+ setCodeBlockData({ ...codeBlockData });
59
+ });
60
+ return () => {
61
+ disposable.dispose();
62
+ };
63
+ }, []);
64
+
94
65
  // 多次迭代时,仅在首处tool组件中展示
95
- if (!args || !codeBlockData || (toolCallId && toolCallId !== codeBlockData.initToolCallId)) {
66
+ // FIXME: 这个优化有必要吗?每次都展示也挺好?
67
+ if (!args || !codeBlockData) {
96
68
  return null;
97
69
  }
98
70
 
99
71
  return [
100
72
  instructions && <p>{instructions}</p>,
101
- <div className={styles['edit-file-tool']} key={`edit-file-tool-${codeBlockData.id}`}>
73
+ <div className={styles['edit-file-tool']} key={'edit-file-tool'}>
102
74
  <div
103
75
  className={cls(styles['edit-file-tool-header'], {
104
76
  clickable: codeBlockData.status === 'pending' || codeBlockData.status === 'success',
105
77
  })}
106
78
  onClick={() => {
107
79
  if (codeBlockData.status === 'pending') {
108
- applyService.reRenderPendingApply();
80
+ applyService.renderApplyResult(codeBlockData, codeBlockData.updatedCode!);
109
81
  } else if (codeBlockData.status === 'success') {
110
- applyService.revealApplyPosition(codeBlockData.id);
82
+ applyService.revealApplyPosition(codeBlockData);
111
83
  }
112
84
  }}
113
85
  >
114
- {icon && <span className={icon}></span>}
115
- <span className={styles['edit-file-tool-file-name']}>{target_file}</span>
116
- {codeBlockData.iterationCount > 1 && (
117
- <span className={styles['edit-file-tool-iteration-count']}>{codeBlockData.iterationCount}/3</span>
118
- )}
119
- {renderStatus(codeBlockData)}
86
+ <div className={styles.left}>
87
+ {icon && <span className={icon}></span>}
88
+ <span className={styles['edit-file-tool-file-name']}>{target_file}</span>
89
+ {codeBlockData.iterationCount > 1 && (
90
+ <span className={styles['edit-file-tool-iteration-count']}>{codeBlockData.iterationCount}/3</span>
91
+ )}
92
+ {renderStatus(codeBlockData)}
93
+ </div>
94
+ <div className={styles.right}>
95
+ <Popover title={'Show Code'} id={'edit-file-tool-show-code'}>
96
+ <Icon iconClass='codicon codicon-file-code' onClick={() => setMode('code')} />
97
+ </Popover>
98
+ {codeBlockData.applyResult?.diff && (
99
+ <Popover title={'Show Diff'} id={'edit-file-tool-show-diff'}>
100
+ <Icon iconClass='codicon codicon-diff-multiple' onClick={() => setMode('diff')} />
101
+ </Popover>
102
+ )}
103
+ </div>
120
104
  </div>
121
- <ChatMarkdown markdown={`\`\`\`${languageId || ''}\n${code_edit}\n\`\`\``} hideInsert={true} />
105
+ <ChatMarkdown
106
+ markdown={
107
+ mode === 'code'
108
+ ? `\`\`\`${languageId || ''}\n${code_edit}\n\`\`\``
109
+ : `\`\`\`diff\n${codeBlockData.applyResult?.diff}\n\`\`\``
110
+ }
111
+ hideInsert={true}
112
+ />
122
113
  </div>,
123
114
  codeBlockData.applyResult && codeBlockData.applyResult.diagnosticInfos.length > 0 && (
124
- <div
125
- className={styles['edit-file-tool-diagnostic-errors']}
126
- key={`edit-file-tool-diagnostic-errors-${codeBlockData.id}`}
127
- >
115
+ <div className={styles['edit-file-tool-diagnostic-errors']} key={'edit-file-tool-diagnostic-errors'}>
128
116
  <div className={styles['title']}>Found Lints:</div>
129
117
  {codeBlockData.applyResult?.diagnosticInfos.map((info) => (
130
118
  <div
@@ -142,3 +130,37 @@ export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
142
130
  ),
143
131
  ];
144
132
  };
133
+
134
+ const renderStatus = (codeBlockData: CodeBlockData) => {
135
+ const status = codeBlockData.status;
136
+ switch (status) {
137
+ case 'generating':
138
+ return <Loading />;
139
+ case 'pending':
140
+ return (
141
+ <Popover title='Pending' id={'edit-file-tool-status-pending'}>
142
+ <Icon iconClass='codicon codicon-circle-large' />
143
+ </Popover>
144
+ );
145
+ case 'success':
146
+ return (
147
+ <Popover title='Success' id={'edit-file-tool-status-success'}>
148
+ <Icon iconClass='codicon codicon-check-all' />
149
+ </Popover>
150
+ );
151
+ case 'failed':
152
+ return (
153
+ <Popover title='Failed' id={'edit-file-tool-status-failed'}>
154
+ <Icon iconClass='codicon codicon-error' style={{ color: 'var(--debugConsole-errorForeground)' }} />
155
+ </Popover>
156
+ );
157
+ case 'cancelled':
158
+ return (
159
+ <Popover title='Cancelled' id={'edit-file-tool-status-cancelled'}>
160
+ <Icon iconClass='codicon codicon-close' style={{ color: 'var(--input-placeholderForeground)' }} />
161
+ </Popover>
162
+ );
163
+ default:
164
+ return null;
165
+ }
166
+ };
@@ -1,15 +1,14 @@
1
1
  .edit-file-tool-header {
2
2
  display: flex;
3
3
  align-items: center;
4
+ justify-content: space-between;
4
5
  padding: 2px 8px;
5
6
  border-bottom: 1px solid var(--vscode-commandCenter-inactiveBorder);
6
7
  background-color: var(--design-block-background);
7
8
  font-size: 10px;
8
9
  margin-bottom: -4px;
9
10
  border-radius: 8px 8px 0 0;
10
- > span {
11
- margin-right: 4px;
12
- }
11
+ white-space: nowrap;
13
12
  :global(span.codicon) {
14
13
  font-size: 12px;
15
14
  }
@@ -18,6 +17,26 @@
18
17
  align-items: center;
19
18
  justify-content: center;
20
19
  }
20
+ .left,
21
+ .right {
22
+ display: flex;
23
+ align-items: center;
24
+ }
25
+ &::after {
26
+ display: none;
27
+ }
28
+ .left {
29
+ > span {
30
+ margin-right: 4px;
31
+ }
32
+ }
33
+ .right > div {
34
+ margin-left: 4px;
35
+ }
36
+ }
37
+ .edit-file-tool-file-name {
38
+ text-overflow: ellipsis;
39
+ overflow: hidden;
21
40
  }
22
41
  .edit-file-tool {
23
42
  border: 1px solid var(--vscode-commandCenter-inactiveBorder);
@@ -69,8 +69,8 @@ You should specify the following arguments before the others: [target_file]`,
69
69
  };
70
70
  }
71
71
 
72
- private async handler(args: z.infer<typeof inputSchema>, logger: MCPLogger) {
73
- const result = await this.editFileHandler.handler(args.targetFile, args.codeEdit, args.instructions);
72
+ private async handler(args: z.infer<typeof inputSchema> & { toolCallId: string }, logger: MCPLogger) {
73
+ const result = await this.editFileHandler.handler(args, args.toolCallId);
74
74
  return {
75
75
  content: [
76
76
  {
@@ -12,10 +12,10 @@ export class EditFileHandler {
12
12
  @Autowired(BaseApplyService)
13
13
  private applyService: BaseApplyService;
14
14
 
15
- async handler(relativePath: string, updateContent: string, instructions?: string) {
16
- // TODO: ignore file
17
- this.applyService.registerCodeBlock(relativePath, updateContent);
18
- const blockData = await this.applyService.apply(relativePath, updateContent, instructions);
15
+ async handler(params: { targetFile: string; codeEdit: string; instructions?: string }, toolCallId: string) {
16
+ const { targetFile, codeEdit } = params;
17
+ const block = this.applyService.registerCodeBlock(targetFile, codeEdit, toolCallId);
18
+ const blockData = await this.applyService.apply(block);
19
19
  return blockData;
20
20
  }
21
21
  }
@@ -66,6 +66,11 @@ export class MsgHistoryManager extends Disposable {
66
66
  return this.startIndex;
67
67
  }
68
68
 
69
+ public get lastMessageId(): string | undefined {
70
+ const list = this.messageList;
71
+ return list[list.length - 1]?.id;
72
+ }
73
+
69
74
  public getMessages(maxTokens?: number): IHistoryChatMessage[] {
70
75
  if (maxTokens && this.totalTokens > maxTokens) {
71
76
  while (this.totalTokens > maxTokens) {
@@ -109,9 +114,14 @@ export class MsgHistoryManager extends Disposable {
109
114
  return;
110
115
  }
111
116
 
112
- this.messageAdditionalMap.set(id, additional);
117
+ const oldAdditional = this.messageAdditionalMap.get(id) || {};
118
+ const newAdditional = {
119
+ ...oldAdditional,
120
+ ...additional,
121
+ };
113
122
 
114
- this._onMessageAdditionalChange.fire(additional);
123
+ this.messageAdditionalMap.set(id, newAdditional);
124
+ this._onMessageAdditionalChange.fire(newAdditional);
115
125
  }
116
126
 
117
127
  public getMessageAdditional(id: string): Record<string, any> {
@@ -368,7 +368,7 @@ export interface IMCPServerToolComponentProps {
368
368
  result?: any;
369
369
  index?: number;
370
370
  messageId?: string;
371
- toolCallId?: string;
371
+ toolCallId: string;
372
372
  }
373
373
 
374
374
  export interface IMCPServerRegistry {
@@ -0,0 +1,38 @@
1
+ import React, { useEffect, useState } from 'react';
2
+
3
+ import { Button } from '@opensumi/ide-components';
4
+ import { localize, useInjectable } from '@opensumi/ide-core-browser';
5
+ import { IResource } from '@opensumi/ide-editor';
6
+
7
+ import { BaseApplyService } from '../../mcp/base-apply.service';
8
+
9
+ import styles from './inline-diff-widget.module.less';
10
+
11
+ export const InlineDiffManager: React.FC<{ resource: IResource }> = (props) => {
12
+ const applyService = useInjectable<BaseApplyService>(BaseApplyService);
13
+ const [show, setShow] = useState(true);
14
+ useEffect(() => {
15
+ applyService.onCodeBlockUpdate((codeBlock) => {
16
+ setShow(codeBlock.status === 'pending');
17
+ });
18
+ }, []);
19
+ return (
20
+ <div className={styles.inlineDiffManager} style={{ display: show ? 'flex' : 'none' }}>
21
+ <Button
22
+ onClick={() => {
23
+ applyService.processAll(props.resource.uri, 'accept');
24
+ }}
25
+ >
26
+ {localize('aiNative.inlineDiff.acceptAll')}
27
+ </Button>
28
+ <Button
29
+ type='ghost'
30
+ onClick={() => {
31
+ applyService.processAll(props.resource.uri, 'reject');
32
+ }}
33
+ >
34
+ {localize('aiNative.inlineDiff.rejectAll')}
35
+ </Button>
36
+ </div>
37
+ );
38
+ };
@@ -19,3 +19,15 @@
19
19
  display: flex;
20
20
  position: relative;
21
21
  }
22
+
23
+ .inlineDiffManager {
24
+ display: flex;
25
+ padding: 12px 16px;
26
+ justify-content: center;
27
+ position: absolute;
28
+ bottom: 0;
29
+ left: 50%;
30
+ transform: translateX(-50%);
31
+ gap: 12px;
32
+ z-index: 999;
33
+ }
@@ -52,6 +52,9 @@ export class InlineStreamDiffHandler extends Disposable implements IInlineDiffPr
52
52
  protected readonly _onDidEditChange = this.registerDispose(new Emitter<void>());
53
53
  public readonly onDidEditChange: Event<void> = this._onDidEditChange.event;
54
54
 
55
+ protected readonly onDiffFinishedEmitter = this.registerDispose(new Emitter<IComputeDiffData>());
56
+ public readonly onDiffFinished: Event<IComputeDiffData> = this.onDiffFinishedEmitter.event;
57
+
55
58
  public previewerOptions: IDiffPreviewerOptions;
56
59
 
57
60
  private originalModel: ITextModel;
@@ -445,6 +448,8 @@ export class InlineStreamDiffHandler extends Disposable implements IInlineDiffPr
445
448
  this.diffModel.set(currentDiffModel, tx);
446
449
  });
447
450
 
451
+ this.onDiffFinishedEmitter.fire(currentDiffModel);
452
+
448
453
  if (this.originalModel.id === this.monacoEditor.getModel()?.id) {
449
454
  this.renderDiffEdits(currentDiffModel);
450
455
  }
@@ -471,10 +476,13 @@ export class InlineStreamDiffHandler extends Disposable implements IInlineDiffPr
471
476
  }
472
477
 
473
478
  public pushRateFinallyDiffStack(diffModel: IComputeDiffData): void {
474
- // 可能存在 rate editor controller 处理完之后接口层流式才结束
475
- if (this.isEditing === false) {
476
- this.finallyRender(diffModel);
477
- }
479
+ transaction((tx) => {
480
+ this.finallyDiffModel.set(diffModel, tx);
481
+ // 可能存在 rate editor controller 处理完之后接口层流式才结束
482
+ if (this.isEditing === false) {
483
+ this.finallyRender(diffModel);
484
+ }
485
+ });
478
486
  }
479
487
 
480
488
  public finallyRender(diffModel: IComputeDiffData): void {
@@ -487,6 +495,7 @@ export class InlineStreamDiffHandler extends Disposable implements IInlineDiffPr
487
495
  return;
488
496
  }
489
497
 
498
+ this.onDiffFinishedEmitter.fire(diffModel);
490
499
  this.renderPartialEditWidgets(diffModel);
491
500
  this.renderDiffEdits(diffModel);
492
501
  this.pushStackElement();
@@ -1,3 +1,5 @@
1
+ import { IMarker } from '@opensumi/ide-core-browser';
2
+
1
3
  export enum NearestCodeBlockType {
2
4
  Block = 'block',
3
5
  Line = 'line',
@@ -46,3 +48,21 @@ export interface MCPTool {
46
48
  inputSchema: any;
47
49
  providerName: string;
48
50
  }
51
+
52
+ export interface CodeBlockData {
53
+ toolCallId: string;
54
+ codeEdit: string;
55
+ updatedCode?: string;
56
+ relativePath: string;
57
+ status: CodeBlockStatus;
58
+ iterationCount: number;
59
+ createdAt: number;
60
+ version: number;
61
+ instructions?: string;
62
+ applyResult?: {
63
+ diff: string;
64
+ diagnosticInfos: IMarker[];
65
+ };
66
+ }
67
+
68
+ export type CodeBlockStatus = 'generating' | 'pending' | 'success' | 'rejected' | 'failed' | 'cancelled';
@@ -60,6 +60,7 @@ export abstract class BaseLanguageModel {
60
60
  options.topP,
61
61
  options.topK,
62
62
  options.providerOptions,
63
+ options.trimTexts,
63
64
  cancellationToken,
64
65
  );
65
66
  }
@@ -87,6 +88,7 @@ export abstract class BaseLanguageModel {
87
88
  topP?: number,
88
89
  topK?: number,
89
90
  providerOptions?: Record<string, any>,
91
+ trimTexts?: [string, string],
90
92
  cancellationToken?: CancellationToken,
91
93
  ): Promise<any> {
92
94
  try {
@@ -120,9 +122,42 @@ export abstract class BaseLanguageModel {
120
122
  providerOptions,
121
123
  });
122
124
 
125
+ // 状态跟踪变量
126
+ let isFirstChunk = true;
127
+ let bufferedText = '';
128
+ const pendingLines: string[] = [];
123
129
  for await (const chunk of stream.fullStream) {
124
130
  if (chunk.type === 'text-delta') {
125
- chatReadableStream.emitData({ kind: 'content', content: chunk.textDelta });
131
+ if (trimTexts?.length) {
132
+ // 将收到的文本追加到缓冲区
133
+ bufferedText += chunk.textDelta;
134
+
135
+ // 处理第一个文本块的前缀(只处理一次)
136
+ if (isFirstChunk && bufferedText.includes(trimTexts[0])) {
137
+ bufferedText = bufferedText.substring(bufferedText.indexOf(trimTexts[0]) + trimTexts[0].length);
138
+ isFirstChunk = false;
139
+ }
140
+
141
+ // 检查是否有完整的行,并将它们添加到待发送行队列
142
+ const lines = bufferedText.split('\n');
143
+
144
+ // 最后一个元素可能是不完整的行,保留在缓冲区
145
+ bufferedText = lines.pop() || '';
146
+
147
+ // 将完整的行添加到待发送队列
148
+ if (lines.length > 0) {
149
+ pendingLines.push(...lines);
150
+ }
151
+
152
+ // 发送除最后几行外的所有行(保留足够的行以处理后缀)
153
+ while (pendingLines.length > 3) {
154
+ // 保留最后3行以确保能完整识别后缀
155
+ const lineToSend = pendingLines.shift() + '\n';
156
+ chatReadableStream.emitData({ kind: 'content', content: lineToSend });
157
+ }
158
+ } else {
159
+ chatReadableStream.emitData({ kind: 'content', content: chunk.textDelta });
160
+ }
126
161
  } else if (chunk.type === 'tool-call') {
127
162
  chatReadableStream.emitData({
128
163
  kind: 'toolCall',
@@ -169,6 +204,33 @@ export abstract class BaseLanguageModel {
169
204
  }
170
205
  }
171
206
 
207
+ if (trimTexts?.[1]) {
208
+ // 完成处理所有块后,检查并发送剩余文本
209
+
210
+ // 将剩余缓冲区加入待发送行
211
+ if (bufferedText) {
212
+ pendingLines.push(bufferedText);
213
+ }
214
+
215
+ // 处理最后一行可能存在的后缀
216
+ if (pendingLines.length > 0) {
217
+ let lastLine = pendingLines[pendingLines.length - 1];
218
+
219
+ if (lastLine.endsWith(trimTexts[1])) {
220
+ // 移除后缀
221
+ lastLine = lastLine.substring(0, lastLine.length - trimTexts[1].length);
222
+ pendingLines[pendingLines.length - 1] = lastLine;
223
+ }
224
+ }
225
+
226
+ // 发送所有剩余的行
227
+ for (let i = 0; i < pendingLines.length; i++) {
228
+ const isLastLine = i === pendingLines.length - 1;
229
+ const lineToSend = pendingLines[i] + (isLastLine ? '' : '\n');
230
+ chatReadableStream.emitData({ kind: 'content', content: lineToSend });
231
+ }
232
+ }
233
+
172
234
  chatReadableStream.end();
173
235
  } catch (error) {
174
236
  // Use a logger service in production instead of console