Compare commits

..

2 Commits

Author SHA1 Message Date
arvinxx c3c7826362 update 2025-10-14 21:08:39 +08:00
arvinxx 206105502a 📝 docs(test): add Zustand store action testing best practices guide
Added comprehensive testing guide for Zustand store actions based on generateAIChat refactoring experience.

## New Guide Content

**File**: `.cursor/rules/testing-guide/zustand-store-action-test.mdc`

### Key Topics Covered

1. **Test Layering Principles** 🎯
   - Each test layer should only spy on direct dependencies
   - Avoid cross-layer spying
   - Clear separation of concerns

2. **Mock Strategy** 🎭
   - Per-test mocking (recommended)
   - Avoid global mocks causing implicit coupling
   - Only spy common services in beforeEach

3. **File Organization** 📁
   - `fixtures.ts`: Test constants and mock data factories
   - `helpers.ts`: Reusable test utility functions
   - `[action].test.ts`: Actual test files

4. **Template Code** 📝
   - Basic action test template
   - Internal methods testing
   - Streaming/async flow testing
   - Toggle/loading state testing

5. **Common Issues & Solutions** ⚠️
   - React state update warnings
   - Cross-layer spy problems
   - Mock type mismatches
   - Global mock pollution

6. **Real Case Study** 🎓
   - generateAIChat refactoring experience
   - Before/after comparisons
   - Coverage improvement: 54.44% → 82.03%

### Benefits

 Clear test architecture guidelines
 Ready-to-use template code
 Common pitfalls prevention
 Real-world examples

