@lobehub/lobehub 2.0.0-next.375 → 2.0.0-next.376
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/.github/workflows/claude-migration-support.yml +1 -1
- package/.github/workflows/revalidate-docs.yml +25 -0
- package/CHANGELOG.md +41 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +7 -1
- package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +23 -10
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +261 -0
- package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +36 -0
- package/apps/desktop/src/main/core/infrastructure/BackendProxyProtocolManager.ts +2 -1
- package/changelog/v1.json +12 -0
- package/docs/development/database-schema.dbml +89 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +6 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +5 -0
- package/docs/usage/features/auth.mdx +54 -33
- package/docs/usage/features/auth.zh-CN.mdx +56 -33
- package/locales/en-US/common.json +24 -0
- package/locales/en-US/setting.json +19 -0
- package/locales/zh-CN/authError.json +2 -0
- package/locales/zh-CN/common.json +24 -0
- package/locales/zh-CN/setting.json +19 -0
- package/package.json +2 -1
- package/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx +1 -11
- package/packages/builtin-tool-local-system/src/client/Render/ListFiles/index.tsx +6 -11
- package/packages/database/migrations/0074_add_fk_indexes_for_cascade_delete.sql +56 -0
- package/packages/database/migrations/meta/0074_snapshot.json +10901 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/models/message.ts +84 -81
- package/packages/database/src/repositories/search/index.ts +330 -570
- package/packages/database/src/schemas/agent.ts +4 -0
- package/packages/database/src/schemas/chatGroup.ts +2 -0
- package/packages/database/src/schemas/file.ts +4 -0
- package/packages/database/src/schemas/generation.ts +1 -0
- package/packages/database/src/schemas/message.ts +16 -0
- package/packages/database/src/schemas/nextauth.ts +15 -8
- package/packages/database/src/schemas/oidc.ts +104 -68
- package/packages/database/src/schemas/rag.ts +4 -0
- package/packages/database/src/schemas/ragEvals.ts +97 -73
- package/packages/database/src/schemas/relations.ts +7 -0
- package/packages/database/src/schemas/session.ts +1 -0
- package/packages/database/src/schemas/topic.ts +8 -1
- package/packages/desktop-bridge/src/index.ts +17 -0
- package/scripts/_shared/checkDeprecatedAuth.js +26 -0
- package/src/app/(backend)/trpc/async/[trpc]/route.ts +2 -0
- package/src/app/(backend)/trpc/lambda/[trpc]/route.ts +2 -5
- package/src/app/(backend)/trpc/mobile/[trpc]/route.ts +2 -5
- package/src/app/(backend)/trpc/tools/[trpc]/route.ts +2 -0
- package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +7 -4
- package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/index.tsx +3 -23
- package/src/app/[variants]/(main)/group/features/Conversation/AgentWelcome/index.tsx +3 -23
- package/src/app/[variants]/(main)/resource/(home)/index.tsx +4 -1
- package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +3 -0
- package/src/app/[variants]/(main)/resource/library/_layout/Header/index.tsx +2 -1
- package/src/app/[variants]/(main)/settings/storage/features/Advanced.tsx +19 -12
- package/src/app/[variants]/(main)/settings/storage/index.tsx +19 -1
- package/src/business/client/features/AccountDeletion/index.tsx +3 -0
- package/src/business/server/lambda-routers/accountDeletion.ts +3 -0
- package/src/components/DragUpload/index.tsx +1 -1
- package/src/config/routes/index.ts +9 -1
- package/src/features/CommandMenu/ContextCommands.tsx +10 -2
- package/src/features/CommandMenu/MainMenu.tsx +29 -22
- package/src/features/CommandMenu/SearchResults.tsx +19 -0
- package/src/features/CommandMenu/components/CommandInput.tsx +1 -0
- package/src/features/CommandMenu/index.tsx +25 -3
- package/src/features/CommandMenu/styles.ts +1 -0
- package/src/features/CommandMenu/useCommandMenu.ts +8 -2
- package/src/features/CommandMenu/utils/contextCommands.ts +17 -2
- package/src/features/CommandMenu/utils/queryParser.ts +1 -0
- package/src/features/Conversation/Messages/AssistantGroup/index.tsx +6 -1
- package/src/features/Electron/connection/Connection.tsx +20 -15
- package/src/features/FileViewer/index.tsx +28 -5
- package/src/features/ModelSwitchPanel/components/List/ListItemRenderer.tsx +10 -9
- package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +1 -0
- package/src/features/ModelSwitchPanel/components/Toolbar.tsx +1 -1
- package/src/features/ResourceManager/components/Editor/index.tsx +7 -4
- package/src/features/ResourceManager/components/Explorer/Header/index.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +12 -1
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +23 -43
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/ImageFileItem.tsx +1 -0
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +24 -44
- package/src/features/ResourceManager/components/Explorer/hooks/useFileItemClick.ts +78 -0
- package/src/features/ResourceManager/components/Header/AddButton.tsx +0 -5
- package/src/features/ResourceManager/index.tsx +38 -24
- package/src/libs/better-auth/define-config.ts +17 -0
- package/src/libs/better-auth/plugins/email-whitelist.ts +4 -1
- package/src/libs/redis/manager.ts +5 -9
- package/src/libs/trpc/utils/responseMeta.test.ts +82 -0
- package/src/libs/trpc/utils/responseMeta.ts +41 -0
- package/src/locales/default/authError.ts +3 -0
- package/src/locales/default/common.ts +82 -4
- package/src/locales/default/setting.ts +23 -1
- package/src/server/modules/AgentRuntime/redis.ts +3 -0
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/search.ts +3 -2
- package/src/utils/errorResponse.ts +2 -1
- package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/AddButton.tsx +0 -37
- package/src/app/[variants]/(main)/group/features/Conversation/AgentWelcome/AddButton.tsx +0 -32
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
name: Claude Migration Support
|
|
2
|
-
description: Automatically respond to migration feedback issues using Claude Code
|
|
3
2
|
|
|
4
3
|
on:
|
|
5
4
|
issue_comment:
|
|
@@ -76,6 +75,7 @@ jobs:
|
|
|
76
75
|
|
|
77
76
|
3. Read additional reference files:
|
|
78
77
|
- Main auth documentation: `cat docs/self-hosting/advanced/auth.mdx`
|
|
78
|
+
- Migration internals: `cat docs/self-hosting/advanced/auth/migration-internals.mdx`
|
|
79
79
|
- Deprecated env vars checker: `cat scripts/_shared/checkDeprecatedAuth.js`
|
|
80
80
|
|
|
81
81
|
4. Analyze the user's comment and determine:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Revalidate Docs
|
|
2
|
+
permissions:
|
|
3
|
+
contents: read
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
- next
|
|
10
|
+
paths:
|
|
11
|
+
- 'docs/**'
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
revalidate:
|
|
15
|
+
name: Revalidate Docs
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Trigger docs revalidation
|
|
19
|
+
run: |
|
|
20
|
+
response=$(curl "${{ secrets.DOCS_REVALIDATE_URL }}" --silent --show-error)
|
|
21
|
+
echo "Response: $response"
|
|
22
|
+
if [ "$response" != '{"success":true}' ]; then
|
|
23
|
+
echo "Error: Unexpected response"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.376](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.375...v2.0.0-next.376)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-25**</sup>
|
|
8
|
+
|
|
9
|
+
#### ♻ Code Refactoring
|
|
10
|
+
|
|
11
|
+
- **misc**: Refactor search model implement.
|
|
12
|
+
|
|
13
|
+
#### ✨ Features
|
|
14
|
+
|
|
15
|
+
- **trpc**: Add response metadata and auth header handling.
|
|
16
|
+
|
|
17
|
+
#### 🐛 Bug Fixes
|
|
18
|
+
|
|
19
|
+
- **misc**: Fix add message and improve local system tool.
|
|
20
|
+
|
|
21
|
+
<br/>
|
|
22
|
+
|
|
23
|
+
<details>
|
|
24
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
25
|
+
|
|
26
|
+
#### Code refactoring
|
|
27
|
+
|
|
28
|
+
- **misc**: Refactor search model implement, closes [#11825](https://github.com/lobehub/lobe-chat/issues/11825) ([3cf0bfa](https://github.com/lobehub/lobe-chat/commit/3cf0bfa))
|
|
29
|
+
|
|
30
|
+
#### What's improved
|
|
31
|
+
|
|
32
|
+
- **trpc**: Add response metadata and auth header handling, closes [#11816](https://github.com/lobehub/lobe-chat/issues/11816) ([1276a87](https://github.com/lobehub/lobe-chat/commit/1276a87))
|
|
33
|
+
|
|
34
|
+
#### What's fixed
|
|
35
|
+
|
|
36
|
+
- **misc**: Fix add message and improve local system tool, closes [#11815](https://github.com/lobehub/lobe-chat/issues/11815) ([3b41009](https://github.com/lobehub/lobe-chat/commit/3b41009))
|
|
37
|
+
|
|
38
|
+
</details>
|
|
39
|
+
|
|
40
|
+
<div align="right">
|
|
41
|
+
|
|
42
|
+
[](#readme-top)
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
5
46
|
## [Version 2.0.0-next.375](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.374...v2.0.0-next.375)
|
|
6
47
|
|
|
7
48
|
<sup>Released on **2026-01-25**</sup>
|
|
@@ -548,7 +548,13 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
548
548
|
filesToSearch = [searchPath];
|
|
549
549
|
} else {
|
|
550
550
|
// Use glob pattern if provided, otherwise search all files
|
|
551
|
-
|
|
551
|
+
// If glob doesn't contain directory separator and doesn't start with **,
|
|
552
|
+
// auto-prefix with **/ to make it recursive
|
|
553
|
+
let globPattern = params.glob || '**/*';
|
|
554
|
+
if (params.glob && !params.glob.includes('/') && !params.glob.startsWith('**')) {
|
|
555
|
+
globPattern = `**/${params.glob}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
552
558
|
filesToSearch = await fg(globPattern, {
|
|
553
559
|
absolute: true,
|
|
554
560
|
cwd: searchPath,
|
|
@@ -15,6 +15,19 @@ import { ControllerModule, IpcMethod } from './index';
|
|
|
15
15
|
|
|
16
16
|
const logger = createLogger('controllers:ShellCommandCtr');
|
|
17
17
|
|
|
18
|
+
// Maximum output length to prevent context explosion
|
|
19
|
+
const MAX_OUTPUT_LENGTH = 10_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Truncate string to max length with ellipsis indicator
|
|
23
|
+
*/
|
|
24
|
+
const truncateOutput = (str: string, maxLength: number = MAX_OUTPUT_LENGTH): string => {
|
|
25
|
+
if (str.length <= maxLength) return str;
|
|
26
|
+
return (
|
|
27
|
+
str.slice(0, maxLength) + '\n... [truncated, ' + (str.length - maxLength) + ' more characters]'
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
18
31
|
interface ShellProcess {
|
|
19
32
|
lastReadStderr: number;
|
|
20
33
|
lastReadStdout: number;
|
|
@@ -104,8 +117,8 @@ export default class ShellCommandCtr extends ControllerModule {
|
|
|
104
117
|
childProcess.kill();
|
|
105
118
|
resolve({
|
|
106
119
|
error: `Command timed out after ${effectiveTimeout}ms`,
|
|
107
|
-
stderr,
|
|
108
|
-
stdout,
|
|
120
|
+
stderr: truncateOutput(stderr),
|
|
121
|
+
stdout: truncateOutput(stdout),
|
|
109
122
|
success: false,
|
|
110
123
|
});
|
|
111
124
|
}, effectiveTimeout);
|
|
@@ -125,9 +138,9 @@ export default class ShellCommandCtr extends ControllerModule {
|
|
|
125
138
|
logger.info(`${logPrefix} Command completed`, { code, success });
|
|
126
139
|
resolve({
|
|
127
140
|
exit_code: code || 0,
|
|
128
|
-
output: stdout + stderr,
|
|
129
|
-
stderr,
|
|
130
|
-
stdout,
|
|
141
|
+
output: truncateOutput(stdout + stderr),
|
|
142
|
+
stderr: truncateOutput(stderr),
|
|
143
|
+
stdout: truncateOutput(stdout),
|
|
131
144
|
success,
|
|
132
145
|
});
|
|
133
146
|
}
|
|
@@ -138,8 +151,8 @@ export default class ShellCommandCtr extends ControllerModule {
|
|
|
138
151
|
logger.error(`${logPrefix} Command failed:`, error);
|
|
139
152
|
resolve({
|
|
140
153
|
error: error.message,
|
|
141
|
-
stderr,
|
|
142
|
-
stdout,
|
|
154
|
+
stderr: truncateOutput(stderr),
|
|
155
|
+
stdout: truncateOutput(stdout),
|
|
143
156
|
success: false,
|
|
144
157
|
});
|
|
145
158
|
});
|
|
@@ -205,10 +218,10 @@ export default class ShellCommandCtr extends ControllerModule {
|
|
|
205
218
|
});
|
|
206
219
|
|
|
207
220
|
return {
|
|
208
|
-
output,
|
|
221
|
+
output: truncateOutput(output),
|
|
209
222
|
running,
|
|
210
|
-
stderr: newStderr,
|
|
211
|
-
stdout: newStdout,
|
|
223
|
+
stderr: truncateOutput(newStderr),
|
|
224
|
+
stdout: truncateOutput(newStdout),
|
|
212
225
|
success: true,
|
|
213
226
|
};
|
|
214
227
|
}
|
|
@@ -552,4 +552,265 @@ describe('LocalFileCtr', () => {
|
|
|
552
552
|
expect(result.diffText).toContain('+modified line 2');
|
|
553
553
|
});
|
|
554
554
|
});
|
|
555
|
+
|
|
556
|
+
describe('handleGrepContent', () => {
|
|
557
|
+
it('should search content in a single file', async () => {
|
|
558
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
559
|
+
isFile: () => true,
|
|
560
|
+
isDirectory: () => false,
|
|
561
|
+
} as any);
|
|
562
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('Hello world\nTest line\nAnother test');
|
|
563
|
+
|
|
564
|
+
const result = await localFileCtr.handleGrepContent({
|
|
565
|
+
'pattern': 'test',
|
|
566
|
+
'path': '/test/file.txt',
|
|
567
|
+
'-i': true,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
expect(result.success).toBe(true);
|
|
571
|
+
expect(result.matches).toContain('/test/file.txt');
|
|
572
|
+
expect(result.total_matches).toBe(1);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('should search content in directory with default glob pattern', async () => {
|
|
576
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
577
|
+
if (filePath === '/test') {
|
|
578
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
579
|
+
}
|
|
580
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
581
|
+
});
|
|
582
|
+
vi.mocked(mockFg).mockResolvedValue(['/test/file1.txt', '/test/file2.txt']);
|
|
583
|
+
vi.mocked(mockFsPromises.readFile).mockImplementation(async (filePath) => {
|
|
584
|
+
if (filePath === '/test/file1.txt') return 'Hello world';
|
|
585
|
+
if (filePath === '/test/file2.txt') return 'Test content';
|
|
586
|
+
return '';
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const result = await localFileCtr.handleGrepContent({
|
|
590
|
+
pattern: 'Hello',
|
|
591
|
+
path: '/test',
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
expect(result.success).toBe(true);
|
|
595
|
+
expect(result.matches).toContain('/test/file1.txt');
|
|
596
|
+
expect(result.total_matches).toBe(1);
|
|
597
|
+
expect(mockFg).toHaveBeenCalledWith('**/*', expect.objectContaining({ cwd: '/test' }));
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('should auto-prefix glob pattern with **/ for non-recursive patterns', async () => {
|
|
601
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
602
|
+
if (filePath === '/test') {
|
|
603
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
604
|
+
}
|
|
605
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
606
|
+
});
|
|
607
|
+
vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts', '/test/lib/file2.tsx']);
|
|
608
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
|
|
609
|
+
|
|
610
|
+
const result = await localFileCtr.handleGrepContent({
|
|
611
|
+
pattern: 'test',
|
|
612
|
+
path: '/test',
|
|
613
|
+
glob: '*.{ts,tsx}',
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
expect(result.success).toBe(true);
|
|
617
|
+
// Should auto-prefix *.{ts,tsx} with **/ to make it recursive
|
|
618
|
+
expect(mockFg).toHaveBeenCalledWith(
|
|
619
|
+
'**/*.{ts,tsx}',
|
|
620
|
+
expect.objectContaining({ cwd: '/test' }),
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should not modify glob pattern that already contains path separator', async () => {
|
|
625
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
626
|
+
if (filePath === '/test') {
|
|
627
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
628
|
+
}
|
|
629
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
630
|
+
});
|
|
631
|
+
vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts']);
|
|
632
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
|
|
633
|
+
|
|
634
|
+
const result = await localFileCtr.handleGrepContent({
|
|
635
|
+
pattern: 'test',
|
|
636
|
+
path: '/test',
|
|
637
|
+
glob: 'src/*.ts',
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
expect(result.success).toBe(true);
|
|
641
|
+
// Should not modify glob pattern that already contains /
|
|
642
|
+
expect(mockFg).toHaveBeenCalledWith('src/*.ts', expect.objectContaining({ cwd: '/test' }));
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('should not modify glob pattern that starts with **', async () => {
|
|
646
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
647
|
+
if (filePath === '/test') {
|
|
648
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
649
|
+
}
|
|
650
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
651
|
+
});
|
|
652
|
+
vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts']);
|
|
653
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
|
|
654
|
+
|
|
655
|
+
const result = await localFileCtr.handleGrepContent({
|
|
656
|
+
pattern: 'test',
|
|
657
|
+
path: '/test',
|
|
658
|
+
glob: '**/components/*.tsx',
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
expect(result.success).toBe(true);
|
|
662
|
+
// Should not modify glob pattern that already starts with **
|
|
663
|
+
expect(mockFg).toHaveBeenCalledWith(
|
|
664
|
+
'**/components/*.tsx',
|
|
665
|
+
expect.objectContaining({ cwd: '/test' }),
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should filter by type when provided', async () => {
|
|
670
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
671
|
+
if (filePath === '/test') {
|
|
672
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
673
|
+
}
|
|
674
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
675
|
+
});
|
|
676
|
+
// fast-glob returns all files, then type filter is applied
|
|
677
|
+
vi.mocked(mockFg).mockResolvedValue(['/test/file1.ts', '/test/file2.js', '/test/file3.ts']);
|
|
678
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('unique_pattern');
|
|
679
|
+
|
|
680
|
+
const result = await localFileCtr.handleGrepContent({
|
|
681
|
+
pattern: 'unique_pattern',
|
|
682
|
+
path: '/test',
|
|
683
|
+
type: 'ts',
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
expect(result.success).toBe(true);
|
|
687
|
+
// Type filter should exclude .js files from being searched
|
|
688
|
+
// Only .ts files should be in the results
|
|
689
|
+
expect(result.matches).not.toContain('/test/file2.js');
|
|
690
|
+
// At least one .ts file should match
|
|
691
|
+
expect(result.matches.length).toBeGreaterThan(0);
|
|
692
|
+
expect(result.matches.every((m) => m.endsWith('.ts'))).toBe(true);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('should return content mode with line numbers', async () => {
|
|
696
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
697
|
+
if (filePath === '/test') {
|
|
698
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
699
|
+
}
|
|
700
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
701
|
+
});
|
|
702
|
+
vi.mocked(mockFg).mockResolvedValue(['/test/file.txt']);
|
|
703
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('line 1\ntest line\nline 3');
|
|
704
|
+
|
|
705
|
+
const result = await localFileCtr.handleGrepContent({
|
|
706
|
+
'pattern': 'test',
|
|
707
|
+
'path': '/test',
|
|
708
|
+
'output_mode': 'content',
|
|
709
|
+
'-n': true,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
expect(result.success).toBe(true);
|
|
713
|
+
expect(result.matches.some((m) => m.includes('2:'))).toBe(true);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('should return count mode', async () => {
|
|
717
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
718
|
+
if (filePath === '/test') {
|
|
719
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
720
|
+
}
|
|
721
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
722
|
+
});
|
|
723
|
+
vi.mocked(mockFg).mockResolvedValue(['/test/file.txt']);
|
|
724
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('test one\ntest two\ntest three');
|
|
725
|
+
|
|
726
|
+
const result = await localFileCtr.handleGrepContent({
|
|
727
|
+
pattern: 'test',
|
|
728
|
+
path: '/test',
|
|
729
|
+
output_mode: 'count',
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
expect(result.success).toBe(true);
|
|
733
|
+
expect(result.matches).toContain('/test/file.txt:3');
|
|
734
|
+
expect(result.total_matches).toBe(3);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should respect head_limit', async () => {
|
|
738
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
739
|
+
if (filePath === '/test') {
|
|
740
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
741
|
+
}
|
|
742
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
743
|
+
});
|
|
744
|
+
vi.mocked(mockFg).mockResolvedValue([
|
|
745
|
+
'/test/file1.txt',
|
|
746
|
+
'/test/file2.txt',
|
|
747
|
+
'/test/file3.txt',
|
|
748
|
+
'/test/file4.txt',
|
|
749
|
+
'/test/file5.txt',
|
|
750
|
+
]);
|
|
751
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('test content');
|
|
752
|
+
|
|
753
|
+
const result = await localFileCtr.handleGrepContent({
|
|
754
|
+
pattern: 'test',
|
|
755
|
+
path: '/test',
|
|
756
|
+
head_limit: 2,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
expect(result.success).toBe(true);
|
|
760
|
+
expect(result.matches.length).toBe(2);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('should handle case insensitive search', async () => {
|
|
764
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
765
|
+
isFile: () => true,
|
|
766
|
+
isDirectory: () => false,
|
|
767
|
+
} as any);
|
|
768
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue('Hello World\nHELLO world\nhello WORLD');
|
|
769
|
+
|
|
770
|
+
const result = await localFileCtr.handleGrepContent({
|
|
771
|
+
'pattern': 'hello',
|
|
772
|
+
'path': '/test/file.txt',
|
|
773
|
+
'-i': true,
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
expect(result.success).toBe(true);
|
|
777
|
+
expect(result.matches).toContain('/test/file.txt');
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('should handle grep error gracefully', async () => {
|
|
781
|
+
vi.mocked(mockFsPromises.stat).mockRejectedValue(new Error('Path not found'));
|
|
782
|
+
|
|
783
|
+
const result = await localFileCtr.handleGrepContent({
|
|
784
|
+
pattern: 'test',
|
|
785
|
+
path: '/nonexistent',
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
expect(result.success).toBe(false);
|
|
789
|
+
expect(result.matches).toEqual([]);
|
|
790
|
+
expect(result.total_matches).toBe(0);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('should skip unreadable files gracefully', async () => {
|
|
794
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
795
|
+
if (filePath === '/test') {
|
|
796
|
+
return { isFile: () => false, isDirectory: () => true } as any;
|
|
797
|
+
}
|
|
798
|
+
return { isFile: () => true, isDirectory: () => false } as any;
|
|
799
|
+
});
|
|
800
|
+
vi.mocked(mockFg).mockResolvedValue(['/test/file1.txt', '/test/file2.txt']);
|
|
801
|
+
vi.mocked(mockFsPromises.readFile).mockImplementation(async (filePath) => {
|
|
802
|
+
if (filePath === '/test/file1.txt') throw new Error('Permission denied');
|
|
803
|
+
return 'test content';
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const result = await localFileCtr.handleGrepContent({
|
|
807
|
+
pattern: 'test',
|
|
808
|
+
path: '/test',
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
expect(result.success).toBe(true);
|
|
812
|
+
// Should still find match in file2.txt despite file1.txt error
|
|
813
|
+
expect(result.matches).toContain('/test/file2.txt');
|
|
814
|
+
});
|
|
815
|
+
});
|
|
555
816
|
});
|
|
@@ -193,6 +193,42 @@ describe('ShellCommandCtr', () => {
|
|
|
193
193
|
expect(result.stderr).toBe('error message\n');
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
it('should truncate long output to prevent context explosion', async () => {
|
|
197
|
+
let exitCallback: (code: number) => void;
|
|
198
|
+
let stdoutCallback: (data: Buffer) => void;
|
|
199
|
+
|
|
200
|
+
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
|
|
201
|
+
if (event === 'exit') {
|
|
202
|
+
exitCallback = callback;
|
|
203
|
+
setTimeout(() => exitCallback(0), 10);
|
|
204
|
+
}
|
|
205
|
+
return mockChildProcess;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
|
|
209
|
+
if (event === 'data') {
|
|
210
|
+
stdoutCallback = callback;
|
|
211
|
+
// Simulate very long output (15k characters)
|
|
212
|
+
const longOutput = 'x'.repeat(15_000);
|
|
213
|
+
setTimeout(() => stdoutCallback(Buffer.from(longOutput)), 5);
|
|
214
|
+
}
|
|
215
|
+
return mockChildProcess.stdout;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
219
|
+
|
|
220
|
+
const result = await shellCommandCtr.handleRunCommand({
|
|
221
|
+
command: 'command-with-long-output',
|
|
222
|
+
description: 'long output command',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result.success).toBe(true);
|
|
226
|
+
// Output should be truncated to ~10k + truncation message
|
|
227
|
+
expect(result.stdout!.length).toBeLessThan(15_000);
|
|
228
|
+
expect(result.stdout).toContain('truncated');
|
|
229
|
+
expect(result.stdout).toContain('more characters');
|
|
230
|
+
});
|
|
231
|
+
|
|
196
232
|
it('should enforce timeout limits', async () => {
|
|
197
233
|
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
|
198
234
|
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AUTH_REQUIRED_HEADER } from '@lobechat/desktop-bridge';
|
|
1
2
|
import { BrowserWindow, type Session } from 'electron';
|
|
2
3
|
|
|
3
4
|
import { isDev } from '@/const/env';
|
|
@@ -167,7 +168,7 @@ export class BackendProxyProtocolManager {
|
|
|
167
168
|
// The server sets X-Auth-Required header for real authentication failures (e.g., token expired)
|
|
168
169
|
// Other 401 errors (e.g., invalid API keys) should not trigger re-authentication
|
|
169
170
|
if (upstreamResponse.status === 401) {
|
|
170
|
-
const authRequired = upstreamResponse.headers.get(
|
|
171
|
+
const authRequired = upstreamResponse.headers.get(AUTH_REQUIRED_HEADER) === 'true';
|
|
171
172
|
if (authRequired) {
|
|
172
173
|
this.notifyAuthorizationRequired();
|
|
173
174
|
}
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"improvements": [
|
|
5
|
+
"Refactor search model implement."
|
|
6
|
+
],
|
|
7
|
+
"fixes": [
|
|
8
|
+
"Fix add message and improve local system tool."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"date": "2026-01-25",
|
|
12
|
+
"version": "2.0.0-next.376"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"children": {
|
|
4
16
|
"fixes": [
|