Updated `rules-index.mdc` to include the new guide.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 20:53:52 +08:00
2500 changed files with 7946 additions and 223661 deletions
-38
View File
@@ -1,38 +0,0 @@
---
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh api:*), Bash(gh issue comment:*)
description: Find duplicate GitHub issues
---
Find up to 3 likely duplicate issues for a given GitHub issue.
To do this, follow these steps precisely:
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
Notes (be sure to tell this to your agents, too):
- Use `gh` to interact with Github, rather than web fetch
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
---
Found 3 possible duplicate issues:
1. <link to issue>
2. <link to issue>
3. <link to issue>
This issue will be automatically closed as a duplicate in 3 days.
- If your issue is a duplicate, please close it and 👍 the existing issue instead
- To prevent auto-closure, add a comment or 👎 this comment
> 🤖 Generated with Claude Code
---
-253
View File
@@ -1,253 +0,0 @@
# Issue Triage Guide
This guide is used for batch triaging GitHub issues - analyzing issues and applying appropriate labels.
## Workflow
For EACH issue, follow these steps:
### Step 1: Get Available Labels (run once per batch)
```bash
gh label list --json name,description --limit 300
```
### Step 2: Get Issue Details
For each issue number, run:
```bash
gh issue view [ISSUE_NUMBER] --json number,title,body,labels,comments
```
### Step 3: Analyze and Select Labels
Extract information from the issue template and content:
#### Template Fields Mapping
- 📦 Platform field → `platform:web/desktop/mobile`
- 💻 Operating System → `os:windows/macos/linux/ios`
- 🌐 Browser → `device:pc/mobile`
- 📦 Deployment mode → `deployment:server/client/pglite`
- Platform (hosting) → `hosting:cloud/self-host/vercel/zeabur/railway`
#### Provider Detection
**IMPORTANT**: Always check issue title and body for provider mentions!
**Official Providers** (check for these keywords in title/body):
- `openai`, `gpt``provider:openai`
- `gemini``provider:gemini`
- `claude`, `anthropic``provider:claude`
- `deepseek``provider:deepseek`
- `google``provider:google`
- `ollama``provider:ollama`
- `azure``provider:azure`
- `bedrock``provider:bedrock`
- `vertex``provider:vertex`
- `groq`, `grok``provider:groq`
- `mistral``provider:mistral`
- `moonshot``provider:moonshot`
- `zhipu``provider:zhipu`
- `minimax``provider:minimax`
- `doubao``provider:doubao`
**Third-party Aggregation Providers**:
- `aihubmix`, `AIHubMix`, `AIHUBMIX``provider:aihubmix`
- Check environment variables like `AIHUBMIX_*` in issue body
**Multiple Providers**: If issue mentions multiple providers, add ALL applicable provider labels.
### Label Categories
#### a) Issue Type (select ONE if applicable)
- `💄 Design` - UI/UX design issues
- `📝 Documentation` - Documentation improvements
- `⚡️ Performance` - Performance optimization
#### b) Priority (select ONE if applicable)
- `priority:high` - Critical issues, data loss, security, maintainer mentions "urgent"/"serious"/"critical"
- `priority:medium` - Important issues affecting multiple users, significant functionality impact
- `priority:low` - Nice to have, minor issues, edge cases
**Priority Guidelines**:
- Set `priority:high` for: data loss, authentication failures, deployment blockers, critical bugs
- Set `priority:medium` for: feature bugs affecting multiple users, workflow issues
- Set `priority:low` for: cosmetic issues, feature requests, configuration questions
#### c) Platform (select ALL applicable)
- `platform:web`
- `platform:desktop`
- `platform:mobile`
#### d) Device (for platform:web, select ONE)
- `device:pc`
- `device:mobile`
#### e) Operating System (select ALL applicable)
- `os:windows`
- `os:macos`
- `os:linux`
- `os:ios`
- `os:android`
#### f) Hosting Platform (select ONE)
- `hosting:cloud` - Official LobeHub Cloud
- `hosting:self-host` - Self-hosted deployment
- `hosting:vercel` - Vercel deployment
- `hosting:zeabur` - Zeabur deployment
- `hosting:railway` - Railway deployment
#### g) Deployment Mode (select ONE if mentioned)
- `deployment:server` - Server-side database mode
- `deployment:client` - Client-side database mode
- `deployment:pglite` - PGLite mode
**Additional deployment tags**:
- `docker` - If using Docker deployment
- `electron` - If desktop/Electron specific
#### h) Model Provider (select ALL applicable)
See "Provider Detection" section above for complete list.
**IMPORTANT**: Always scan issue title and body for provider keywords!
#### i) Feature/Component (select ALL applicable)
Core Features:
- `feature:settings` - Settings and configuration
- `feature:agent` - Agent/Assistant functionality
- `feature:topic` - Topic/Conversation management
- `feature:marketplace` - Agent marketplace
File & Knowledge:
- `feature:files` - File upload/management
- `feature:knowledge-base` - Knowledge base and RAG
- `feature:export` - Export functionality
Model Capabilities:
- `feature:streaming` - Streaming responses
- `feature:tool` - Tool calling
- `feature:vision` - Vision/multimodal capabilities
- `feature:image` - AI image generation
- `feature:dalle` - DALL-E specific
- `feature:tts` - Text-to-speech
Technical:
- `feature:api` - Backend API
- `feature:auth` - Authentication/authorization
- `feature:sync` - Cloud sync functionality
- `feature:search` - Search functionality
- `feature:mcp` - MCP integration
- `feature:editor` - Lobe Editor
- `feature:markdown` - Markdown rendering
- `feature:thread` - Thread/Subtopic functionality
Collaboration:
- `feature:group-chat` - Group chat functionality
- `feature:memory` - Memory feature
- `feature:team-workspace` - Team workspace
#### j) Workflow/Status
- `Duplicate` - Only if duplicate of an OPEN issue (mention issue number)
- `needs-reproduction` - Cannot reproduce, needs more information
- `good-first-issue` - Good for first-time contributors
- `🤔 Need Reproduce` - Needs reproduction steps
### Step 4: Apply Labels
Add labels (comma-separated, no spaces after commas):
```bash
gh issue edit [ISSUE_NUMBER] --add-label "label1,label2,label3"
```
Remove "unconfirm" label if adding other labels:
```bash
gh issue edit [ISSUE_NUMBER] --remove-label "unconfirm"
```
**Important**: Combine both commands when possible for efficiency.
### Step 5: Log Summary
For each issue, provide reasoning (2-4 sentences):
- Labels applied and why
- Key factors from issue template/comments
- Provider detection reasoning (if applicable)
## Important Rules
1. **Read Carefully**: Read issue template fields AND issue body/title for complete context
2. **Provider Detection**: ALWAYS check title and body for provider keywords (including aihubmix, etc.)
3. **Multiple Categories**: Use ALL applicable labels from different categories
4. **Label Prefixes**: Always use proper prefixes (`feature:`, `provider:`, `os:`, `platform:`, etc.)
5. **Maintainer Comments**: Check maintainer comments for priority/status hints
6. **No Comments**: Only apply labels, DO NOT post comments to issues
7. **Batch Efficiency**: Process issues in parallel when possible
## Common Patterns
### Provider in Environment Variables
If issue body contains `AIHUBMIX_*`, add `provider:aihubmix`
### Multiple Provider Issues
If comparing providers (e.g., "works with OpenAI but not Gemini"), add both provider labels
### Desktop Issues
Desktop issues often need: `platform:desktop`, `electron`, specific `os:*`, and `deployment:client` or `deployment:server`
### Knowledge Base Issues
Usually need: `feature:knowledge-base`, often with `feature:files`, may need `provider:*` for embedding models
### Tool Calling Issues
Usually need: `feature:tool`, specific `provider:*`, may need `feature:mcp` if MCP-related
### Streaming Issues
Usually need: `feature:streaming`, specific `provider:*`, check for timeout/performance issues
## Example Triage
**Issue #8850**: "aihubmix 的优惠 app 没有生效"
**Analysis**:
- Title contains "aihubmix" → `provider:aihubmix`
- Template shows: Windows, Chrome, Docker, Client mode
- About API discount codes not working
**Labels Applied**:
```bash
gh issue edit 8850 --add-label "provider:aihubmix,platform:web,os:windows,deployment:client,hosting:self-host,docker"
gh issue edit 8850 --remove-label "unconfirm"
```
**Reasoning**: AIHubMix provider discount feature not working. Client mode deployment on Windows with Docker. Provider detection from title keyword "aihubmix".
-135
View File
@@ -1,135 +0,0 @@
# Team Assignment Guide
## Quick Reference by Name
- **@arvinxx**: Last resort only, mention for priority:high issues, tool calling , mcp
- **@canisminor1990**: Design, UI components, editor
- **@tjx666**: Image/video generation, vision, cloud, documentation, TTS
- **@ONLY-yours**: Performance, streaming, settings, general bugs, web platform, marketplace
- **@RiverTwilight**: Knowledge base, files (KB-related), group chat
- **@nekomeowww**: Memory, backend, deployment, DevOps
- **@sudongyuer**: Mobile app (React Native)
- **@sxjeru**: Model providers and configuration
- **@cy948**: Auth Modules
- **@rdmclin2**: Team workspace
Quick reference for assigning issues based on labels.
## Label to Team Member Mapping
### Provider Labels (provider:\*)
| Label | Owner | Notes |
| ---------------- | ------- | -------------------------------------------- |
| All `provider:*` | @sxjeru | Model configuration and provider integration |
### Platform Labels (platform:\*)
| Label | Owner | Notes |
| ------------------ | ----------- | -------------------------------------- |
| `platform:mobile` | @sudongyuer | React Native mobile app |
| `platform:desktop` | @ONLY-yours | Electron desktop client (general) |
| `platform:web` | @ONLY-yours | Web platform (unless specific feature) |
### Feature Labels (feature:\*)
| Label | Owner | Notes |
| ------------------------ | --------------- | ----------------------------------------------------------------------- |
| `feature:image` | @tjx666 | AI image generation |
| `feature:dalle` | @tjx666 | DALL-E related |
| `feature:vision` | @tjx666 | Vision/multimodal generation |
| `feature:knowledge-base` | @RiverTwilight | Knowledge base and RAG |
| `feature:files` | @RiverTwilight | File upload/management (when KB-related)<br>@ONLY-yours (general files) |
| `feature:editor` | @canisminor1990 | Lobe Editor |
| `feature:auth` | @cy948 | Authentication/authorization |
| `feature:api` | @nekomeowww | Backend API |
| `feature:streaming` | @arvinxx | Streaming response |
| `feature:settings` | @ONLY-yours | Settings and configuration |
| `feature:agent` | @ONLY-yours | Agent/Assistant |
| `feature:topic` | @ONLY-yours | Topic/Conversation management |
| `feature:thread` | @arvinxx | Thread/Subtopic |
| `feature:marketplace` | @ONLY-yours | Agent marketplace |
| `feature:tool` | @arvinxx | Tool calling |
| `feature:mcp` | @arvinxx | MCP integration |
| `feature:search` | @ONLY-yours | Search functionality |
| `feature:tts` | @tjx666 | Text-to-speech |
| `feature:export` | @ONLY-yours | Export functionality |
| `feature:group-chat` | @RiverTwilight | Group chat functionality |
| `feature:memory` | @nekomeowww | Memory feature |
| `feature:team-workspace` | @rdmclin2 | Team workspace application |
### Deployment Labels (deployment:\*)
| Label | Owner | Notes |
| ------------------ | ----------- | -------------------------- |
| All `deployment:*` | @nekomeowww | Server/client/pglite modes |
### Hosting Labels (hosting:\*)
| Label | Owner | Notes |
| ------------------- | ----------- | ---------------------- |
| `hosting:cloud` | @tjx666 | Official LobeHub Cloud |
| `hosting:self-host` | @nekomeowww | Self-hosting issues |
| `hosting:vercel` | @nekomeowww | Vercel deployment |
| `hosting:zeabur` | @nekomeowww | Zeabur deployment |
| `hosting:railway` | @nekomeowww | Railway deployment |
### Issue Type Labels
| Label | Owner | Notes |
| ------------------ | -------------------- | ---------------------------- |
| 💄 Design | @canisminor1990 | Design and styling |
| 📝 Documentation | @tjx666 | Documentation |
| ⚡️ Performance | @ONLY-yours | Performance optimization |
| 🐛 Bug | (depends on feature) | Assign based on other labels |
| 🌠 Feature Request | (depends on feature) | Assign based on other labels |
## Assignment Rules
### Priority Order (apply in order)
1. **Specific feature owner** - e.g., `feature:knowledge-base`@RiverTwilight
2. **Platform owner** - e.g., `platform:mobile`@sudongyuer
3. **Provider owner** - e.g., `provider:*`@sxjeru
4. **Component owner** - e.g., 💄 Design → @canisminor1990
5. **Infrastructure owner** - e.g., `deployment:*`@nekomeowww
6. **General maintainer** - @ONLY-yours for general bugs/issues
7. **Last resort** - @arvinxx (only if no clear owner)
### Special Cases
**Multiple labels with different owners:**
- Mention the **most specific** feature owner first
- Mention secondary owners if their input is valuable
- Example: `feature:knowledge-base` + `deployment:server`@RiverTwilight (primary), @nekomeowww (secondary)
**Priority:high issues:**
- Mention feature owner + @arvinxx
- Example: `priority:high` + `feature:image`@tjx666 @arvinxx
**No clear owner:**
- Assign to @ONLY-yours for general issues
- Only mention @arvinxx if critical and truly unclear
## Comment Templates
**Single owner:**
```
@username - This is a [feature/component] issue. Please take a look.
```
**Multiple owners:**
```
@primary @secondary - This involves [features]. Please coordinate.
```
**High priority:**
```
@owner @arvinxx - High priority [feature] issue.
```
+1
View File
@@ -39,5 +39,6 @@ All following rules are saved under `.cursor/rules/` directory:
## Testing
- `testing-guide/testing-guide.mdc` Comprehensive testing guide for Vitest
- `testing-guide/zustand-store-action-test.mdc` Zustand store action testing best practices
- `testing-guide/electron-ipc-test.mdc` Electron IPC interface testing strategy
- `testing-guide/db-model-test.mdc` Database Model testing guide
@@ -1,46 +1,103 @@
---
description: Best practices for testing Zustand store actions
globs: "src/store/**/*.test.ts"
globs: src/store/**/__tests__/*.test.ts
alwaysApply: false
---
# Zustand Store Action Testing Guide
# 🏪 Zustand Store Action Testing Guide
This guide provides best practices for testing Zustand store actions, based on our proven testing patterns.
Testing guide for Zustand store actions under `src/store`. This guide is based on lessons learned from the `generateAIChat` refactoring practice.
## Basic Test Structure
## Core Principles
### 1. Test Layering Principle 🎯
**Each layer should only test direct dependencies, never spy across layers**
```
❌ Bad Example - Cross-layer spying
describe('internal_coreProcessMessage', () => {
it('test', async () => {
// ❌ Skipping internal_fetchAIChatMessage, directly spying on lower-level service
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream');
});
});
✅ Good Example - Spy on direct dependencies
describe('internal_coreProcessMessage', () => {
it('test', async () => {
// ✅ Only spy on directly called methods
const fetchSpy = vi.spyOn(result.current, 'internal_fetchAIChatMessage')
.mockResolvedValue({ isFunctionCall: false, content: 'response' });
});
});
```
### 2. Mocking Strategy 🎭
#### Per-Test Mocking (Recommended)
```typescript
// ✅ Spy on-demand in each test
describe('myAction', () => {
it('should do something', async () => {
// Only spy when needed in this specific test
const serviceSpy = vi.spyOn(someService, 'method').mockResolvedValue(result);
// Test logic...
serviceSpy.mockRestore(); // Optional: cleanup
});
});
```
#### Avoid Global Mocks
```typescript
// ❌ Avoid globally spying on everything in beforeEach
beforeEach(() => {
spyOnEverything(); // Creates implicit coupling between tests
});
// ✅ Only spy on base services that almost all tests need
beforeEach(() => {
spyOnMessageService(); // Most tests need this
// Other services should be spied on-demand within tests
});
```
## Action Test Templates 📝
### Basic Action Test
```typescript
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { messageService } from '@/services/message';
import { useChatStore } from '../../store';
import { useStore } from '../store';
// Keep zustand mock as it's needed globally
// Mock zustand
vi.mock('zustand/traditional');
// Test constants
const TEST_IDS = {
DATA_ID: 'test-data-id',
} as const;
// Mock data factory
const createMockData = (overrides = {}) => ({
id: TEST_IDS.DATA_ID,
status: 'initial',
...overrides,
});
beforeEach(() => {
// Reset store state
vi.clearAllMocks();
useChatStore.setState(
{
activeId: 'test-session-id',
messagesMap: {},
loadingIds: [],
},
false,
);
// Setup only spies that MOST tests need
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
// ❌ Don't setup spies that only few tests need - spy only when needed
// Setup common mock methods
// Setup common mocks that most tests need
act(() => {
useChatStore.setState({
refreshMessages: vi.fn(),
internal_coreProcessMessage: vi.fn(),
useStore.setState({
refreshData: vi.fn(),
internalMethod: vi.fn(),
});
});
});
@@ -49,531 +106,305 @@ afterEach(() => {
vi.restoreAllMocks();
});
describe('action name', () => {
describe('myAction', () => {
describe('validation', () => {
// Validation tests
it('should return early when conditions not met', async () => {
act(() => {
useStore.setState({ requiredData: undefined });
});
const { result } = renderHook(() => useStore());
await act(async () => {
await result.current.myAction();
});
expect(result.current.internalMethod).not.toHaveBeenCalled();
});
});
describe('normal flow', () => {
// Happy path tests
describe('main flow', () => {
it('should process data correctly', async () => {
const { result } = renderHook(() => useStore());
const mockData = createMockData();
await act(async () => {
await result.current.myAction(mockData);
});
expect(result.current.internalMethod).toHaveBeenCalledWith(
expect.objectContaining({
id: TEST_IDS.DATA_ID,
status: 'processed',
}),
);
});
});
describe('error handling', () => {
// Error case tests
it('should handle errors gracefully', async () => {
const { result } = renderHook(() => useStore());
vi.spyOn(result.current, 'internalMethod').mockRejectedValue(
new Error('Test error'),
);
await act(async () => {
await result.current.myAction();
});
expect(result.current.errorState).toBeDefined();
});
});
});
```
## Testing Best Practices
### 1. Test Layering - Spy Direct Dependencies Only
✅ **Good**: Spy on the direct dependency
### Testing Internal Methods
```typescript
// When testing internal_coreProcessMessage, spy its direct dependency
const fetchAIChatSpy = vi
.spyOn(result.current, 'internal_fetchAIChatMessage')
.mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
// Save the real implementation for testing
const realInternalMethod = useStore.getState().internal_method;
describe('internal_method', () => {
it('should call correct dependencies', async () => {
// Restore the real implementation
act(() => {
useStore.setState({ internal_method: realInternalMethod });
});
const { result } = renderHook(() => useStore());
// ✅ Spy on direct dependencies
const dependencySpy = vi
.spyOn(result.current, 'internal_dependency')
.mockResolvedValue(expectedResult);
await act(async () => {
await result.current.internal_method(input);
});
expect(dependencySpy).toHaveBeenCalledWith(
expect.objectContaining({ /* expected params */ }),
);
});
});
```
❌ **Bad**: Spy on lower-level implementation details
### Testing Streaming/Async Flows
```typescript
// Don't spy on services that internal_fetchAIChatMessage uses
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(...);
describe('streamingAction', () => {
it('should handle streaming chunks', async () => {
const { result } = renderHook(() => useStore());
const dispatchSpy = vi.spyOn(result.current, 'internal_dispatch');
// Mock streaming service
const streamSpy = vi
.spyOn(streamService, 'stream')
.mockImplementation(async ({ onChunk, onFinish }) => {
await onChunk?.({ type: 'data', content: 'chunk1' });
await onChunk?.({ type: 'data', content: 'chunk2' });
await onFinish?.('complete');
});
await act(async () => {
await result.current.streamingAction();
});
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: 'update',
value: expect.objectContaining({ content: 'chunk1' }),
}),
);
streamSpy.mockRestore();
});
});
```
**Why**: Each test should only mock its direct dependencies, not the entire call chain. This makes tests more maintainable and less brittle.
### 2. Mock Management - Minimize Global Spies
✅ **Good**: Spy only when needed
### Testing Toggle/Loading States
```typescript
describe('internal_toggleLoading', () => {
it('should enable loading state with abort controller', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.internal_toggleLoading(true, TEST_IDS.ITEM_ID, 'action');
});
const state = useStore.getState();
expect(state.loadingIds).toEqual([TEST_IDS.ITEM_ID]);
expect(state.abortController).toBeInstanceOf(AbortController);
});
it('should disable loading state and clear abort controller', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.internal_toggleLoading(true, TEST_IDS.ITEM_ID, 'start');
result.current.internal_toggleLoading(false, undefined, 'stop');
});
const state = useStore.getState();
expect(state.loadingIds).toEqual([]);
expect(state.abortController).toBeUndefined();
});
});
```
## Common Issues and Solutions ⚠️
### Issue 1: React State Update Warning
```typescript
// ❌ Wrong: setState without wrapping
useStore.setState({ data: newData });
// ✅ Correct: Wrap all setState with act()
act(() => {
useStore.setState({ data: newData });
});
```
### Issue 2: Cross-Layer Spying
```typescript
// ❌ Wrong: Spy on lower-level services across layers
describe('highLevelAction', () => {
const lowLevelServiceSpy = vi.spyOn(lowLevelService, 'method');
});
// ✅ Correct: Spy on direct dependencies
describe('highLevelAction', () => {
const directDependencySpy = vi.spyOn(result.current, 'directMethod');
});
```
### Issue 3: Mock Type Mismatch
```typescript
// ❌ Wrong: Return type doesn't match
vi.spyOn(service, 'method').mockResolvedValue('string');
// But method returns Response
// ✅ Correct: Return correct type
vi.spyOn(service, 'method').mockResolvedValue(new Response('string'));
```
### Issue 4: Global Mock Pollution
```typescript
// ❌ Wrong: Spy on all services in beforeEach
beforeEach(() => {
// ✅ Only spy services that most tests need
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
// ✅ Don't spy chatService globally
spyOnServiceA();
spyOnServiceB();
spyOnServiceC(); // Creates coupling between tests
});
it('should process message', async () => {
// ✅ Spy chatService only in tests that need it
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(...);
// test logic
streamSpy.mockRestore();
});
```
❌ **Bad**: Setup all spies globally
```typescript
// ✅ Correct: Spy on-demand
beforeEach(() => {
vi.spyOn(messageService, 'createMessage').mockResolvedValue('id');
vi.spyOn(chatService, 'createAssistantMessageStream').mockResolvedValue({}); // ❌ Not all tests need this
vi.spyOn(fileService, 'uploadFile').mockResolvedValue({}); // ❌ Creates implicit coupling
spyOnCommonService(); // Only spy on common services
});
describe('specific test', () => {
it('test', () => {
const specificSpy = vi.spyOn(specificService, 'method'); // Spy on-demand
});
});
```
### 3. Service Mocking - Mock the Correct Layer
## Test Coverage Goals 📊
✅ **Good**: Mock the service method
### Coverage Requirements
- **Minimum target**: 70%
- **Recommended target**: 85%+
- **Excellent target**: 90%+
### Check Coverage
```bash
# Run coverage for a single test file
bunx vitest run --coverage 'src/store/[domain]/__tests__/[action].test.ts'
# View coverage for a specific file
bunx vitest run --coverage --silent='passed-only' 'src/store/[domain]/__tests__/[action].test.ts' | grep "[action].ts"
```
### Priority Test Scenarios
1. ✅ **Main Flow**: Normal business flow
2. ✅ **Edge Cases**: Empty data, undefined values, boundary values
3. ✅ **Error Handling**: Exception scenarios, failure retries
4. ✅ **State Management**: Loading, Toggle, Abort
5. ⚠️ **Corner Cases**: Optional, but don't write meaningless tests just for coverage
## Real-World Case: generateAIChat Refactoring 🎓
### Problems Before Refactoring
```typescript
it('should fetch AI chat response', async () => {
// ❌ Problem 1: Cross-layer spying
describe('internal_coreProcessMessage', () => {
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream');
// Skipped the internal_fetchAIChatMessage layer
});
// ❌ Problem 2: Mocking wrong objects
describe('internal_fetchAIChatMessage', () => {
vi.stubGlobal('fetch', ...); // But doesn't actually call fetch
});
// ❌ Problem 3: Global spy pollution
beforeEach(() => {
spyOnChatService(); // All tests now have this spy
});
```
### Solutions After Refactoring
```typescript
// ✅ Solution 1: Spy on direct dependencies
describe('internal_coreProcessMessage', () => {
const fetchSpy = vi
.spyOn(result.current, 'internal_fetchAIChatMessage')
.mockResolvedValue({ isFunctionCall: false, content: 'response' });
});
// ✅ Solution 2: Mock correct service
describe('internal_fetchAIChatMessage', () => {
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
await onFinish?.('Hello', {});
await onMessageHandle?.({ type: 'text', text: 'response' });
await onFinish?.('response', {});
});
// test logic
});
```
❌ **Bad**: Mock global fetch
```typescript
it('should fetch AI chat response', async () => {
global.fetch = vi.fn().mockResolvedValue(...); // ❌ Too low level
});
```
### 4. Test Organization - Use Descriptive Nesting
✅ **Good**: Clear nested structure
```typescript
describe('sendMessage', () => {
describe('validation', () => {
it('should not send when session is inactive', async () => {});
it('should not send when message is empty', async () => {});
});
describe('message creation', () => {
it('should create user message and trigger AI processing', async () => {});
it('should send message with files attached', async () => {});
});
describe('error handling', () => {
it('should handle message creation errors gracefully', async () => {});
});
});
```
❌ **Bad**: Flat structure
```typescript
describe('sendMessage', () => {
it('test 1', async () => {});
it('test 2', async () => {});
it('test 3', async () => {});
});
```
### 5. Testing Async Actions
Always wrap async operations in `act()`:
```typescript
it('should send message', async () => {
const { result } = renderHook(() => useChatStore());
await act(async () => {
await result.current.sendMessage({ message: 'Hello' });
});
expect(messageService.createMessage).toHaveBeenCalled();
});
```
### 6. State Setup - Use act() for setState
```typescript
it('should handle disabled state', async () => {
act(() => {
useChatStore.setState({ activeId: undefined });
});
const { result } = renderHook(() => useChatStore());
// test logic
});
```
### 7. Testing Complex Flows
For complex flows with multiple steps, use clear spy setup:
```typescript
it('should handle topic creation flow', async () => {
// Setup store state
act(() => {
useChatStore.setState({
activeTopicId: undefined,
messagesMap: {
'test-session-id': [
{ id: 'msg-1', role: 'user', content: 'Message 1' },
{ id: 'msg-2', role: 'assistant', content: 'Response 1' },
{ id: 'msg-3', role: 'user', content: 'Message 2' },
],
},
});
});
const { result } = renderHook(() => useChatStore());
// Spy on action dependencies
const createTopicSpy = vi.spyOn(result.current, 'createTopic')
.mockResolvedValue('new-topic-id');
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleMessageLoading');
// Execute
await act(async () => {
await result.current.sendMessage({ message: 'Test message' });
});
// Assert
expect(createTopicSpy).toHaveBeenCalled();
expect(toggleLoadingSpy).toHaveBeenCalledWith(true, expect.any(String));
});
```
### 8. Streaming Response Mocking
When testing streaming responses, simulate the flow properly:
```typescript
it('should handle streaming chunks', async () => {
const { result } = renderHook(() => useChatStore());
const messages = [
{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' },
];
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
// Simulate streaming chunks
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
await onMessageHandle?.({ type: 'text', text: ' World' } as any);
await onFinish?.('Hello World', {});
});
await act(async () => {
await result.current.internal_fetchAIChatMessage({
messages,
messageId: 'test-message-id',
model: 'gpt-4o-mini',
provider: 'openai',
});
});
expect(result.current.internal_dispatchMessage).toHaveBeenCalled();
streamSpy.mockRestore();
});
```
### 9. Error Handling Tests
Always test error scenarios:
```typescript
it('should handle errors gracefully', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(messageService, 'createMessage').mockRejectedValue(
new Error('create message error'),
);
await act(async () => {
try {
await result.current.sendMessage({ message: 'Test message' });
} catch {
// Expected to throw
}
});
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
});
```
### 10. Cleanup After Tests
Always restore mocks after each test:
```typescript
afterEach(() => {
vi.restoreAllMocks();
});
// For individual test cleanup:
it('should test something', async () => {
const spy = vi.spyOn(service, 'method').mockImplementation(...);
// test logic
spy.mockRestore(); // Optional: cleanup immediately after test
});
```
## Common Patterns
### Testing Store Methods That Call Other Store Methods
```typescript
it('should call internal methods', async () => {
const { result } = renderHook(() => useChatStore());
const internalMethodSpy = vi.spyOn(result.current, 'internal_method')
.mockResolvedValue();
await act(async () => {
await result.current.publicMethod();
});
expect(internalMethodSpy).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ key: 'value' }),
);
});
```
### Testing Conditional Logic
```typescript
describe('conditional behavior', () => {
it('should execute when condition is true', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(true);
await act(async () => {
await result.current.sendMessage({ message: 'test' });
});
expect(result.current.internal_retrieveChunks).toHaveBeenCalled();
});
it('should not execute when condition is false', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(false);
await act(async () => {
await result.current.sendMessage({ message: 'test' });
});
expect(result.current.internal_retrieveChunks).not.toHaveBeenCalled();
});
});
```
### Testing AbortController
```typescript
it('should abort generation and clear loading state', () => {
const abortController = new AbortController();
act(() => {
useChatStore.setState({ chatLoadingIdsAbortController: abortController });
});
const { result } = renderHook(() => useChatStore());
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
act(() => {
result.current.stopGenerateMessage();
});
expect(abortController.signal.aborted).toBe(true);
expect(toggleLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
});
```
## Anti-Patterns to Avoid
❌ **Don't**: Mock the entire store
```typescript
vi.mock('../../store', () => ({
useChatStore: vi.fn(() => ({
sendMessage: vi.fn(),
})),
}));
```
❌ **Don't**: Test implementation details
```typescript
// Bad: testing internal state structure
expect(result.current.messagesMap).toHaveProperty('test-session');
// Good: testing behavior
expect(result.current.refreshMessages).toHaveBeenCalled();
```
❌ **Don't**: Create tight coupling between tests
```typescript
// Bad: Tests depend on order
let messageId: string;
it('test 1', () => {
messageId = 'some-id'; // Side effect
});
it('test 2', () => {
expect(messageId).toBeDefined(); // Depends on test 1
});
```
❌ **Don't**: Over-mock services
```typescript
// Bad: Mocking everything
// ✅ Solution 3: Spy on-demand
beforeEach(() => {
vi.mock('@/services/chat');
vi.mock('@/services/message');
vi.mock('@/services/file');
vi.mock('@/services/agent');
// ... too many global mocks
spyOnMessageService(); // Only spy on common services
// Spy on chatService on-demand in tests
});
```
## Testing SWR Hooks in Zustand Stores
### Refactoring Results
Some Zustand store slices use SWR hooks for data fetching. These require a different testing approach.
- 📈 Coverage improvement: 54.44% → 82.03% (+27.59%)
- ✅ Test pass rate: 52/52 (100%)
- 🎯 Type errors: 6 → 0
- 📝 Clearer tests: Explicit test layering
### Basic SWR Hook Test Structure
## Best Practices Checklist ✅
```typescript
import { renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
Check before testing:
import { discoverService } from '@/services/discover';
import { globalHelpers } from '@/store/global/helpers';
import { useDiscoverStore as useStore } from '../../store';
vi.mock('zustand/traditional');
beforeEach(() => {
vi.clearAllMocks();
});
describe('SWR Hook Actions', () => {
it('should fetch data and return correct response', async () => {
const mockData = [{ id: '1', name: 'Item 1' }];
// Mock the service call (the fetcher)
vi.spyOn(discoverService, 'getPluginCategories').mockResolvedValue(mockData as any);
vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('en-US');
const params = {} as any;
const { result } = renderHook(() => useStore.getState().usePluginCategories(params));
// Use waitFor to wait for async data loading
await waitFor(() => {
expect(result.current.data).toEqual(mockData);
});
expect(discoverService.getPluginCategories).toHaveBeenCalledWith(params);
});
});
```
**Key points**:
- **DO NOT mock useSWR** - let it use the real implementation
- Only mock the **service methods** (fetchers)
- Use `waitFor` from `@testing-library/react` to wait for async operations
- Check `result.current.data` directly after waitFor completes
### Testing SWR Key Generation
```typescript
it('should generate correct SWR key with locale and params', () => {
vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('zh-CN');
const useSWRMock = vi.mocked(useSWR);
let capturedKey: string | null = null;
useSWRMock.mockImplementation(((key: string) => {
capturedKey = key;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
const params = { page: 2, category: 'tools' } as any;
renderHook(() => useStore.getState().usePluginList(params));
expect(capturedKey).toBe('plugin-list-zh-CN-2-tools');
});
```
### Testing SWR Configuration
```typescript
it('should have correct SWR configuration', () => {
const useSWRMock = vi.mocked(useSWR);
let capturedOptions: any = null;
useSWRMock.mockImplementation(((key: string, fetcher: any, options: any) => {
capturedOptions = options;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
renderHook(() => useStore.getState().usePluginIdentifiers());
expect(capturedOptions).toMatchObject({ revalidateOnFocus: false });
});
```
### Testing Conditional Fetching
```typescript
it('should not fetch when required parameter is missing', () => {
const useSWRMock = vi.mocked(useSWR);
let capturedKey: string | null = null;
useSWRMock.mockImplementation(((key: string | null) => {
capturedKey = key;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
// When identifier is undefined, SWR key should be null
renderHook(() => useStore.getState().usePluginDetail({ identifier: undefined }));
expect(capturedKey).toBeNull();
});
```
### Key Differences from Regular Action Tests
1. **Mock useSWR globally**: Use `vi.mock('swr')` at the top level
2. **Mock the fetcher, not the result**:
- ✅ **Correct**: `const data = fetcher?.()` - call fetcher and return its Promise
- ❌ **Wrong**: `return { data: mockData }` - hardcode the result
3. **Await Promise results**: The `data` field is a Promise, use `await result.current.data`
4. **No act() wrapper needed**: SWR hooks don't trigger React state updates in these tests
5. **Test SWR key generation**: Verify keys include locale and parameters
6. **Test configuration**: Verify revalidation and other SWR options
7. **Type assertions**: Use `as any` for test mock data where type definitions are strict
**Why this matters**:
- The fetcher (service method) is what we're testing - it must be called
- Hardcoding the return value bypasses the actual fetcher logic
- SWR returns Promises in real usage, tests should mirror this behavior
## Benefits of This Approach
✅ **Clear test layers** - Each test only spies on direct dependencies
✅ **Correct mocks** - Mocks match actual implementation
✅ **Better maintainability** - Changes to implementation require fewer test updates
✅ **Improved coverage** - Structured approach ensures all branches are tested
✅ **Reduced coupling** - Tests are independent and can run in any order
## Reference
See example implementation in:
- `src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts` (Regular actions)
- `src/store/discover/slices/plugin/action.test.ts` (SWR hooks)
- `src/store/discover/slices/mcp/action.test.ts` (SWR hooks)
- [ ] Following test layering principle?
- [ ] Mock objects match actual calls?
- [ ] Avoiding global spy pollution?
- [ ] All setState wrapped with act()?
- [ ] Tests sufficiently atomic?
- [ ] Test descriptions clear?
- [ ] Coverage meets target?
-3
View File
@@ -256,9 +256,6 @@ OPENAI_API_KEY=sk-xxxxxxxxx
# you need to config the clerk webhook secret key if you want to use the clerk with database
#CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx
# Clear allow origin https://clerk.com/docs/guides/dashboard/dns-domains/satellite-domains
# Authentication across different domains , use,to splite different origin
# NEXT_PUBLIC_CLERK_AUTH_ALLOW_ORIGINS='https://market.lobehub.com,https://lobehub.com'
# NextAuth related configurations
# NEXT_PUBLIC_ENABLE_NEXT_AUTH=1
+23 -67
View File
@@ -5,17 +5,34 @@ type: Bug
body:
- type: dropdown
attributes:
label: '📱 Client Type'
description: 'Select how you are accessing LobeChat'
label: '📦 Platform'
multiple: true
options:
- 'Web (Desktop Browser)'
- 'Web (Mobile Browser)'
- 'Desktop App (Electron)'
- 'Mobile App (React Native)'
- 'Official Preview'
- 'Official Cloud'
- 'Vercel'
- 'Zeabur'
- 'Sealos'
- 'Netlify'
- 'Self hosting Docker'
- 'Other'
validations:
required: true
- type: dropdown
attributes:
label: '📦 Deploymenet mode'
multiple: true
options:
- 'client db (lobe-chat image)'
- 'client pgelite db (lobe-chat-pglite image)'
- 'server db(lobe-chat-database image)'
validations:
required: true
- type: input
attributes:
label: '📌 Version'
validations:
required: true
- type: dropdown
attributes:
@@ -31,39 +48,6 @@ body:
- 'Other'
validations:
required: true
- type: dropdown
attributes:
label: '📦 Deployment Platform'
multiple: true
options:
- 'Official Cloud'
- 'Vercel'
- 'Zeabur'
- 'Sealos'
- 'Netlify'
- 'Self hosting Docker'
- 'Other'
validations:
required: false
- type: dropdown
attributes:
label: '🔧 Deployment Mode'
multiple: true
options:
- 'client db (lobe-chat image)'
- 'client pgelite db (lobe-chat-pglite image)'
- 'server db (lobe-chat-database image)'
validations:
required: true
- type: input
attributes:
label: '📌 Version'
validations:
required: true
- type: dropdown
attributes:
label: '🌐 Browser'
@@ -76,49 +60,21 @@ body:
- 'Other'
validations:
required: true
- type: textarea
attributes:
label: '🐛 Bug Description'
description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
validations:
required: true
- type: textarea
attributes:
label: '📷 Recurrence Steps'
description: A clear and concise description of how to recurrence.
- type: textarea
attributes:
label: '🚦 Expected Behavior'
description: A clear and concise description of what you expected to happen.
- type: textarea
attributes:
label: '📝 Additional Information'
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
- type: dropdown
attributes:
label: '🛠️ Willing to Submit a PR?'
description: Would you be willing to submit a pull request to fix this bug?
options:
- 'Yes, I am willing to submit a PR'
- 'No, but I am happy to help test the fix'
validations:
required: false
- type: checkboxes
attributes:
label: '✅ Validations'
description: Before submitting the issue, please make sure you do the following
options:
- label: Read the [docs](https://lobehub.com/zh/docs).
required: true
- label: Check that there isn't [already an issue](https://github.com/lobehub/lobe-chat/issues) that reports the same bug to avoid creating a duplicate.
required: true
- label: Make sure this is a LobeChat issue and not a third-party library or provider issue.
required: true
- label: Check that this is a concrete bug. For Q&A, please use [GitHub Discussions](https://github.com/lobehub/lobe-chat/discussions) or join our [Discord Server](https://discord.gg/rGHwKq4R).
required: true
-26
View File
@@ -12,36 +12,10 @@
- [ ] 📝 docs
- [ ] 🔨 chore
#### 🔗 Related Issue
<!-- Link to the issue that is fixed by this PR -->
<!-- Example: Fixes #123, Closes #456, Related to #789 -->
#### 🔀 Description of Change
<!-- Thank you for your Pull Request. Please provide a description above. -->
#### 🧪 How to Test
<!-- Please describe how you tested your changes -->
<!-- For AI features, please include test prompts or scenarios -->
- [ ] Tested locally
- [ ] Added/updated tests
- [ ] No tests needed
#### 📸 Screenshots / Videos
<!-- If this PR includes UI changes, please provide screenshots or videos -->
| Before | After |
| ------ | ----- |
| ... | ... |
#### 📝 Additional Information
<!-- Add any other context about the Pull Request here. -->
<!-- Breaking changes? Migration guide? Performance impact? -->
-260
View File
@@ -1,260 +0,0 @@
#!/usr/bin/env bun
declare global {
// @ts-ignore
// eslint-disable-next-line no-var
var process: {
env: Record<string, string | undefined>;
};
}
interface GitHubIssue {
created_at: string;
number: number;
title: string;
user: { id: number };
}
interface GitHubComment {
body: string;
created_at: string;
id: number;
user: { id: number; type: string };
}
interface GitHubReaction {
content: string;
user: { id: number };
}
async function githubRequest<T>(
endpoint: string,
token: string,
method: string = 'GET',
body?: any,
): Promise<T> {
const response = await fetch(`https://api.github.com${endpoint}`, {
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `Bearer ${token}`,
'User-Agent': 'auto-close-duplicates-script',
...(body && { 'Content-Type': 'application/json' }),
},
method,
...(body && { body: JSON.stringify(body) }),
});
if (!response.ok) {
throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
function extractDuplicateIssueNumber(commentBody: string): number | null {
// Try to match #123 format first
let match = commentBody.match(/#(\d+)/);
if (match) {
return parseInt(match[1], 10);
}
// Try to match GitHub issue URL format: https://github.com/owner/repo/issues/123
match = commentBody.match(/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
if (match) {
return parseInt(match[1], 10);
}
return null;
}
async function closeIssueAsDuplicate(
owner: string,
repo: string,
issueNumber: number,
duplicateOfNumber: number,
token: string,
): Promise<void> {
await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}`, token, 'PATCH', {
labels: ['duplicate'],
state: 'closed',
state_reason: 'duplicate',
});
await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, token, 'POST', {
body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
If this is incorrect, please re-open this issue or create a new one.
🤖 Generated with [Claude Code](https://claude.ai/code)`,
});
}
async function autoCloseDuplicates(): Promise<void> {
console.log('[DEBUG] Starting auto-close duplicates script');
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN environment variable is required');
}
console.log('[DEBUG] GitHub token found');
const owner = process.env.GITHUB_REPOSITORY_OWNER || 'lobehub';
const repo = process.env.GITHUB_REPOSITORY_NAME || 'lobe-chat';
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
const threeDaysAgo = new Date();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
console.log(`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`);
console.log('[DEBUG] Fetching open issues created more than 3 days ago...');
const allIssues: GitHubIssue[] = [];
let page = 1;
const perPage = 100;
// eslint-disable-next-line no-constant-condition
while (true) {
const pageIssues: GitHubIssue[] = await githubRequest(
`/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`,
token,
);
if (pageIssues.length === 0) break;
// Filter for issues created more than 3 days ago
const oldEnoughIssues = pageIssues.filter(
(issue) => new Date(issue.created_at) <= threeDaysAgo,
);
allIssues.push(...oldEnoughIssues);
page++;
// Safety limit to avoid infinite loops
if (page > 20) break;
}
const issues = allIssues;
console.log(`[DEBUG] Found ${issues.length} open issues`);
let processedCount = 0;
let candidateCount = 0;
for (const issue of issues) {
processedCount++;
console.log(
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`,
);
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
const comments: GitHubComment[] = await githubRequest(
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
token,
);
console.log(`[DEBUG] Issue #${issue.number} has ${comments.length} comments`);
const dupeComments = comments.filter(
(comment) =>
comment.body.includes('Found') &&
comment.body.includes('possible duplicate') &&
comment.user.type === 'Bot',
);
console.log(
`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`,
);
if (dupeComments.length === 0) {
console.log(`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`);
continue;
}
const lastDupeComment = dupeComments.at(-1);
// @ts-ignore
const dupeCommentDate = new Date(lastDupeComment.created_at);
console.log(
`[DEBUG] Issue #${
issue.number
} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`,
);
if (dupeCommentDate > threeDaysAgo) {
console.log(`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`);
continue;
}
console.log(
`[DEBUG] Issue #${issue.number} - duplicate comment is old enough (${Math.floor(
(Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24),
)} days)`,
);
const commentsAfterDupe = comments.filter(
(comment) => new Date(comment.created_at) > dupeCommentDate,
);
console.log(
`[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`,
);
if (commentsAfterDupe.length > 0) {
console.log(
`[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`,
);
continue;
}
console.log(`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`);
const reactions: GitHubReaction[] = await githubRequest(
// @ts-ignore
`/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
token,
);
console.log(
`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`,
);
const authorThumbsDown = reactions.some(
(reaction) => reaction.user.id === issue.user.id && reaction.content === '-1',
);
console.log(
`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`,
);
if (authorThumbsDown) {
console.log(
`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`,
);
continue;
}
// @ts-ignore
const duplicateIssueNumber = extractDuplicateIssueNumber(lastDupeComment.body);
if (!duplicateIssueNumber) {
console.log(
`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`,
);
continue;
}
candidateCount++;
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
try {
console.log(
`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`,
);
await closeIssueAsDuplicate(owner, repo, issue.number, duplicateIssueNumber, token);
console.log(
`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`,
);
} catch (error) {
console.error(`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`);
}
}
console.log(
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`,
);
}
// eslint-disable-next-line unicorn/prefer-top-level-await
autoCloseDuplicates().catch(console.error);
// Make it a module
export {};
@@ -1,33 +0,0 @@
name: Claude Issue Dedupe
description: Automatically dedupe GitHub issues using Claude Code
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to process for duplicate detection'
required: true
type: string
jobs:
claude-dedupe-issues:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Run Claude Code slash command
uses: anthropics/claude-code-action@main
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: '/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}'
-65
View File
@@ -1,65 +0,0 @@
name: Claude Issue Triage
description: Automatically triage GitHub issues using Claude Code
on:
issues:
types: [opened, labeled]
jobs:
triage-issue:
runs-on: ubuntu-latest
timeout-minutes: 10
# Only run on issue opened, or when "trigger:triage" label is added
if: github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'trigger:triage')
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Copy triage prompts
run: |
mkdir -p /tmp/claude-prompts
cp .claude/prompts/team-assignment.md /tmp/claude-prompts/
cp .claude/prompts/issue-triage.md /tmp/claude-prompts/
- name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-action@main
with:
github_token: ${{ secrets.GH_TOKEN }}
allowed_non_write_users: "*"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--allowed-tools Bash(gh *),Read"
prompt: |
You're an issue triage assistant for GitHub issues. Your task is to analyze issues, apply appropriate labels, and mention the responsible team member.
REPOSITORY: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
## Instructions
Follow the complete triage guide located at:
```bash
cat /tmp/claude-prompts/issue-triage.md
```
Read the team assignment guide for determining team members:
```bash
cat /tmp/claude-prompts/team-assignment.md
```
**IMPORTANT**:
- Follow ALL steps in the issue-triage.md guide
- Apply labels according to the guide's rules
- Post a mention comment to the appropriate team member(s) based on team-assignment.md
- Replace [ISSUE_NUMBER] with: ${{ github.event.issue.number }}
**Start the triage process now.**
- name: Remove trigger label
if: github.event.action == 'labeled' && github.event.label.name == 'trigger:triage'
run: |
gh issue edit ${{ github.event.issue.number }} --remove-label "trigger:triage"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
+5 -5
View File
@@ -18,8 +18,8 @@ env:
jobs:
test:
name: Code quality check
# 添加 PR label 触发条件,只有添加了 trigger:build-desktop 标签的 PR 才会触发构建
if: contains(github.event.pull_request.labels.*.name, 'trigger:build-desktop')
# 添加 PR label 触发条件,只有添加了 Build Desktop 标签的 PR 才会触发构建
if: contains(github.event.pull_request.labels.*.name, 'Build Desktop')
runs-on: ubuntu-latest # 只在 ubuntu 上运行一次检查
steps:
- name: Checkout base
@@ -51,7 +51,7 @@ jobs:
version:
name: Determine version
# 与 test job 相同的触发条件
if: contains(github.event.pull_request.labels.*.name, 'trigger:build-desktop')
if: contains(github.event.pull_request.labels.*.name, 'Build Desktop')
runs-on: ubuntu-latest
outputs:
# 输出版本信息,供后续 job 使用
@@ -238,7 +238,7 @@ jobs:
# 下载所有平台的构建产物
- name: Download artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
path: release
pattern: release-*
@@ -287,7 +287,7 @@ jobs:
# 下载合并后的构建产物
- name: Download merged artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: merged-release-pr
path: release
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
# 添加 PR label 触发条件
if: |
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'trigger:build-docker')) ||
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
github.event_name != 'pull_request'
strategy:
@@ -118,7 +118,7 @@ jobs:
fetch-depth: 0
- name: Download digests
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
# 添加 PR label 触发条件
if: |
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'trigger:build-docker')) ||
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
github.event_name != 'pull_request'
strategy:
@@ -118,7 +118,7 @@ jobs:
fetch-depth: 0
- name: Download digests
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
# 添加 PR label 触发条件
if: |
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'trigger:build-docker')) ||
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
github.event_name != 'pull_request'
strategy:
@@ -118,7 +118,7 @@ jobs:
fetch-depth: 0
- name: Download digests
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
+7 -7
View File
@@ -22,7 +22,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.23
bun-version: latest
- name: Install dependencies (bun)
run: bun install
@@ -35,18 +35,18 @@ jobs:
PORT: 3010
run: bun run e2e
- name: Upload Cucumber HTML report (on failure)
- name: Upload Playwright HTML report (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: cucumber-report
path: e2e/reports
name: playwright-report
path: playwright-report
if-no-files-found: ignore
- name: Upload screenshots (on failure)
- name: Upload Playwright traces (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-screenshots
path: e2e/screenshots
name: test-results
path: test-results
if-no-files-found: ignore
@@ -1,30 +0,0 @@
name: Auto-close duplicate issues
description: Auto-closes issues that are duplicates of existing issues
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
jobs:
auto-close-duplicates:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Auto-close duplicate issues
run: bun run .github/scripts/auto-close-duplicates.ts
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
+3 -3
View File
@@ -15,7 +15,7 @@ permissions: read-all
jobs:
test:
name: Code quality check
# 添加 PR label 触发条件,只有添加了 trigger:build-desktop 标签的 PR 才会触发构建
# 添加 PR label 触发条件,只有添加了 Build Desktop 标签的 PR 才会触发构建
runs-on: ubuntu-latest # 只在 ubuntu 上运行一次检查
steps:
- name: Checkout base
@@ -220,7 +220,7 @@ jobs:
# 下载所有平台的构建产物
- name: Download artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
path: release
pattern: release-*
@@ -262,7 +262,7 @@ jobs:
steps:
# 下载合并后的构建产物
- name: Download merged artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: merged-release
path: release
+2 -2
View File
@@ -50,5 +50,5 @@ jobs:
![](https://github-production-user-asset-6210df.s3.amazonaws.com/17870709/273954625-df80c890-0822-4ac2-95e6-c990785cbed5.png)
[lobechat]: https://github.com/lobehub/lobe-chat
[tutorial-zh-CN]: https://lobehub.com/zh/docs/self-hosting/advanced/upstream-sync
[tutorial-en-US]: https://lobehub.com/docs/self-hosting/advanced/upstream-sync
[tutorial-zh-CN]: https://github.com/lobehub/lobe-chat/wiki/Upstream-Sync.zh-CN
[tutorial-en-US]: https://github.com/lobehub/lobe-chat/wiki/Upstream-Sync
+8 -6
View File
@@ -145,24 +145,26 @@ jobs:
node-version: 22
package-manager-cache: false
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.23
- name: Install deps
run: pnpm i
run: bun i
- name: Lint
run: npm run lint
run: bun run lint
- name: Test Client DB
run: pnpm --filter @lobechat/database test:client-db
run: bun run --filter @lobechat/database test:client-db
env:
KEY_VAULTS_SECRET: LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=
S3_PUBLIC_DOMAIN: https://example.com
APP_URL: https://home.com
- name: Test Coverage
run: pnpm --filter @lobechat/database test:coverage
run: bun run --filter @lobechat/database test:coverage
env:
DATABASE_TEST_URL: postgresql://postgres:postgres@localhost:5432/postgres
DATABASE_DRIVER: node
+1 -5
View File
@@ -93,6 +93,7 @@ robots.txt
.husky/prepare-commit-msg
# Documents and media
*.patch
*.pdf
# Cloud service keys
@@ -116,8 +117,3 @@ CLAUDE.local.md
prd
GEMINI.md
e2e/reports
# local eas account key for android
service-account-key.json
-6
View File
@@ -1,8 +1,2 @@
npm run type-check
# Check if there are changes in apps/mobile directory
if git diff --cached --name-only | grep -q "^apps/mobile/"; then
(cd apps/mobile && npm run type-check)
fi
npx --no-install lint-staged
+2
View File
@@ -4,6 +4,8 @@ resolution-mode=highest
ignore-workspace-root-check=true
enable-pre-post-scripts=true
# Load dotenv files for all the npm scripts
node-options="--require dotenv-expand/config"
public-hoist-pattern[]=*@umijs/lint*
public-hoist-pattern[]=*changelog*
-904
View File
@@ -2,910 +2,6 @@
# Changelog
### [Version 1.142.5](https://github.com/lobehub/lobe-chat/compare/v1.142.4...v1.142.5)
<sup>Released on **2025-10-27**</sup>
#### 💄 Styles
- **misc**: Add MiniMax-M2 model.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Add MiniMax-M2 model, closes [#9897](https://github.com/lobehub/lobe-chat/issues/9897) ([d6fded2](https://github.com/lobehub/lobe-chat/commit/d6fded2))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.142.4](https://github.com/lobehub/lobe-chat/compare/v1.142.3...v1.142.4)
<sup>Released on **2025-10-27**</sup>
#### 💄 Styles
- **misc**: Pre render ModelSwitchPanel, The error details of the connectivity check lead to a layout problem.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Pre render ModelSwitchPanel, closes [#9499](https://github.com/lobehub/lobe-chat/issues/9499) ([840382b](https://github.com/lobehub/lobe-chat/commit/840382b))
- **misc**: The error details of the connectivity check lead to a layout problem, closes [#9872](https://github.com/lobehub/lobe-chat/issues/9872) ([ea42e60](https://github.com/lobehub/lobe-chat/commit/ea42e60))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.142.3](https://github.com/lobehub/lobe-chat/compare/v1.142.2...v1.142.3)
<sup>Released on **2025-10-27**</sup>
#### 💄 Styles
- **misc**: Adjust modal setting form styles for improved layout and responsiveness, Unzip file when uploading in knowledge base \[LOB-500].
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Adjust modal setting form styles for improved layout and responsiveness, closes [#9890](https://github.com/lobehub/lobe-chat/issues/9890) ([1997ec5](https://github.com/lobehub/lobe-chat/commit/1997ec5))
- **misc**: Unzip file when uploading in knowledge base \[LOB-500], closes [#9854](https://github.com/lobehub/lobe-chat/issues/9854) ([e568ce6](https://github.com/lobehub/lobe-chat/commit/e568ce6))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.142.2](https://github.com/lobehub/lobe-chat/compare/v1.142.1...v1.142.2)
<sup>Released on **2025-10-26**</sup>
#### 💄 Styles
- **misc**: Improve provider modal height when creating custom provider.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Improve provider modal height when creating custom provider, closes [#9870](https://github.com/lobehub/lobe-chat/issues/9870) ([55d92c0](https://github.com/lobehub/lobe-chat/commit/55d92c0))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.142.1](https://github.com/lobehub/lobe-chat/compare/v1.142.0...v1.142.1)
<sup>Released on **2025-10-26**</sup>
#### 💄 Styles
- **misc**: Update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Update i18n, closes [#9862](https://github.com/lobehub/lobe-chat/issues/9862) ([8d3bc91](https://github.com/lobehub/lobe-chat/commit/8d3bc91))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.142.0](https://github.com/lobehub/lobe-chat/compare/v1.141.10...v1.142.0)
<sup>Released on **2025-10-24**</sup>
#### ✨ Features
- **misc**: Use env to control clerk allow origin feature.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Use env to control clerk allow origin feature, closes [#9863](https://github.com/lobehub/lobe-chat/issues/9863) ([490fee0](https://github.com/lobehub/lobe-chat/commit/490fee0))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.10](https://github.com/lobehub/lobe-chat/compare/v1.141.9...v1.141.10)
<sup>Released on **2025-10-23**</sup>
#### 🐛 Bug Fixes
- **misc**: Loadmore not work & navbar not show in pwa.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Loadmore not work & navbar not show in pwa, closes [#9855](https://github.com/lobehub/lobe-chat/issues/9855) ([411f875](https://github.com/lobehub/lobe-chat/commit/411f875))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.9](https://github.com/lobehub/lobe-chat/compare/v1.141.8...v1.141.9)
<sup>Released on **2025-10-23**</sup>
#### 💄 Styles
- **misc**: Improve local system tools render.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Improve local system tools render, closes [#9853](https://github.com/lobehub/lobe-chat/issues/9853) ([295e8fc](https://github.com/lobehub/lobe-chat/commit/295e8fc))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.8](https://github.com/lobehub/lobe-chat/compare/v1.141.7...v1.141.8)
<sup>Released on **2025-10-23**</sup>
#### 💄 Styles
- **misc**: Improvement for Agent Team After Alpha Launch \[LOB-517].
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Improvement for Agent Team After Alpha Launch \[LOB-517], closes [#9748](https://github.com/lobehub/lobe-chat/issues/9748) ([28245be](https://github.com/lobehub/lobe-chat/commit/28245be))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.7](https://github.com/lobehub/lobe-chat/compare/v1.141.6...v1.141.7)
<sup>Released on **2025-10-23**</sup>
#### 💄 Styles
- **misc**: Allow removal of `top_p` and similar request parameters.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Allow removal of `top_p` and similar request parameters, closes [#9498](https://github.com/lobehub/lobe-chat/issues/9498) ([4c313ce](https://github.com/lobehub/lobe-chat/commit/4c313ce))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.6](https://github.com/lobehub/lobe-chat/compare/v1.141.5...v1.141.6)
<sup>Released on **2025-10-22**</sup>
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.5](https://github.com/lobehub/lobe-chat/compare/v1.141.4...v1.141.5)
<sup>Released on **2025-10-22**</sup>
#### ♻ Code Refactoring
- **misc**: Change discover page from RSC to SPA to improve performance.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Code refactoring
- **misc**: Change discover page from RSC to SPA to improve performance, closes [#9828](https://github.com/lobehub/lobe-chat/issues/9828) ([b59ee0a](https://github.com/lobehub/lobe-chat/commit/b59ee0a))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.4](https://github.com/lobehub/lobe-chat/compare/v1.141.3...v1.141.4)
<sup>Released on **2025-10-22**</sup>
#### ♻ Code Refactoring
- **misc**: Fix model runtime cost calculate with CNY.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Code refactoring
- **misc**: Fix model runtime cost calculate with CNY, closes [#9834](https://github.com/lobehub/lobe-chat/issues/9834) ([2e911ea](https://github.com/lobehub/lobe-chat/commit/2e911ea))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.3](https://github.com/lobehub/lobe-chat/compare/v1.141.2...v1.141.3)
<sup>Released on **2025-10-22**</sup>
#### 💄 Styles
- **misc**: Update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Update i18n, closes [#9832](https://github.com/lobehub/lobe-chat/issues/9832) ([80b0999](https://github.com/lobehub/lobe-chat/commit/80b0999))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.2](https://github.com/lobehub/lobe-chat/compare/v1.141.1...v1.141.2)
<sup>Released on **2025-10-21**</sup>
#### 💄 Styles
- **settings**: Broadcast locale changes and update switchLocale action.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **settings**: Broadcast locale changes and update switchLocale action, closes [#9620](https://github.com/lobehub/lobe-chat/issues/9620) ([0eb02ca](https://github.com/lobehub/lobe-chat/commit/0eb02ca))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.141.1](https://github.com/lobehub/lobe-chat/compare/v1.141.0...v1.141.1)
<sup>Released on **2025-10-21**</sup>
#### ♻ Code Refactoring
- **misc**: Refactor context engine.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Code refactoring
- **misc**: Refactor context engine, closes [#9821](https://github.com/lobehub/lobe-chat/issues/9821) ([e99f12f](https://github.com/lobehub/lobe-chat/commit/e99f12f))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.141.0](https://github.com/lobehub/lobe-chat/compare/v1.140.0...v1.141.0)
<sup>Released on **2025-10-21**</sup>
#### ✨ Features
- **misc**: Add PDF export functionality to share modal.
#### 🐛 Bug Fixes
- **misc**: Ignore abort signal errors in TRPC client, slove when pwa user info have code cannot be viewed in full.
#### 💄 Styles
- **misc**: Add knowledge base mansory layout \[LOB-496], improve rich text link display.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Add PDF export functionality to share modal, closes [#9300](https://github.com/lobehub/lobe-chat/issues/9300) [#9299](https://github.com/lobehub/lobe-chat/issues/9299) ([2b7761c](https://github.com/lobehub/lobe-chat/commit/2b7761c))
#### What's fixed
- **misc**: Ignore abort signal errors in TRPC client, closes [#9809](https://github.com/lobehub/lobe-chat/issues/9809) [#9401](https://github.com/lobehub/lobe-chat/issues/9401) ([7f7dcfb](https://github.com/lobehub/lobe-chat/commit/7f7dcfb))
- **misc**: Slove when pwa user info have code cannot be viewed in full, closes [#9817](https://github.com/lobehub/lobe-chat/issues/9817) ([6734a47](https://github.com/lobehub/lobe-chat/commit/6734a47))
#### Styles
- **misc**: Add knowledge base mansory layout \[LOB-496], closes [#9722](https://github.com/lobehub/lobe-chat/issues/9722) ([69f21da](https://github.com/lobehub/lobe-chat/commit/69f21da))
- **misc**: Improve rich text link display, closes [#9816](https://github.com/lobehub/lobe-chat/issues/9816) ([af33543](https://github.com/lobehub/lobe-chat/commit/af33543))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.140.0](https://github.com/lobehub/lobe-chat/compare/v1.139.5...v1.140.0)
<sup>Released on **2025-10-21**</sup>
#### ✨ Features
- **misc**: Add ComfyUI integration Phase1(RFC-128).
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Add ComfyUI integration Phase1(RFC-128), closes [#9043](https://github.com/lobehub/lobe-chat/issues/9043) ([15ffe28](https://github.com/lobehub/lobe-chat/commit/15ffe28))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.139.5](https://github.com/lobehub/lobe-chat/compare/v1.139.4...v1.139.5)
<sup>Released on **2025-10-21**</sup>
#### 🐛 Bug Fixes
- **desktop**: Fix desktop open error in some edge cases.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **desktop**: Fix desktop open error in some edge cases, closes [#9813](https://github.com/lobehub/lobe-chat/issues/9813) ([6334f62](https://github.com/lobehub/lobe-chat/commit/6334f62))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.139.4](https://github.com/lobehub/lobe-chat/compare/v1.139.3...v1.139.4)
<sup>Released on **2025-10-21**</sup>
#### 🐛 Bug Fixes
- **misc**: Pass threadId to messages in sendMessageInServer.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Pass threadId to messages in sendMessageInServer, closes [#9808](https://github.com/lobehub/lobe-chat/issues/9808) ([d99a3a8](https://github.com/lobehub/lobe-chat/commit/d99a3a8))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.139.3](https://github.com/lobehub/lobe-chat/compare/v1.139.2...v1.139.3)
<sup>Released on **2025-10-21**</sup>
#### 💄 Styles
- **misc**: Show message author in minimap.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Show message author in minimap, closes [#9797](https://github.com/lobehub/lobe-chat/issues/9797) ([f6daefb](https://github.com/lobehub/lobe-chat/commit/f6daefb))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.139.2](https://github.com/lobehub/lobe-chat/compare/v1.139.1...v1.139.2)
<sup>Released on **2025-10-20**</sup>
#### 💄 Styles
- **misc**: Solve when desktop the sider agent list too long.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Solve when desktop the sider agent list too long, closes [#9792](https://github.com/lobehub/lobe-chat/issues/9792) ([778dea3](https://github.com/lobehub/lobe-chat/commit/778dea3))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.139.1](https://github.com/lobehub/lobe-chat/compare/v1.139.0...v1.139.1)
<sup>Released on **2025-10-20**</sup>
#### ♻ Code Refactoring
- **i18n**: Rm qa.
#### 💄 Styles
- **misc**: Update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Code refactoring
- **i18n**: Rm qa, closes [#9783](https://github.com/lobehub/lobe-chat/issues/9783) ([6d14dfe](https://github.com/lobehub/lobe-chat/commit/6d14dfe))
#### Styles
- **misc**: Update i18n, closes [#9787](https://github.com/lobehub/lobe-chat/issues/9787) ([b43d4b2](https://github.com/lobehub/lobe-chat/commit/b43d4b2))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.139.0](https://github.com/lobehub/lobe-chat/compare/v1.138.5...v1.139.0)
<sup>Released on **2025-10-19**</sup>
#### ✨ Features
- **misc**: Support image generation for siliconcloud.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Support image generation for siliconcloud, closes [#9447](https://github.com/lobehub/lobe-chat/issues/9447) ([5ebcfa5](https://github.com/lobehub/lobe-chat/commit/5ebcfa5))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.138.5](https://github.com/lobehub/lobe-chat/compare/v1.138.4...v1.138.5)
<sup>Released on **2025-10-18**</sup>
#### ♻ Code Refactoring
- **misc**: Refactor upload router into lambda and decide to remove it in V2.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Code refactoring
- **misc**: Refactor upload router into lambda and decide to remove it in V2, closes [#9766](https://github.com/lobehub/lobe-chat/issues/9766) ([d1c7f41](https://github.com/lobehub/lobe-chat/commit/d1c7f41))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.138.4](https://github.com/lobehub/lobe-chat/compare/v1.138.3...v1.138.4)
<sup>Released on **2025-10-18**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix response API tools calling issue.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix response API tools calling issue, closes [#9760](https://github.com/lobehub/lobe-chat/issues/9760) ([0596692](https://github.com/lobehub/lobe-chat/commit/0596692))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.138.3](https://github.com/lobehub/lobe-chat/compare/v1.138.2...v1.138.3)
<sup>Released on **2025-10-18**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix topic fetch not correct in custom agent.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix topic fetch not correct in custom agent, closes [#9761](https://github.com/lobehub/lobe-chat/issues/9761) ([ceffce2](https://github.com/lobehub/lobe-chat/commit/ceffce2))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.138.2](https://github.com/lobehub/lobe-chat/compare/v1.138.1...v1.138.2)
<sup>Released on **2025-10-16**</sup>
#### 💄 Styles
- **misc**: Improve welcome message.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Improve welcome message, closes [#9747](https://github.com/lobehub/lobe-chat/issues/9747) ([c83fe13](https://github.com/lobehub/lobe-chat/commit/c83fe13))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.138.1](https://github.com/lobehub/lobe-chat/compare/v1.138.0...v1.138.1)
<sup>Released on **2025-10-16**</sup>
#### 🐛 Bug Fixes
- **misc**: Automatic topic creation switch does not work.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Automatic topic creation switch does not work, closes [#9693](https://github.com/lobehub/lobe-chat/issues/9693) ([a02b301](https://github.com/lobehub/lobe-chat/commit/a02b301))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.138.0](https://github.com/lobehub/lobe-chat/compare/v1.137.10...v1.138.0)
<sup>Released on **2025-10-16**</sup>
#### ✨ Features
- **misc**: Support Group Chat, Mention, and Multi-Agent Orchestration with feature flag.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Support Group Chat, Mention, and Multi-Agent Orchestration with feature flag, closes [#8976](https://github.com/lobehub/lobe-chat/issues/8976) ([03c2838](https://github.com/lobehub/lobe-chat/commit/03c2838))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.137.10](https://github.com/lobehub/lobe-chat/compare/v1.137.9...v1.137.10)
<sup>Released on **2025-10-16**</sup>
#### 💄 Styles
- **misc**: Add Claude Haiku 4.5 model.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Add Claude Haiku 4.5 model, closes [#9735](https://github.com/lobehub/lobe-chat/issues/9735) ([1cfbc87](https://github.com/lobehub/lobe-chat/commit/1cfbc87))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.137.9](https://github.com/lobehub/lobe-chat/compare/v1.137.8...v1.137.9)
<sup>Released on **2025-10-15**</sup>
#### 💄 Styles
- **misc**: Improve update notification.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Improve update notification, closes [#9717](https://github.com/lobehub/lobe-chat/issues/9717) ([16de38a](https://github.com/lobehub/lobe-chat/commit/16de38a))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.137.8](https://github.com/lobehub/lobe-chat/compare/v1.137.7...v1.137.8)
<sup>Released on **2025-10-15**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix duplicate tools id issue and fix link dialog issue.
#### 💄 Styles
- **misc**: Add region support for Vertex AI provider.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix duplicate tools id issue and fix link dialog issue, closes [#9731](https://github.com/lobehub/lobe-chat/issues/9731) ([0a8c80d](https://github.com/lobehub/lobe-chat/commit/0a8c80d))
#### Styles
- **misc**: Add region support for Vertex AI provider, closes [#9720](https://github.com/lobehub/lobe-chat/issues/9720) ([d17b50c](https://github.com/lobehub/lobe-chat/commit/d17b50c))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.137.7](https://github.com/lobehub/lobe-chat/compare/v1.137.6...v1.137.7)
<sup>Released on **2025-10-15**</sup>
#### 💄 Styles
- **misc**: Use different favicon.ico in dev mode.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Use different favicon.ico in dev mode, closes [#9723](https://github.com/lobehub/lobe-chat/issues/9723) ([2f7317b](https://github.com/lobehub/lobe-chat/commit/2f7317b))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.137.6](https://github.com/lobehub/lobe-chat/compare/v1.137.5...v1.137.6)
<sup>Released on **2025-10-14**</sup>
#### 🐛 Bug Fixes
- **misc**: Update Claude workflows to use oauth token, vertext ai create image.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Update Claude workflows to use oauth token, closes [#9711](https://github.com/lobehub/lobe-chat/issues/9711) ([8dcb00e](https://github.com/lobehub/lobe-chat/commit/8dcb00e))
- **misc**: Vertext ai create image, closes [#9710](https://github.com/lobehub/lobe-chat/issues/9710) ([790d8fd](https://github.com/lobehub/lobe-chat/commit/790d8fd))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.137.5](https://github.com/lobehub/lobe-chat/compare/v1.137.4...v1.137.5)
<sup>Released on **2025-10-14**</sup>
+2 -5
View File
@@ -156,7 +156,7 @@ ENV \
# Anthropic
ANTHROPIC_API_KEY="" ANTHROPIC_MODEL_LIST="" ANTHROPIC_PROXY_URL="" \
# Amazon Bedrock
ENABLED_AWS_BEDROCK="" AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
# Azure OpenAI
AZURE_API_KEY="" AZURE_API_VERSION="" AZURE_ENDPOINT="" AZURE_MODEL_LIST="" \
# Baichuan
@@ -165,9 +165,6 @@ ENV \
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
# Cohere
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
# ComfyUI
ENABLED_COMFYUI="" COMFYUI_BASE_URL="" COMFYUI_AUTH_TYPE="" \
COMFYUI_API_KEY="" COMFYUI_USERNAME="" COMFYUI_PASSWORD="" COMFYUI_CUSTOM_HEADERS="" \
# DeepSeek
DEEPSEEK_API_KEY="" DEEPSEEK_MODEL_LIST="" \
# Fireworks AI
@@ -209,7 +206,7 @@ ENV \
# Ollama
ENABLED_OLLAMA="" OLLAMA_MODEL_LIST="" OLLAMA_PROXY_URL="" \
# OpenAI
ENABLED_OPENAI="" OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
# OpenRouter
OPENROUTER_API_KEY="" OPENROUTER_MODEL_LIST="" \
# Perplexity
+2 -5
View File
@@ -209,7 +209,7 @@ ENV \
# Anthropic
ANTHROPIC_API_KEY="" ANTHROPIC_MODEL_LIST="" ANTHROPIC_PROXY_URL="" \
# Amazon Bedrock
ENABLED_AWS_BEDROCK="" AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
# Azure OpenAI
AZURE_API_KEY="" AZURE_API_VERSION="" AZURE_ENDPOINT="" AZURE_MODEL_LIST="" \
# Baichuan
@@ -218,9 +218,6 @@ ENV \
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
# Cohere
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
# ComfyUI
ENABLED_COMFYUI="" COMFYUI_BASE_URL="" COMFYUI_AUTH_TYPE="" \
COMFYUI_API_KEY="" COMFYUI_USERNAME="" COMFYUI_PASSWORD="" COMFYUI_CUSTOM_HEADERS="" \
# DeepSeek
DEEPSEEK_API_KEY="" DEEPSEEK_MODEL_LIST="" \
# Fireworks AI
@@ -262,7 +259,7 @@ ENV \
# Ollama
ENABLED_OLLAMA="" OLLAMA_MODEL_LIST="" OLLAMA_PROXY_URL="" \
# OpenAI
ENABLED_OPENAI="" OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
# OpenRouter
OPENROUTER_API_KEY="" OPENROUTER_MODEL_LIST="" \
# Perplexity
+2 -5
View File
@@ -158,7 +158,7 @@ ENV \
# Anthropic
ANTHROPIC_API_KEY="" ANTHROPIC_MODEL_LIST="" ANTHROPIC_PROXY_URL="" \
# Amazon Bedrock
ENABLED_AWS_BEDROCK="" AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
# Azure OpenAI
AZURE_API_KEY="" AZURE_API_VERSION="" AZURE_ENDPOINT="" AZURE_MODEL_LIST="" \
# Baichuan
@@ -167,9 +167,6 @@ ENV \
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
# Cohere
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
# ComfyUI
ENABLED_COMFYUI="" COMFYUI_BASE_URL="" COMFYUI_AUTH_TYPE="" \
COMFYUI_API_KEY="" COMFYUI_USERNAME="" COMFYUI_PASSWORD="" COMFYUI_CUSTOM_HEADERS="" \
# DeepSeek
DEEPSEEK_API_KEY="" DEEPSEEK_MODEL_LIST="" \
# Fireworks AI
@@ -211,7 +208,7 @@ ENV \
# Ollama
ENABLED_OLLAMA="" OLLAMA_MODEL_LIST="" OLLAMA_PROXY_URL="" \
# OpenAI
ENABLED_OPENAI="" OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
# OpenRouter
OPENROUTER_API_KEY="" OPENROUTER_MODEL_LIST="" \
# Perplexity
+10 -9
View File
@@ -250,7 +250,7 @@ We have implemented support for the following model service providers:
- **[HuggingFace](https://lobechat.com/discover/provider/huggingface)**: The HuggingFace Inference API provides a fast and free way for you to explore thousands of models for various tasks. Whether you are prototyping for a new application or experimenting with the capabilities of machine learning, this API gives you instant access to high-performance models across multiple domains.
- **[Cloudflare Workers AI](https://lobechat.com/discover/provider/cloudflare)**: Run serverless GPU-powered machine learning models on Cloudflare's global network.
<details><summary><kbd>See more providers (+31)</kbd></summary>
<details><summary><kbd>See more providers (+32)</kbd></summary>
- **[GitHub](https://lobechat.com/discover/provider/github)**: With GitHub Models, developers can become AI engineers and leverage the industry's leading AI models.
- **[Novita](https://lobechat.com/discover/provider/novita)**: Novita AI is a platform providing a variety of large language models and AI image generation API services, flexible, reliable, and cost-effective. It supports the latest open-source models like Llama3 and Mistral, offering a comprehensive, user-friendly, and auto-scaling API solution for generative AI application development, suitable for the rapid growth of AI startups.
@@ -275,6 +275,7 @@ We have implemented support for the following model service providers:
- **[SenseNova](https://lobechat.com/discover/provider/sensenova)**: SenseNova, backed by SenseTime's robust infrastructure, offers efficient and user-friendly full-stack large model services.
- **[Stepfun](https://lobechat.com/discover/provider/stepfun)**: StepFun's large model possesses industry-leading multimodal and complex reasoning capabilities, supporting ultra-long text understanding and powerful autonomous scheduling search engine functions.
- **[Baichuan](https://lobechat.com/discover/provider/baichuan)**: Baichuan Intelligence is a company focused on the research and development of large AI models, with its models excelling in domestic knowledge encyclopedias, long text processing, and generative creation tasks in Chinese, surpassing mainstream foreign models. Baichuan Intelligence also possesses industry-leading multimodal capabilities, performing excellently in multiple authoritative evaluations. Its models include Baichuan 4, Baichuan 3 Turbo, and Baichuan 3 Turbo 128k, each optimized for different application scenarios, providing cost-effective solutions.
- **[Minimax](https://lobechat.com/discover/provider/minimax)**: MiniMax is a general artificial intelligence technology company established in 2021, dedicated to co-creating intelligence with users. MiniMax has independently developed general large models of different modalities, including trillion-parameter MoE text models, voice models, and image models, and has launched applications such as Conch AI.
- **[InternLM](https://lobechat.com/discover/provider/internlm)**: An open-source organization dedicated to the research and development of large model toolchains. It provides an efficient and user-friendly open-source platform for all AI developers, making cutting-edge large models and algorithm technologies easily accessible.
- **[Higress](https://lobechat.com/discover/provider/higress)**: Higress is a cloud-native API gateway that was developed internally at Alibaba to address the issues of Tengine reload affecting long-lived connections and the insufficient load balancing capabilities for gRPC/Dubbo.
- **[Gitee AI](https://lobechat.com/discover/provider/giteeai)**: Gitee AI's Serverless API provides AI developers with an out of the box large model inference API service.
@@ -286,7 +287,7 @@ We have implemented support for the following model service providers:
</details>
> 📊 Total providers: [<kbd>**41**</kbd>](https://lobechat.com/discover/providers)
> 📊 Total providers: [<kbd>**42**</kbd>](https://lobechat.com/discover/providers)
<!-- PROVIDER LIST -->
@@ -381,12 +382,12 @@ In addition, these plugins are not limited to news aggregation, but can also ext
<!-- PLUGIN LIST -->
| Recent Submits | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
| [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
| Recent Submits | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
| [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
| [Google CSE](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | Searches Google through their official CSE API.<br/>`web` `search` |
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
@@ -901,7 +902,7 @@ This project is [LobeHub Community License](./LICENSE) licensed.
[github-release-shield]: https://img.shields.io/github/v/release/lobehub/lobe-chat?color=369eff&labelColor=black&logo=github&style=flat-square
[github-releasedate-link]: https://github.com/lobehub/lobe-chat/releases
[github-releasedate-shield]: https://img.shields.io/github/release-date/lobehub/lobe-chat?labelColor=black&style=flat-square
[github-stars-link]: https://github.com/lobehub/lobe-chat/stargazers
[github-stars-link]: https://github.com/lobehub/lobe-chat/network/stargazers
[github-stars-shield]: https://img.shields.io/github/stars/lobehub/lobe-chat?color=ffcb47&labelColor=black&style=flat-square
[github-trending-shield]: https://trendshift.io/api/badge/repositories/2256
[github-trending-url]: https://trendshift.io/repositories/2256
+10 -9
View File
@@ -250,7 +250,7 @@ LobeChat 支持文件上传与知识库功能,你可以上传文件、图片
- **[HuggingFace](https://lobechat.com/discover/provider/huggingface)**: HuggingFace Inference API 提供了一种快速且免费的方式,让您可以探索成千上万种模型,适用于各种任务。无论您是在为新应用程序进行原型设计,还是在尝试机器学习的功能,这个 API 都能让您即时访问多个领域的高性能模型。
- **[Cloudflare Workers AI](https://lobechat.com/discover/provider/cloudflare)**: 在 Cloudflare 的全球网络上运行由无服务器 GPU 驱动的机器学习模型。
<details><summary><kbd>See more providers (+31)</kbd></summary>
<details><summary><kbd>See more providers (+32)</kbd></summary>
- **[GitHub](https://lobechat.com/discover/provider/github)**: 通过 GitHub 模型,开发人员可以成为 AI 工程师,并使用行业领先的 AI 模型进行构建。
- **[Novita](https://lobechat.com/discover/provider/novita)**: Novita AI 是一个提供多种大语言模型与 AI 图像生成的 API 服务的平台,灵活、可靠且具有成本效益。它支持 Llama3、Mistral 等最新的开源模型,并为生成式 AI 应用开发提供了全面、用户友好且自动扩展的 API 解决方案,适合 AI 初创公司的快速发展。
@@ -275,6 +275,7 @@ LobeChat 支持文件上传与知识库功能,你可以上传文件、图片
- **[SenseNova](https://lobechat.com/discover/provider/sensenova)**: 商汤日日新,依托商汤大装置的强大的基础支撑,提供高效易用的全栈大模型服务。
- **[Stepfun](https://lobechat.com/discover/provider/stepfun)**: 阶级星辰大模型具备行业领先的多模态及复杂推理能力,支持超长文本理解和强大的自主调度搜索引擎功能。
- **[Baichuan](https://lobechat.com/discover/provider/baichuan)**: 百川智能是一家专注于人工智能大模型研发的公司,其模型在国内知识百科、长文本处理和生成创作等中文任务上表现卓越,超越了国外主流模型。百川智能还具备行业领先的多模态能力,在多项权威评测中表现优异。其模型包括 Baichuan 4、Baichuan 3 Turbo 和 Baichuan 3 Turbo 128k 等,分别针对不同应用场景进行优化,提供高性价比的解决方案。
- **[Minimax](https://lobechat.com/discover/provider/minimax)**: MiniMax 是 2021 年成立的通用人工智能科技公司,致力于与用户共创智能。MiniMax 自主研发了不同模态的通用大模型,其中包括万亿参数的 MoE 文本大模型、语音大模型以及图像大模型。并推出了海螺 AI 等应用。
- **[InternLM](https://lobechat.com/discover/provider/internlm)**: 致力于大模型研究与开发工具链的开源组织。为所有 AI 开发者提供高效、易用的开源平台,让最前沿的大模型与算法技术触手可及
- **[Higress](https://lobechat.com/discover/provider/higress)**: Higress 是一款云原生 API 网关,在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
- **[Gitee AI](https://lobechat.com/discover/provider/giteeai)**: Gitee AI 的 Serverless API 为 AI 开发者提供开箱即用的大模型推理 API 服务。
@@ -286,7 +287,7 @@ LobeChat 支持文件上传与知识库功能,你可以上传文件、图片
</details>
> 📊 Total providers: [<kbd>**41**</kbd>](https://lobechat.com/discover/providers)
> 📊 Total providers: [<kbd>**42**</kbd>](https://lobechat.com/discover/providers)
<!-- PROVIDER LIST -->
@@ -374,12 +375,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
<!-- PLUGIN LIST -->
| 最近新增 | 描述 |
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
| [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
| 最近新增 | 描述 |
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
| [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
| [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网络` `搜索` |
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
@@ -922,7 +923,7 @@ This project is [LobeHub Community License](./LICENSE) licensed.
[github-release-shield]: https://img.shields.io/github/v/release/lobehub/lobe-chat?color=369eff&labelColor=black&logo=github&style=flat-square
[github-releasedate-link]: https://github.com/lobehub/lobe-chat/releases
[github-releasedate-shield]: https://img.shields.io/github/release-date/lobehub/lobe-chat?labelColor=black&style=flat-square
[github-stars-link]: https://github.com/lobehub/lobe-chat/stargazers
[github-stars-link]: https://github.com/lobehub/lobe-chat/network/stargazers
[github-stars-shield]: https://img.shields.io/github/stars/lobehub/lobe-chat?color=ffcb47&labelColor=black&style=flat-square
[github-trending-shield]: https://trendshift.io/api/badge/repositories/2256
[github-trending-url]: https://trendshift.io/repositories/2256
+1 -2
View File
@@ -39,7 +39,7 @@
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/tsconfig": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron-toolkit/utils": "^4.0.0",
"@lobechat/electron-client-ipc": "workspace:*",
"@lobechat/electron-server-ipc": "workspace:*",
@@ -59,7 +59,6 @@
"electron-store": "^8.2.0",
"electron-vite": "^3.0.0",
"execa": "^9.5.2",
"fast-glob": "^3.3.3",
"fix-path": "^5.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
@@ -1,11 +1,7 @@
import { InterceptRouteParams, OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc';
import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
import { extractSubPath, findMatchingRoute } from '~common/routes';
import {
AppBrowsersIdentifiers,
BrowsersIdentifiers,
WindowTemplateIdentifiers,
} from '@/appBrowsers';
import { AppBrowsersIdentifiers, BrowsersIdentifiers, WindowTemplateIdentifiers } from '@/appBrowsers';
import { IpcClientEventSender } from '@/types/ipcClientEvent';
import { ControllerModule, ipcClientEvent, shortcut } from './index';
@@ -18,16 +14,11 @@ export default class BrowserWindowsCtr extends ControllerModule {
}
@ipcClientEvent('openSettingsWindow')
async openSettingsWindow(options?: string | OpenSettingsWindowOptions) {
const normalizedOptions: OpenSettingsWindowOptions =
typeof options === 'string' || options === undefined
? { tab: typeof options === 'string' ? options : undefined }
: options;
console.log('[BrowserWindowsCtr] Received request to open settings window', normalizedOptions);
async openSettingsWindow(tab?: string) {
console.log('[BrowserWindowsCtr] Received request to open settings window', tab);
try {
await this.app.browserManager.showSettingsWindowWithTab(normalizedOptions);
await this.app.browserManager.showSettingsWindowWithTab(tab);
return { success: true };
} catch (error) {
@@ -77,37 +68,15 @@ export default class BrowserWindowsCtr extends ControllerModule {
try {
if (matchedRoute.targetWindow === BrowsersIdentifiers.settings) {
const extractedSubPath = extractSubPath(path, matchedRoute.pathPrefix);
const sanitizedSubPath =
extractedSubPath && !extractedSubPath.startsWith('?') ? extractedSubPath : undefined;
let searchParams: Record<string, string> | undefined;
try {
const url = new URL(params.url);
const entries = Array.from(url.searchParams.entries());
if (entries.length > 0) {
searchParams = entries.reduce<Record<string, string>>((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
}
} catch (error) {
console.warn(
'[BrowserWindowsCtr] Failed to parse URL for settings route interception:',
params.url,
error,
);
}
const subPath = extractSubPath(path, matchedRoute.pathPrefix);
await this.app.browserManager.showSettingsWindowWithTab({
searchParams,
tab: sanitizedSubPath,
});
await this.app.browserManager.showSettingsWindowWithTab(subPath);
return {
intercepted: true,
path,
source,
subPath: sanitizedSubPath,
subPath,
targetWindow: matchedRoute.targetWindow,
};
} else {
@@ -136,8 +105,8 @@ export default class BrowserWindowsCtr extends ControllerModule {
*/
@ipcClientEvent('createMultiInstanceWindow')
async createMultiInstanceWindow(params: {
path: string;
templateId: WindowTemplateIdentifiers;
path: string;
uniqueId?: string;
}) {
try {
+52 -279
View File
@@ -1,10 +1,4 @@
import {
EditLocalFileParams,
EditLocalFileResult,
GlobFilesParams,
GlobFilesResult,
GrepContentParams,
GrepContentResult,
ListLocalFileParams,
LocalMoveFilesResultItem,
LocalReadFileParams,
@@ -19,10 +13,10 @@ import {
} from '@lobechat/electron-client-ipc';
import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
import { shell } from 'electron';
import fg from 'fast-glob';
import { Stats, constants } from 'node:fs';
import { access, mkdir, readFile, readdir, rename, stat, writeFile } from 'node:fs/promises';
import * as fs from 'node:fs';
import { rename as renamePromise } from 'node:fs/promises';
import * as path from 'node:path';
import { promisify } from 'node:util';
import FileSearchService from '@/services/fileSearchSrv';
import { FileResult, SearchOptions } from '@/types/fileSearch';
@@ -31,15 +25,40 @@ import { createLogger } from '@/utils/logger';
import { ControllerModule, ipcClientEvent } from './index';
// Create logger
// 创建日志记录器
const logger = createLogger('controllers:LocalFileCtr');
const statPromise = promisify(fs.stat);
const readdirPromise = promisify(fs.readdir);
const renamePromiseFs = promisify(fs.rename);
const accessPromise = promisify(fs.access);
const writeFilePromise = promisify(fs.writeFile);
export default class LocalFileCtr extends ControllerModule {
private get searchService() {
return this.app.getService(FileSearchService);
}
// ==================== File Operation ====================
/**
* Handle IPC event for local file search
*/
@ipcClientEvent('searchLocalFiles')
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
logger.debug('Received file search request:', { keywords: params.keywords });
const options: Omit<SearchOptions, 'keywords'> = {
limit: 30,
};
try {
const results = await this.searchService.search(params.keywords, options);
logger.debug('File search completed', { count: results.length });
return results;
} catch (error) {
logger.error('File search failed:', error);
return [];
}
}
@ipcClientEvent('openLocalFile')
async handleOpenLocalFile({ path: filePath }: OpenLocalFileParams): Promise<{
@@ -83,7 +102,7 @@ export default class LocalFileCtr extends ControllerModule {
const results: LocalReadFileResult[] = [];
for (const filePath of paths) {
// Initialize result object
// 初始化结果对象
logger.debug('Reading single file:', { filePath });
const result = await this.readFile({ path: filePath });
results.push(result);
@@ -139,7 +158,7 @@ export default class LocalFileCtr extends ControllerModule {
};
try {
const stats = await stat(filePath);
const stats = await statPromise(filePath);
if (stats.isDirectory()) {
logger.warn('Attempted to read directory content:', { filePath });
result.content = 'This is a directory and cannot be read as plain text.';
@@ -178,7 +197,7 @@ export default class LocalFileCtr extends ControllerModule {
const results: FileResult[] = [];
try {
const entries = await readdir(dirPath);
const entries = await readdirPromise(dirPath);
logger.debug('Directory entries retrieved successfully:', {
dirPath,
entriesCount: entries.length,
@@ -193,7 +212,7 @@ export default class LocalFileCtr extends ControllerModule {
const fullPath = path.join(dirPath, entry);
try {
const stats = await stat(fullPath);
const stats = await statPromise(fullPath);
const isDirectory = stats.isDirectory();
results.push({
createdTime: stats.birthtime,
@@ -241,7 +260,7 @@ export default class LocalFileCtr extends ControllerModule {
return [];
}
// Process each move request
// 逐个处理移动请求
for (const item of items) {
const { oldPath: sourcePath, newPath } = item;
const logPrefix = `[Moving file ${sourcePath} -> ${newPath}]`;
@@ -253,7 +272,7 @@ export default class LocalFileCtr extends ControllerModule {
success: false,
};
// Basic validation
// 基本验证
if (!sourcePath || !newPath) {
logger.error(`${logPrefix} Parameter validation failed: source or target path is empty`);
resultItem.error = 'Both oldPath and newPath are required for each item.';
@@ -262,9 +281,9 @@ export default class LocalFileCtr extends ControllerModule {
}
try {
// Check if source exists
// 检查源是否存在
try {
await access(sourcePath, constants.F_OK);
await accessPromise(sourcePath, fs.constants.F_OK);
logger.debug(`${logPrefix} Source file exists`);
} catch (accessError: any) {
if (accessError.code === 'ENOENT') {
@@ -278,28 +297,28 @@ export default class LocalFileCtr extends ControllerModule {
}
}
// Check if target path is the same as source path
// 检查目标路径是否与源路径相同
if (path.normalize(sourcePath) === path.normalize(newPath)) {
logger.info(`${logPrefix} Source and target paths are identical, skipping move`);
resultItem.success = true;
resultItem.newPath = newPath; // Report target path even if not moved
resultItem.newPath = newPath; // 即使未移动,也报告目标路径
results.push(resultItem);
continue;
}
// LBYL: Ensure target directory exists
// LBYL: 确保目标目录存在
const targetDir = path.dirname(newPath);
makeSureDirExist(targetDir);
logger.debug(`${logPrefix} Ensured target directory exists: ${targetDir}`);
// Execute move (rename)
await rename(sourcePath, newPath);
// 执行移动 (rename)
await renamePromiseFs(sourcePath, newPath);
resultItem.success = true;
resultItem.newPath = newPath;
logger.info(`${logPrefix} Move successful`);
} catch (error) {
logger.error(`${logPrefix} Move failed:`, error);
// Use similar error handling logic as handleMoveFile
// 使用与 handleMoveFile 类似的错误处理逻辑
let errorMessage = (error as Error).message;
if ((error as any).code === 'ENOENT')
errorMessage = `Source path not found: ${sourcePath}.`;
@@ -315,7 +334,7 @@ export default class LocalFileCtr extends ControllerModule {
errorMessage = `The target directory ${newPath} is not empty (relevant on some systems if target exists and is a directory).`;
else if ((error as any).code === 'EEXIST')
errorMessage = `An item already exists at the target path: ${newPath}.`;
// Keep more specific errors from access or directory checks
// 保留来自访问检查或目录检查的更具体错误
else if (
!errorMessage.startsWith('Source path not found') &&
!errorMessage.startsWith('Permission denied accessing source path') &&
@@ -392,9 +411,9 @@ export default class LocalFileCtr extends ControllerModule {
};
}
// Perform the rename operation using rename directly
// Perform the rename operation using fs.promises.rename directly
try {
await rename(currentPath, newPath);
await renamePromise(currentPath, newPath);
logger.info(`${logPrefix} Rename successful: ${currentPath} -> ${newPath}`);
// Optionally return the newPath if frontend needs it
// return { success: true, newPath: newPath };
@@ -425,7 +444,7 @@ export default class LocalFileCtr extends ControllerModule {
const logPrefix = `[Writing file ${filePath}]`;
logger.debug(`${logPrefix} Starting to write file`, { contentLength: content?.length });
// Validate parameters
// 验证参数
if (!filePath) {
logger.error(`${logPrefix} Parameter validation failed: path is empty`);
return { error: 'Path cannot be empty', success: false };
@@ -437,14 +456,14 @@ export default class LocalFileCtr extends ControllerModule {
}
try {
// Ensure target directory exists (use async to avoid blocking main thread)
// 确保目标目录存在
const dirname = path.dirname(filePath);
logger.debug(`${logPrefix} Creating directory: ${dirname}`);
await mkdir(dirname, { recursive: true });
fs.mkdirSync(dirname, { recursive: true });
// Write file content
// 写入文件内容
logger.debug(`${logPrefix} Starting to write content to file`);
await writeFile(filePath, content, 'utf8');
await writeFilePromise(filePath, content, 'utf8');
logger.info(`${logPrefix} File written successfully`, {
path: filePath,
size: content.length,
@@ -459,250 +478,4 @@ export default class LocalFileCtr extends ControllerModule {
};
}
}
// ==================== Search & Find ====================
/**
* Handle IPC event for local file search
*/
@ipcClientEvent('searchLocalFiles')
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
logger.debug('Received file search request:', { keywords: params.keywords });
const options: Omit<SearchOptions, 'keywords'> = {
limit: 30,
};
try {
const results = await this.searchService.search(params.keywords, options);
logger.debug('File search completed', { count: results.length });
return results;
} catch (error) {
logger.error('File search failed:', error);
return [];
}
}
@ipcClientEvent('grepContent')
async handleGrepContent(params: GrepContentParams): Promise<GrepContentResult> {
const {
pattern,
path: searchPath = process.cwd(),
output_mode = 'files_with_matches',
} = params;
const logPrefix = `[grepContent: ${pattern}]`;
logger.debug(`${logPrefix} Starting content search`, { output_mode, searchPath });
try {
const regex = new RegExp(
pattern,
`g${params['-i'] ? 'i' : ''}${params.multiline ? 's' : ''}`,
);
// Determine files to search
let filesToSearch: string[] = [];
const stats = await stat(searchPath);
if (stats.isFile()) {
filesToSearch = [searchPath];
} else {
// Use glob pattern if provided, otherwise search all files
const globPattern = params.glob || '**/*';
filesToSearch = await fg(globPattern, {
absolute: true,
cwd: searchPath,
dot: true,
ignore: ['**/node_modules/**', '**/.git/**'],
});
// Filter by type if provided
if (params.type) {
const ext = `.${params.type}`;
filesToSearch = filesToSearch.filter((file) => file.endsWith(ext));
}
}
logger.debug(`${logPrefix} Found ${filesToSearch.length} files to search`);
const matches: string[] = [];
let totalMatches = 0;
for (const filePath of filesToSearch) {
try {
const fileStats = await stat(filePath);
if (!fileStats.isFile()) continue;
const content = await readFile(filePath, 'utf8');
const lines = content.split('\n');
switch (output_mode) {
case 'files_with_matches': {
if (regex.test(content)) {
matches.push(filePath);
totalMatches++;
if (params.head_limit && matches.length >= params.head_limit) break;
}
break;
}
case 'content': {
const matchedLines: string[] = [];
for (let i = 0; i < lines.length; i++) {
if (regex.test(lines[i])) {
const contextBefore = params['-B'] || params['-C'] || 0;
const contextAfter = params['-A'] || params['-C'] || 0;
const startLine = Math.max(0, i - contextBefore);
const endLine = Math.min(lines.length - 1, i + contextAfter);
for (let j = startLine; j <= endLine; j++) {
const lineNum = params['-n'] ? `${j + 1}:` : '';
matchedLines.push(`${filePath}:${lineNum}${lines[j]}`);
}
totalMatches++;
}
}
matches.push(...matchedLines);
if (params.head_limit && matches.length >= params.head_limit) break;
break;
}
case 'count': {
const fileMatches = (content.match(regex) || []).length;
if (fileMatches > 0) {
matches.push(`${filePath}:${fileMatches}`);
totalMatches += fileMatches;
}
break;
}
}
} catch (error) {
logger.debug(`${logPrefix} Skipping file ${filePath}:`, error);
}
}
logger.info(`${logPrefix} Search completed`, {
matchCount: matches.length,
totalMatches,
});
return {
matches: params.head_limit ? matches.slice(0, params.head_limit) : matches,
success: true,
total_matches: totalMatches,
};
} catch (error) {
logger.error(`${logPrefix} Grep failed:`, error);
return {
matches: [],
success: false,
total_matches: 0,
};
}
}
@ipcClientEvent('globLocalFiles')
async handleGlobFiles({
path: searchPath = process.cwd(),
pattern,
}: GlobFilesParams): Promise<GlobFilesResult> {
const logPrefix = `[globFiles: ${pattern}]`;
logger.debug(`${logPrefix} Starting glob search`, { searchPath });
try {
const files = await fg(pattern, {
absolute: true,
cwd: searchPath,
dot: true,
onlyFiles: false,
stats: true,
});
// Sort by modification time (most recent first)
const sortedFiles = (files as unknown as Array<{ path: string; stats: Stats }>)
.sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime())
.map((f) => f.path);
logger.info(`${logPrefix} Glob completed`, { fileCount: sortedFiles.length });
return {
files: sortedFiles,
success: true,
total_files: sortedFiles.length,
};
} catch (error) {
logger.error(`${logPrefix} Glob failed:`, error);
return {
files: [],
success: false,
total_files: 0,
};
}
}
// ==================== File Editing ====================
@ipcClientEvent('editLocalFile')
async handleEditFile({
file_path: filePath,
new_string,
old_string,
replace_all = false,
}: EditLocalFileParams): Promise<EditLocalFileResult> {
const logPrefix = `[editFile: ${filePath}]`;
logger.debug(`${logPrefix} Starting file edit`, { replace_all });
try {
// Read file content
const content = await readFile(filePath, 'utf8');
// Check if old_string exists
if (!content.includes(old_string)) {
logger.error(`${logPrefix} Old string not found in file`);
return {
error: 'The specified old_string was not found in the file',
replacements: 0,
success: false,
};
}
// Perform replacement
let newContent: string;
let replacements: number;
if (replace_all) {
const regex = new RegExp(old_string.replaceAll(/[$()*+.?[\\\]^{|}]/g, '\\$&'), 'g');
const matches = content.match(regex);
replacements = matches ? matches.length : 0;
newContent = content.replaceAll(old_string, new_string);
} else {
// Replace only first occurrence
const index = content.indexOf(old_string);
if (index === -1) {
return {
error: 'Old string not found',
replacements: 0,
success: false,
};
}
newContent =
content.slice(0, index) + new_string + content.slice(index + old_string.length);
replacements = 1;
}
// Write back to file
await writeFile(filePath, newContent, 'utf8');
logger.info(`${logPrefix} File edited successfully`, { replacements });
return {
replacements,
success: true,
};
} catch (error) {
logger.error(`${logPrefix} Edit failed:`, error);
return {
error: (error as Error).message,
replacements: 0,
success: false,
};
}
}
}
@@ -77,7 +77,6 @@ export default class SystemController extends ControllerModule {
// 更新i18n实例的语言
await this.app.i18n.changeLanguage(locale === 'auto' ? app.getLocale() : locale);
this.app.browserManager.broadcastToAllWindows('localeChanged', { locale });
return { success: true };
}
@@ -64,7 +64,7 @@ describe('BrowserWindowsCtr', () => {
it('should show the settings window with the specified tab', async () => {
const tab = 'appearance';
const result = await browserWindowsCtr.openSettingsWindow(tab);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({ tab });
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith(tab);
expect(result).toEqual({ success: true });
});
@@ -120,11 +120,11 @@ describe('BrowserWindowsCtr', () => {
it('should show settings window if matched route target is settings', async () => {
const params: InterceptRouteParams = {
...baseParams,
path: '/settings/provider',
url: 'app://host/settings/provider?active=provider&provider=ollama',
path: '/settings?active=common',
url: 'app://host/settings?active=common',
};
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
const subPath = 'provider';
const subPath = 'common';
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
(extractSubPath as Mock).mockReturnValue(subPath);
@@ -132,10 +132,7 @@ describe('BrowserWindowsCtr', () => {
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
expect(extractSubPath).toHaveBeenCalledWith(params.path, matchedRoute.pathPrefix);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({
searchParams: { active: 'provider', provider: 'ollama' },
tab: subPath,
});
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith(subPath);
expect(result).toEqual({
intercepted: true,
path: params.path,
@@ -173,11 +170,11 @@ describe('BrowserWindowsCtr', () => {
it('should return error if processing route interception fails for settings', async () => {
const params: InterceptRouteParams = {
...baseParams,
path: '/settings',
path: '/settings?active=general',
url: 'app://host/settings?active=general',
};
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
const subPath = undefined;
const subPath = 'general';
const errorMessage = 'Processing error for settings';
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
(extractSubPath as Mock).mockReturnValue(subPath);
@@ -185,10 +182,6 @@ describe('BrowserWindowsCtr', () => {
const result = await browserWindowsCtr.interceptRoute(params);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({
searchParams: { active: 'general' },
tab: subPath,
});
expect(result).toEqual({
error: errorMessage,
intercepted: false,
@@ -1,392 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { App } from '@/core/App';
import LocalFileCtr from '../LocalFileCtr';
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock file-loaders
vi.mock('@lobechat/file-loaders', () => ({
SYSTEM_FILES_TO_IGNORE: ['.DS_Store', 'Thumbs.db'],
loadFile: vi.fn(),
}));
// Mock electron
vi.mock('electron', () => ({
shell: {
openPath: vi.fn(),
},
}));
// Mock fast-glob
vi.mock('fast-glob', () => ({
default: vi.fn(),
}));
// Mock node:fs/promises and node:fs
vi.mock('node:fs/promises', () => ({
stat: vi.fn(),
readdir: vi.fn(),
rename: vi.fn(),
access: vi.fn(),
writeFile: vi.fn(),
readFile: vi.fn(),
mkdir: vi.fn(),
}));
vi.mock('node:fs', () => ({
Stats: class Stats {},
constants: {
F_OK: 0,
},
stat: vi.fn(),
readdir: vi.fn(),
rename: vi.fn(),
access: vi.fn(),
writeFile: vi.fn(),
readFile: vi.fn(),
}));
// Mock FileSearchService
const mockSearchService = {
search: vi.fn(),
};
// Mock makeSureDirExist
vi.mock('@/utils/file-system', () => ({
makeSureDirExist: vi.fn(),
}));
const mockApp = {
getService: vi.fn(() => mockSearchService),
} as unknown as App;
describe('LocalFileCtr', () => {
let localFileCtr: LocalFileCtr;
let mockShell: any;
let mockFg: any;
let mockLoadFile: any;
let mockFsPromises: any;
beforeEach(async () => {
vi.clearAllMocks();
// Import mocks
mockShell = (await import('electron')).shell;
mockFg = (await import('fast-glob')).default;
mockLoadFile = (await import('@lobechat/file-loaders')).loadFile;
mockFsPromises = await import('node:fs/promises');
localFileCtr = new LocalFileCtr(mockApp);
});
describe('handleOpenLocalFile', () => {
it('should open file successfully', async () => {
vi.mocked(mockShell.openPath).mockResolvedValue('');
const result = await localFileCtr.handleOpenLocalFile({ path: '/test/file.txt' });
expect(result).toEqual({ success: true });
expect(mockShell.openPath).toHaveBeenCalledWith('/test/file.txt');
});
it('should return error when opening file fails', async () => {
const error = new Error('Failed to open');
vi.mocked(mockShell.openPath).mockRejectedValue(error);
const result = await localFileCtr.handleOpenLocalFile({ path: '/test/file.txt' });
expect(result).toEqual({ success: false, error: 'Failed to open' });
});
});
describe('handleOpenLocalFolder', () => {
it('should open directory when isDirectory is true', async () => {
vi.mocked(mockShell.openPath).mockResolvedValue('');
const result = await localFileCtr.handleOpenLocalFolder({
path: '/test/folder',
isDirectory: true,
});
expect(result).toEqual({ success: true });
expect(mockShell.openPath).toHaveBeenCalledWith('/test/folder');
});
it('should open parent directory when isDirectory is false', async () => {
vi.mocked(mockShell.openPath).mockResolvedValue('');
const result = await localFileCtr.handleOpenLocalFolder({
path: '/test/folder/file.txt',
isDirectory: false,
});
expect(result).toEqual({ success: true });
expect(mockShell.openPath).toHaveBeenCalledWith('/test/folder');
});
it('should return error when opening folder fails', async () => {
const error = new Error('Failed to open folder');
vi.mocked(mockShell.openPath).mockRejectedValue(error);
const result = await localFileCtr.handleOpenLocalFolder({
path: '/test/folder',
isDirectory: true,
});
expect(result).toEqual({ success: false, error: 'Failed to open folder' });
});
});
describe('readFile', () => {
it('should read file successfully with default location', async () => {
const mockFileContent = 'line1\nline2\nline3\nline4\nline5';
vi.mocked(mockLoadFile).mockResolvedValue({
content: mockFileContent,
filename: 'test.txt',
fileType: 'txt',
createdTime: new Date('2024-01-01'),
modifiedTime: new Date('2024-01-02'),
});
const result = await localFileCtr.readFile({ path: '/test/file.txt' });
expect(result.filename).toBe('test.txt');
expect(result.fileType).toBe('txt');
expect(result.totalLineCount).toBe(5);
expect(result.content).toBe(mockFileContent);
});
it('should read file with custom location range', async () => {
const mockFileContent = 'line1\nline2\nline3\nline4\nline5';
vi.mocked(mockLoadFile).mockResolvedValue({
content: mockFileContent,
filename: 'test.txt',
fileType: 'txt',
createdTime: new Date('2024-01-01'),
modifiedTime: new Date('2024-01-02'),
});
const result = await localFileCtr.readFile({ path: '/test/file.txt', loc: [1, 3] });
expect(result.content).toBe('line2\nline3');
expect(result.lineCount).toBe(2);
expect(result.totalLineCount).toBe(5);
});
it('should handle file read error', async () => {
vi.mocked(mockLoadFile).mockRejectedValue(new Error('File not found'));
const result = await localFileCtr.readFile({ path: '/test/missing.txt' });
expect(result.content).toContain('Error accessing or processing file');
expect(result.lineCount).toBe(0);
expect(result.charCount).toBe(0);
});
});
describe('readFiles', () => {
it('should read multiple files successfully', async () => {
vi.mocked(mockLoadFile).mockResolvedValue({
content: 'file content',
filename: 'test.txt',
fileType: 'txt',
createdTime: new Date('2024-01-01'),
modifiedTime: new Date('2024-01-02'),
});
const result = await localFileCtr.readFiles({
paths: ['/test/file1.txt', '/test/file2.txt'],
});
expect(result).toHaveLength(2);
expect(mockLoadFile).toHaveBeenCalledTimes(2);
});
});
describe('handleWriteFile', () => {
it('should write file successfully', async () => {
vi.mocked(mockFsPromises.mkdir).mockResolvedValue(undefined);
vi.mocked(mockFsPromises.writeFile).mockResolvedValue(undefined);
const result = await localFileCtr.handleWriteFile({
path: '/test/file.txt',
content: 'test content',
});
expect(result).toEqual({ success: true });
});
it('should return error when path is empty', async () => {
const result = await localFileCtr.handleWriteFile({
path: '',
content: 'test content',
});
expect(result).toEqual({ success: false, error: 'Path cannot be empty' });
});
it('should return error when content is undefined', async () => {
const result = await localFileCtr.handleWriteFile({
path: '/test/file.txt',
content: undefined as any,
});
expect(result).toEqual({ success: false, error: 'Content cannot be empty' });
});
it('should handle write error', async () => {
vi.mocked(mockFsPromises.mkdir).mockResolvedValue(undefined);
vi.mocked(mockFsPromises.writeFile).mockRejectedValue(new Error('Write failed'));
const result = await localFileCtr.handleWriteFile({
path: '/test/file.txt',
content: 'test content',
});
expect(result).toEqual({ success: false, error: 'Failed to write file: Write failed' });
});
});
describe('handleRenameFile', () => {
it('should rename file successfully', async () => {
vi.mocked(mockFsPromises.rename).mockResolvedValue(undefined);
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: 'new.txt',
});
expect(result).toEqual({ success: true, newPath: '/test/new.txt' });
expect(mockFsPromises.rename).toHaveBeenCalledWith('/test/old.txt', '/test/new.txt');
});
it('should skip rename when paths are identical', async () => {
const result = await localFileCtr.handleRenameFile({
path: '/test/file.txt',
newName: 'file.txt',
});
expect(result).toEqual({ success: true, newPath: '/test/file.txt' });
expect(mockFsPromises.rename).not.toHaveBeenCalled();
});
it('should reject invalid new name with path separators', async () => {
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: '../new.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid new name');
});
it('should reject invalid new name with special characters', async () => {
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: 'new:file.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid new name');
});
it('should handle file not found error', async () => {
const error: any = new Error('File not found');
error.code = 'ENOENT';
vi.mocked(mockFsPromises.rename).mockRejectedValue(error);
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: 'new.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('File or directory not found');
});
it('should handle file already exists error', async () => {
const error: any = new Error('File exists');
error.code = 'EEXIST';
vi.mocked(mockFsPromises.rename).mockRejectedValue(error);
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: 'new.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('already exists');
});
});
describe('handleLocalFilesSearch', () => {
it('should search files successfully', async () => {
const mockResults = [
{
name: 'test.txt',
path: '/test/test.txt',
isDirectory: false,
size: 100,
type: 'txt',
},
];
mockSearchService.search.mockResolvedValue(mockResults);
const result = await localFileCtr.handleLocalFilesSearch({ keywords: 'test' });
expect(result).toEqual(mockResults);
expect(mockSearchService.search).toHaveBeenCalledWith('test', { limit: 30 });
});
it('should return empty array on search error', async () => {
mockSearchService.search.mockRejectedValue(new Error('Search failed'));
const result = await localFileCtr.handleLocalFilesSearch({ keywords: 'test' });
expect(result).toEqual([]);
});
});
describe('handleGlobFiles', () => {
it('should glob files successfully', async () => {
const mockFiles = [
{ path: '/test/file1.txt', stats: { mtime: new Date('2024-01-02') } },
{ path: '/test/file2.txt', stats: { mtime: new Date('2024-01-01') } },
];
vi.mocked(mockFg).mockResolvedValue(mockFiles);
const result = await localFileCtr.handleGlobFiles({
pattern: '*.txt',
path: '/test',
});
expect(result.success).toBe(true);
expect(result.files).toEqual(['/test/file1.txt', '/test/file2.txt']);
expect(result.total_files).toBe(2);
});
it('should handle glob error', async () => {
vi.mocked(mockFg).mockRejectedValue(new Error('Glob failed'));
const result = await localFileCtr.handleGlobFiles({
pattern: '*.txt',
});
expect(result).toEqual({
success: false,
files: [],
total_files: 0,
});
});
});
});
+1 -26
View File
@@ -1,12 +1,11 @@
import { ElectronIPCEventHandler, ElectronIPCServer } from '@lobechat/electron-server-ipc';
import { Session, app, ipcMain, protocol } from 'electron';
import { macOS, windows } from 'electron-is';
import { pathExistsSync, remove } from 'fs-extra';
import os from 'node:os';
import { join } from 'node:path';
import { name } from '@/../../package.json';
import { buildDir, LOCAL_DATABASE_DIR, nextStandaloneDir } from '@/const/dir';
import { buildDir, nextStandaloneDir } from '@/const/dir';
import { isDev } from '@/const/env';
import { IControlModule } from '@/controllers';
import { IServiceModule } from '@/services';
@@ -130,9 +129,6 @@ export class App {
this.initDevBranding();
// Clean up stale database lock file before starting IPC server
await this.cleanupDatabaseLock();
// ==============
await this.ipcServer.start();
logger.debug('IPC server started');
@@ -375,27 +371,6 @@ export class App {
}
};
/**
* Clean up stale database lock file from previous crashes or abnormal exits
*/
private cleanupDatabaseLock = async () => {
try {
const dbPath = join(this.appStoragePath, LOCAL_DATABASE_DIR);
const lockPath = `${dbPath}.lock`;
if (pathExistsSync(lockPath)) {
logger.info(`Cleaning up stale database lock file: ${lockPath}`);
await remove(lockPath);
logger.info('Database lock file removed successfully');
} else {
logger.debug('No database lock file found, skipping cleanup');
}
} catch (error) {
logger.error('Failed to cleanup database lock file:', error);
// Non-fatal error, allow application to continue
}
};
private registerNextHandler() {
logger.debug('Registering Next.js handler');
const handler = createHandler({
@@ -1,282 +0,0 @@
import { app } from 'electron';
import { pathExistsSync, remove } from 'fs-extra';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LOCAL_DATABASE_DIR } from '@/const/dir';
// Mock electron modules
vi.mock('electron', () => ({
app: {
getAppPath: vi.fn(() => '/mock/app/path'),
getLocale: vi.fn(() => 'en-US'),
getPath: vi.fn(() => '/mock/user/path'),
requestSingleInstanceLock: vi.fn(() => true),
whenReady: vi.fn(() => Promise.resolve()),
on: vi.fn(),
commandLine: {
appendSwitch: vi.fn(),
},
dock: {
setIcon: vi.fn(),
},
exit: vi.fn(),
},
ipcMain: {
handle: vi.fn(),
},
nativeTheme: {
on: vi.fn(),
shouldUseDarkColors: false,
},
protocol: {
registerSchemesAsPrivileged: vi.fn(),
},
}));
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock fs-extra module
vi.mock('fs-extra', async () => {
const actual = await vi.importActual('fs-extra');
return {
...actual,
pathExistsSync: vi.fn(),
remove: vi.fn(),
};
});
// Mock common/routes
vi.mock('~common/routes', () => ({
findMatchingRoute: vi.fn(),
extractSubPath: vi.fn(),
}));
// Mock other dependencies
vi.mock('electron-is', () => ({
macOS: vi.fn(() => false),
windows: vi.fn(() => false),
}));
vi.mock('fix-path', () => ({
default: vi.fn(),
}));
vi.mock('@/const/env', () => ({
isDev: false,
}));
vi.mock('@/const/dir', () => ({
buildDir: '/mock/build',
nextStandaloneDir: '/mock/standalone',
LOCAL_DATABASE_DIR: 'lobehub-local-db',
appStorageDir: '/mock/storage/path',
userDataDir: '/mock/user/data',
DB_SCHEMA_HASH_FILENAME: 'lobehub-local-db-schema-hash',
FILE_STORAGE_DIR: 'file-storage',
INSTALL_PLUGINS_DIR: 'plugins',
LOCAL_STORAGE_URL_PREFIX: '/lobe-desktop-file',
}));
vi.mock('@lobechat/electron-server-ipc', () => ({
ElectronIPCServer: vi.fn().mockImplementation(() => ({
start: vi.fn().mockResolvedValue(undefined),
})),
}));
// Mock all infrastructure managers
vi.mock('../infrastructure/I18nManager', () => ({
I18nManager: vi.fn().mockImplementation(() => ({
init: vi.fn().mockResolvedValue(undefined),
})),
}));
vi.mock('../infrastructure/StoreManager', () => ({
StoreManager: vi.fn().mockImplementation(() => ({
get: vi.fn((key) => {
if (key === 'storagePath') return '/mock/storage/path';
return undefined;
}),
set: vi.fn(),
})),
}));
vi.mock('../infrastructure/StaticFileServerManager', () => ({
StaticFileServerManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn().mockResolvedValue(undefined),
destroy: vi.fn(),
})),
}));
vi.mock('../infrastructure/UpdaterManager', () => ({
UpdaterManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn().mockResolvedValue(undefined),
})),
}));
vi.mock('../infrastructure/ProtocolManager', () => ({
ProtocolManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
processPendingUrls: vi.fn().mockResolvedValue(undefined),
})),
}));
vi.mock('../browser/BrowserManager', () => ({
BrowserManager: vi.fn().mockImplementation(() => ({
initializeBrowsers: vi.fn(),
getIdentifierByWebContents: vi.fn(),
})),
}));
vi.mock('../ui/MenuManager', () => ({
MenuManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
})),
}));
vi.mock('../ui/ShortcutManager', () => ({
ShortcutManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
})),
}));
vi.mock('../ui/TrayManager', () => ({
TrayManager: vi.fn().mockImplementation(() => ({
initializeTrays: vi.fn(),
destroyAll: vi.fn(),
})),
}));
vi.mock('@/utils/next-electron-rsc', () => ({
createHandler: vi.fn(() => ({
createInterceptor: vi.fn(),
registerCustomHandler: vi.fn(),
})),
}));
// Mock controllers and services
vi.mock('../../controllers/*Ctr.ts', () => ({}));
vi.mock('../../services/*Srv.ts', () => ({}));
// Import after mocks are set up
import { App } from '../App';
describe('App - Database Lock Cleanup', () => {
let appInstance: App;
let mockLockPath: string;
beforeEach(() => {
vi.clearAllMocks();
// Mock glob imports to return empty arrays
(import.meta as any).glob = vi.fn(() => ({}));
mockLockPath = join('/mock/storage/path', LOCAL_DATABASE_DIR) + '.lock';
});
afterEach(() => {
vi.clearAllMocks();
});
describe('bootstrap - database lock cleanup', () => {
it('should remove stale lock file if it exists during bootstrap', async () => {
// Setup: simulate existing lock file
vi.mocked(pathExistsSync).mockReturnValue(true);
vi.mocked(remove).mockResolvedValue(undefined);
// Create app instance
appInstance = new App();
// Call bootstrap which should trigger cleanup
await appInstance.bootstrap();
// Verify: lock file check was called
expect(pathExistsSync).toHaveBeenCalledWith(mockLockPath);
// Verify: lock file was removed
expect(remove).toHaveBeenCalledWith(mockLockPath);
});
it('should not attempt to remove lock file if it does not exist', async () => {
// Setup: no lock file exists
vi.mocked(pathExistsSync).mockReturnValue(false);
// Create app instance
appInstance = new App();
// Call bootstrap
await appInstance.bootstrap();
// Verify: lock file check was called
expect(pathExistsSync).toHaveBeenCalledWith(mockLockPath);
// Verify: remove was NOT called since file doesn't exist
expect(remove).not.toHaveBeenCalled();
});
it('should continue bootstrap even if lock cleanup fails', async () => {
// Setup: simulate lock file exists but cleanup fails
vi.mocked(pathExistsSync).mockReturnValue(true);
vi.mocked(remove).mockRejectedValue(new Error('Permission denied'));
// Create app instance
appInstance = new App();
// Bootstrap should not throw even if cleanup fails
await expect(appInstance.bootstrap()).resolves.not.toThrow();
// Verify: cleanup was attempted
expect(pathExistsSync).toHaveBeenCalledWith(mockLockPath);
expect(remove).toHaveBeenCalledWith(mockLockPath);
});
it('should clean up lock file before starting IPC server', async () => {
// Setup
vi.mocked(pathExistsSync).mockReturnValue(true);
const callOrder: string[] = [];
vi.mocked(remove).mockImplementation(async () => {
callOrder.push('remove');
});
// Mock IPC server start to track call order
const { ElectronIPCServer } = await import('@lobechat/electron-server-ipc');
const mockStart = vi.fn().mockImplementation(() => {
callOrder.push('ipcServer.start');
return Promise.resolve();
});
vi.mocked(ElectronIPCServer).mockImplementation(
() =>
({
start: mockStart,
}) as any,
);
// Create app instance and bootstrap
appInstance = new App();
await appInstance.bootstrap();
// Verify: cleanup happens before IPC server starts
expect(callOrder).toEqual(['remove', 'ipcServer.start']);
});
});
describe('appStoragePath', () => {
it('should return storage path from store manager', () => {
appInstance = new App();
const storagePath = appInstance.appStoragePath;
expect(storagePath).toBe('/mock/storage/path');
});
});
});
@@ -1,18 +1,9 @@
import {
MainBroadcastEventKey,
MainBroadcastParams,
OpenSettingsWindowOptions,
} from '@lobechat/electron-client-ipc';
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
import { WebContents } from 'electron';
import { createLogger } from '@/utils/logger';
import {
AppBrowsersIdentifiers,
WindowTemplateIdentifiers,
appBrowsers,
windowTemplates,
} from '../../appBrowsers';
import { AppBrowsersIdentifiers, appBrowsers, WindowTemplate, WindowTemplateIdentifiers, windowTemplates } from '../../appBrowsers';
import type { App } from '../App';
import type { BrowserWindowOpts } from './Browser';
import Browser from './Browser';
@@ -72,35 +63,14 @@ export class BrowserManager {
* Display the settings window and navigate to a specific tab
* @param tab Settings window sub-path tab
*/
async showSettingsWindowWithTab(options?: OpenSettingsWindowOptions) {
const tab = options?.tab;
const searchParams = options?.searchParams;
const query = new URLSearchParams();
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => {
if (value !== undefined) query.set(key, value);
});
}
if (tab && tab !== 'common' && !query.has('active')) {
query.set('active', tab);
}
const queryString = query.toString();
const activeTab = query.get('active') ?? tab;
logger.debug(
`Showing settings window with navigation: active=${activeTab || 'default'}, query=${
queryString || 'none'
}`,
);
if (queryString) {
const browser = await this.redirectToPage('settings', undefined, queryString);
async showSettingsWindowWithTab(tab?: string) {
logger.debug(`Showing settings window with tab: ${tab || 'default'}`);
// common is the main path for settings route
if (tab && tab !== 'common') {
const browser = await this.redirectToPage('settings', tab);
// make provider page more large
if (activeTab?.startsWith('provider')) {
if (tab.startsWith('provider/')) {
logger.debug('Resizing window for provider settings');
browser.setWindowSize({ height: 1000, width: 1400 });
browser.moveToCenter();
@@ -117,7 +87,7 @@ export class BrowserManager {
* @param identifier Window identifier
* @param subPath Sub-path, such as 'agent', 'about', etc.
*/
async redirectToPage(identifier: string, subPath?: string, search?: string) {
async redirectToPage(identifier: string, subPath?: string) {
try {
// Ensure window is retrieved or created
const browser = this.retrieveByIdentifier(identifier);
@@ -135,14 +105,11 @@ export class BrowserManager {
// Build complete URL path
const fullPath = subPath ? `${baseRoute}/${subPath}` : baseRoute;
const normalizedSearch =
search && search.length > 0 ? (search.startsWith('?') ? search : `?${search}`) : '';
const fullUrl = `${fullPath}${normalizedSearch}`;
logger.debug(`Redirecting to: ${fullUrl}`);
logger.debug(`Redirecting to: ${fullPath}`);
// Load URL and show window
await browser.loadUrl(fullUrl);
await browser.loadUrl(fullPath);
browser.show();
return browser;
@@ -176,20 +143,14 @@ export class BrowserManager {
* @param uniqueId Optional unique identifier, will be generated if not provided
* @returns The window identifier and Browser instance
*/
createMultiInstanceWindow(
templateId: WindowTemplateIdentifiers,
path: string,
uniqueId?: string,
) {
createMultiInstanceWindow(templateId: WindowTemplateIdentifiers, path: string, uniqueId?: string) {
const template = windowTemplates[templateId];
if (!template) {
throw new Error(`Window template ${templateId} not found`);
}
// Generate unique identifier
const windowId =
uniqueId ||
`${template.baseIdentifier}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
const windowId = uniqueId || `${template.baseIdentifier}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Create browser options from template
const browserOpts: BrowserWindowOpts = {
@@ -203,8 +164,8 @@ export class BrowserManager {
const browser = this.retrieveOrInitialize(browserOpts);
return {
browser: browser,
identifier: windowId,
browser: browser,
};
}
@@ -215,7 +176,7 @@ export class BrowserManager {
*/
getWindowsByTemplate(templateId: string): string[] {
const prefix = `${templateId}_`;
return Array.from(this.browsers.keys()).filter((id) => id.startsWith(prefix));
return Array.from(this.browsers.keys()).filter(id => id.startsWith(prefix));
}
/**
@@ -224,7 +185,7 @@ export class BrowserManager {
*/
closeWindowsByTemplate(templateId: string): void {
const windowIds = this.getWindowsByTemplate(templateId);
windowIds.forEach((id) => {
windowIds.forEach(id => {
const browser = this.browsers.get(id);
if (browser) {
browser.close();
@@ -274,7 +235,8 @@ export class BrowserManager {
});
browser.browserWindow.on('show', () => {
if (browser.webContents) this.webContentsMap.set(browser.webContents, browser.identifier);
if (browser.webContents)
this.webContentsMap.set(browser.webContents, browser.identifier);
});
return browser;
@@ -1,110 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# API Integration and Services
## Service Architecture
The app integrates with various APIs and services for chat functionality and data management.
### Core Services
#### Session Service
- [services/session.ts](mdc:services/session.ts) - Session management API
- [services/type.ts](mdc:services/type.ts) - Service type definitions
- [services/mock/](mdc:services/mock) - Mock data for development
- [services/mock/getGroupedSessions.json](mdc:services/mock/getGroupedSessions.json) - Mock session data
#### OpenAI Integration
- [store/openai.ts](mdc:store/openai.ts) - OpenAI configuration store
- Manages API keys and proxy settings
- Persists configuration in AsyncStorage
### API Utilities
#### HTTP Utilities
- [utils/fetchSSE.ts](mdc:utils/fetchSSE.ts) - Server-Sent Events implementation
- [utils/trpc.ts](mdc:utils/trpc.ts) - tRPC client configuration
- [utils/jwt.ts](mdc:utils/jwt.ts) - JWT token handling
#### Data Processing
- [utils/merge.ts](mdc:utils/merge.ts) - Deep merge utility
- [utils/componentScanner.ts](mdc:utils/componentScanner.ts) - Component scanning utilities
## Chat Integration
### OpenAI API
- Uses Server-Sent Events for streaming responses
- Supports multiple models and configurations
- Handles API key management and proxy settings
### Message Handling
- Real-time message streaming
- Error handling and retry logic
- Message persistence in local storage
## Data Flow Patterns
### State Management Integration
```typescript
// Store updates trigger API calls
const sendMessage = async (message: string) => {
setLoading(true);
try {
const response = await api.sendMessage(message);
updateChatState(response);
} catch (error) {
handleError(error);
} finally {
setLoading(false);
}
};
```
### Error Handling
- Implement proper error boundaries
- Show user-friendly error messages
- Retry failed requests with exponential backoff
- Log errors for debugging
### Loading States
- Show loading indicators during API calls
- Implement skeleton screens for better UX
- Handle partial loading states
## Development Patterns
### Mock Data
- Use mock services during development
- [services/mock/](mdc:services/mock) - Mock data files
- Switch between mock and real APIs easily
### Environment Configuration
- Use environment variables for API keys
- Support different environments (dev, staging, prod)
- Secure sensitive configuration
### API Versioning
- Support multiple API versions
- Implement graceful deprecation
- Maintain backward compatibility
## Security Considerations
### API Key Management
- Never commit API keys to version control
- Use secure storage for sensitive data
- Implement proper key rotation
### Data Validation
- Validate all API responses
- Use TypeScript for type safety
- Implement input sanitization
### Network Security
- Use HTTPS for all API calls
- Implement certificate pinning if needed
- Handle network errors gracefully
@@ -1,46 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# App Structure and Routing
## Expo Router File Structure
The app uses Expo Router with file-based routing. Each directory represents a route group or screen.
### Main Routes
- [src/app/index.tsx](mdc:src/app/index.tsx) - Root redirect logic (signed in → session, signed out → sign-in)
- [src/app/_layout.tsx](mdc:src/app/_layout.tsx) - Root layout with providers ( Toast, Portal, ActionSheet)
### Chat Routes
- [src/app/(chat)/_layout.tsx](mdc:src/app/(chat)/_layout.tsx) - Chat layout with bottom tabs
- [src/app/(chat)/session/index.tsx](mdc:src/app/(chat)/session/index.tsx) - Session list screen
- [src/app/(chat)/session/_layout.tsx](mdc:src/app/(chat)/session/_layout.tsx) - Session layout
- [src/app/(chat)/chat/index.tsx](mdc:src/app/(chat)/chat/index.tsx) - Main chat interface
- [src/app/(chat)/chat/_layout.tsx](mdc:src/app/(chat)/chat/_layout.tsx) - Chat layout
- [src/app/(chat)/detail/index.tsx](mdc:src/app/(chat)/detail/index.tsx) - Chat detail view
- [src/app/(chat)/detail/_layout.tsx](mdc:src/app/(chat)/detail/_layout.tsx) - Detail layout
### Settings Routes
- [src/app/(setting)/_layout.tsx](mdc:src/app/(setting)/_layout.tsx) - Settings layout
- [src/app/(setting)/setting/index.tsx](mdc:src/app/(setting)/setting/index.tsx) - Settings screen
- [src/app/(setting)/setting/_layout.tsx](mdc:src/app/(setting)/setting/_layout.tsx) - Settings layout
- [src/app/(setting)/setting/provider.tsx](mdc:src/app/(setting)/setting/provider.tsx) - Settings provider
### Playground Routes
- [src/app/playground/_layout.tsx](mdc:src/app/playground/_layout.tsx) - Playground layout
- [src/app/playground/index.tsx](mdc:src/app/playground/index.tsx) - Component playground
- [src/app/playground/highlighter.tsx](mdc:src/app/playground/highlighter.tsx) - Code highlighting demo
- [src/app/playground/markdown.tsx](mdc:src/app/playground/markdown.tsx) - Markdown rendering demo
- [src/app/playground/toast.tsx](mdc:src/app/playground/toast.tsx) - Toast notifications demo
- [src/app/playground/tooltip.tsx](mdc:src/app/playground/tooltip.tsx) - Tooltip component demo
## Route Groups
- `(chat)` - Main chat functionality
- `(setting)` - App settings and configuration
- `playground` - Component development and testing
## Navigation Patterns
- Use `Redirect` component for programmatic navigation
- Use bottom tabs for main navigation in chat section
@@ -1,288 +0,0 @@
---
description: 组件导入规范 - demos 和组件内部的导入路径规范
globs: src/components/**/*.tsx,src/components/**/*.ts
---
# 组件导入规范
本规范定义了 LobeChat Mobile 组件库中不同文件类型应该如何导入依赖。
## 规范概述
### 1. Demo 文件 (`demos/*.tsx`)
**✅ 正确做法:从 `@lobehub/ui-rn` 导入所有公开组件**
```tsx
// demos/basic.tsx
import { Button, Space, Text, useTheme } from '@lobehub/ui-rn';
import React from 'react';
import { View } from 'react-native';
export default () => {
const token = useTheme();
return (
<Space gap={16}>
<Button>示例按钮</Button>
<Text>示例文本</Text>
</Space>
);
};
```
**❌ 错误做法:使用 `@/components` 路径**
```tsx
// ❌ 不要这样做
import Text from '@/components/Text';
import { useTheme } from '@/components/styles';
```
### 2. 组件主文件 (`ComponentName.tsx`)
**✅ 正确做法:使用相对路径引用其他组件**
```tsx
// Card/Card.tsx
import React from 'react';
import Block from '../Block'; // ✅ 相对路径
import Text from '../Text'; // ✅ 相对路径
import { useStyles } from './style';
import type { CardProps } from './type';
```
**❌ 错误做法:使用绝对路径 `@/components`**
```tsx
// ❌ 不要这样做
import Block from '@/components/Block';
import Text from '@/components/Text';
```
### 3. 样式文件 (`style.ts`)
**✅ 正确做法:使用 `@/components/styles` 导入主题工具**
```tsx
// style.ts
import { createStyles } from '@/components/styles'; // ✅ 允许
export const useStyles = createStyles(({ token, stylish }) => ({
root: {
backgroundColor: token.colorBgContainer,
},
}));
```
### 4. 类型文件 (`type.ts`)
**✅ 正确做法:使用相对路径引用其他组件的类型**
```tsx
// Card/type.ts
import type { BlockProps } from '../Block'; // ✅ 相对路径
import type { ViewProps } from 'react-native';
export interface CardProps extends ViewProps {
// ...
}
```
**❌ 错误做法:使用绝对路径**
```tsx
// ❌ 不要这样做
import type { BlockProps } from '@/components/Block';
```
## 导入顺序规范
遵循以下导入顺序:
```tsx
// 1. 外部库 - React 相关
import React, { useState, useCallback } from 'react';
import { View, Text } from 'react-native';
// 2. 外部库 - 第三方库
import { Lucide } from '@lobehub/ui';
import dayjs from 'dayjs';
// 3. 内部组件 (demos 使用 @lobehub/ui-rn)
import { Button, Space, Text, useTheme } from '@lobehub/ui-rn';
// 4. 相对路径导入 (组件内部使用)
import { useStyles } from './style';
import type { ComponentProps } from './type';
```
## 常见场景示例
### Demo 文件完整示例
```tsx
// demos/basic.tsx
import { Button, Space, Text } from '@lobehub/ui-rn';
import React from 'react';
export default () => {
return (
<Space gap={16}>
<Button type="primary">主要按钮</Button>
<Button type="default">默认按钮</Button>
</Space>
);
};
```
### 组件文件完整示例
```tsx
// Alert/index.tsx
import React, { memo } from 'react';
import { View } from 'react-native';
// 相对路径引用其他组件
import ActionIcon from '../ActionIcon';
import Icon from '../Icon';
import Text from '../Text';
// 本地文件
import { useStyles } from './style';
import type { AlertProps } from './type';
const Alert = memo<AlertProps>((props) => {
// ...
});
export default Alert;
```
### 包含 Theme 的示例
```tsx
// demos/advanced.tsx
import { Button, Text, useTheme } from '@lobehub/ui-rn'; // ✅ 从 @lobehub/ui-rn 导入
import React from 'react';
import { View } from 'react-native';
export default () => {
const token = useTheme(); // 使用主题 token
return (
<View style={{ padding: token.padding }}>
<Text>示例内容</Text>
</View>
);
};
```
## 特殊情况
### 1. ThemeProvider 内部文件
ThemeProvider 模块内部可以使用绝对路径引用自己的子模块:
```tsx
// ThemeProvider/getDesignToken.ts
import { lightAlgorithm } from '@/components/styles/algorithm/light'; // ✅ 允许
import type { AliasToken } from '@/components/styles/interface'; // ✅ 允许
```
### 2. 工具函数和类型
非组件的工具文件可以使用 `@/components/styles`
```tsx
// theme/createStyles.ts
import type { ThemeAppearance } from '@/components/ThemeProvider/types'; // ✅ 允许
```
## 验证命令
使用以下命令验证导入规范:
```bash
# 检查 demos 文件中的 @/components 引用(应该为 0
find src/components -path "*/demos/*.tsx" -exec grep -l "@/components" {} \;
# 检查组件间的绝对路径引用(应该很少)
grep -r "from '@/components/[A-Z]" src/components --include="*.tsx" --exclude-dir="demos"
```
## 自动修复脚本
如果发现违反规范的导入,可以使用以下脚本修复:
```javascript
// fix-imports.js
const fs = require('fs');
const path = require('path');
// 检测并修复 demos 文件
function fixDemoImports(filePath) {
let content = fs.readFileSync(filePath, 'utf-8');
// 将 @/components/ComponentName 改为从 @lobehub/ui-rn 导入
content = content.replace(
/import\s+(\w+)\s+from\s+['"]@\/components\/(\w+)['"]/g,
(match, name, component) => {
// 添加到 @lobehub/ui-rn 导入中
return `// import ${name} from '@lobehub/ui-rn'`;
}
);
fs.writeFileSync(filePath, content);
}
```
## 常见错误和解决方案
### 错误 1: Demo 文件使用 `@/components`
```tsx
// ❌ 错误
import Text from '@/components/Text';
// ✅ 修复
import { Text } from '@lobehub/ui-rn';
```
### 错误 2: 组件文件使用绝对路径引用其他组件
```tsx
// ❌ 错误
import Block from '@/components/Block';
// ✅ 修复
import Block from '../Block';
```
### 错误 3: 重复导入
```tsx
// ❌ 错误
import { Button } from '@lobehub/ui-rn';
import Button from '../../Button';
// ✅ 修复 - 只保留一个
import { Button } from '@lobehub/ui-rn'; // demos 中
// 或
import Button from '../../Button'; // 组件内部
```
## 总结
| 文件类型 | 导入其他组件 | 导入 Theme | 示例 |
|---------|------------|-----------|------|
| `demos/*.tsx` | `@lobehub/ui-rn` | `@lobehub/ui-rn` | `import { Button, useTheme } from '@lobehub/ui-rn'` |
| `ComponentName.tsx` | 相对路径 | 相对路径或 `@/components/styles` | `import Block from '../Block'` |
| `style.ts` | - | `@/components/styles` | `import { createStyles } from '@/components/styles'` |
| `type.ts` | 相对路径(类型) | 相对路径(类型) | `import type { BlockProps } from '../Block'` |
**核心原则:**
- ✅ Demos 展示公开 API,使用 `@lobehub/ui-rn`
- ✅ 组件内部保持模块化,使用相对路径
- ✅ 样式文件可以使用 `@/components/styles` 获取主题工具
- ✅ 保持导入语句的一致性和清晰性
@@ -1,791 +0,0 @@
---
globs: src/components/**
description: 组件库标准结构、样式管理和开发流程规范
---
# 组件库结构规范
本规范定义了 LobeChat Mobile 组件库的标准结构、样式管理方式和开发流程。
## 组件目录结构
每个组件应遵循以下标准目录结构:
```
src/components/ComponentName/
├── ComponentName.tsx # 组件主文件
├── index.ts # 导出文件
├── index.md # 组件文档
├── style.ts # 样式定义
├── type.ts # TypeScript 类型定义
└── demos/ # 示例目录
├── index.tsx # 示例入口
├── basic.tsx # 基础示例
└── ... # 其他示例
```
### 文件职责说明
#### 1. `ComponentName.tsx` - 组件主文件
组件的核心实现文件,命名采用 PascalCase
```tsx
import React, { memo, useMemo } from 'react';
import { cva } from '@/components/styles';
import { useStyles } from './style';
import type { ComponentNameProps } from './type';
const ComponentName = memo<ComponentNameProps>(
({ variant = 'default', children, style, ...rest }) => {
const { styles } = useStyles();
// 使用 CVA 管理样式变体
const variants = useMemo(
() =>
cva(styles.root, {
variants: {
variant: {
default: styles.default,
primary: styles.primary,
},
},
compoundVariants: [
{
variant: 'primary',
pressed: true,
style: styles.primaryActive,
},
],
defaultVariants: {
variant: 'default',
},
}),
[styles],
);
return (
<View
style={({ hovered, pressed }) => [
variants({ variant, hovered, pressed }),
style,
]}
{...rest}
>
{children}
</View>
);
},
);
ComponentName.displayName = 'ComponentName';
export default ComponentName;
```
**关键要点:**
- 使用 `memo` 包裹组件优化性能
- 从 `./style` 导入 `useStyles` 钩子
- 从 `./type` 导入类型定义
- 使用 `cva` 管理复杂的样式变体逻辑
- 使用 `useMemo` 缓存 CVA 配置,依赖 `styles`
- 设置 `displayName` 便于调试
#### 2. `style.ts` - 样式定义文件
使用 `createStyles` 创建主题感知的样式:
```tsx
import { createStyles } from '@/components/styles';
export const useStyles = createStyles(({ token, stylish }) => ({
// 基础样式
root: {
borderRadius: token.borderRadius,
position: 'relative' as const,
},
// 变体样式 - 使用 stylish 预设
default: stylish.variantFilled,
primary: stylish.variantOutlined,
// 交互状态样式
primaryActive: stylish.variantOutlinedActive,
primaryHover: stylish.variantOutlinedHover,
// 附加效果
shadow: stylish.shadow,
glass: stylish.blur,
}));
```
**关键要点:**
- 统一使用 `createStyles` 工厂函数
- 通过 `token` 访问主题变量(颜色、间距、圆角等)
- 通过 `stylish` 访问预设样式(变体、阴影、模糊等)
- 使用 `as const` 确保字面量类型
- 样式名称应语义化,便于在 CVA 中引用
#### 3. `type.ts` - 类型定义文件
定义组件的 Props 接口:
```tsx
import type { ViewProps } from 'react-native';
export interface ComponentNameProps extends ViewProps {
/**
* 样式变体
* @default 'default'
*/
variant?: 'default' | 'primary' | 'secondary';
/**
* 是否显示阴影
*/
shadow?: boolean;
/**
* 是否启用玻璃效果
*/
glass?: boolean;
/**
* 点击回调
*/
onPress?: () => void;
}
```
**关键要点:**
- 继承自基础组件的 Props(如 `ViewProps`, `FlexboxProps`
- 为每个 prop 添加 JSDoc 注释
- 使用 `@default` 标注默认值
- 使用字面量类型限制可选值
#### 4. `index.ts` - 导出文件
标准导出格式:
```tsx
export { default } from './ComponentName';
export type * from './type';
```
**关键要点:**
- 默认导出组件本身
- 使用 `export type *` 导出所有类型
#### 5. `index.md` - 组件文档
使用 YAML frontmatter 定义元数据:
```markdown
---
group: Layout
title: ComponentName
description: 组件的简短描述,说明主要功能和特点。
---
## Features
- ✅ 功能特性 1
- ✅ 功能特性 2
- ✅ TypeScript 支持
- ✅ 主题适配
## Basic Usage
\`\`\`tsx
import { ComponentName } from '@lobehub/ui-rn';
<ComponentName variant="primary">
<Text>示例内容</Text>
</ComponentName>
\`\`\`
## API
详细的 Props 说明...
```
**关键要点:**
- `group`: 组件分类(Layout、Form、Display 等)
- `title`: 组件名称
- `description`: 简短描述
- 包含功能特性、使用示例、API 文档
#### 6. `demos/` - 示例目录
demos 目录用于存放组件的交互式示例,这些示例会在 Playground 中展示。
**目录结构:**
```
demos/
├── index.tsx # Demo 配置入口(必需)
├── basic.tsx # 基础用法示例(必需)
├── variants.tsx # 变体示例
├── sizes.tsx # 尺寸示例
├── colors.tsx # 颜色示例
├── states.tsx # 状态示例
└── ... # 其他特定功能示例
```
##### 6.1 `demos/index.tsx` - Demo 配置文件
配置文件定义了所有 demo 的元数据和加载方式:
```tsx
import type { DemoConfig } from '@lobehub/ui-rn';
import React from 'react';
import BasicDemo from './basic';
import ColorsDemo from './colors';
import SizesDemo from './sizes';
import VariantsDemo from './variants';
const demos: DemoConfig = [
{ component: <BasicDemo />, key: 'basic', title: '基础用法' },
{ component: <VariantsDemo />, key: 'variants', title: '不同视觉风格' },
{ component: <SizesDemo />, key: 'sizes', title: '尺寸' },
{ component: <ColorsDemo />, key: 'colors', title: '颜色' },
];
export default demos;
```
**DemoConfig 类型定义:**
```tsx
interface DemoItem {
/** Demo 组件实例 */
component: React.ReactNode;
/** Demo 唯一标识,使用 kebab-case */
key: string;
/** Demo 标题,用于在 Playground 中显示 */
title: string;
}
type DemoConfig = DemoItem[];
```
**关键要点:**
- 从 `@lobehub/ui-rn` 导入 `DemoConfig` 类型
- 导入所有 demo 组件(使用 PascalCase + Demo 后缀)
- 配置数组按重要性排序,`basic` 应始终放在第一位
- `key` 使用 kebab-case 命名(如 `basic`, `variants`, `disabled-state`
- `title` 使用中文描述,简洁明了
- 使用默认导出导出配置数组
##### 6.2 Demo 文件规范
每个 demo 文件应遵循以下规范:
```tsx
// demos/basic.tsx
import { ComponentName, Flexbox } from '@lobehub/ui-rn';
import React from 'react';
import { Text } from 'react-native';
export default () => {
return (
<Flexbox gap={16}>
<ComponentName variant="default">
<Text>默认示例</Text>
</ComponentName>
<ComponentName variant="primary">
<Text>主要示例</Text>
</ComponentName>
</Flexbox>
);
};
```
**关键要点:**
- 使用默认导出
- 使用箭头函数组件
- 不需要包裹额外的容器组件(Playground 会自动处理)
- 使用 `Flexbox` 组件进行布局,设置合适的 `gap` 值
- 示例应简洁明了,专注于展示单一特性或相关特性组
##### 6.3 Demo 文件命名规范
推荐的 demo 文件名及其用途:
| 文件名 | 用途 | 优先级 |
|--------|------|--------|
| `basic.tsx` | 基础用法,展示最常见的使用场景 | 必需 ⭐ |
| `variants.tsx` | 展示所有样式变体(filled, outlined 等) | 推荐 |
| `sizes.tsx` | 展示不同尺寸(small, medium, large | 推荐 |
| `colors.tsx` | 展示不同颜色主题 | 可选 |
| `disabled.tsx` | 展示禁用状态 | 可选 |
| `loading.tsx` | 展示加载状态 | 可选 |
| `interactive.tsx` | 展示交互行为(点击、hover 等) | 可选 |
| `layout.tsx` | 展示布局应用场景 | 可选 |
| `advanced.tsx` | 展示高级用法或复杂场景 | 可选 |
##### 6.4 Demo 最佳实践
**✅ 推荐的做法:**
```tsx
// 1. 清晰的分组和间距
export default () => {
return (
<Flexbox gap={16}>
<Text style={{ fontWeight: 'bold' }}>默认变体</Text>
<Block variant="filled">
<Text>Filled Block</Text>
</Block>
<Text style={{ fontWeight: 'bold' }}>轮廓变体</Text>
<Block variant="outlined">
<Text>Outlined Block</Text>
</Block>
</Flexbox>
);
};
// 2. 使用状态展示交互
export default () => {
const [count, setCount] = useState(0);
return (
<Button onPress={() => setCount(c => c + 1)}>
<Text>点击次数: {count}</Text>
</Button>
);
};
// 3. 使用 Alert 或 Toast 展示反馈
export default () => {
return (
<Button onPress={() => Alert.alert('提示', '按钮被点击了')}>
<Text>点击我</Text>
</Button>
);
};
```
**❌ 避免的做法:**
```tsx
// ❌ 不要使用外部状态管理
import { useStore } from '@/store'; // 避免
// ❌ 不要使用复杂的业务逻辑
export default () => {
const data = fetchDataFromAPI(); // 避免
// ...
};
// ❌ 不要使用过多嵌套
export default () => {
return (
<View>
<View>
<View>
<View> // 过度嵌套
<Component />
</View>
</View>
</View>
</View>
);
};
// ❌ 不要在 demo 中定义复杂组件
const ComplexComponent = () => { /* 大量代码 */ }; // 应该独立文件
export default () => <ComplexComponent />;
```
##### 6.5 常见 Demo 模板
**基础用法模板:**
```tsx
import { ComponentName } from '@lobehub/ui-rn';
import React from 'react';
export default () => {
return <ComponentName>基本示例</ComponentName>;
};
```
**变体展示模板:**
```tsx
import { ComponentName, Flexbox } from '@lobehub/ui-rn';
import React from 'react';
export default () => {
return (
<Flexbox gap={16}>
<ComponentName variant="filled">Filled</ComponentName>
<ComponentName variant="outlined">Outlined</ComponentName>
<ComponentName variant="borderless">Borderless</ComponentName>
</Flexbox>
);
};
```
**交互示例模板:**
```tsx
import { ComponentName, Flexbox } from '@lobehub/ui-rn';
import React, { useState } from 'react';
import { Alert, Text } from 'react-native';
export default () => {
const [value, setValue] = useState('');
return (
<Flexbox gap={16}>
<ComponentName
value={value}
onChange={setValue}
onPress={() => Alert.alert('提示', `当前值: ${value}`)}
/>
<Text>当前值: {value}</Text>
</Flexbox>
);
};
```
## CVA (Class Variance Authority) 用法
CVA 用于管理组件的样式变体逻辑,提供类型安全的样式组合。
### 基础用法
```tsx
import { cva } from '@/components/styles';
const variants = useMemo(
() =>
cva(styles.root, {
// 定义变体
variants: {
variant: {
filled: styles.filled,
outlined: styles.outlined,
borderless: styles.borderless,
},
size: {
small: styles.small,
medium: styles.medium,
large: styles.large,
},
},
// 默认值
defaultVariants: {
variant: 'filled',
size: 'medium',
},
}),
[styles],
);
// 使用变体
<View style={variants({ variant: 'outlined', size: 'large' })} />
```
### 复合变体 (Compound Variants)
处理多个变体组合时的特殊样式:
```tsx
const variants = useMemo(
() =>
cva(styles.root, {
variants: {
variant: {
filled: styles.filled,
outlined: styles.outlined,
},
hovered: {
false: null,
true: styles.hover,
},
pressed: {
false: null,
true: styles.pressed,
},
},
// 复合变体:特定组合的特殊样式
compoundVariants: [
{
variant: 'outlined',
hovered: true,
style: styles.outlinedHover,
},
{
variant: 'outlined',
pressed: true,
style: styles.outlinedActive,
},
{
variant: 'filled',
pressed: true,
style: styles.filledActive,
},
],
defaultVariants: {
variant: 'filled',
},
}),
[styles],
);
```
### 与交互状态结合
结合 React Native 的交互状态(hovered、pressed):
```tsx
<Pressable
style={({ hovered, pressed }) => [
variants({ variant, hovered, pressed }),
style,
]}
>
{children}
</Pressable>
```
**CVA 最佳实践:**
- 始终使用 `useMemo` 缓存 CVA 配置,避免重复创建
- 将 `styles` 对象加入依赖数组
- 将交互状态(hovered、pressed)作为变体处理
- 使用 `compoundVariants` 处理组合逻辑,保持代码清晰
- 为布尔变体提供 `false: null` 选项
## 开发流程
### 添加新组件
1. **创建组件目录结构**
```bash
mkdir -p src/components/NewComponent/demos
```
2. **创建必需文件**
按以下顺序创建文件:
- `type.ts` - TypeScript 类型定义
- `style.ts` - 样式定义
- `NewComponent.tsx` - 组件实现
- `index.ts` - 导出文件
- `index.md` - 组件文档(包含 frontmatter
- `demos/basic.tsx` - 基础示例
- `demos/index.tsx` - Demo 配置文件(必需)
3. **配置 demos/index.tsx**
创建 demo 配置文件,引入所有示例:
```tsx
import type { DemoConfig } from '@lobehub/ui-rn';
import React from 'react';
import BasicDemo from './basic';
// 引入其他 demo...
const demos: DemoConfig = [
{ component: <BasicDemo />, key: 'basic', title: '基础用法' },
// 添加其他 demo 配置...
];
export default demos;
```
4. **更新组件索引**
在 [src/components/index.ts](mdc:src/components/index.ts) 中添加导出:
```tsx
export { default as NewComponent type NewComponentProps } from './NewComponent';
```
5. **生成 Playground 数据** ⚠️
添加或修改组件后,**必须**运行以下命令更新 Playground
```bash
pnpm run build:playground
```
或使用完整命令:
```bash
pnpm run workflow:playground
```
这个命令会:
- 扫描所有组件目录
- 读取 `index.md` 中的 frontmatter 元数据
- 生成 `app/playground/.data/index.json` 数据文件
- 生成 `app/playground/.data/import.ts` 导入文件
- 更新 Playground 应用的组件列表
**注意**:如果不运行此命令,新组件将不会出现在 Playground 中。
### 更新现有组件
1. **修改组件实现**
- 更新 `ComponentName.tsx` 的组件逻辑
- 更新 `style.ts` 的样式定义
- 更新 `type.ts` 的类型定义
2. **更新文档和示例**
- 更新 `index.md` 文档,确保示例代码与实际 API 一致
- 如果添加了新功能,创建对应的 demo 文件(如 `demos/newFeature.tsx`
- 在 `demos/index.tsx` 中注册新的 demo
3. **运行 Playground 更新**
- 如果修改了组件的 `group` 或 `title`,运行 `pnpm run build:playground`
- 如果添加了新的 demo,也需要运行此命令
### 测试组件
在 Playground 中测试组件:
```bash
# 启动开发服务器
pnpm start
# 在 iOS 模拟器中运行
pnpm ios
# 在 Android 模拟器中运行
pnpm android
```
导航到 Playground 页面查看和测试组件。
## 命名规范
- **组件文件**PascalCase(如 `ActionIcon.tsx`
- **样式文件**camelCase(如 `style.ts`
- **类型文件**camelCase(如 `type.ts`
- **导出文件**:固定名称 `index.ts`
- **文档文件**:固定名称 `index.md`
### Demo 命名规范
- **Demo 文件**camelCase(如 `basic.tsx`, `variants.tsx`, `disabled.tsx`
- **Demo 配置文件**:固定名称 `demos/index.tsx`
- **Demo 导入名称**PascalCase + Demo 后缀(如 `BasicDemo`, `VariantsDemo`
- **Demo key**kebab-case(如 `'basic'`, `'variants'`, `'disabled-state'`
- **Demo title**:中文描述(如 `'基础用法'`, `'不同视觉风格'`, `'禁用状态'`
**示例:**
```tsx
// demos/disabled-state.tsx
export default () => { /* ... */ };
// demos/index.tsx
import DisabledStateDemo from './disabled-state';
const demos: DemoConfig = [
{ component: <DisabledStateDemo />, key: 'disabled-state', title: '禁用状态' },
];
```
## 样式最佳实践
1. **优先使用 stylish 预设**
```tsx
filled: stylish.variantFilled, // ✅ 推荐
filled: { backgroundColor: token.colorBgContainer }, // ❌ 避免
```
2. **保持样式的可组合性**
```tsx
// ✅ 好的做法:细粒度的样式定义
root: { borderRadius: token.borderRadius },
shadow: stylish.shadow,
glass: stylish.blur,
// ❌ 避免:耦合多个效果
rootWithShadow: { ...stylish.shadow, borderRadius: token.borderRadius },
```
3. **使用语义化的样式名称**
```tsx
// ✅ 清晰的语义
variantPrimary: stylish.variantFilled,
variantPrimaryActive: stylish.variantFilledActive,
// ❌ 避免无意义的名称
style1: stylish.variantFilled,
activeStyle: stylish.variantFilledActive,
```
## 相关文件参考
### 组件结构示例
- [Block 组件实现](mdc:src/components/Block/Block.tsx) - CVA 用法示例
- [Block 样式定义](mdc:src/components/Block/style.ts) - createStyles 示例
- [Block 类型定义](mdc:src/components/Block/type.ts) - Props 接口示例
- [Block 文档](mdc:src/components/Block/index.md) - 组件文档示例
### Demo 示例
- [Block Demo 配置](mdc:src/components/Block/demos/index.tsx) - Demo 配置文件示例
- [Block 基础示例](mdc:src/components/Block/demos/basic.tsx) - 基础 demo 示例
- [Block 交互示例](mdc:src/components/Block/demos/clickable.tsx) - 交互 demo 示例
- [ActionIcon Demo 配置](mdc:src/components/ActionIcon/demos/index.tsx) - 多 demo 配置示例
### 工具和类型
- [DemoConfig 类型定义](mdc:src/components/types.ts) - Demo 配置类型
- [Playground 数据生成脚本](mdc:scripts/playground/index.ts) - 自动化流程
- [组件导出索引](mdc:src/components/index.ts) - 导出规范
## 快速检查清单
在提交新组件或更新组件之前,请确认:
### 新组件清单
- [ ] 创建了组件目录 `src/components/ComponentName/`
- [ ] 创建了 `ComponentName.tsx` 主文件
- [ ] 创建了 `type.ts` 类型定义
- [ ] 创建了 `style.ts` 样式定义
- [ ] 创建了 `index.ts` 导出文件
- [ ] 创建了 `index.md` 文档,包含正确的 frontmattergroup, title, description
- [ ] 创建了 `demos/basic.tsx` 基础示例
- [ ] 创建了 `demos/index.tsx` 配置文件
- [ ] 在 `src/components/index.ts` 中添加了导出
- [ ] 运行了 `pnpm run build:playground`
- [ ] 在 Playground 中测试了组件
### Demo 清单
- [ ] `demos/index.tsx` 从 `@lobehub/ui-rn` 导入了 `DemoConfig` 类型
- [ ] 所有 demo 文件使用默认导出箭头函数组件
- [ ] `demos/index.tsx` 中的 demo 配置使用了正确的命名:
- `key` 使用 kebab-case
- `title` 使用中文
- 导入名称使用 PascalCase + Demo 后缀
- [ ] `basic` demo 放在配置数组的第一位
- [ ] demo 代码简洁,专注于单一功能展示
### 样式清单
- [ ] 使用 `createStyles` 创建样式
- [ ] 优先使用 `stylish` 预设而非自定义样式
- [ ] 从 `token` 获取主题变量
- [ ] 如果使用 CVA,已用 `useMemo` 包裹并依赖 `styles`
- [ ] 样式名称语义化清晰
### 文档清单
- [ ] `index.md` 包含正确的 frontmatter
- [ ] 文档包含 Features 列表
- [ ] 文档包含 Basic Usage 示例
- [ ] 文档示例代码与实际 API 一致
-78
View File
@@ -1,78 +0,0 @@
---
description: 包含添加 debug 日志请求时
globs:
alwaysApply: false
---
# Debug 包使用指南
本项目使用 [debug](mdc:https:/github.com/debug-js/debug) 包进行调试日志记录。使用此规则来确保团队成员统一调试日志格式。
## 基本用法
1. 导入 debug 包:
```typescript
import debug from 'debug';
```
2. 创建一个命名空间的日志记录器:
```typescript
// 格式: lobe:[模块]:[子模块]
const log = debug('lobe-[模块名]:[子模块名]');
```
一个文件尽量只创建一个 log 命名空间,子模块名使用文件名
3. 使用日志记录器:
```typescript
log('简单消息');
log('带变量的消息: %O', object);
log('格式化数字: %d', number);
```
## 命名空间约定
- 桌面应用相关: `lobe-desktop:[模块]`
- 服务端相关: `lobe-server:[模块]`
- 客户端相关: `lobe-client:[模块]`
- 路由相关: `lobe-[类型]-router:[模块]`
## 格式说明符
- `%O` - 对象展开(推荐用于复杂对象)
- `%o` - 对象
- `%s` - 字符串
- `%d` - 数字
## 示例
查看 [market/index.ts](mdc:src/server/routers/edge/market/index.ts) 中的使用示例:
```typescript
import debug from 'debug';
const log = debug('lobe-edge-router:market');
log('getAgent input: %O', input);
```
## 启用调试
要在开发时启用调试输出,需设置环境变量:
### 在浏览器中
在控制台执行:
```javascript
localStorage.debug = 'lobe-*'
```
### 在 Node.js 环境中
```bash
DEBUG=lobe-* npm run dev
# 或者
DEBUG=lobe-* pnpm dev
```
@@ -1,118 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# Development Workflow
## Package Management
### Using pnpm
- **Install dependencies**: `pnpm install`
- **Add new package**: `pnpm add <package-name>`
- **Add dev dependency**: `pnpm add -D <package-name>`
- **Run scripts**: `pnpm <script-name>`
### Available Scripts
- `pnpm start` - Start Expo development server
- `pnpm android` - Run on Android device/emulator
- `pnpm ios` - Run on iOS device/simulator
- `pnpm web` - Run in web browser
- `pnpm test` - Run Jest tests in watch mode
- `pnpm lint` - Run ESLint
- `pnpm i18n` - Generate translations for all languages
## Internationalization (i18n)
### Workflow
1. **Add translations** to `i18n/default/common.ts` (Chinese source)
2. **Use in components** with `useTranslation` hook
3. **Generate translations** with `pnpm run i18n`
4. **Test** with different languages
### Key Files
- `i18n/default/common.ts` - Source translations (Chinese)
- `locales/*/common.json` - Generated translations (18 languages)
- `scripts/i18nWorkflow/` - Translation generation scripts
### Quick Reference
```typescript
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
<Text>{t('chat.history')}</Text>
```
See [internationalization.mdc](mdc:.cursor/rules/internationalization.mdc) for detailed guidelines.
## Development Environment
### Configuration Files
- [package.json](mdc:package.json) - Dependencies and scripts
- [tsconfig.json](mdc:tsconfig.json) - TypeScript configuration
- [metro.config.js](mdc:metro.config.js) - Metro bundler configuration
- [app.config.ts](mdc:app.config.ts) - Expo app configuration (TypeScript, 类型安全)
- [eas.json](mdc:eas.json) - EAS Build configuration
### Environment Setup
- [src/polyfills.ts](mdc:src/polyfills.ts) - Polyfills for React Native
- [global.d.ts](mdc:global.d.ts) - Global TypeScript declarations
## Code Organization
### File Naming Conventions
- Components: PascalCase (e.g., `ChatBubble.tsx`)
- Hooks: camelCase with `use` prefix (e.g., `useChat.ts`)
- Utilities: camelCase (e.g., `fetchSSE.ts`)
- Types: camelCase (e.g., `session.ts`)
- Constants: UPPER_SNAKE_CASE (e.g., `DEFAULT_AGENT_META`)
### Import Patterns
- Use absolute imports with `@/` prefix
- Group imports: React, third-party, internal
- Use named exports for components
- Use default exports for main components
### TypeScript Guidelines
- Enable strict mode in [tsconfig.json](mdc:tsconfig.json)
- Define interfaces for all props and state
- Use proper type annotations
- Avoid `any` type - use `unknown` or proper types
## Testing
### Test Structure
- [components/__tests__/](mdc:components/__tests__/) - Component tests
- [components/__tests__/__snapshots__/](mdc:components/__tests__/__snapshots__/) - Jest snapshots
- Use Jest with React Native Testing Library
### Testing Patterns
- Test component rendering
- Test user interactions
- Test error states
- Use snapshots for UI regression testing
## Code Quality
### Linting
- ESLint configuration for React Native
- Prettier for code formatting
- TypeScript strict checking
### Best Practices
- Follow existing code patterns
- Don't change component styles without reason
- Implement proper error boundaries
- Use proper loading states
- Handle edge cases gracefully
## Build and Deployment
### EAS Build
- [eas.json](mdc:eas.json) - Build configuration
- Support for Android and iOS builds
- Environment-specific configurations
### Assets
- [assets/](mdc:assets/) - App icons, fonts, and images
- [assets/fonts/](mdc:assets/fonts/) - Custom fonts
- [assets/images/](mdc:assets/images/) - App images and icons
@@ -1,192 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# Internationalization (i18n) Guidelines
## Overview
This project uses i18next for internationalization with a custom workflow that generates translations from a single source file.
## Architecture
- **Source**: `i18n/default` - Contains all Chinese translations as the source
- **Generated**: `locales/* - Auto-generated translations for 18 languages
- **Script**: `scripts/i18nWorkflow/` - Handles translation generation
## Supported Languages
18 languages: ar, bg-BG, de-DE, en-US, es-ES, fa-IR, fr-FR, it-IT, ja-JP, ko-KR, nl-NL, pl-PL, pt-BR, ru-RU, tr-TR, vi-VN, zh-CN, zh-TW
## Adding New Translations
### Step 1: Add to Source File
Add new translation keys to suitable file in `i18n/default` like `chat.ts` and `common.ts`:
```typescript
export default {
// ... existing translations
newSection: {
title: '新标题',
description: '新描述',
actions: {
save: '保存',
cancel: '取消'
}
}
};
```
### Step 2: Use in Components
Use the `useTranslation` hook in React components:
```typescript
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
// Usage
<Text>{t('newSection.title')}</Text>
<Text>{t('newSection.actions.save')}</Text>
```
### Step 3: Generate Translations
Run the i18n script to generate all language translations:
```bash
pnpm run i18n
```
## Translation Key Naming Convention
use simpilifed naming keys
### Structure
- Use nested objects for related translations
- Use camelCase for keys
- Group by feature/section
### Examples
```typescript
// Good
settings: {
theme: {
title: '主题设置',
light: '浅色模式',
dark: '深色模式'
}
}
// Good
chat: {
actions: {
copy: '复制',
delete: '删除',
retry: '重试'
},
messages: {
copied: '已复制',
failed: '失败'
}
}
// Avoid
settingsThemeTitle: '主题设置',
settingsThemeLight: '浅色模式'
```
## Common Translation Keys
### Chat Features
```typescript
chat: {
history: '对话历史',
messageCopied: '消息已复制',
confirmDelete: '确认删除',
copy: '复制',
retry: '重试',
delete: '删除',
send: '发送',
stop: '停止',
thinking: '思考中...',
placeholder: '输入您的消息...'
}
```
### Settings Features
```typescript
settings: {
title: '设置',
theme: {
title: '主题设置',
light: '浅色模式',
dark: '深色模式',
auto: '跟随系统',
},
locale: {
title: '语言设置',
auto: {
title: '跟随系统',
description: '跟随系统语言设置'
}
},
}
```
### Common Actions
```typescript
common: {
cancel: '取消',
confirm: '确认',
save: '保存',
delete: '删除',
edit: '编辑',
add: '添加',
search: '搜索',
loading: '加载中...',
error: '错误',
success: '成功',
warning: '警告',
info: '信息'
}
```
## Important Notes
### App Name
- **Do NOT translate** the app name "LobeChat"
- Keep it hardcoded in components: `<Text>LobeChat</Text>`
- Do not add `app.name` to translation keys
### Code Comments
- Chinese comments in code (like `{/* 状态栏 */}`) do not need translation
- Only user-facing text needs internationalization
### Error Messages
- Always use translation keys for error messages
- Provide fallback messages in English
### Placeholders
- Translate placeholder text in input fields
- Use `placeholderTextColor` for styling
## Workflow Checklist
When adding new UI text:
1. ✅ Add translation key to `i18n/default/common.ts`
2. ✅ Use `useTranslation` hook in component
3. ✅ Replace hardcoded text with `t('key.path')`
4. ✅ Run `pnpm run i18n` to generate translations
5. ✅ Test with different languages
6. ✅ Verify all 18 languages are generated correctly
## Testing
- Switch languages in settings to verify translations
- Check that all user-facing text is translated
- Ensure app name remains "LobeChat" in all languages
- Verify error messages and placeholders are translated
## Troubleshooting
- If translations don't appear, run `pnpm run i18n` again
- Check that translation keys match exactly between source and usage
- Verify `useTranslation` hook is imported and used correctly
- Ensure translation keys follow the nested object structure
@@ -1,631 +0,0 @@
---
globs: *.ts,*.tsx
description: 优先使用自定义组件规范 - 使用 Text、Flexbox、Center、Block 等自定义组件替代 React Native 原生组件
---
# 优先使用自定义组件规范
本规范定义了在 LobeChat Mobile 项目中应该优先使用自定义组件而不是 React Native 原生组件的场景和原因。
## 核心原则
**✅ 优先使用自定义组件**:项目提供了一系列增强的自定义组件,它们提供了更友好的 API、更好的类型安全、主题集成以及样式变体支持。
**❌ 避免直接使用原生组件**:除非有特殊需求,否则应避免直接使用 `Text`、`View`、`Pressable` 等 React Native 原生组件。
## 组件映射关系
| 原生组件 | 自定义组件 | 使用场景 |
|---------|----------|---------|
| `Text` | `@/components/Text` | 所有文本显示场景 |
| `View` (flex 布局) | `@/components/Flexbox` | 需要 flex 布局的容器 |
| `View` (居中布局) | `@/components/Center` | 需要内容居中的容器 |
| `View` (带样式) | `@/components/Block` | 需要背景、边框、阴影等样式的容器 |
| `Pressable` | `@/components/Flexbox` 或 `@/components/Block` | 可点击的布局容器(使用 `onPress` prop |
## 1. Text 组件
### 为什么使用 Text
`@/components/Text` 提供了比原生 `Text` 更丰富的功能:
- ✅ **语义化标题**:支持 `h1` ~ `h5` 标题级别
- ✅ **文本样式**`strong`、`italic`、`underline`、`delete`、`code` 等
- ✅ **语义化类型**`danger`、`success`、`warning`、`info`、`secondary`
- ✅ **省略号支持**:更友好的 `ellipsis` 配置
- ✅ **主题集成**:自动适配主题颜色和字体
- ✅ **更好的类型提示**:完整的 TypeScript 支持
### 使用示例
```tsx
// ❌ 避免使用原生 Text
import { Text } from 'react-native';
<Text style={{ fontWeight: 'bold', color: 'red' }}>错误信息</Text>
// ✅ 推荐使用自定义 Text
import Text from '@/components/Text';
<Text type="danger" strong>错误信息</Text>
```
### 常见用法
```tsx
import Text from '@/components/Text';
// 标题
<Text as="h1">一级标题</Text>
<Text as="h2">二级标题</Text>
// 文本样式
<Text strong>加粗文本</Text>
<Text italic>斜体文本</Text>
<Text underline>下划线文本</Text>
<Text delete>删除线文本</Text>
<Text code>代码文本</Text>
// 语义化类型
<Text type="danger">错误提示</Text>
<Text type="success">成功提示</Text>
<Text type="warning">警告提示</Text>
<Text type="info">信息提示</Text>
<Text type="secondary">次要文本</Text>
// 省略号
<Text ellipsis>这是一段很长的文本会被截断...</Text>
<Text ellipsis={{ rows: 2 }}>这是一段很长的文本,最多显示两行,超出部分会被截断...</Text>
// 组合使用
<Text type="danger" strong numberOfLines={1}>重要错误信息</Text>
```
## 2. Flexbox 组件
### 为什么使用 Flexbox
`@/components/Flexbox` 提供了比原生 `View` 更友好的 flex 布局配置:
- ✅ **简化的 API**:直观的 props 命名 (`horizontal`、`justify`、`align`、`gap`)
- ✅ **自动处理方向**`horizontal` prop 自动切换 `flexDirection`
- ✅ **内置交互**:支持 `onPress`,自动切换 `Pressable`/`View`
- ✅ **间距支持**:原生支持 `gap` prop
- ✅ **更少的样式代码**:常用布局属性作为 props 直接配置
### 使用示例
```tsx
// ❌ 避免使用原生 View
import { View, Pressable } from 'react-native';
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
<Text>内容 1</Text>
<Text>内容 2</Text>
</View>
// ✅ 推荐使用 Flexbox
import Flexbox from '@/components/Flexbox';
import Text from '@/components/Text';
<Flexbox horizontal justify="space-between" align="center" gap={16}>
<Text>内容 1</Text>
<Text>内容 2</Text>
</Flexbox>
```
### 常见用法
```tsx
import Flexbox from '@/components/Flexbox';
// 垂直布局(默认)
<Flexbox gap={16}>
<Text>项目 1</Text>
<Text>项目 2</Text>
</Flexbox>
// 水平布局
<Flexbox horizontal gap={8}>
<Text>项目 1</Text>
<Text>项目 2</Text>
</Flexbox>
// 对齐方式
<Flexbox justify="center" align="center">
<Text>居中内容</Text>
</Flexbox>
<Flexbox horizontal justify="space-between" align="flex-start">
<Text>左侧</Text>
<Text>右侧</Text>
</Flexbox>
// 可点击容器
<Flexbox
horizontal
gap={8}
padding={16}
onPress={() => console.log('clicked')}
>
<Icon name="home" />
<Text>首页</Text>
</Flexbox>
// 弹性布局
<Flexbox horizontal gap={8}>
<Flexbox flex={1}>
<Text>占据剩余空间</Text>
</Flexbox>
<Text>固定宽度</Text>
</Flexbox>
// 换行
<Flexbox horizontal wrap="wrap" gap={8}>
<Text>标签 1</Text>
<Text>标签 2</Text>
<Text>标签 3</Text>
</Flexbox>
```
## 3. Center 组件
### 为什么使用 Center
`@/components/Center` 是专门用于居中布局的组件:
- ✅ **语义化**:明确表达居中意图
- ✅ **简化代码**:不需要手动设置 `justify` 和 `align`
- ✅ **默认居中**`justify="center"` 和 `align="center"` 是默认值
- ✅ **基于 Flexbox**:继承所有 Flexbox 的功能
### 使用示例
```tsx
// ❌ 避免使用原生 View
import { View } from 'react-native';
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>居中内容</Text>
</View>
// ✅ 推荐使用 Center
import Center from '@/components/Center';
import Text from '@/components/Text';
<Center flex={1}>
<Text>居中内容</Text>
</Center>
```
### 常见用法
```tsx
import Center from '@/components/Center';
// 基础居中
<Center>
<Text>居中内容</Text>
</Center>
// 占满容器并居中
<Center flex={1}>
<Text>垂直和水平居中</Text>
</Center>
// 固定尺寸居中
<Center width={200} height={200}>
<Icon name="loading" />
</Center>
// 水平居中(垂直方向可以自定义)
<Center horizontal justify="flex-start" gap={8}>
<Icon name="user" />
<Text>用户名</Text>
</Center>
// 可点击的居中容器
<Center
width={100}
height={100}
onPress={() => console.log('clicked')}
>
<Icon name="add" />
<Text>添加</Text>
</Center>
```
## 4. Block 组件
### 为什么使用 Block
`@/components/Block` 是增强的样式容器组件:
- ✅ **样式变体**`filled`、`outlined`、`borderless` 三种预设样式
- ✅ **视觉效果**:内置 `shadow` 和 `glass` 效果
- ✅ **交互状态**:自动处理 hover 和 press 状态样式
- ✅ **主题集成**:使用 `stylish` 预设,自动适配主题
- ✅ **CVA 管理**:类型安全的样式组合
- ✅ **基于 Flexbox**:继承所有 Flexbox 的布局能力
### 使用示例
```tsx
// ❌ 避免使用原生 View 或 Pressable
import { Pressable } from 'react-native';
<Pressable
style={({ pressed }) => ({
backgroundColor: pressed ? '#f0f0f0' : '#fff',
borderRadius: 8,
padding: 16,
shadowColor: '#000',
shadowOpacity: 0.1,
// ... 大量样式代码
})}
onPress={handlePress}
>
<Text>卡片内容</Text>
</Pressable>
// ✅ 推荐使用 Block
import Block from '@/components/Block';
import Text from '@/components/Text';
<Block variant="filled" shadow onPress={handlePress}>
<Text>卡片内容</Text>
</Block>
```
### 常见用法
```tsx
import Block from '@/components/Block';
// 样式变体
<Block variant="filled">
<Text>填充背景</Text>
</Block>
<Block variant="outlined">
<Text>轮廓边框</Text>
</Block>
<Block variant="borderless">
<Text>无边框</Text>
</Block>
// 视觉效果
<Block variant="filled" shadow>
<Text>带阴影的卡片</Text>
</Block>
<Block variant="filled" glass>
<Text>玻璃效果背景</Text>
</Block>
// 可点击卡片
<Block
variant="outlined"
padding={16}
gap={8}
onPress={() => console.log('clicked')}
>
<Text as="h3">卡片标题</Text>
<Text type="secondary">卡片描述</Text>
</Block>
// 组合布局和样式
<Block variant="filled" shadow padding={24}>
<Flexbox gap={16}>
<Text as="h2">用户信息</Text>
<Flexbox horizontal gap={8}>
<Icon name="user" />
<Text>用户名</Text>
</Flexbox>
</Flexbox>
</Block>
```
## 5. 组件组合使用
### 完整示例:用户卡片
```tsx
import Block from '@/components/Block';
import Center from '@/components/Center';
import Flexbox from '@/components/Flexbox';
import Text from '@/components/Text';
const UserCard = ({ user }) => {
return (
<Block variant="filled" shadow padding={16} gap={12}>
{/* 头部:头像和名称 */}
<Flexbox horizontal gap={12} align="center">
<Center width={48} height={48}>
<Avatar source={user.avatar} />
</Center>
<Flexbox flex={1} gap={4}>
<Text as="h4" strong>{user.name}</Text>
<Text type="secondary" fontSize={12}>{user.email}</Text>
</Flexbox>
</Flexbox>
{/* 统计信息 */}
<Flexbox horizontal gap={24}>
<Flexbox gap={4} align="center">
<Text strong fontSize={18}>{user.posts}</Text>
<Text type="secondary" fontSize={12}>帖子</Text>
</Flexbox>
<Flexbox gap={4} align="center">
<Text strong fontSize={18}>{user.followers}</Text>
<Text type="secondary" fontSize={12}>关注者</Text>
</Flexbox>
<Flexbox gap={4} align="center">
<Text strong fontSize={18}>{user.following}</Text>
<Text type="secondary" fontSize={12}>关注中</Text>
</Flexbox>
</Flexbox>
{/* 操作按钮 */}
<Flexbox horizontal gap={8}>
<Block
flex={1}
variant="filled"
padding={12}
onPress={() => console.log('follow')}
>
<Center>
<Text>关注</Text>
</Center>
</Block>
<Block
flex={1}
variant="outlined"
padding={12}
onPress={() => console.log('message')}
>
<Center>
<Text>消息</Text>
</Center>
</Block>
</Flexbox>
</Block>
);
};
```
## 6. 何时可以使用原生组件
以下场景可以考虑使用原生组件:
### 6.1 特殊需求场景
```tsx
// ScrollView, FlatList, SectionList 等列表组件
import { FlatList } from 'react-native';
<FlatList data={data} renderItem={renderItem} />
// Image 组件
import { Image } from 'react-native';
<Image source={require('./image.png')} />
// TextInput 组件(如果没有自定义封装)
import { TextInput } from 'react-native';
<TextInput value={text} onChangeText={setText} />
// 特殊的原生组件
import { SafeAreaView, KeyboardAvoidingView, Modal } from 'react-native';
```
### 6.2 性能关键场景
在列表项等性能关键场景,如果自定义组件带来性能问题,可以考虑使用原生组件,但需要:
```tsx
// ⚠️ 性能优化场景,谨慎使用
import { View, Text } from 'react-native';
import { memo } from 'react';
const ListItem = memo(({ item }) => (
<View style={styles.item}>
<Text style={styles.text}>{item.title}</Text>
</View>
));
```
## 7. 导入规范
### 7.1 应用代码中的导入
在应用代码(`app/`、`src/features/` 等)中:
```tsx
// ✅ 推荐:使用绝对路径导入
import Block from '@/components/Block';
import Center from '@/components/Center';
import Flexbox from '@/components/Flexbox';
import Text from '@/components/Text';
```
### 7.2 组件内部的导入
在 `src/components/` 目录下的组件中:
```tsx
// ✅ 推荐:使用相对路径导入其他组件
import Block from '../Block';
import Center from '../Center';
import Flexbox from '../Flexbox';
import Text from '../Text';
```
### 7.3 Demo 文件中的导入
在组件的 `demos/` 目录下:
```tsx
// ✅ 推荐:从 @lobehub/ui-rn 导入(展示公开 API
import { Block, Center, Flexbox, Text } from '@lobehub/ui-rn';
```
## 8. 快速参考
| 需求 | 推荐组件 | 关键 Props |
|-----|---------|-----------|
| 显示普通文本 | `Text` | `children` |
| 显示标题 | `Text` | `as="h1"` ~ `as="h5"` |
| 显示错误/成功提示 | `Text` | `type="danger"` / `type="success"` |
| 垂直布局 | `Flexbox` | `gap={16}` |
| 水平布局 | `Flexbox` | `horizontal gap={8}` |
| 内容居中 | `Center` | `flex={1}` |
| 卡片容器 | `Block` | `variant="filled" shadow` |
| 可点击容器 | `Flexbox` / `Block` | `onPress={handler}` |
| 列表项 | `Block` / `Flexbox` | `horizontal align="center" gap={12}` |
## 9. 常见错误和修复
### 错误 1: 直接使用原生 Text
```tsx
// ❌ 错误
import { Text, View } from 'react-native';
<View>
<Text style={{ fontWeight: 'bold', color: '#e74c3c' }}>
错误信息
</Text>
</View>
// ✅ 修复
import Flexbox from '@/components/Flexbox';
import Text from '@/components/Text';
<Flexbox>
<Text type="danger" strong>
错误信息
</Text>
</Flexbox>
```
### 错误 2: 复杂的 View 样式
```tsx
// ❌ 错误
import { View } from 'react-native';
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 16,
padding: 12,
}}
>
{children}
</View>
// ✅ 修复
import Flexbox from '@/components/Flexbox';
<Flexbox
horizontal
justify="space-between"
align="center"
gap={16}
padding={12}
>
{children}
</Flexbox>
```
### 错误 3: 手动实现居中布局
```tsx
// ❌ 错误
import { View } from 'react-native';
<View style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}>
{children}
</View>
// ✅ 修复
import Center from '@/components/Center';
<Center flex={1}>
{children}
</Center>
```
### 错误 4: 复杂的 Pressable 样式
```tsx
// ❌ 错误
import { Pressable, StyleSheet } from 'react-native';
<Pressable
style={({ pressed }) => [
styles.card,
pressed && styles.cardPressed,
]}
onPress={handlePress}
>
{children}
</Pressable>
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 16,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 8,
shadowOffset: { width: 0, height: 2 },
},
cardPressed: {
opacity: 0.8,
},
});
// ✅ 修复
import Block from '@/components/Block';
<Block
variant="filled"
shadow
padding={16}
onPress={handlePress}
>
{children}
</Block>
```
## 10. 总结
### 核心优势
使用自定义组件的核心优势:
1. **更少的代码**:友好的 API 减少样式代码
2. **更好的可读性**:语义化的 props 和组件名
3. **类型安全**:完整的 TypeScript 支持
4. **主题集成**:自动适配明暗主题
5. **统一风格**:使用预设样式保持 UI 一致性
6. **易于维护**:集中管理样式逻辑
### 记住这个规则
**在 LobeChat Mobile 中,优先使用:**
- ✅ `Text` 而不是 `react-native` 的 `Text`
- ✅ `Flexbox` 而不是 `react-native` 的 `View` (flex 布局)
- ✅ `Center` 而不是 `react-native` 的 `View` (居中布局)
- ✅ `Block` 而不是 `react-native` 的 `View` / `Pressable` (样式容器)
**这些组件提供了更友好的配置方式,让代码更简洁、更易维护!**
@@ -1,45 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# Lobe Chat React Native - Project Overview
This is a React Native chat application built with Expo Router, featuring AI-powered conversations and modern UI components.
## Core Architecture
### Entry Points
- Main app entry: [src/app/_layout.tsx](mdc:src/app/_layout.tsx) - Root layout with providers
- App router: [src/app/index.tsx](mdc:src/app/index.tsx) - Main routing logic
- Package config: [package.json](mdc:package.json) - Dependencies and scripts
### Key Technologies
- **Expo Router**: File-based routing system
- **Zustand**: State management with persistence
- **React Native Markdown**: Rich text rendering
- **Shiki**: Syntax highlighting for code blocks
- **MathJax**: Mathematical formula rendering
### State Management
- OpenAI config: [store/openai.ts](mdc:store/openai.ts) - API key and proxy settings
- Session management: [store/session/](mdc:store/session) - Chat sessions and groups
- Chat state: [store/chat/](mdc:store/chat) - Active chat functionality
### Type Definitions
- Session types: [types/session.ts](mdc:types/session.ts) - Chat session interfaces
- Agent types: [types/agent.ts](mdc:types/agent.ts) - AI agent configurations
- Message types: [types/message.ts](mdc:types/message.ts) - Chat message structures
- Component types: [types/component.ts](mdc:types/component.ts) - UI component interfaces
### Constants and Configuration
- Session constants: [const/session.ts](mdc:const/session.ts) - Default session configurations
- Settings: [const/settings/](mdc:const/settings) - App configuration options
- Branding: [const/branding.ts](mdc:const/branding.ts) - App branding constants
## Development Guidelines
- Use pnpm as the package manager
- Follow existing code patterns and component styles
- Maintain TypeScript strict typing
- Use Expo Router for navigation
- Implement proper error handling and loading states
@@ -1,160 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# Quick Reference Guide
## Essential Commands
```bash
pnpm start # Start development server
pnpm ios # Run on iOS
pnpm android # Run on Android
pnpm web # Run in browser
pnpm test # Run tests
pnpm lint # Lint code
pnpm i18n # Generate translations
```
## Internationalization (i18n)
### Quick Setup
```typescript
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
// Usage
<Text>{t('chat.history')}</Text>
<Text>{t('settings.theme.title')}</Text>
```
### Key Files
- `i18n/default/common.ts` - Add new translations here
- `locales/*/common.json` - Generated translations
- `pnpm run i18n` - Generate all languages
### Common Keys
```typescript
// Chat
t('chat.history') // 对话历史
t('chat.copy') // 复制
t('chat.delete') // 删除
// Settings
t('settings.theme.title') // 主题设置
t('settings.locale.title') // 语言设置
// Common
t('common.cancel') // 取消
t('common.confirm') // 确认
t('common.save') // 保存
```
See [internationalization.mdc](mdc:.cursor/rules/internationalization.mdc) for full guidelines.
## Key Files to Know
### Entry Points
- [src/app/_layout.tsx](mdc:src/app/_layout.tsx) - Root layout with providers
- [src/app/index.tsx](mdc:src/app/index.tsx) - Main routing logic
- [package.json](mdc:package.json) - Dependencies and scripts
### State Management
- [store/openai.ts](mdc:store/openai.ts) - OpenAI configuration
- [store/session/index.ts](mdc:store/session/index.ts) - Session management
- [store/chat/index.ts](mdc:store/chat/index.ts) - Chat state
### Type Definitions
- [types/session.ts](mdc:types/session.ts) - Session interfaces
- [types/agent.ts](mdc:types/agent.ts) - Agent configurations
- [types/message.ts](mdc:types/message.ts) - Message structures
### Core Components
- [components/Markdown/index.tsx](mdc:components/Markdown/index.tsx) - Markdown renderer
- [components/Highlighter/index.tsx](mdc:components/Highlighter/index.tsx) - Code highlighting
- [components/Toast/index.ts](mdc:components/Toast/index.ts) - Toast system
## Common Patterns
### Component Structure
```typescript
interface ComponentProps {
// Props definition
}
const Component: React.FC<ComponentProps> = ({ prop1, prop2 }) => {
const { t } = useTranslation();
const { colors, dark } = useTheme();
const styles = StyleSheet.create({
// Styles
});
return (
<View style={styles.container}>
<Text>{t('component.title')}</Text>
</View>
);
};
```
### Store Usage
```typescript
const { data, setData } = useStore();
const derivedData = useStore(selector);
```
### Theme Integration
```typescript
const { colors, dark } = useTheme();
const backgroundColor = dark ? colors.background : '#fff';
```
## Development Rules
### Code Style
- Use pnpm for package management
- Follow existing component patterns
- Don't change styles without reason
- Use TypeScript strict mode
- Implement proper error handling
- **Always use translation keys for user-facing text**
### File Organization
- Components in `components/` directory
- Types in `types/` directory
- Constants in `const/` directory
- Utilities in `utils/` directory
- Store logic in `store/` directory
- Translations in `i18n/default/common.ts`
### Import Patterns
```typescript
// React imports
import React from 'react';
import { useTranslation } from 'react-i18next';
// Internal imports (use @/ prefix)
import { Component } from '@/components/Component';
import { useStore } from '@/store/store';
```
## Troubleshooting
### Common Issues
1. **Metro bundler issues**: Clear cache with `pnpm start --clear`
2. **TypeScript errors**: Check [tsconfig.json](mdc:tsconfig.json) configuration
3. **Theme issues**: Verify [constants/Colors.ts](mdc:constants/Colors.ts) setup
4. **API errors**: Check [store/openai.ts](mdc:store/openai.ts) configuration
5. **Translation issues**: Run `pnpm run i18n` to regenerate translations
### Debug Tools
- [src/app/playground/](mdc:src/app/playground/) - Component testing playground
- React Native Debugger for debugging
- Expo DevTools for development
## Performance Tips
- Use `useMemo` and `useCallback` for expensive operations
- Implement proper list virtualization for large lists
- Use selectors to prevent unnecessary re-renders
- Optimize images and assets
@@ -1,7 +0,0 @@
---
description:
globs:
alwaysApply: true
---
1. 本项目使用 expo 框架,你是 expo 开发专家辅助我编程
2. 该项目原有 web 端,你需要辅助我将其转化为 native 端
@@ -1,115 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# State Management with Zustand
## Store Architecture
The app uses Zustand for state management. Configuration data persists through **react-native-mmkv** (replacing AsyncStorage for better performance).
### Core Stores
#### Configuration Stores (with MMKV persistence)
**Settings**
- [store/setting.ts](mdc:store/setting.ts) - App settings
- Manages: Theme, font, language, developer mode
- Persistence: **MMKV** with JSON serialization (appStorage instance)
**OpenAI Configuration**
- [store/openai.ts](mdc:store/openai.ts) - OpenAI API configuration
- Manages: API key, proxy settings
- Persistence: **MMKV** with JSON serialization (appStorage instance)
**Agent Configuration**
- [store/agent/store.ts](mdc:store/agent/store.ts) - AI Agent configuration
- Manages: Agent config, initialization data
- Persistence: **MMKV** with JSON serialization (appStorage instance)
#### Runtime Stores (no persistence)
**Session Management**
- [store/session/store.ts](mdc:store/session/store.ts) - Main session store
- Manages: Current session state, session list
- Persistence: Runtime only (future: SWR + local database)
**Chat State**
- [store/chat/store.ts](mdc:store/chat/store.ts) - Active chat functionality
- Manages: Current chat state, messages, loading states
- Persistence: Runtime only (future: SWR + local database)
**User State**
- [store/user/index.ts](mdc:store/user/index.ts) - User authentication state
- Manages: Authentication state, user info
- Persistence: Runtime only (Token stored in expo-secure-store separately)
## State Patterns
### Store Structure
```typescript
interface StoreState {
// State properties
data: DataType;
// Actions
setData: (data: DataType) => void;
updateData: (updater: (data: DataType) => DataType) => void;
resetData: () => void;
}
```
### Persistence Pattern
```typescript
import { createJSONStorage, persist } from 'zustand/middleware';
import { appStorage, createMMKVStorage } from '@/utils/storage';
export const useStore = create<StoreState>()(
persist(
(set, get) => ({
// State and actions
}),
{
name: 'store-name',
storage: createJSONStorage(() => createMMKVStorage(appStorage)),
},
),
);
```
### Storage Instances
The app uses a single MMKV instance for configuration data:
- **appStorage**: Global app configuration (settings, openai, agent config)
For sensitive data (tokens, API keys), use **expo-secure-store** instead.
### Selector Pattern
- Use selectors for derived state
- Implement memoization for performance
- Keep selectors in separate files for organization
### Type Safety
- Define interfaces for all store states
- Use TypeScript for compile-time type checking
- Export types for use in components
## Usage Guidelines
### In Components
```typescript
const { data, setData } = useStore();
const derivedData = useStore(selector);
```
### Store Updates
- Use immutable updates
- Implement proper error handling
- Add loading states for async operations
### Performance
- Use selectors to prevent unnecessary re-renders
- Implement proper memoization
- Avoid storing computed values in state
@@ -1,9 +0,0 @@
build:
steps:
- eas/build
- run:
command: |
curl \
-H 'Content-Type: application/json' \
-d '{"content": "Build android development succeeded! URL: ${ eas.job.expoBuildUrl }"}' \
$DISCORD_WEBHOOK_URL
@@ -1,9 +0,0 @@
build:
steps:
- eas/build
- run:
command: |
curl \
-H 'Content-Type: application/json' \
-d '{"content": "Build ios development succeeded! URL: ${ eas.job.expoBuildUrl }"}' \
$DISCORD_WEBHOOK_URL
@@ -1,9 +0,0 @@
build:
steps:
- eas/build
- run:
command: |
curl \
-H 'Content-Type: application/json' \
-d '{"content": "Build android preview succeeded! URL: ${ eas.job.expoBuildUrl }"}' \
$DISCORD_WEBHOOK_URL
-9
View File
@@ -1,9 +0,0 @@
build:
steps:
- eas/build
- run:
command: |
curl \
-H 'Content-Type: application/json' \
-d '{"content": "Build ios preview succeeded! URL: ${ eas.job.expoBuildUrl }"}' \
$DISCORD_WEBHOOK_URL
@@ -1,9 +0,0 @@
build:
steps:
- eas/build
- run:
command: |
curl \
-H 'Content-Type: application/json' \
-d '{"content": "Build android production succeeded! URL: ${ eas.job.expoBuildUrl }"}' \
$DISCORD_WEBHOOK_URL
@@ -1,9 +0,0 @@
build:
steps:
- eas/build
- run:
command: |
curl \
-H 'Content-Type: application/json' \
-d '{"content": "Build ios production succeeded! URL: ${ eas.job.expoBuildUrl }"}' \
$DISCORD_WEBHOOK_URL
@@ -1,21 +0,0 @@
name: Create development builds
jobs:
android_development_build:
name: Build Android
type: build
params:
platform: android
profile: development
ios_device_development_build:
name: Build iOS device
type: build
params:
platform: ios
profile: development
ios_simulator_development_build:
name: Build iOS simulator
type: build
params:
platform: ios
profile: development-simulator
@@ -1,15 +0,0 @@
name: Create Production Builds
# on:
# push:
# branches: ['main']
jobs:
build_android:
type: build # This job type creates a production build for Android
params:
platform: android
build_ios:
type: build # This job type creates a production build for iOS
params:
platform: ios
@@ -1,68 +0,0 @@
name: Deploy to production
# on:
# push:
# branches: ['main']
jobs:
fingerprint:
name: Fingerprint
type: fingerprint
get_android_build:
name: Check for existing android build
needs: [fingerprint]
type: get-build
params:
fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
profile: production
get_ios_build:
name: Check for existing ios build
needs: [fingerprint]
type: get-build
params:
fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
profile: production
build_android:
name: Build Android
needs: [get_android_build]
if: ${{ !needs.get_android_build.outputs.build_id }}
type: build
params:
platform: android
profile: production
build_ios:
name: Build iOS
needs: [get_ios_build]
if: ${{ !needs.get_ios_build.outputs.build_id }}
type: build
params:
platform: ios
profile: production
submit_android_build:
name: Submit Android Build
needs: [build_android]
type: submit
params:
build_id: ${{ needs.build_android.outputs.build_id }}
submit_ios_build:
name: Submit iOS Build
needs: [build_ios]
type: submit
params:
build_id: ${{ needs.build_ios.outputs.build_id }}
publish_android_update:
name: Publish Android update
needs: [get_android_build]
if: ${{ needs.get_android_build.outputs.build_id }}
type: update
params:
branch: production
platform: android
publish_ios_update:
name: Publish iOS update
needs: [get_ios_build]
if: ${{ needs.get_ios_build.outputs.build_id }}
type: update
params:
branch: production
platform: ios
@@ -1,12 +0,0 @@
name: Publish preview update
# on:
# push:
# branches: ['*']
jobs:
publish_preview_update:
name: Publish preview update
type: update
params:
branch: ${{ github.ref_name || 'test' }}
@@ -1,19 +0,0 @@
# on:
# push:
# branches: ['main']
jobs:
build_android:
name: Build Android app
type: build
params:
platform: android
profile: production
submit_android:
name: Submit to Google Play Store
needs: [build_android]
type: submit
params:
platform: android
build_id: ${{ needs.build_android.outputs.build_id }}
-19
View File
@@ -1,19 +0,0 @@
# on:
# push:
# branches: ['main']
jobs:
build_ios:
name: Build iOS app
type: build
params:
platform: ios
profile: production
submit_ios:
name: Submit to Apple App Store
needs: [build_ios]
type: submit
params:
platform: ios
build_id: ${{ needs.build_ios.outputs.build_id }}
-9
View File
@@ -1,9 +0,0 @@
# OAuth 2.0 OIDC 认证配置
# 客户端 ID - 从认证服务器获取
EXPO_PUBLIC_OAUTH_CLIENT_ID=lobehub-mobile
# 官方云服务器地址
EXPO_PUBLIC_OFFICIAL_CLOUD_SERVER=https://lobechat.com
# 回调地址 - 必须与认证服务器配置一致
EXPO_PUBLIC_OAUTH_REDIRECT_URI=com.lobehub.app://auth/callback
-36
View File
@@ -1,36 +0,0 @@
# Eslintignore for LobeHub
################################################################
# dependencies
node_modules
# ci
coverage
.coverage
# test
jest*
*.test.ts
*.test.tsx
# umi
.umi
.umi-production
.umi-test
.dumi/tmp*
!.dumirc.ts
# production
dist
es
lib
logs
ios
android
# misc
# add other ignore file below
.expo
polyfills.ts
temp
-55
View File
@@ -1,55 +0,0 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
package-lock.json
# pnpm-lock.yaml
# typescript
*.tsbuildinfo
app-example
android
ios
yarn.lock
repomix-output.xml
lobe-ui-repomix-output.xml
lobe-chat-repomix-output.xml
repomix-output-ant-design-theme.xml
credentials.json
build-*.aab
build-*.apk
build-*.ipa
coverage
-50
View File
@@ -1,50 +0,0 @@
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
entry: 'locales/zh-CN',
entryLocale: 'zh-CN',
output: 'locales',
outputLocales: [
'ar',
'bg-BG',
'zh-TW',
'en-US',
'ru-RU',
'ja-JP',
'ko-KR',
'fr-FR',
'tr-TR',
'es-ES',
'pt-BR',
'de-DE',
'it-IT',
'nl-NL',
'pl-PL',
'vi-VN',
'fa-IR',
],
temperature: 0,
saveImmediately: true,
// chatgpt-4o-latest 和 gpt-chat 翻译效果更好
modelName: 'chatgpt-4o-latest',
experimental: {
jsonMode: true,
},
markdown: {
reference: '你需要保持 mdx 的组件格式,输出文本不需要在最外层包裹任何代码块语法',
entry: ['./README.zh-CN.md'],
entryLocale: 'zh-CN',
outputLocales: ['en-US'],
includeMatter: true,
exclude: ['./src/**/*'],
outputExtensions: (locale, { filePath }) => {
if (filePath.includes('.mdx')) {
if (locale === 'en-US') return '.mdx';
return `.${locale}.mdx`;
} else {
if (locale === 'en-US') return '.md';
return `.${locale}.md`;
}
},
},
});
-23
View File
@@ -1,23 +0,0 @@
lockfile=true
node-linker=hoisted
ignore-workspace-root-check=true
enable-pre-post-scripts=true
# Load dotenv files for all the npm scripts
node-options="--require dotenv-expand/config"
public-hoist-pattern[]=*@umijs/lint*
public-hoist-pattern[]=*changelog*
public-hoist-pattern[]=*commitlint*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*postcss*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=*remark*
public-hoist-pattern[]=*semantic-release*
public-hoist-pattern[]=*stylelint*
public-hoist-pattern[]=@auth/core
public-hoist-pattern[]=@clerk/backend
public-hoist-pattern[]=@clerk/types
public-hoist-pattern[]=pdfjs-dist
-67
View File
@@ -1,67 +0,0 @@
# Prettierignore for LobeHub
################################################################
# general
.DS_Store
.editorconfig
.idea
.vscode
.history
.temp
.env.local
.husky
.npmrc
.gitkeep
venv
temp
tmp
LICENSE
# dependencies
node_modules
*.log
*.lock
package-lock.json
# ci
coverage
.coverage
.eslintcache
.stylelintcache
test-output
__snapshots__
*.snap
# production
dist
es
lib
logs
# umi
.umi
.umi-production
.umi-test
.dumi/tmp*
# ignore files
.*ignore
# docker
docker
Dockerfile*
# image
*.webp
*.gif
*.png
*.jpg
# misc
# add other ignore file below
./src/icons/MaterialFileTypeIcon/icon-map.json
*.ico
*.backup
*.mdc
*.ttf
-1
View File
@@ -1 +0,0 @@
module.exports = require('@lobehub/lint').prettier;
-1
View File
@@ -1 +0,0 @@
module.exports = require('@lobehub/lint').remarklint;
-10
View File
@@ -1,10 +0,0 @@
const config = require('@lobehub/lint').stylelint;
module.exports = {
...config,
rules: {
'custom-property-pattern': null,
'no-descending-specificity': null,
...config.rules,
},
};
-55
View File
@@ -1,55 +0,0 @@
# LobeChat Mobile Agent Guide
## Project Overview
- React Native + Expo app delivering the LobeChat AI chat experience for iOS and Android from a shared codebase.
- Core entry points: `src/app/_layout.tsx` wires global providers, while `src/app/index.tsx` (and nested routes under `src/app/(main)`) drive navigation via Expo Router.
- State flows through colocated Zustand stores such as `store/chat`, `store/session`, and `store/openai`; selectors in `store/session/selectors` keep components efficient.
- Key platform features include Markdown + math rendering (React Native Markdown, Shiki, MathJax), streaming chat transport via `utils/fetchSSE`, and tRPC clients configured in `utils/trpc`.
- Follow the domain guides in `rules/` (e.g., `project-overview.mdc`, `state-management.mdc`, `api-integration.mdc`) for deeper architecture context before implementing changes.
## Build and Test Commands
| Task | Command | Notes |
| ------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------- |
| Install dependencies | `pnpm install` | Requires Node 18+ and pnpm 8+. |
| Start Expo dev server | `pnpm start` | Opens the Metro bundler with QR code pairing. |
| Run on iOS simulator/device | `pnpm ios` / `pnpm device:ios` | Uses `expo run:ios`; ensure Xcode tooling is installed. |
| Run on Android emulator/device | `pnpm android` / `pnpm device:android` | Uses `expo run:android`; start an emulator first. |
| Web preview | `pnpm web` | Launches the web bundle for quick UI checks. |
| Jest unit tests | `pnpm test` | Runs through `jest-expo` with coverage enabled. |
| Lint TypeScript/JS | `pnpm lint` | Delegates to Expo + ESLint rules defined in repo. |
| Format sources | `pnpm prettier` | Applies Prettier across the workspace. |
| Generate translations | `pnpm i18n` | Executes scripted workflow from `rules/internationalization.mdc`. |
| Production builds | `pnpm production:ios` / `pnpm production:android` | Wraps EAS build profiles from `eas.json`. |
## Code Style Guidelines
- TypeScript runs in strict mode; define interfaces/types for component props and store state, avoiding `any`. Reference `rules/app-structure.mdc` and `rules/state-management.mdc` for patterns.
- File naming: Components in PascalCase, hooks in camelCase prefixed with `use`, utilities in camelCase, constants in UPPER_SNAKE_CASE (see `rules/development-workflow.mdc`).
- Use absolute imports with the `@/` alias; group imports by React, third-party, then internal modules.
- Keep UI logic declarative; colocate styles in `styles.ts` companions and respect theming conventions from `rules/color-system.mdc`.
- Run `pnpm lint` and `pnpm prettier` before committing. Git hooks enforce Conventional Commits and formatting, so align commit messages with `feat|fix|chore` etc.
## Testing Instructions
- Place tests under `test/` or alongside components as `*.test.ts(x)` per `rules/react-native.mdc` guidance.
- Use `@testing-library/react-native` for rendering and interaction assertions; rely on user-centered queries rather than implementation details.
- Mock async integrations (SecureStore, **MMKV**, network services) using Jest mocks; reference `rules/debug-usage.mdc` for logging utilities during tests.
- MMKV is mocked in `test/utils.tsx` with an in-memory Map for test isolation.
- Prefer explicit assertions over brittle snapshots; when snapshots are required, keep them under `__snapshots__` directories.
- Run `pnpm test --watch` during iteration and ensure `pnpm test` + `pnpm lint` pass before opening a PR.
## Security Considerations
- Keep provider keys in `.env.local` (copied from `.env.example`) and load them through the secure configuration flows in `store/openai`; never commit secrets.
- Persist sensitive tokens with `expo-secure-store`; configuration data uses MMKV as outlined in `rules/state-management.mdc`.
- Validate API responses and sanitize Markdown before render—Shiki/remark plugins are already configured, but keep dependencies patched.
- Enforce HTTPS endpoints for remote calls; review EAS credentials and signing configs before production builds.
- Monitor dependency upgrades touching auth libraries (`expo-auth-session`, `jose`, `jwt-decode`) and align with guidance in `rules/development-workflow.mdc`.
## Additional References
- `rules/quick-reference.mdc` for command cheatsheets.
- `rules/internationalization.mdc` to extend locale coverage.
- `rules/debug-usage.mdc` for logging and diagnostics standards.
-407
View File
@@ -1,407 +0,0 @@
Attribution-NonCommercial 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
d. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
g. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
i. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
j. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
k. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
l. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the Adapted
Material from complying with this Public License.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the "Licensor." The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.
-558
View File
@@ -1,558 +0,0 @@
<div align="center"><a name="readme-top"></a>
<img width="128" height="128" alt="appicon" src="https://github.com/user-attachments/assets/13c89551-5dd5-4dfb-9397-1aaf467c841f" />
# LobeHub Mobile
Your Workspace, Anywhere
The LobeHub application for iOS, and Android
[Parent Project][parent-project] · [Changelog][changelog] · [Report Bug][issues-link] · [Request Feature][issues-link]
<!-- SHIELD GROUP -->
[![][expo-sdk-shield]][expo-link]
[![][react-native-shield]][react-native-link]
[![][typescript-shield]][typescript-link]<br/>
[![][license-shield]][license-link]
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-issues-shield]][github-issues-link]
**Share LobeHub Mobile**
[![][share-x-shield]][share-x-link]
[![][share-telegram-shield]][share-telegram-link]
[![][share-whatsapp-shield]][share-whatsapp-link]
[![][share-reddit-shield]][share-reddit-link]
<sup>Experience AI conversations on your mobile device. Built for you, the Super Individual.</sup>
![](https://github.com/user-attachments/assets/a411daaf-11b8-4c0c-9623-1b80650aa3fa)
</div>
> \[!IMPORTANT]
>
> **📱 iOS Open Beta Now Available!**
>
> Join our TestFlight beta program and be among the first to experience LobeHub Mobile on your iPhone or iPad!
>
> 🔗 **[Join TestFlight Beta](https://testflight.apple.com/join/2ZbjX4Qp)**
>
> We'd love to hear your feedback! Share your experience with us on [Discord][discord-link] or [GitHub Issues][issues-link]. 🫰
<details>
<summary><kbd>Table of contents</kbd></summary>
#### TOC
- [👋🏻 Getting Started](#-getting-started)
- [✨ Features](#-features)
- [📱 Cross-Platform Native Experience](#-cross-platform-native-experience)
- [🎨 Modern UI Design](#-modern-ui-design)
- [🤖 Multi-Model AI Provider Support](#-multi-model-ai-provider-support)
- [💬 Rich Conversation Features](#-rich-conversation-features)
- [🔒 Privacy & Security First](#-privacy--security-first)
- [`*` What's more](#-whats-more)
- [📱 Platform Support](#-platform-support)
- [🚀 Quick Start](#-quick-start)
- [Installation](#installation)
- [Configuration](#configuration)
- [Development](#development)
- [🛠️ Tech Stack](#-tech-stack)
- [⌨️ Local Development](#-local-development)
- [Available Scripts](#available-scripts)
- [Project Structure](#project-structure)
- [Development Workflow](#development-workflow)
- [🤝 Contributing](#-contributing)
- [Contribution Workflow](#contribution-workflow)
- [Development Standards](#development-standards)
- [📦 Ecosystem](#-ecosystem)
- [❤️ Community](#-community)
####
<br/>
</details>
## 👋🏻 Getting Started
We are bringing the powerful LobeHub experience to your mobile devices! Whether you're an iOS or Android user, LobeHub Mobile provides a seamless, native AI chat experience on the go.
| [![][testflight-shield]][testflight-link] | Download the iOS beta now! Join our TestFlight program to experience LobeHub Mobile. |
| :---------------------------------------: | :----------------------------------------------------------------------------------- |
| [![][discord-shield-badge]][discord-link] | Join our Discord community! Connect with other users and share your feedback. |
> \[!IMPORTANT]
>
> **Star Us**, You will receive all release notifications from GitHub without any delay \~ ⭐️
<div align="right">
[![][back-to-top]](#readme-top)
</div>
## ✨ Features
### 📱 Cross-Platform Native Experience
**True Native Performance on Both Platforms**
Built with React Native and Expo SDK 54, LobeHub Mobile delivers genuine native performance across iOS and Android. Enjoy smooth 60fps animations, instant touch feedback, and platform-specific UI patterns that feel right at home on your device.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
### 🎨 Modern UI Design
**Beautifully Crafted for Mobile**
- 💎 **Refined Interface**: Carefully crafted UI with elegant visuals and smooth interactions
- 🌗 **Adaptive Themes**: Seamless dark/light mode switching that follows system preferences
- 📱 **Mobile-First**: Optimized touch interactions and gestures for the best mobile experience
-**Fluid Animations**: Powered by React Native Reanimated for buttery-smooth 60fps animations
- 🎯 **Native Patterns**: Platform-specific UI components following iOS and Android design guidelines
<div align="right">
[![][back-to-top]](#readme-top)
</div>
### 🤖 Multi-Model AI Provider Support
**60+ AI Service Providers at Your Fingertips**
LobeHub Mobile supports an extensive range of AI service providers, giving you unparalleled flexibility to choose the best models for your needs:
**Major Providers:**
- **OpenAI**: GPT-4o, GPT-4 Turbo, GPT-3.5, and more
- **Anthropic**: Claude 3.5 Sonnet, Claude 3 Opus/Sonnet/Haiku
- **Google**: Gemini 2.0 Flash, Gemini Pro, and Vision models
- **Microsoft**: Azure OpenAI, Azure AI services
- **xAI**: Grok models
**Local & Self-Hosted:**
- Ollama, LM Studio, vLLM, Xinference
**Chinese Providers:**
- DeepSeek, Moonshot, Qwen, ZhiPu, Baichuan, Minimax
- Hunyuan, Spark, SenseNova, Wenxin, and more
**Additional Providers:**
- Groq, Perplexity, Mistral, Together AI, Fireworks AI
- OpenRouter, HuggingFace, Cloudflare Workers AI
- Bedrock, Vertex AI, and 40+ more providers
> \[!TIP]
>
> Seamlessly switch between providers and models. All API keys are stored securely using Expo SecureStore (iOS Keychain / Android Keystore).
<div align="right">
[![][back-to-top]](#readme-top)
</div>
### 💬 Rich Conversation Features
**Everything You Need for Powerful AI Conversations**
- 🗣️ **Streaming Responses**: Real-time AI replies with smooth streaming animation
- 📝 **Rich Markdown**: Full Markdown support with tables, lists, GFM, and alerts
- 🎨 **Code Highlighting**: Professional syntax highlighting powered by Shiki (100+ languages)
- 📐 **Math Rendering**: Beautiful LaTeX formula rendering with KaTeX
- 🎙️ **Voice Interaction**: Built-in TTS (Text-to-Speech) and STT (Speech-to-Text) support
- 🖼️ **Vision Models**: Upload images and chat with vision-enabled AI models
- 🎨 **Image Generation**: Create images with DALL·E, Midjourney, and more
- 💾 **Lightning Fast Storage**: MMKV-powered local storage for instant access
- 📤 **Export & Share**: Export conversations in multiple formats
- 🔄 **Multi-Session**: Manage unlimited conversations with smart organization
<div align="right">
[![][back-to-top]](#readme-top)
</div>
### 🔒 Privacy & Security First
**Your Data, Your Control**
- 🔐 **Secure Storage**: API keys protected with Expo SecureStore (iOS Keychain / Android Keystore)
- 💾 **Local First**: All data stored locally on your device using MMKV
- 🚫 **No Tracking**: Zero analytics or tracking - your conversations stay private
- 🔓 **Open Source**: Fully transparent codebase you can audit and trust
- 📴 **Offline Access**: View your chat history even without internet connection
<div align="right">
[![][back-to-top]](#readme-top)
</div>
### `*` What's more
Beyond these features, LobeHub Mobile also offers:
- [x] 🌐 **i18n Support**: Built-in support for 18 languages with auto-detection
- [x] 🎯 **Context Menu**: Long-press for quick actions (copy, delete, retry, regenerate)
- [x] 📋 **Smart Copy**: Intelligent content detection for code blocks, text, or entire messages
- [x] 🔍 **Global Search**: Quickly find messages across all conversations with full-text search
- [x] 🏷️ **Session Groups**: Organize conversations with custom groups, folders, and tags
- [x] 🗂️ **Topic Management**: Auto-create topics and organize conversations by context
- [x] ⚙️ **Advanced Customization**: Fine-tune model parameters (temperature, top-p, frequency penalty, etc.)
- [x] 📱 **Haptic Feedback**: Native haptic feedback for enhanced touch experience
- [x] 🎨 **Theme System**: Dynamic theming with dark/light modes and system preferences
- [x] 🔔 **Push Notifications**: Stay updated with conversation notifications
- [x] 📊 **Token Usage Tracking**: Monitor your API usage and costs
- [x] 🔄 **Pull to Refresh**: Natural gesture-based UI updates
- [x] ⌨️ **Keyboard Shortcuts**: Enhanced productivity with keyboard controls
- [x] 📤 **Import/Export**: Backup and restore your conversations
> ✨ More features will be added as LobeHub Mobile evolves.
---
> \[!NOTE]
>
> Check out our [Roadmap](https://github.com/lobehub/lobe-chat/projects) to see what's coming next!
<div align="right">
[![][back-to-top]](#readme-top)
</div>
## 📱 Platform Support
| Platform | Status | Recommended |
| -------- | --------------- | --------------------- |
| iOS | ✅ Fully Tested | iOS 18.0+ |
| Android | ✅ Fully Tested | Android 15.0 (API 35) |
<div align="right">
[![][back-to-top]](#readme-top)
</div>
## 🚀 Quick Start
### Installation
1. Clone the repository:
```bash
git clone https://github.com/lobehub/lobe-chat.git
cd lobe-chat/apps/mobile
```
2. Install dependencies:
```bash
pnpm install
```
### Configuration
1. Copy the environment template:
```bash
cp .env.example .env.local
```
2. Configure your API keys in `.env.local`:
```bash
# OpenAI API Key (Required)
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
# Optional: Custom API Base URL
# OPENAI_PROXY_URL=https://api.openai.com/v1
```
### Development
Start the development server:
```bash
# Start Expo development server
pnpm start
# Or run directly on a specific platform
pnpm ios # iOS simulator (macOS only)
pnpm android # Android emulator
pnpm web # Web browser preview
```
**Testing on Physical Devices:**
1. Install [Expo Go](https://expo.dev/client) on your device:
- [📱 iOS App Store](https://apps.apple.com/app/expo-go/id982107779)
- [🤖 Google Play Store](https://play.google.com/store/apps/details?id=host.exp.exponent)
2. Scan the QR code in the terminal with Expo Go to launch the app
> \[!TIP]
>
> For a faster development experience on physical devices, consider using [EAS Build](https://docs.expo.dev/build/introduction/) to create custom development builds.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
## 🛠️ Tech Stack
Our technology choices focus on **performance**, **developer experience**, and **maintainability**:
| Technology | Version | Purpose |
| -------------------------------- | -------- | --------------------------------------- |
| **React Native** | 0.81.5 | Core framework for cross-platform apps |
| **Expo SDK** | \~54.0.0 | Development platform and native modules |
| **TypeScript** | ^5.8.2 | Type safety and better DX |
| **Expo Router** | \~4.0.17 | File-based navigation |
| **Zustand** | ^5.0.3 | Lightweight state management |
| **MMKV** | ^3.1.0 | Lightning-fast local storage |
| **React Native Reanimated** | \~3.16.7 | 60fps animations on native thread |
| **React Native Gesture Handler** | \~2.20.2 | Native touch gestures |
| **Shiki** | ^3.1.0 | Beautiful code syntax highlighting |
| **React Native Markdown** | Latest | Rich markdown rendering |
| **React i18next** | ^15.2.0 | Internationalization (18 languages) |
| **Jest** | Latest | Testing framework |
<div align="right">
[![][back-to-top]](#readme-top)
</div>
## ⌨️ Local Development
### Available Scripts
```bash
# Development
pnpm start # Start Expo dev server
pnpm ios # Run on iOS simulator
pnpm android # Run on Android emulator
pnpm web # Run in web browser
# Testing & Quality
pnpm test # Run Jest tests
pnpm test:watch # Run tests in watch mode
pnpm lint # Run ESLint
pnpm type-check # Run TypeScript compiler check
# Internationalization
pnpm i18n # Generate translations for all languages
# Production
pnpm build # Create production build
pnpm production:ios # Build iOS production app
pnpm production:android # Build Android production app
```
### Project Structure
```bash
apps/mobile/
├── app/ # Expo Router pages
│ ├── (main)/ # Main app routes
│ ├── (setting)/ # Settings routes
│ └── playground/ # Component playground
├── src/
│ ├── components/ # Reusable UI components
│ ├── features/ # Feature-based modules
│ ├── store/ # Zustand stores
│ ├── types/ # TypeScript types
│ ├── utils/ # Utility functions
│ └── locales/ # i18n source files
├── assets/ # Images, fonts, etc.
├── locales/ # Generated translations
└── test/ # Test utilities
```
### Development Workflow
1. **Feature Development**
- Create feature branch from `feat/mobile-app`
- Follow [Development Guidelines](./docs/DEVELOPMENT.md)
- Write tests for new features
2. **Code Quality**
- Run `pnpm lint` before committing
- Use TypeScript strictly (no `any`)
- Follow existing code patterns
3. **Internationalization**
- Add translations to `src/locales/default/common.ts`
- Run `pnpm i18n` to generate all languages
- Test with different locales in app settings
4. **Testing**
- Write unit tests for utilities and hooks
- Write component tests with React Native Testing Library
- Ensure `pnpm test` passes before PR
<div align="right">
[![][back-to-top]](#readme-top)
</div>
## 🤝 Contributing
Contributions of all types are more than welcome! If you are interested in contributing code, feel free to check out our GitHub [Issues](https://github.com/lobehub/lobe-chat/issues) and [Projects](https://github.com/lobehub/lobe-chat/projects).
> \[!TIP]
>
> We are building a modern mobile AI chat application. Join us in creating an amazing user experience!
>
> Whether it's **reporting bugs**, **requesting features**, **improving documentation**, or **contributing code** - we appreciate it all.
[![][pr-welcome-shield]][pr-welcome-link]
### Contribution Workflow
1. **Fork** the repository
2. **Clone** your fork: `git clone https://github.com/your-username/lobe-chat.git`
3. **Create** a feature branch: `git checkout -b feature/amazing-feature`
4. **Make** your changes
5. **Test** your changes: `pnpm test && pnpm lint`
6. **Commit** with conventional commit messages: `git commit -m 'feat: add amazing feature'`
7. **Push** to your fork: `git push origin feature/amazing-feature`
8. **Open** a Pull Request
### Development Standards
- 📝 **Commit Messages**: Follow [Conventional Commits](https://conventionalcommits.org/)
- `feat:` New features
- `fix:` Bug fixes
- `docs:` Documentation changes
- `style:` Code style changes (formatting, etc.)
- `refactor:` Code refactoring
- `test:` Adding or updating tests
- `chore:` Build process or auxiliary tool changes
- 💻 **Code Style**
- Use TypeScript strictly
- Follow ESLint and Prettier rules
- Write meaningful variable and function names
- Add comments for complex logic
- 🧪 **Testing**
- Write tests for new features
- Ensure all tests pass
- Maintain or improve code coverage
<div align="right">
[![][back-to-top]](#readme-top)
</div>
## 📦 Ecosystem
| NPM | Repository | Description | Version |
| --------------------------------- | --------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------- |
| [@lobehub/ui][lobe-ui-link] | [lobehub/lobe-ui][lobe-ui-github] | Open-source UI component library for building AIGC web applications | [![][lobe-ui-shield]][lobe-ui-link] |
| [@lobehub/icons][lobe-icons-link] | [lobehub/lobe-icons][lobe-icons-github] | Popular AI / LLM Model Brand SVG Logo and Icon Collection | [![][lobe-icons-shield]][lobe-icons-link] |
| [@lobehub/tts][lobe-tts-link] | [lobehub/lobe-tts][lobe-tts-github] | High-quality & reliable TTS/STT React Hooks library | [![][lobe-tts-shield]][lobe-tts-link] |
| [@lobehub/lint][lobe-lint-link] | [lobehub/lobe-lint][lobe-lint-github] | ESlint, Stylelint, Commitlint, Prettier configurations for LobeHub | [![][lobe-lint-shield]][lobe-lint-link] |
<div align="right">
[![][back-to-top]](#readme-top)
</div>
## ❤️ Community
We are a group of e/acc design-engineers, hoping to provide modern design components and tools for AIGC. By adopting the Bootstrapping approach, we aim to provide developers and users with a more open, transparent, and user-friendly product ecosystem.
| [![][parent-shield]][parent-project] | No installation or registration necessary! Visit our website to experience the web version firsthand. |
| :---------------------------------------: | :---------------------------------------------------------------------------------------------------- |
| [![][discord-shield-badge]][discord-link] | Join our Discord community! Connect with developers and other enthusiastic users of LobeHub. |
> \[!IMPORTANT]
>
> **Star Us**, You will receive all release notifications from GitHub without any delay \~ ⭐️
<div align="right">
[![][back-to-top]](#readme-top)
</div>
---
#### 📝 License
[![][license-image]][license-link]
Copyright © 2025 [LobeHub][profile-link]. <br />
This project is licensed under a [Creative Commons Attribution-NonCommercial 4.0 International License][license-link].
<!-- LINK GROUP -->
[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square
[changelog]: https://github.com/lobehub/lobe-chat/blob/main/CHANGELOG.md
[discord-link]: https://discord.gg/AYFPHvv2jT
[discord-shield-badge]: https://img.shields.io/discord/1127171173982154893?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge
[expo-link]: https://expo.dev
[expo-sdk-shield]: https://img.shields.io/badge/Expo-54.0.0-000020?labelColor=black&logo=expo&style=flat-square
[github-forks-link]: https://github.com/lobehub/lobe-chat/network/members
[github-forks-shield]: https://img.shields.io/github/forks/lobehub/lobe-chat?color=8ae8ff&labelColor=black&style=flat-square
[github-issues-link]: https://github.com/lobehub/lobe-chat/issues
[github-issues-shield]: https://img.shields.io/github/issues/lobehub/lobe-chat?color=ff80eb&labelColor=black&style=flat-square
[github-stars-link]: https://github.com/lobehub/lobe-chat/network/stargazers
[github-stars-shield]: https://img.shields.io/github/stars/lobehub/lobe-chat?color=ffcb47&labelColor=black&style=flat-square
[issues-link]: https://github.com/lobehub/lobe-chat/issues/new/choose
[license-image]: https://licensebuttons.net/l/by-nc/4.0/88x31.png
[license-link]: https://creativecommons.org/licenses/by-nc/4.0/
[license-shield]: https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey?labelColor=black&style=flat-square
[lobe-icons-github]: https://github.com/lobehub/lobe-icons
[lobe-icons-link]: https://www.npmjs.com/package/@lobehub/icons
[lobe-icons-shield]: https://img.shields.io/npm/v/@lobehub/icons?color=369eff&labelColor=black&logo=npm&logoColor=white&style=flat-square
[lobe-lint-github]: https://github.com/lobehub/lobe-lint
[lobe-lint-link]: https://www.npmjs.com/package/@lobehub/lint
[lobe-lint-shield]: https://img.shields.io/npm/v/@lobehub/lint?color=369eff&labelColor=black&logo=npm&logoColor=white&style=flat-square
[lobe-tts-github]: https://github.com/lobehub/lobe-tts
[lobe-tts-link]: https://www.npmjs.com/package/@lobehub/tts
[lobe-tts-shield]: https://img.shields.io/npm/v/@lobehub/tts?color=369eff&labelColor=black&logo=npm&logoColor=white&style=flat-square
[lobe-ui-github]: https://github.com/lobehub/lobe-ui
[lobe-ui-link]: https://www.npmjs.com/package/@lobehub/ui
[lobe-ui-shield]: https://img.shields.io/npm/v/@lobehub/ui?color=369eff&labelColor=black&logo=npm&logoColor=white&style=flat-square
[parent-project]: https://github.com/lobehub/lobe-chat
[parent-shield]: https://img.shields.io/badge/🤯_Lobe_Chat-Web_Version-55b467?labelColor=black&style=for-the-badge
[pr-welcome-link]: https://github.com/lobehub/lobe-chat/pulls
[pr-welcome-shield]: https://img.shields.io/badge/🤯_PR_WELCOME-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge
[profile-link]: https://github.com/lobehub
[react-native-link]: https://reactnative.dev
[react-native-shield]: https://img.shields.io/badge/React%20Native-0.81.5-61dafb?labelColor=black&logo=react&style=flat-square
[share-reddit-link]: https://www.reddit.com/submit?title=Check%20this%20GitHub%20repository%20out%20%F0%9F%A4%AF%20LobeHub%20Mobile%20-%20An%20open-source%2C%20modern-design%20AI%20chat%20mobile%20application.%20One-click%20FREE%20deployment%20of%20your%20private%20ChatGPT%2FClaude%2FGemini%20mobile%20app.&url=https%3A%2F%2Fgithub.com%2Flobehub%2Flobe-chat
[share-reddit-shield]: https://img.shields.io/badge/-share%20on%20reddit-black?labelColor=black&logo=reddit&logoColor=white&style=flat-square
[share-telegram-link]: https://t.me/share/url?text=Check%20this%20GitHub%20repository%20out%20%F0%9F%A4%AF%20LobeHub%20Mobile%20-%20An%20open-source%2C%20modern-design%20AI%20chat%20mobile%20application.%20One-click%20FREE%20deployment%20of%20your%20private%20ChatGPT%2FClaude%2FGemini%20mobile%20app.&url=https%3A%2F%2Fgithub.com%2Flobehub%2Flobe-chat
[share-telegram-shield]: https://img.shields.io/badge/-share%20on%20telegram-black?labelColor=black&logo=telegram&logoColor=white&style=flat-square
[share-whatsapp-link]: https://api.whatsapp.com/send?text=Check%20this%20GitHub%20repository%20out%20%F0%9F%A4%AF%20LobeHub%20Mobile%20-%20An%20open-source%2C%20modern-design%20AI%20chat%20mobile%20application.%20One-click%20FREE%20deployment%20of%20your%20private%20ChatGPT%2FClaude%2FGemini%20mobile%20app.%20https%3A%2F%2Fgithub.com%2Flobehub%2Flobe-chat
[share-whatsapp-shield]: https://img.shields.io/badge/-share%20on%20whatsapp-black?labelColor=black&logo=whatsapp&logoColor=white&style=flat-square
[share-x-link]: https://x.com/intent/tweet?hashtags=chatbot%2CchatGPT%2Cmobile%2Creactnative&text=Check%20this%20GitHub%20repository%20out%20%F0%9F%A4%AF%20LobeHub%20Mobile%20-%20An%20open-source%2C%20modern-design%20AI%20chat%20mobile%20application.%20One-click%20FREE%20deployment%20of%20your%20private%20ChatGPT%2FClaude%2FGemini%20mobile%20app.&url=https%3A%2F%2Fgithub.com%2Flobehub%2Flobe-chat
[share-x-shield]: https://img.shields.io/badge/-share%20on%20x-black?labelColor=black&logo=x&logoColor=white&style=flat-square
[testflight-link]: https://testflight.apple.com/join/2ZbjX4Qp
[testflight-shield]: https://img.shields.io/badge/TestFlight-iOS_Beta-0D96F6?labelColor=black&logo=apple&style=for-the-badge
[typescript-link]: https://www.typescriptlang.org
[typescript-shield]: https://img.shields.io/badge/TypeScript-5.8.2-3178c6?labelColor=black&logo=typescript&style=flat-square
-200
View File
@@ -1,200 +0,0 @@
import { ConfigContext, ExpoConfig } from 'expo/config';
import 'tsx/cjs';
import { version } from './package.json';
/**
* Expo 配置
* 使用 TypeScript 提供类型安全和自动补全
* @see https://docs.expo.dev/workflow/configuration/
*/
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
android: {
adaptiveIcon: {
backgroundColor: '#000000',
backgroundImage: './assets/images/icon-android-background.png',
foregroundImage: './assets/images/icon-android-foreground.png',
monochromeImage: './assets/images/icon-android-foreground.png',
},
edgeToEdgeEnabled: true,
icon: './assets/images/icon-android.png',
package: 'com.lobehub.app',
permissions: [
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE',
'android.permission.READ_MEDIA_IMAGES',
],
},
androidNavigationBar: {
barStyle: 'light-content',
},
androidStatusBar: {
barStyle: 'light-content',
},
experiments: {
// @ts-ignore
appDir: './src/app',
typedRoutes: true,
},
extra: {
eas: {
projectId: 'f02d6f4f-e042-4c95-ba0d-ac06bb474ef0',
},
router: {
origin: false,
},
},
icon: './assets/images/icon.png',
ios: {
appleTeamId: '4684H589ZU',
bundleIdentifier: 'com.lobehub.app',
icon: './assets/images/ios.icon',
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
NSAppTransportSecurity: {
NSAllowsArbitraryLoads: true,
},
NSPhotoLibraryAddUsageDescription: '需要保存图片到相册',
NSPhotoLibraryUsageDescription: '需要访问相册以保存图片',
// 仅支持 iPhone
UIDeviceFamily: [1],
},
// 声明不支持平板
supportsTablet: false,
userInterfaceStyle: 'automatic',
},
name: 'LobeHub',
newArchEnabled: true,
orientation: 'portrait',
owner: 'lobehub',
plugins: [
['expo-build-properties', { ios: { deploymentTarget: '16.4' } }],
'expo-router',
'expo-video',
['react-native-edge-to-edge', { android: { enforceNavigationBarContrast: false } }],
[
'expo-notifications',
{
color: '#000',
icon: './assets/images/icon-android-notification.png',
},
],
[
'expo-splash-screen',
{
android: {
backgroundColor: '#f5f5f5',
dark: {
backgroundColor: '#000',
image: './assets/images/splash-icon.png',
imageWidth: 150,
},
image: './assets/images/splash-icon.png',
imageWidth: 150,
},
ios: {
backgroundColor: '#f5f5f5',
dark: {
backgroundColor: '#000',
enableFullScreenImage_legacy: true,
image: './assets/images/splash-dark.png',
resizeMode: 'cover',
},
enableFullScreenImage_legacy: true,
image: './assets/images/splash.png',
resizeMode: 'cover',
},
},
],
[
'expo-font',
{
android: {
fonts: [
{
fontDefinitions: [
{
path: './assets/fonts/Hack-Regular.ttf',
weight: 400,
},
{
path: './assets/fonts/Hack-Bold.ttf',
weight: 700,
},
{
path: './assets/fonts/Hack-Italic.ttf',
style: 'italic',
weight: 400,
},
{
path: './assets/fonts/Hack-BoldItalic.ttf',
style: 'italic',
weight: 700,
},
],
fontFamily: 'Hack',
},
{
fontDefinitions: [
{
path: './assets/fonts/HarmonyOS_Sans_SC_Regular.ttf',
weight: 400,
},
{
path: './assets/fonts/HarmonyOS_Sans_SC_Medium.ttf',
weight: 500,
},
{
path: './assets/fonts/HarmonyOS_Sans_SC_Bold.ttf',
weight: 700,
},
],
fontFamily: 'HarmonyOS-Sans-SC',
},
],
},
fonts: [
'./assets/fonts/Hack-Regular.ttf',
'./assets/fonts/Hack-Bold.ttf',
'./assets/fonts/Hack-Italic.ttf',
'./assets/fonts/Hack-BoldItalic.ttf',
'./assets/fonts/HarmonyOS_Sans_SC_Regular.ttf',
'./assets/fonts/HarmonyOS_Sans_SC_Medium.ttf',
'./assets/fonts/HarmonyOS_Sans_SC_Bold.ttf',
],
ios: {
fonts: [
'./assets/fonts/Hack-Regular.ttf',
'./assets/fonts/Hack-Bold.ttf',
'./assets/fonts/Hack-Italic.ttf',
'./assets/fonts/Hack-BoldItalic.ttf',
'./assets/fonts/HarmonyOS_Sans_SC_Regular.ttf',
'./assets/fonts/HarmonyOS_Sans_SC_Medium.ttf',
'./assets/fonts/HarmonyOS_Sans_SC_Bold.ttf',
],
},
},
],
'expo-secure-store',
'expo-localization',
'./plugins/withFbjniFix.ts',
'./plugins/withAndroidTransparentNavigation.ts',
],
runtimeVersion: {
policy: 'appVersion',
},
scheme: 'com.lobehub.app',
slug: 'lobe-chat-react-native',
updates: {
url: 'https://u.expo.dev/f02d6f4f-e042-4c95-ba0d-ac06bb474ef0',
},
userInterfaceStyle: 'automatic',
version: version,
web: {
bundler: 'metro',
favicon: './assets/images/favicon.ico',
output: 'static',
},
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 KiB

@@ -1,44 +0,0 @@
{
"fill": "automatic",
"groups": [
{
"blur-material": 1,
"hidden": false,
"layers": [
{
"glass": false,
"image-name": "dLogo.png",
"name": "dLogo",
"position": {
"scale": 0.65,
"translation-in-points": [0, 0]
}
},
{
"glass": false,
"hidden": true,
"image-name": "Group 72.png",
"name": "Group 72",
"position": {
"scale": 1.3,
"translation-in-points": [0, 0]
}
}
],
"lighting": "individual",
"shadow": {
"kind": "neutral",
"opacity": 0.5
},
"specular": true,
"translucency": {
"enabled": true,
"value": 1
}
}
],
"supported-platforms": {
"circles": ["watchOS"],
"squares": "shared"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.
Binary file not shown.
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
plugins: ['@babel/plugin-transform-class-static-block', 'react-native-worklets/plugin'],
presets: [['babel-preset-expo', { unstable_transformImportMeta: true }]],
};

Some files were not shown because too many files have changed in this diff Show More