mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 11:40:07 +00:00
Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebf5a8f649 | |||
| 80b0999467 | |||
| be952f95fd | |||
| d46bb9ad8b | |||
| 8e7f541a0b | |||
| 0eb02ca1c5 | |||
| b90436421b | |||
| e284330678 | |||
| ea9da27e72 | |||
| e99f12f840 | |||
| bbc037912c | |||
| 9881b7b303 | |||
| e9de9e3b52 | |||
| 69f21da3e1 | |||
| 2b7761c36e | |||
| 6734a47759 | |||
| 7f7dcfbff9 | |||
| af33543cba | |||
| 8b619f0a8e | |||
| b81d8f79a4 | |||
| 15ffe289f5 | |||
| 2606f93146 | |||
| c882e75580 | |||
| 6334f62aa1 | |||
| 0af13ca057 | |||
| 68d6457659 | |||
| 3466055968 | |||
| d99a3a80f8 | |||
| cc37acb30b | |||
| eb708e04fe | |||
| 11df190f36 | |||
| a094d5c04c | |||
| f6daefb0a2 | |||
| b8c59be3a7 | |||
| 284faf799b | |||
| 88d194c3b6 | |||
| 6c53a5f026 | |||
| d430836f20 | |||
| 7571b66284 | |||
| 3ec394d77a | |||
| 778dea3853 | |||
| f599617042 | |||
| cc8f5d0639 | |||
| 32d365b544 | |||
| 0ff41dba9c | |||
| fd68cc364c | |||
| ce7a74242f | |||
| 7f9bfda652 | |||
| 5f10aace5b | |||
| 6d14dfe50d | |||
| 12f54759df | |||
| b43d4b27ad | |||
| dab987f64b | |||
| 2e24f72b2a | |||
| 5ebcfa531e | |||
| 1b3f88346b | |||
| 808cb21b99 | |||
| c6dd22eea6 | |||
| da5427ccdf | |||
| d1c7f41bd4 | |||
| 6681989fe6 | |||
| 48a7a08110 | |||
| 0596692e95 | |||
| 59ea77d746 | |||
| 4aa570c904 | |||
| c9ea5336d3 | |||
| ceffce27c3 | |||
| b56c9c51b8 | |||
| 97a6c8e172 | |||
| 727c92ad5b | |||
| 25e45a4cd4 | |||
| 296bad9fa4 | |||
| d8d28371d6 | |||
| a588319cf1 | |||
| 527ab0add7 | |||
| f2230ae536 | |||
| c83fe13d7c | |||
| f4b9d6795b | |||
| 77bc2614a4 | |||
| a02b301904 | |||
| 846a7a5986 | |||
| bc7aa88a8f | |||
| 85c4553821 | |||
| 03c28385e5 | |||
| e525cb2ed6 | |||
| ea44ef1b4c | |||
| 1cfbc878bd | |||
| 61bbd596f0 | |||
| 6508e2fcaf | |||
| 7cef9d88ea | |||
| 16de38ae32 | |||
| c6257f1dba | |||
| b2c28d0810 | |||
| d17b50c6dc | |||
| 0a8c80dfd2 | |||
| 6fd337de18 | |||
| 6266a85cf1 | |||
| 2f7317b98f | |||
| 317df489ce | |||
| ff41f4bb82 | |||
| 60f43d90e5 | |||
| b8136ac17b | |||
| f0e387fa29 | |||
| 8dcb00e2c9 | |||
| 790d8fd498 | |||
| 3719bf4d52 | |||
| bba23bf5a3 | |||
| 509619ba15 | |||
| 5de9e9fea3 | |||
| 3b2a2c1c54 | |||
| c07d900648 | |||
| 938cf6ba71 | |||
| 189081d546 | |||
| 96e2ccd3fb | |||
| 8693d95e0d | |||
| 543db87745 | |||
| e31e7b5d2d | |||
| 15ebcb414b | |||
| cb986040d5 | |||
| ad0fae3c2a | |||
| 79955e8f8d | |||
| d379112d74 | |||
| c1a0868ac3 | |||
| af71a7933a | |||
| cfd52210d5 | |||
| 166d715f17 | |||
| 1238d7fbd5 | |||
| 06507fea7f | |||
| 4b7e838008 | |||
| 9e09099313 | |||
| e2c5e4f5a6 | |||
| de7368bc25 | |||
| 941b6ec4cb | |||
| 3cf7df5748 | |||
| d0e2acac13 | |||
| 7a16e7e539 | |||
| 2518d7eabf | |||
| 0038a64819 | |||
| 02096ea82d | |||
| d330cf4e94 | |||
| 5c2e1faa94 | |||
| 590a9275ce | |||
| 9242e83824 | |||
| 52742254d6 | |||
| 4a82daf329 | |||
| 84baabc1b4 | |||
| 7ef16b73f3 | |||
| 4e445690f1 | |||
| eb0c0696d0 | |||
| 8624f84c8d | |||
| 6db99c090d | |||
| cd337011fa | |||
| 236b8f53cc | |||
| 658c2945a0 | |||
| f71d1f48cd | |||
| 515dee110e | |||
| e9b5c692c7 | |||
| ba3c890f62 | |||
| 639032e201 | |||
| 321b414931 | |||
| 9349ce2d2f | |||
| 3985c13488 | |||
| 8e3494857d | |||
| 55d5ae91ee | |||
| 54b6217256 | |||
| 020ef51141 | |||
| fa07f44436 | |||
| 522fc09a85 | |||
| 72fcc7afab | |||
| 05c0ef084a | |||
| 23d61aebc9 | |||
| 771b585f3a | |||
| 65c8dbb525 | |||
| a11925fbb2 | |||
| d9485809a1 | |||
| 143784b474 | |||
| 26504d0017 | |||
| fa2f9d4eb4 | |||
| 70d356d524 | |||
| 8d59583dca | |||
| c609c77f24 | |||
| c050c39c45 | |||
| ac75d30e0d | |||
| b2a1dc66d5 | |||
| 954c6343d2 | |||
| d3afc75c59 | |||
| 9cceaad241 | |||
| 7ab30fc77e | |||
| 7ce0d1fbc4 | |||
| 8f1b38a24e | |||
| bf57bfe59f | |||
| ca033420b9 | |||
| d8bfec02ad | |||
| 992ecdfdef | |||
| ed267a4d96 | |||
| d9da405ff1 | |||
| f0f132696a | |||
| 775f30b614 | |||
| 1f36158a2f | |||
| 18bcd08327 | |||
| f13c4598c3 | |||
| 319fbfb6fd | |||
| 892844a17f | |||
| 219250c7bb | |||
| b7d51c51dd | |||
| 94d447b563 | |||
| dddbfcd094 | |||
| 5719e4ff90 | |||
| 1725f7d3ee | |||
| 1695f2f289 | |||
| 2ffced0773 | |||
| 8cc70ddf38 | |||
| 1dd0e5efce | |||
| 08ea8561f9 | |||
| 20be3cfb38 | |||
| 562ef7fd8e | |||
| af59bfe013 | |||
| c0974ea955 | |||
| c83d7afbe6 | |||
| 210a41bb8b | |||
| e8c08335c3 | |||
| cf82bc0628 | |||
| e702064e39 | |||
| d14debc7d7 | |||
| 8231aea1c7 | |||
| af274190a8 | |||
| eb50c8b781 | |||
| c25492e377 | |||
| 082b641270 | |||
| 33874c20d1 | |||
| 0d48ebddd9 | |||
| 95393ec093 | |||
| 565e0d9a39 | |||
| 5fe2518dae | |||
| 1cb36da520 | |||
| cdded292f4 | |||
| 9b62685b08 | |||
| 249b46e5cf | |||
| dbe7d7ef7c | |||
| 1e4011e489 | |||
| 3a8229a2c6 | |||
| ed8174fc3d | |||
| 304e6c13d8 | |||
| a63485d915 | |||
| 01d66a9368 | |||
| 794748373b | |||
| a060901c65 | |||
| 7a34c8babe | |||
| 63e3b70681 | |||
| 476b897fe0 | |||
| ae3ed6ec47 | |||
| 5a69857e09 | |||
| e01c72cf03 | |||
| 95d4a3a4be | |||
| 6430f57665 | |||
| 99ded4ac33 | |||
| 582f6d1fbf | |||
| 8b12fdfb82 | |||
| ba3f67f7d4 | |||
| 0a6d3ad3f9 | |||
| e10aa05a11 | |||
| d835c33699 | |||
| 55c45a5197 | |||
| b51839fc54 | |||
| 1e9b05d7ce | |||
| 92da716028 | |||
| 635d0d649b | |||
| 3ae352b79e | |||
| a194d48545 | |||
| 0ee4f18d89 | |||
| 49ea508cc4 | |||
| 88592d3f08 | |||
| 7e0b715e22 | |||
| 8db47eb816 | |||
| 6325602480 | |||
| b745f11873 | |||
| 7f31eba0d9 | |||
| dfeb42ce1c | |||
| 138071d0e3 | |||
| 1aaa5f5152 | |||
| e1acbf21fd | |||
| 79bc08a076 | |||
| fa6ef94067 | |||
| a30a65cd4c | |||
| 2e6018a496 | |||
| 1776a24943 | |||
| 27d133a417 | |||
| de3478b17a | |||
| 694cdbea8f | |||
| ffbb804b3f | |||
| 5947148c01 | |||
| b0cb96e5c2 | |||
| f1d732d166 | |||
| c6de50e385 | |||
| b04a5d7906 | |||
| 088cc2c56c | |||
| acb49c1393 | |||
| e82d4b7274 | |||
| 2dc03b47d6 | |||
| 3e64ee659e | |||
| f653ce1737 | |||
| eeabb69088 | |||
| 356cf029dd | |||
| 6e7b420347 | |||
| ee464838ac | |||
| ec5af1b4c7 |
@@ -0,0 +1,38 @@
|
||||
---
|
||||
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
|
||||
|
||||
---
|
||||
@@ -0,0 +1,253 @@
|
||||
# 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".
|
||||
@@ -0,0 +1,135 @@
|
||||
# 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.
|
||||
```
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
description: Guide for adding environment variables to configure user settings
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Adding Environment Variable for User Settings
|
||||
|
||||
Add server-side environment variables to configure default values for user settings.
|
||||
|
||||
**Priority**: User Custom > Server Env Var > Hardcoded Default
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Define Environment Variable
|
||||
|
||||
Create `src/envs/<domain>.ts`:
|
||||
|
||||
```typescript
|
||||
import { createEnv } from '@t3-oss/env-nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const get<Domain>Config = () => {
|
||||
return createEnv({
|
||||
server: {
|
||||
YOUR_ENV_VAR: z.coerce.number().min(MIN).max(MAX).optional(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
YOUR_ENV_VAR: process.env.YOUR_ENV_VAR,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const <domain>Env = get<Domain>Config();
|
||||
```
|
||||
|
||||
### 2. Update Type (Optional)
|
||||
|
||||
**Skip this step if the domain field already exists in `GlobalServerConfig`.**
|
||||
|
||||
Add to `packages/types/src/serverConfig.ts`:
|
||||
|
||||
```typescript
|
||||
export interface GlobalServerConfig {
|
||||
<domain>?: {
|
||||
<settingName>?: <type>;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Prefer reusing existing types** from `packages/types/src/user/settings` with `PartialDeep`:
|
||||
|
||||
```typescript
|
||||
import { User<Domain>Config } from './user/settings';
|
||||
|
||||
export interface GlobalServerConfig {
|
||||
<domain>?: PartialDeep<User<Domain>Config>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Assemble Server Config (Optional)
|
||||
|
||||
**Skip this step if the domain field already exists in server config.**
|
||||
|
||||
In `src/server/globalConfig/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { <domain>Env } from '@/envs/<domain>';
|
||||
import { cleanObject } from '@/utils/object';
|
||||
|
||||
export const getServerGlobalConfig = async () => {
|
||||
const config: GlobalServerConfig = {
|
||||
// ...
|
||||
<domain>: cleanObject({
|
||||
<settingName>: <domain>Env.YOUR_ENV_VAR,
|
||||
}),
|
||||
};
|
||||
return config;
|
||||
};
|
||||
```
|
||||
|
||||
If the domain already exists, just add the new field to the existing `cleanObject()`:
|
||||
|
||||
```typescript
|
||||
<domain>: cleanObject({
|
||||
existingField: <domain>Env.EXISTING_VAR,
|
||||
<settingName>: <domain>Env.YOUR_ENV_VAR, // Add this line
|
||||
}),
|
||||
```
|
||||
|
||||
### 4. Merge to User Store (Optional)
|
||||
|
||||
**Skip this step if the domain field already exists in `serverSettings`.**
|
||||
|
||||
In `src/store/user/slices/common/action.ts`, add to `serverSettings`:
|
||||
|
||||
```typescript
|
||||
const serverSettings: PartialDeep<UserSettings> = {
|
||||
defaultAgent: serverConfig.defaultAgent,
|
||||
<domain>: serverConfig.<domain>, // Add this line
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Update .env.example
|
||||
|
||||
```bash
|
||||
# <Description> (range/options, default: X)
|
||||
# YOUR_ENV_VAR=<example>
|
||||
```
|
||||
|
||||
### 6. Update Documentation
|
||||
|
||||
Update both English and Chinese documentation:
|
||||
- `docs/self-hosting/environment-variables/basic.mdx`
|
||||
- `docs/self-hosting/environment-variables/basic.zh-CN.mdx`
|
||||
|
||||
Add new section or subsection with environment variable details (type, description, default, example, range/constraints).
|
||||
|
||||
## Type Reuse
|
||||
|
||||
**Prefer reusing existing types** from `packages/types/src/user/settings` instead of defining inline types in `serverConfig.ts`.
|
||||
|
||||
```typescript
|
||||
// ✅ Good - reuse existing type
|
||||
import { UserImageConfig } from './user/settings';
|
||||
|
||||
export interface GlobalServerConfig {
|
||||
image?: PartialDeep<UserImageConfig>;
|
||||
}
|
||||
|
||||
// ❌ Bad - inline type definition
|
||||
export interface GlobalServerConfig {
|
||||
image?: {
|
||||
defaultImageNum?: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Example: AI_IMAGE_DEFAULT_IMAGE_NUM
|
||||
|
||||
```typescript
|
||||
// src/envs/image.ts
|
||||
export const getImageConfig = () => {
|
||||
return createEnv({
|
||||
server: {
|
||||
AI_IMAGE_DEFAULT_IMAGE_NUM: z.coerce.number().min(1).max(20).optional(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
AI_IMAGE_DEFAULT_IMAGE_NUM: process.env.AI_IMAGE_DEFAULT_IMAGE_NUM,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// packages/types/src/serverConfig.ts
|
||||
import { UserImageConfig } from './user/settings';
|
||||
|
||||
export interface GlobalServerConfig {
|
||||
image?: PartialDeep<UserImageConfig>;
|
||||
}
|
||||
|
||||
// src/server/globalConfig/index.ts
|
||||
image: cleanObject({
|
||||
defaultImageNum: imageEnv.AI_IMAGE_DEFAULT_IMAGE_NUM,
|
||||
}),
|
||||
|
||||
// src/store/user/slices/common/action.ts
|
||||
const serverSettings: PartialDeep<UserSettings> = {
|
||||
image: serverConfig.image,
|
||||
// ...
|
||||
};
|
||||
|
||||
// .env.example
|
||||
# AI_IMAGE_DEFAULT_IMAGE_NUM=4
|
||||
```
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs: src/services/**/*,src/database/**/*,src/server/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# LobeChat 后端技术架构指南
|
||||
|
||||
本指南旨在阐述 LobeChat 项目的后端分层架构,重点介绍各核心目录的职责以及它们之间的协作方式。
|
||||
|
||||
## 目录结构映射
|
||||
|
||||
```
|
||||
src/
|
||||
├── server/
|
||||
│ ├── routers/ # tRPC API 路由定义
|
||||
│ └── services/ # 业务逻辑服务层
|
||||
│ └── */impls/ # 平台特定实现
|
||||
├── database/
|
||||
│ ├── models/ # 数据模型 (单表 CRUD)
|
||||
│ ├── repositories/ # 仓库层 (复杂查询/聚合)
|
||||
│ └── schemas/ # Drizzle ORM 表定义
|
||||
└── services/ # 客户端服务 (调用 tRPC 或直接访问 Model)
|
||||
```
|
||||
|
||||
## 核心架构分层
|
||||
|
||||
LobeChat 的后端设计注重模块化、可测试性和灵活性,以适应不同的运行环境(如浏览器端 PGLite、服务端远程 PostgreSQL 以及 Electron 桌面应用)。
|
||||
|
||||
其主要分层如下:
|
||||
|
||||
1. 客户端服务层 (`src/services`):
|
||||
- 位于 src/services/。
|
||||
- 这是客户端业务逻辑的核心层,负责封装各种业务操作和数据处理逻辑。
|
||||
- 环境适配: 根据不同的运行环境,服务层会选择合适的数据访问方式:
|
||||
- 本地数据库模式: 直接调用 `Model` 层进行数据操作,适用于浏览器 PGLite 和本地 Electron 应用。
|
||||
- 远程数据库模式: 通过 `tRPC` 客户端调用服务端 API,适用于需要云同步的场景。
|
||||
- 类型转换: 对于简单的数据类型转换,直接在此层进行类型断言,如 `this.pluginModel.query() as Promise<LobeTool[]>`
|
||||
- 每个服务模块通常包含 `client.ts`(本地模式)、`server.ts`(远程模式)和 `type.ts`(接口定义)文件,在实现时应该确保本地模式和远程模式业务逻辑实现一致,只是数据库不同。
|
||||
|
||||
2. API 接口层 (`TRPC`):
|
||||
- 位于 src/server/routers/
|
||||
- 使用 `tRPC` 构建类型安全的 API。Router 根据运行时环境(如 Edge Functions, Node.js Lambda)进行组织。
|
||||
- 负责接收客户端请求,并将其路由到相应的 `Service` 层进行处理。
|
||||
- 新建 lambda 端点时可以参考 src/server/routers/lambda/\_template.ts
|
||||
|
||||
3. 仓库层 (`Repositories`):
|
||||
- 位于 src/database/repositories/。
|
||||
- 主要处理复杂的跨表查询和数据聚合逻辑,特别是当需要从多个 `Model` 获取数据并进行组合时。
|
||||
- 与 `Model` 层不同,`Repository` 层专注于复杂的业务查询场景,而不涉及简单的领域模型转换。
|
||||
- 当业务逻辑涉及多表关联、复杂的数据统计或需要事务处理时,会使用 `Repository` 层。
|
||||
- 如果数据操作简单(仅涉及单个 `Model`),则通常直接在 `src/services` 层调用 `Model` 并进行简单的类型断言。
|
||||
|
||||
4. 模型层 (`Models`):
|
||||
- 位于 src/database/models/ (例如 src/database/models/plugin.ts 和 src/database/models/document.ts)。
|
||||
- 提供对数据库中各个表(由 src/database/schemas/ 中的 Drizzle ORM schema 定义)的基本 CRUD (创建、读取、更新、删除) 操作和简单的查询能力。
|
||||
- `Model` 类专注于单个数据表的直接操作,不涉及复杂的领域模型转换,这些转换通常在上层的 `src/services` 中通过类型断言完成。
|
||||
- model(例如 Topic) 层接口经常需要从对应的 schema 层导入 NewTopic 和 TopicItem
|
||||
- 创建新的 model 时可以参考 src/database/models/\_template.ts
|
||||
|
||||
5. 数据库 (`Database`):
|
||||
- 客户端模式 (浏览器/PWA): 使用 PGLite (基于 WASM 的 PostgreSQL),数据存储在用户浏览器本地。
|
||||
- 服务端模式 (云部署): 使用远程 PostgreSQL 数据库。
|
||||
- Electron 桌面应用:
|
||||
- Electron 客户端会启动一个本地 Node.js 服务。
|
||||
- 本地服务通过 `tRPC` 与 Electron 的渲染进程通信。
|
||||
- 数据库选择依赖于是否开启云同步功能:
|
||||
- 云同步开启: 连接到远程 PostgreSQL 数据库。
|
||||
- 云同步关闭: 使用 PGLite (通过 Node.js 的 WASM 实现) 在本地存储数据。
|
||||
|
||||
## 数据流向说明
|
||||
|
||||
### 浏览器/PWA 模式
|
||||
|
||||
```
|
||||
UI (React) → Zustand action -> Client Service → Model Layer → PGLite (本地数据库)
|
||||
```
|
||||
|
||||
### 服务端模式
|
||||
|
||||
```
|
||||
UI (React) → Zustand action → Client Service -> TRPC Client → TRPC Routers → Repositories/Models → Remote PostgreSQL
|
||||
```
|
||||
|
||||
### Electron 桌面应用模式
|
||||
|
||||
```
|
||||
UI (Electron Renderer) → Zustand action → Client Service -> TRPC Client → 本地 Node.js 服务 → TRPC Routers → Repositories/Models → PGLite/Remote PostgreSQL (取决于云同步设置)
|
||||
```
|
||||
|
||||
## 服务层 (Server Services)
|
||||
|
||||
- 位于 src/server/services/。
|
||||
- 核心职责是封装独立的、可复用的业务逻辑单元。这些服务应易于测试。
|
||||
- 平台差异抽象: 一个关键特性是通过其内部的 `impls` 子目录(例如 src/server/services/file/impls 包含 s3.ts 和 local.ts)来抹平不同运行环境带来的差异(例如云端使用 S3 存储,桌面版使用本地文件系统)。这使得上层(如 `tRPC` routers)无需关心底层具体实现。
|
||||
- 目标是使 `tRPC` router 层的逻辑尽可能纯粹,专注于请求处理和业务流程编排。
|
||||
- 服务可能会调用 `Repository` 层或直接调用 `Model` 层进行数据持久化和检索,也可能调用其他服务。
|
||||
|
||||
## 最佳实践 (Best Practices)
|
||||
|
||||
### 数据库操作封装原则
|
||||
|
||||
**连续的数据库操作应该封装到 Model 层**
|
||||
|
||||
当业务逻辑涉及多个相关的数据库操作时,建议将这些操作封装到 Model 层中,而不是在上层(Service 或 Router 层)中进行多次数据库调用。
|
||||
|
||||
**优势:**
|
||||
|
||||
- **代码复用**: Client DB 环境的 service 实现和 Server DB 的 lambda 层实现可以复用相同的 Model 方法
|
||||
- **事务一致性**: 相关的数据库操作可以在同一个方法中管理,便于维护数据一致性
|
||||
- **性能优化**: 减少数据库连接次数,提高查询效率
|
||||
- **职责清晰**: Model 层专注数据访问,上层专注业务协调
|
||||
|
||||
**示例:**
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:在 Model 层封装连续的数据库操作
|
||||
class GenerationBatchModel {
|
||||
async delete(id: string): Promise<{ deletedBatch: BatchItem; thumbnailUrls: string[] }> {
|
||||
// 1. 查询相关数据
|
||||
const batchWithGenerations = await this.db.query.generationBatches.findFirst({...});
|
||||
|
||||
// 2. 收集需要处理的数据
|
||||
const thumbnailUrls = [...];
|
||||
|
||||
// 3. 执行删除操作
|
||||
const [deletedBatch] = await this.db.delete(generationBatches)...;
|
||||
|
||||
return { deletedBatch, thumbnailUrls };
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 上层使用简洁
|
||||
const { thumbnailUrls } = await model.delete(id);
|
||||
await fileService.deleteFiles(thumbnailUrls);
|
||||
```
|
||||
|
||||
### 文件操作与数据库操作的执行顺序
|
||||
|
||||
**删除操作原则:数据库删除在前,文件删除在后**
|
||||
|
||||
当业务逻辑同时涉及数据库记录和文件系统操作时,应该遵循"数据库优先"的原则。
|
||||
|
||||
**原因:**
|
||||
|
||||
- **用户体验优先**: 如果先删除文件再删除数据库记录,可能出现文件已删除但数据库记录仍存在的情况,用户访问时会遇到文件不存在的错误
|
||||
- **影响程度较小**: 如果先删除数据库记录再删除文件,即使文件删除失败,用户也看不到这个记录,只是造成一些存储空间浪费,对用户体验影响更小
|
||||
- **数据一致性**: 数据库记录是业务逻辑的核心,应该优先保证其一致性
|
||||
|
||||
**示例:**
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:先删除数据库记录,再删除文件
|
||||
async deleteGeneration(id: string) {
|
||||
// 1. 先删除数据库记录
|
||||
const deletedGeneration = await generationModel.delete(id);
|
||||
|
||||
// 2. 再删除相关文件
|
||||
if (deletedGeneration.asset?.thumbnailUrl) {
|
||||
await fileService.deleteFile(deletedGeneration.asset.thumbnailUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不推荐:先删除文件
|
||||
async deleteGeneration(id: string) {
|
||||
const generation = await generationModel.findById(id);
|
||||
|
||||
// 如果这里删除成功,但后面数据库删除失败,用户会遇到访问错误
|
||||
await fileService.deleteFile(generation.asset.thumbnailUrl);
|
||||
await generationModel.delete(id); // 可能失败
|
||||
}
|
||||
```
|
||||
|
||||
**创建操作原则:数据库创建在前,文件操作在后**
|
||||
|
||||
创建操作同样应该优先处理数据库记录,确保数据的一致性和完整性。
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
description: How to code review
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Role Description
|
||||
|
||||
- You are a senior full-stack engineer skilled in performance optimization, security, and design systems.
|
||||
- You excel at reviewing code and providing constructive feedback.
|
||||
- Your task is to review submitted Git diffs **in Chinese** and return a structured review report.
|
||||
- Review style: concise, direct, focused on what matters most, with actionable suggestions.
|
||||
|
||||
## Before the Review
|
||||
|
||||
Gather the modified code and context. Please strictly follow the process below:
|
||||
|
||||
1. Use `read_file` to read [package.json](mdc:package.json)
|
||||
2. Use terminal to run command `git diff HEAD | cat` to obtain the diff and list the changed files. If you recieived empty result, run the same command once more.
|
||||
3. Use `read_file` to open each changed file.
|
||||
4. Use `read_file` to read [rules-attach.mdc](mdc:.cursor/rules/rules-attach.mdc). Even if you think it's unnecessary, you must read it.
|
||||
5. combine changed files, step3 and `agent_requestable_workspace_rules`, list the rules which need to read
|
||||
6. Use `read_file` to read the rules list in step 5
|
||||
|
||||
## Review
|
||||
|
||||
### Code Style
|
||||
|
||||
read [typescript.mdc](mdc:.cursor/rules/typescript.mdc) for the consolidated project code style and optimization rules.
|
||||
|
||||
### Code Optimization
|
||||
|
||||
The optimization checklist has been consolidated into [typescript.mdc](mdc:.cursor/rules/typescript.mdc): loops, debouncing/throttling, design system components, theming tokens, concurrency with `Promise.*`, minimal DB column selection, and package reuse.
|
||||
|
||||
### Obvious Bugs
|
||||
|
||||
- Do not silently swallow errors in `catch` blocks; at minimum, log them.
|
||||
- Revert temporary code used only for testing (e.g., debug logs, temporary configs).
|
||||
- Remove empty handlers (e.g., an empty `onClick`).
|
||||
- Confirm the UI degrades gracefully for unauthenticated users.
|
||||
- Don't leave any debug logs in the code (except when using the `debug` module properly).
|
||||
- When using the `debug` module, avoid `import { log } from 'debug'` as it logs directly to console. Use proper debug namespaces instead.
|
||||
- Check logs for sensitive information like api key, etc
|
||||
|
||||
## After the Review: output
|
||||
|
||||
1. Summary
|
||||
- Start with a brief explanation of what the change set does.
|
||||
- Summarize the changes for each modified file (or logical group).
|
||||
2. Comments Issues
|
||||
- List the most critical issues first.
|
||||
- Use an ordered list, which will be convenient for me to reference later.
|
||||
- For each issue:
|
||||
- Mark severity tag (`❌ Must fix`, `⚠️ Should fix`, `💅 Nitpick`)
|
||||
- Provode file path to the relevant file.
|
||||
- Provide recommended fix
|
||||
- End with a **git commit** command, instruct the author to run it.
|
||||
- We use gitmoji to label commit messages, format: [emoji] <type>(<scope>): <subject>
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Guide to Optimize Output(Response) Rendering
|
||||
|
||||
## File Path and Code Symbol Rendering
|
||||
|
||||
- When rendering file paths, use backtick wrapping instead of markdown links so they can be parsed as clickable links in Cursor IDE.
|
||||
- Good: `src/components/Button.tsx`
|
||||
- Bad: [src/components/Button.tsx](src/components/Button.tsx)
|
||||
|
||||
- Don't use line and column number in file path, this will make file path not clickable in Cursor IDE.
|
||||
- Good: `src/components/Button.tsx` `10:20` (add a space between the file path and the line and column number)
|
||||
- Bad: `src/components/Button.tsx:10:20`
|
||||
|
||||
- When rendering functions, variables, or other code symbols, use backtick wrapping so they can be parsed as navigable links in Cursor IDE
|
||||
- Good: The `useState` hook in `MyComponent`
|
||||
- Bad: The useState hook in MyComponent
|
||||
|
||||
## Markdown Render
|
||||
|
||||
- don't use br tag to wrap in table cell
|
||||
|
||||
## Terminal Command Output
|
||||
|
||||
- If terminal commands don't produce output, it's likely due to paging issues. Try piping the command to `cat` to ensure full output is displayed.
|
||||
- Good: `git show commit_hash -- file.txt | cat`
|
||||
- Good: `git log --oneline | cat`
|
||||
- Reason: Some git commands use pagers by default, which may prevent output from being captured properly
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
globs: packages/database/migrations/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Database Migrations Guide
|
||||
|
||||
## Defensive Programming - Use Idempotent Clauses
|
||||
|
||||
Always use defensive clauses to make migrations idempotent:
|
||||
|
||||
```sql
|
||||
-- ✅ Good: Idempotent operations
|
||||
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "avatar" text;
|
||||
DROP TABLE IF EXISTS "old_table";
|
||||
CREATE INDEX IF NOT EXISTS "users_email_idx" ON "users" ("email");
|
||||
ALTER TABLE "posts" DROP COLUMN IF EXISTS "deprecated_field";
|
||||
|
||||
-- ❌ Bad: Non-idempotent operations
|
||||
ALTER TABLE "users" ADD COLUMN "avatar" text;
|
||||
DROP TABLE "old_table";
|
||||
CREATE INDEX "users_email_idx" ON "users" ("email");
|
||||
```
|
||||
|
||||
**Important**: After modifying migration SQL (e.g., adding `IF NOT EXISTS` clauses), run `bun run db:generate-client` to update the hash in `packages/database/src/core/migrations.json`.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs: src/database/models/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
1. first read [lobe-chat-backend-architecture.mdc](mdc:.cursor/rules/lobe-chat-backend-architecture.mdc)
|
||||
2. refer to the [_template.ts](mdc:src/database/models/_template.ts) to create new model
|
||||
3. if an operation involves multiple models or complex queries, consider defining it in the `repositories` layer under `src/database/repositories/`
|
||||
@@ -4,41 +4,33 @@ alwaysApply: true
|
||||
|
||||
## Project Description
|
||||
|
||||
You are developing an open-source, modern-design AI chat framework: lobe chat.
|
||||
You are developing an open-source, modern-design AI chat framework: lobehub(previous lobe-chat).
|
||||
|
||||
Emoji logo: 🤯
|
||||
Supported platforms:
|
||||
|
||||
- web desktop/mobile
|
||||
- desktop(electron)
|
||||
- mobile app(react native), coming soon
|
||||
|
||||
logo emoji: 🤯
|
||||
|
||||
## Project Technologies Stack
|
||||
|
||||
read [package.json](mdc:package.json) to know all npm packages you can use.
|
||||
|
||||
The project uses the following technologies:
|
||||
|
||||
- pnpm as package manager
|
||||
- Next.js 15 for frontend and backend, using app router instead of pages router
|
||||
- react 19, using hooks, functional components, react server components
|
||||
- TypeScript programming language
|
||||
- antd, `@lobehub/ui` for component framework
|
||||
- Next.js 15
|
||||
- react 19
|
||||
- TypeScript
|
||||
- `@lobehub/ui`, antd for component framework
|
||||
- antd-style for css-in-js framework
|
||||
- react-layout-kit for flex layout
|
||||
- react-i18next for i18n
|
||||
- lucide-react, `@ant-design/icons` for icons
|
||||
- `@lobehub/icons` for AI provider/model logo icon
|
||||
- `@formkit/auto-animate` for react list animation
|
||||
- zustand for global state management
|
||||
- nuqs for type-safe search params state manager
|
||||
- SWR for react data fetch
|
||||
- react-layout-kit for flex layout component
|
||||
- react-i18next for i18n
|
||||
- zustand for state management
|
||||
- nuqs for search params management
|
||||
- SWR for data fetch
|
||||
- aHooks for react hooks library
|
||||
- dayjs for date and time library
|
||||
- dayjs for time library
|
||||
- lodash-es for utility library
|
||||
- fast-deep-equal for deep comparison of JavaScript objects
|
||||
- zod for data validation
|
||||
- TRPC for type safe backend
|
||||
- PGLite for client DB and PostgreSQL for backend DB
|
||||
- PGLite for client DB and Neon PostgreSQL for backend DB
|
||||
- Drizzle ORM
|
||||
- Vitest for testing, testing-library for react component test
|
||||
- Prettier for code formatting
|
||||
- ESLint for code linting
|
||||
- Cursor AI for code editing and AI coding assistance
|
||||
|
||||
Note: All tools and libraries used are the latest versions. The application only needs to be compatible with the latest browsers;
|
||||
- Vitest for testing
|
||||
|
||||
+104
-221
@@ -5,235 +5,118 @@ alwaysApply: false
|
||||
|
||||
# LobeChat Project Structure
|
||||
|
||||
## Directory Structure
|
||||
## Complete Project Structure
|
||||
|
||||
note: some files are not shown for simplicity.
|
||||
This project uses common monorepo structure. The workspace packages name use `@lobechat/` namespace.
|
||||
|
||||
**note**: some not very important files are not shown for simplicity.
|
||||
|
||||
```plaintext
|
||||
lobe-chat/
|
||||
├── apps/ # Applications directory
|
||||
│ └── desktop/ # Electron desktop application
|
||||
│ ├── src/ # Desktop app source code
|
||||
│ └── resources/ # Desktop app resources
|
||||
├── docs/ # Project documentation
|
||||
│ ├── development/ # Development docs
|
||||
│ ├── self-hosting/ # Self-hosting docs
|
||||
│ └── usage/ # Usage guides
|
||||
├── locales/ # Internationalization files (multiple locales)
|
||||
│ ├── en-US/ # English (example)
|
||||
│ └── zh-CN/ # Simplified Chinese (example)
|
||||
├── packages/ # Monorepo packages directory
|
||||
│ ├── const/ # Constants definition package
|
||||
│ ├── database/ # Database related package
|
||||
│ ├── electron-client-ipc/ # Electron renderer ↔ main IPC client
|
||||
│ ├── electron-server-ipc/ # Electron main process IPC server
|
||||
│ ├── model-bank/ # Built-in model presets/catalog exports
|
||||
│ ├── model-runtime/ # AI model runtime package
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ ├── utils/ # Utility functions package
|
||||
│ ├── file-loaders/ # File processing packages
|
||||
│ ├── prompts/ # AI prompt management
|
||||
│ └── web-crawler/ # Web crawling functionality
|
||||
├── public/ # Static assets
|
||||
│ ├── icons/ # Application icons
|
||||
│ ├── images/ # Image resources
|
||||
│ └── screenshots/ # Application screenshots
|
||||
├── scripts/ # Build and tool scripts
|
||||
├── src/ # Main application source code (see below)
|
||||
├── .cursor/ # Cursor AI configuration
|
||||
├── docker-compose/ # Docker configuration
|
||||
├── package.json # Project dependencies
|
||||
├── pnpm-workspace.yaml # pnpm monorepo configuration
|
||||
├── next.config.ts # Next.js configuration
|
||||
├── drizzle.config.ts # Drizzle ORM configuration
|
||||
└── tsconfig.json # TypeScript configuration
|
||||
```
|
||||
|
||||
## Core Source Directory (`src/`)
|
||||
|
||||
```plaintext
|
||||
src/
|
||||
├── app/ # Next.js App Router routes
|
||||
│ ├── (backend)/ # Backend API routes
|
||||
│ │ ├── api/ # REST API endpoints
|
||||
│ │ │ ├── auth/ # Authentication endpoints
|
||||
│ │ │ └── webhooks/ # Webhook handlers for various auth providers
|
||||
│ │ ├── middleware/ # Request middleware
|
||||
│ │ ├── oidc/ # OpenID Connect endpoints
|
||||
│ │ ├── trpc/ # tRPC API routes
|
||||
│ │ │ ├── async/ # Async tRPC endpoints
|
||||
│ │ │ ├── desktop/ # Desktop runtime endpoints
|
||||
│ │ │ ├── edge/ # Edge runtime endpoints
|
||||
│ │ │ ├── lambda/ # Lambda runtime endpoints
|
||||
│ │ │ └── tools/ # Tools-specific endpoints
|
||||
│ │ └── webapi/ # Web API endpoints
|
||||
│ │ ├── chat/ # Chat-related APIs for various providers
|
||||
│ │ ├── models/ # Model management APIs
|
||||
│ │ ├── plugin/ # Plugin system APIs
|
||||
│ │ ├── stt/ # Speech-to-text APIs
|
||||
│ │ ├── text-to-image/ # Image generation APIs
|
||||
│ │ └── tts/ # Text-to-speech APIs
|
||||
│ ├── [variants]/ # Page route variants
|
||||
│ │ ├── (main)/ # Main application routes
|
||||
│ │ │ ├── chat/ # Chat interface and workspace
|
||||
│ │ │ ├── discover/ # Discover page (assistants, models, providers)
|
||||
│ │ │ ├── files/ # File management interface
|
||||
│ │ │ ├── image/ # Image generation interface
|
||||
│ │ │ ├── profile/ # User profile and stats
|
||||
│ │ │ ├── repos/ # Knowledge base repositories
|
||||
│ │ │ └── settings/ # Application settings
|
||||
│ │ └── @modal/ # Modal routes
|
||||
│ └── manifest.ts # PWA configuration
|
||||
├── components/ # Global shared components
|
||||
│ ├── Analytics/ # Analytics tracking components
|
||||
│ ├── Error/ # Error handling components
|
||||
│ └── Loading/ # Loading state components
|
||||
├── config/ # Application configuration
|
||||
│ ├── featureFlags/ # Feature flags & experiments
|
||||
│ └── modelProviders/ # Model provider configurations
|
||||
├── features/ # Feature components (UI Layer)
|
||||
│ ├── AgentSetting/ # Agent configuration and management
|
||||
│ ├── ChatInput/ # Chat input with file upload and tools
|
||||
│ ├── Conversation/ # Message display and interaction
|
||||
│ ├── FileManager/ # File upload and knowledge base
|
||||
│ └── PluginStore/ # Plugin marketplace and management
|
||||
├── hooks/ # Custom React hooks
|
||||
├── layout/ # Global layout components
|
||||
│ ├── AuthProvider/ # Authentication provider
|
||||
│ └── GlobalProvider/ # Global state provider
|
||||
├── libs/ # External library integrations
|
||||
│ ├── analytics/ # Analytics services integration
|
||||
│ ├── next-auth/ # NextAuth.js configuration
|
||||
│ └── oidc-provider/ # OIDC provider implementation
|
||||
├── locales/ # Internationalization resources
|
||||
│ └── default/ # Default language definitions
|
||||
├── migrations/ # Client-side data migrations
|
||||
├── server/ # Server-side code
|
||||
│ ├── modules/ # Server modules
|
||||
│ ├── routers/ # tRPC routers
|
||||
│ └── services/ # Server services
|
||||
├── services/ # Service layer (per-domain, client/server split)
|
||||
│ ├── user/ # User services
|
||||
│ │ ├── client.ts # Client DB (PGLite) implementation
|
||||
│ │ └── server.ts # Server DB implementation (via tRPC)
|
||||
│ ├── aiModel/ # AI model services
|
||||
│ ├── session/ # Session services
|
||||
│ └── message/ # Message services
|
||||
├── store/ # Zustand state management
|
||||
│ ├── agent/ # Agent state
|
||||
│ ├── chat/ # Chat state
|
||||
│ └── user/ # User state
|
||||
├── styles/ # Global styles
|
||||
├── tools/ # Built-in tool system
|
||||
│ ├── artifacts/ # Code artifacts and preview
|
||||
│ └── web-browsing/ # Web search and browsing
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Utility functions
|
||||
├── client/ # Client-side utilities
|
||||
└── server/ # Server-side utilities
|
||||
```
|
||||
|
||||
## Key Monorepo Packages
|
||||
|
||||
```plaintext
|
||||
packages/
|
||||
├── const/ # Global constants and configurations
|
||||
├── database/ # Database schemas and models
|
||||
│ ├── src/models/ # Data models (CRUD operations)
|
||||
│ ├── src/schemas/ # Drizzle database schemas
|
||||
│ ├── src/repositories/ # Complex query layer
|
||||
│ └── migrations/ # Database migration files
|
||||
├── model-runtime/ # AI model runtime
|
||||
│ └── src/
|
||||
│ ├── openai/ # OpenAI provider integration
|
||||
│ ├── anthropic/ # Anthropic provider integration
|
||||
│ ├── google/ # Google AI provider integration
|
||||
│ ├── ollama/ # Ollama local model integration
|
||||
│ ├── types/ # Runtime type definitions
|
||||
│ └── utils/ # Runtime utilities
|
||||
├── types/ # Shared TypeScript type definitions
|
||||
│ └── src/
|
||||
│ ├── agent/ # Agent-related types
|
||||
│ ├── message/ # Message and chat types
|
||||
│ ├── user/ # User and session types
|
||||
│ └── tool/ # Tool and plugin types
|
||||
├── utils/ # Shared utility functions
|
||||
│ └── src/
|
||||
│ ├── client/ # Client-side utilities
|
||||
│ ├── server/ # Server-side utilities
|
||||
│ ├── fetch/ # HTTP request utilities
|
||||
│ └── tokenizer/ # Token counting utilities
|
||||
├── file-loaders/ # File loaders (PDF, DOCX, etc.)
|
||||
├── prompts/ # AI prompt management
|
||||
└── web-crawler/ # Web crawling functionality
|
||||
├── apps/
|
||||
│ └── desktop/
|
||||
├── docs/
|
||||
├── locales/
|
||||
│ ├── en-US/
|
||||
│ └── zh-CN/
|
||||
├── packages/
|
||||
│ ├── const/
|
||||
│ ├── context-engine/
|
||||
│ ├── database/
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── models/
|
||||
│ │ │ ├── schemas/
|
||||
│ │ │ └── repositories/
|
||||
│ ├── model-bank/
|
||||
│ │ └── src/
|
||||
│ │ └── aiModels/
|
||||
│ ├── model-runtime/
|
||||
│ │ └── src/
|
||||
│ │ ├── core/
|
||||
│ │ └── providers/
|
||||
│ ├── types/
|
||||
│ │ └── src/
|
||||
│ │ ├── message/
|
||||
│ │ └── user/
|
||||
│ └── utils/
|
||||
├── public/
|
||||
├── scripts/
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── (backend)/
|
||||
│ │ │ ├── api/
|
||||
│ │ │ │ ├── auth/
|
||||
│ │ │ │ └── webhooks/
|
||||
│ │ │ ├── middleware/
|
||||
│ │ │ ├── oidc/
|
||||
│ │ │ ├── trpc/
|
||||
│ │ │ └── webapi/
|
||||
│ │ │ ├── chat/
|
||||
│ │ │ └── tts/
|
||||
│ │ ├── [variants]/
|
||||
│ │ │ ├── (main)/
|
||||
│ │ │ │ ├── chat/
|
||||
│ │ │ │ └── settings/
|
||||
│ │ │ └── @modal/
|
||||
│ │ └── manifest.ts
|
||||
│ ├── components/
|
||||
│ ├── config/
|
||||
│ ├── features/
|
||||
│ │ └── ChatInput/
|
||||
│ ├── hooks/
|
||||
│ ├── layout/
|
||||
│ │ ├── AuthProvider/
|
||||
│ │ └── GlobalProvider/
|
||||
│ ├── libs/
|
||||
│ │ └── oidc-provider/
|
||||
│ ├── locales/
|
||||
│ │ └── default/
|
||||
│ ├── server/
|
||||
│ │ ├── modules/
|
||||
│ │ ├── routers/
|
||||
│ │ │ ├── async/
|
||||
│ │ │ ├── desktop/
|
||||
│ │ │ ├── edge/
|
||||
│ │ │ └── lambda/
|
||||
│ │ └── services/
|
||||
│ ├── services/
|
||||
│ │ ├── user/
|
||||
│ │ │ ├── client.ts
|
||||
│ │ │ └── server.ts
|
||||
│ │ └── message/
|
||||
│ ├── store/
|
||||
│ │ ├── agent/
|
||||
│ │ ├── chat/
|
||||
│ │ └── user/
|
||||
│ ├── styles/
|
||||
│ └── utils/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Architecture Map
|
||||
|
||||
- Presentation: `src/features`, `src/components`, `src/layout` — UI composition, global providers
|
||||
- State: `src/store` — Zustand slices, selectors, middleware
|
||||
- Client Services: `src/services/<domain>/{client|server}.ts` — client: PGLite; server: tRPC bridge
|
||||
- API Routers: `src/app/(backend)/webapi` (REST), `src/app/(backend)/trpc/{edge|lambda|async|desktop|tools}`; Lambda router triggers Async router for long-running tasks (e.g., image)
|
||||
- Server Services: `src/server/services` (business logic), `src/server/modules` (infra adapters)
|
||||
- Data Access: `packages/database/src/{schemas,models,repositories}` — Schema (Drizzle), Model (CRUD), Repository (complex queries)
|
||||
- Integrations: `src/libs` — analytics, auth, trpc, logging, runtime helpers
|
||||
- UI Components: `src/components`, `src/features`
|
||||
- Global providers: `src/layout`
|
||||
- Zustand stores: `src/store`
|
||||
- Client Services: `src/services/` cross-platform services
|
||||
- clientDB: `src/services/<domain>/client.ts`
|
||||
- serverDB: `src/services/<domain>/server.ts`
|
||||
- API Routers:
|
||||
- `src/app/(backend)/webapi` (REST)
|
||||
- `src/server/routers/{edge|lambda|async|desktop|tools}` (tRPC)
|
||||
- Server:
|
||||
- Services(can access serverDB): `src/server/services` server-used-only services
|
||||
- Modules(can't access db): `src/server/modules` (Server only Third-party Service Module)
|
||||
- Database:
|
||||
- Schema (Drizzle): `packages/database/src/schemas`
|
||||
- Model (CRUD): `packages/database/src/models`
|
||||
- Repository (bff-queries): `packages/database/src/repositories`
|
||||
- Third-party Integrations: `src/libs` — analytics, oidc etc.
|
||||
|
||||
## Data Flow Architecture
|
||||
|
||||
### Unified Flow Pattern
|
||||
|
||||
```
|
||||
UI Layer → State Management → Client Service → [Environment Branch] → Database
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
React Zustand Environment Local/Remote PGLite/
|
||||
Components Store Adaptation Routing PostgreSQL
|
||||
```
|
||||
|
||||
### Environment-Specific Routing
|
||||
|
||||
| Mode | UI | Service Route | Database |
|
||||
| --------------- | -------- | ---------------------- | ------------------- |
|
||||
| **Browser/PWA** | React | Direct Model Access | PGLite (Local) |
|
||||
| **Server** | React | tRPC → Server Services | PostgreSQL (Remote) |
|
||||
| **Desktop** | Electron | tRPC → Local Node.js | PGLite/PostgreSQL\* |
|
||||
|
||||
_\*Depends on cloud sync configuration_
|
||||
|
||||
### Key Characteristics
|
||||
|
||||
- **Type Safety**: End-to-end type safety via tRPC and Drizzle ORM
|
||||
- **Local/Remote Dual Mode**: PGLite enables user data ownership and local control
|
||||
|
||||
## Quick Map
|
||||
|
||||
- App Routes: `src/app` — UI routes (App Router) and backend routes under `(backend)`
|
||||
- Web API: `src/app/(backend)/webapi` — REST-like endpoints
|
||||
- tRPC Routers: `src/server/routers` — typed RPC endpoints by runtime
|
||||
- Client Services: `src/services` — environment-adaptive client-side business logic
|
||||
- Server Services: `src/server/services` — platform-agnostic business logic
|
||||
- Database: `packages/database` — schemas/models/repositories/migrations
|
||||
- State: `src/store` — Zustand stores and slices
|
||||
- Integrations: `src/libs` — analytics/auth/trpc/logging/runtime helpers
|
||||
- Tools: `src/tools` — built-in tool system
|
||||
|
||||
## Common Tasks
|
||||
|
||||
- Add Web API route: `src/app/(backend)/webapi/<module>/route.ts`
|
||||
- Add tRPC endpoint: `src/server/routers/{edge|lambda|desktop}/...`
|
||||
- Add client/server service: `src/services/<domain>/{client|server}.ts` (client: PGLite; server: tRPC)
|
||||
- Add server service: `src/server/services/<domain>`
|
||||
- Add a new model/provider: `src/config/modelProviders/<provider>.ts` + `packages/model-bank/src/aiModels/<provider>.ts` + `packages/model-runtime/src/<provider>/index.ts`
|
||||
- Add DB schema/model/repository: `packages/database/src/{schemas|models|repositories}`
|
||||
- Add Zustand slice: `src/store/<domain>/slices`
|
||||
|
||||
## Env Modes
|
||||
|
||||
- `NEXT_PUBLIC_CLIENT_DB`: selects client DB mode (e.g., `pglite`) vs server-backed
|
||||
- `NEXT_PUBLIC_IS_DESKTOP_APP`: enables desktop-specific routes and behavior
|
||||
- `NEXT_PUBLIC_SERVICE_MODE`: controls service routing preference (client/server)
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Keep client logic in `src/services`; server-only logic stays in `src/server/services`
|
||||
- Don’t mix Web API (`webapi/`) with tRPC (`src/server/routers/`)
|
||||
- Place business UI under `src/features`, global reusable UI under `src/components`
|
||||
- **Web with ClientDB**: React UI → Client Service → Direct Model Access → PGLite (Web WASM)
|
||||
- **Web with ServerDB**: React UI → Client Service → tRPC Lambda → Server Services → PostgreSQL (Remote)
|
||||
- **Desktop**:
|
||||
- Cloud sync disabled: Electron UI → Client Service → tRPC Lambda → Local Server Services → PGLite (Node WASM)
|
||||
- Cloud sync enabled: Electron UI → Client Service → tRPC Lambda → Cloud Server Services → PostgreSQL (Remote)
|
||||
|
||||
@@ -4,20 +4,12 @@ globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 📋 Available Rules Index
|
||||
# Available project rules index
|
||||
|
||||
The following rules are available via `read_file` from the `.cursor/rules/` directory:
|
||||
|
||||
## General
|
||||
|
||||
- `project-introduce.mdc` – Project description and tech stack
|
||||
- `cursor-rules.mdc` – Cursor rules authoring and optimization guide
|
||||
- `code-review.mdc` – How to code review
|
||||
All following rules are saved under `.cursor/rules/` directory:
|
||||
|
||||
## Backend
|
||||
|
||||
- `backend-architecture.mdc` – Backend layer architecture and design guidelines
|
||||
- `define-database-model.mdc` – Database model definition guidelines
|
||||
- `drizzle-schema-style-guide.mdc` – Style guide for defining Drizzle ORM schemas
|
||||
|
||||
## Frontend
|
||||
@@ -42,7 +34,6 @@ The following rules are available via `read_file` from the `.cursor/rules/` dire
|
||||
|
||||
## Debugging
|
||||
|
||||
- `debug.mdc` – General debugging guide
|
||||
- `debug-usage.mdc` – Using the debug package and namespace conventions
|
||||
|
||||
## Testing
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
## System Role
|
||||
|
||||
You are an expert in full-stack Web development, proficient in JavaScript, TypeScript, CSS, React, Node.js, Next.js, Postgresql, Redis, S3, all kinds of network protocols.
|
||||
|
||||
You are an LLM expert, you are familiar with all kinds of LLM models, ai agents, ai workflow, prompt engineering and context engineering.
|
||||
|
||||
You are an expert in Ai art. In Ai image generation, you are proficient in Stable Diffusion and ComfyUI's architectural principles, workflows, model structures, parameter configurations, training methods, and inference optimization.
|
||||
|
||||
You are an expert in UI/UX design, proficient in web interaction patterns, responsive design, accessibility, and user behavior optimization. You excel at improving user retention and paid conversion rates through various interaction details.
|
||||
|
||||
## Problem Solving
|
||||
|
||||
- When modifying existing code, clearly describe the differences and reasons for the changes
|
||||
- Provide alternative solutions that may be better overall or superior in specific aspects
|
||||
- Provide optimization suggestions for deprecated API usage
|
||||
- Cite sources whenever possible at the end, not inline
|
||||
- When you provide multiple solutions, provide the recommended solution first, and note it as `Recommended`
|
||||
- Express uncertainty when there might not be a correct answer, instead of take action by guessing and assuming
|
||||
|
||||
## Code Implementation
|
||||
|
||||
- Focus on maintainable over being performant
|
||||
- Be sure to reference file path
|
||||
- If doc links or required files are missing, ask for them before proceeding with the task rather than making assumptions
|
||||
- If you're unable to get valid result when using tools, please clearly state in the output
|
||||
@@ -0,0 +1,579 @@
|
||||
---
|
||||
description: Best practices for testing Zustand store actions
|
||||
globs: "src/store/**/*.test.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Zustand Store Action Testing Guide
|
||||
|
||||
This guide provides best practices for testing Zustand store actions, based on our proven testing patterns.
|
||||
|
||||
## Basic Test Structure
|
||||
|
||||
```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';
|
||||
|
||||
// Keep zustand mock as it's needed globally
|
||||
vi.mock('zustand/traditional');
|
||||
|
||||
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
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
refreshMessages: vi.fn(),
|
||||
internal_coreProcessMessage: vi.fn(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('action name', () => {
|
||||
describe('validation', () => {
|
||||
// Validation tests
|
||||
});
|
||||
|
||||
describe('normal flow', () => {
|
||||
// Happy path tests
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
// Error case tests
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### 1. Test Layering - Spy Direct Dependencies Only
|
||||
|
||||
✅ **Good**: Spy on the direct dependency
|
||||
|
||||
```typescript
|
||||
// When testing internal_coreProcessMessage, spy its direct dependency
|
||||
const fetchAIChatSpy = vi
|
||||
.spyOn(result.current, 'internal_fetchAIChatMessage')
|
||||
.mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
|
||||
```
|
||||
|
||||
❌ **Bad**: Spy on lower-level implementation details
|
||||
|
||||
```typescript
|
||||
// Don't spy on services that internal_fetchAIChatMessage uses
|
||||
const streamSpy = vi
|
||||
.spyOn(chatService, 'createAssistantMessageStream')
|
||||
.mockImplementation(...);
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
// ✅ Only spy services that most tests need
|
||||
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
|
||||
// ✅ Don't spy chatService globally
|
||||
});
|
||||
|
||||
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
|
||||
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
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Service Mocking - Mock the Correct Layer
|
||||
|
||||
✅ **Good**: Mock the service method
|
||||
|
||||
```typescript
|
||||
it('should fetch AI chat response', async () => {
|
||||
const streamSpy = vi
|
||||
.spyOn(chatService, 'createAssistantMessageStream')
|
||||
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
|
||||
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
|
||||
await onFinish?.('Hello', {});
|
||||
});
|
||||
|
||||
// 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
|
||||
beforeEach(() => {
|
||||
vi.mock('@/services/chat');
|
||||
vi.mock('@/services/message');
|
||||
vi.mock('@/services/file');
|
||||
vi.mock('@/services/agent');
|
||||
// ... too many global mocks
|
||||
});
|
||||
```
|
||||
|
||||
## Testing SWR Hooks in Zustand Stores
|
||||
|
||||
Some Zustand store slices use SWR hooks for data fetching. These require a different testing approach.
|
||||
|
||||
### Basic SWR Hook Test Structure
|
||||
|
||||
```typescript
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
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)
|
||||
@@ -10,61 +10,11 @@ alwaysApply: false
|
||||
|
||||
- avoid explicit type annotations when TypeScript can infer types.
|
||||
- avoid implicitly `any` variables; explicitly type when necessary (e.g., `let a: number` instead of `let a`).
|
||||
- use the most accurate type possible (e.g., prefer `Record<PropertyKey, unknown>` over `object`).
|
||||
- use the most accurate type possible (e.g., prefer `Record<PropertyKey, unknown>` over `object` and `any`).
|
||||
- prefer `interface` over `type` for object shapes (e.g., React component props). Keep `type` for unions, intersections, and utility types.
|
||||
- prefer `as const satisfies XyzInterface` over plain `as const` when suitable.
|
||||
- prefer `@ts-expect-error` over `@ts-ignore`
|
||||
- prefer `Record<string, any>` over `any`
|
||||
|
||||
- **Avoid unnecessary null checks**: Before adding `xxx !== null`, `?.`, `??`, or `!.`, read the type definition to confirm the necessary. **Example:**
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: budget.spend and budget.maxBudget is number, not number | null
|
||||
if (budget.spend !== null && budget.maxBudget !== null && budget.spend >= budget.maxBudget) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ Right
|
||||
if (budget.spend >= budget.maxBudget) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
- **Avoid redundant runtime checks**: Don't add runtime validation for conditions already guaranteed by types or previous checks. Trust the type system and calling contract. **Example:**
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Adding impossible-to-fail checks
|
||||
const due = await db.query.budgets.findMany({
|
||||
where: and(isNotNull(budgets.budgetDuration)), // Already filtered non-null
|
||||
});
|
||||
const result = due.map(b => {
|
||||
const nextReset = computeNextResetAt(b.budgetResetAt!, b.budgetDuration!);
|
||||
if (!nextReset) { // This check is impossible to fail
|
||||
throw new Error(`Unexpected null nextResetAt`);
|
||||
}
|
||||
return nextReset;
|
||||
});
|
||||
|
||||
// ✅ Right: Trust the contract
|
||||
const due = await db.query.budgets.findMany({
|
||||
where: and(isNotNull(budgets.budgetDuration)),
|
||||
});
|
||||
const result = due.map(b => computeNextResetAt(b.budgetResetAt!, b.budgetDuration!));
|
||||
```
|
||||
|
||||
- **Avoid meaningless null/undefined parameters**: Don't accept null/undefined for parameters that have no business meaning when null. Design strict function contracts. **Example:**
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Function accepts meaningless null input
|
||||
function computeNextResetAt(currentResetAt: Date, durationStr: string | null): Date | null {
|
||||
if (!durationStr) return null; // Why accept null if it just returns null?
|
||||
}
|
||||
|
||||
// ✅ Right: Strict contract, clear responsibility
|
||||
function computeNextResetAt(currentResetAt: Date, durationStr: string): Date {
|
||||
// Function has single clear purpose, caller ensures valid input
|
||||
}
|
||||
```
|
||||
- prefer `@ts-expect-error` over `@ts-ignore` over `as any`
|
||||
- Avoid meaningless null/undefined parameters; design strict function contracts.
|
||||
|
||||
## Imports and Modules
|
||||
|
||||
@@ -79,16 +29,11 @@ alwaysApply: false
|
||||
|
||||
## Code Structure and Readability
|
||||
|
||||
- Refactor repeated logic into reusable functions.
|
||||
- Prefer object destructuring when accessing and using properties.
|
||||
- Use consistent, descriptive naming; avoid obscure abbreviations.
|
||||
- Use semantically meaningful variable, function, and class names.
|
||||
- Replace magic numbers or strings with well-named constants.
|
||||
- Keep meaningful code comments; do not remove them when applying edits. Update comments when behavior changes.
|
||||
- Ensure JSDoc comments accurately reflect the implementation.
|
||||
- Look for opportunities to simplify or modernize code with the latest JavaScript/TypeScript features where it improves clarity.
|
||||
- Defer formatting to tooling; ignore purely formatting-only issues and autofixable lint problems.
|
||||
- Respect project Prettier settings.
|
||||
|
||||
## UI and Theming
|
||||
|
||||
@@ -100,15 +45,14 @@ alwaysApply: false
|
||||
## Performance
|
||||
|
||||
- Prefer `for…of` loops to index-based `for` loops when feasible.
|
||||
- Decide whether callbacks should be debounced or throttled based on UX and performance needs.
|
||||
- Reuse existing npm packages rather than reinventing the wheel (e.g., `lodash-es/omit`).
|
||||
- Reuse existing utils inside `packages/utils` or installed npm packages rather than reinventing the wheel.
|
||||
- Query only the required columns from a database rather than selecting entire rows.
|
||||
|
||||
## Time and Consistency
|
||||
|
||||
- Instead of calling `Date.now()` multiple times, assign it to a constant once and reuse it to ensure consistency and improve readability.
|
||||
|
||||
## Some logging rules
|
||||
## Logging
|
||||
|
||||
- Never log user private information like api key, etc
|
||||
- Don't use `import { log } from 'debug'` to log messages, because it will directly log the message to the console.
|
||||
|
||||
@@ -169,6 +169,13 @@ OPENAI_API_KEY=sk-xxxxxxxxx
|
||||
|
||||
# FAL_API_KEY=fal-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
########################################
|
||||
######### AI Image Settings ############
|
||||
########################################
|
||||
|
||||
# Default image generation count (range: 1-20, default: 4)
|
||||
# AI_IMAGE_DEFAULT_IMAGE_NUM=4
|
||||
|
||||
### Nebius ###
|
||||
|
||||
# NEBIUS_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
@@ -5,34 +5,17 @@ type: Bug
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '📦 Platform'
|
||||
label: '📱 Client Type'
|
||||
description: 'Select how you are accessing LobeChat'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Official Preview'
|
||||
- 'Official Cloud'
|
||||
- 'Vercel'
|
||||
- 'Zeabur'
|
||||
- 'Sealos'
|
||||
- 'Netlify'
|
||||
- 'Self hosting Docker'
|
||||
- 'Web (Desktop Browser)'
|
||||
- 'Web (Mobile Browser)'
|
||||
- 'Desktop App (Electron)'
|
||||
- 'Mobile App (React Native)'
|
||||
- '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:
|
||||
@@ -48,6 +31,39 @@ 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'
|
||||
@@ -60,21 +76,49 @@ 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
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
name: '🐛 反馈缺陷'
|
||||
description: '反馈一个问题缺陷'
|
||||
labels: ['unconfirm']
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
在创建新的 Issue 之前,请先[搜索已有问题](https://github.com/lobehub/lobe-chat/issues),如果发现已有类似的问题,请给它 **👍 点赞**,这样可以帮助我们更快地解决问题。
|
||||
如果你在使用过程中遇到问题,可以尝试以下方式获取帮助:
|
||||
- 在 [GitHub Discussions](https://github.com/lobehub/lobe-chat/discussions) 的版块发起讨论。
|
||||
- 在 [LobeChat 社区](https://discord.gg/AYFPHvv2jT) 提问,与其他用户交流。
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '📦 部署环境'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Official Preview'
|
||||
- 'Official Cloud'
|
||||
- 'Vercel'
|
||||
- 'Zeabur'
|
||||
- 'Sealos'
|
||||
- 'Netlify'
|
||||
- 'Docker'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '📦 部署模式'
|
||||
multiple: true
|
||||
options:
|
||||
- '客户端模式(lobe-chat 镜像)'
|
||||
- '客户端 Pglite 模式(lobe-chat-pglite 镜像)'
|
||||
- '服务端模式(lobe-chat-database 镜像)'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: '📌 软件版本'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '💻 系统环境'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Windows'
|
||||
- 'macOS'
|
||||
- 'Ubuntu'
|
||||
- 'Other Linux'
|
||||
- 'iOS'
|
||||
- 'Android'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '🌐 浏览器'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Chrome'
|
||||
- 'Edge'
|
||||
- 'Safari'
|
||||
- 'Firefox'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🐛 问题描述'
|
||||
description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📷 复现步骤'
|
||||
description: 请提供一个清晰且简洁的描述,说明如何复现问题。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🚦 期望结果'
|
||||
description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📝 补充信息'
|
||||
description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。
|
||||
@@ -1,21 +0,0 @@
|
||||
name: '🌠 功能需求'
|
||||
description: '提出需求或建议'
|
||||
title: '[Request] '
|
||||
type: Feature
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🥰 需求描述'
|
||||
description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🧐 解决方案'
|
||||
description: 请清晰且简洁地描述您想要的解决方案。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📝 补充信息'
|
||||
description: 在这里添加关于问题的任何其他背景信息。
|
||||
@@ -1,7 +1,7 @@
|
||||
contact_links:
|
||||
- name: Ask a question for self-hosting | 咨询自部署问题
|
||||
- name: Ask a question for self-hosting
|
||||
url: https://github.com/lobehub/lobe-chat/discussions/new?category=self-hosting-%E7%A7%81%E6%9C%89%E5%8C%96%E9%83%A8%E7%BD%B2
|
||||
about: Please post questions, and ideas in discussions. | 请在讨论区发布问题和想法。
|
||||
- name: Questions and ideas | 其他问题和想法
|
||||
about: Please post questions, and ideas in discussions.
|
||||
- name: Questions and ideas
|
||||
url: https://github.com/lobehub/lobe-chat/discussions/new/choose
|
||||
about: Please post questions, and ideas in discussions. | 请在讨论区发布问题和想法。
|
||||
about: Please post questions, and ideas in discussions.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#### 💻 变更类型 | Change Type
|
||||
#### 💻 Change Type
|
||||
|
||||
<!-- For change type, change [ ] to [x]. -->
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
- [ ] 📝 docs
|
||||
- [ ] 🔨 chore
|
||||
|
||||
#### 🔀 变更说明 | Description of Change
|
||||
#### 🔀 Description of Change
|
||||
|
||||
<!-- Thank you for your Pull Request. Please provide a description above. -->
|
||||
|
||||
#### 📝 补充信息 | Additional Information
|
||||
#### 📝 Additional Information
|
||||
|
||||
<!-- Add any other context about the Pull Request here. -->
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
#!/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 {};
|
||||
@@ -0,0 +1,78 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Lock closed issues after 7 days of inactivity
|
||||
* @param {object} github - GitHub API client
|
||||
* @param {object} context - GitHub Actions context
|
||||
*/
|
||||
module.exports = async ({ github, context }) => {
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
|
||||
const lockComment = `This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.`;
|
||||
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
let totalLocked = 0;
|
||||
|
||||
while (hasMore) {
|
||||
// Get closed issues (pagination)
|
||||
const { data: issues } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed',
|
||||
sort: 'updated',
|
||||
direction: 'asc',
|
||||
per_page: 100,
|
||||
page: page,
|
||||
});
|
||||
|
||||
if (issues.length === 0) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
// Skip if already locked
|
||||
if (issue.locked) continue;
|
||||
|
||||
// Skip pull requests
|
||||
if (issue.pull_request) continue;
|
||||
|
||||
// Check if updated more than 7 days ago
|
||||
const updatedAt = new Date(issue.updated_at);
|
||||
if (updatedAt > sevenDaysAgo) {
|
||||
// Since issues are sorted by updated_at ascending,
|
||||
// once we hit a recent issue, all remaining will be recent too
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
// Add comment before locking
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: lockComment,
|
||||
});
|
||||
|
||||
// Lock the issue
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
lock_reason: 'resolved',
|
||||
});
|
||||
|
||||
totalLocked++;
|
||||
console.log(`Locked issue #${issue.number}: ${issue.title}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to lock issue #${issue.number}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
console.log(`Total issues locked: ${totalLocked}`);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
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 }}'
|
||||
@@ -0,0 +1,65 @@
|
||||
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 }}
|
||||
@@ -0,0 +1,120 @@
|
||||
name: Claude Translator
|
||||
concurrency:
|
||||
group: translator-${{ github.event.comment.id || github.event.issue.number || github.event.review.id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
pull_request_review:
|
||||
types: [submitted, edited]
|
||||
pull_request_review_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
if: |
|
||||
(github.event_name == 'issues') ||
|
||||
(github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && github.event.sender.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# update issues/comments
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude for translation
|
||||
uses: anthropics/claude-code-action@main
|
||||
id: claude
|
||||
with:
|
||||
# Warning: Permissions should have been controlled by workflow permission.
|
||||
# Now `contents: read` is safe for files, but we could make a fine-grained token to control it.
|
||||
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md
|
||||
github_token: ${{ secrets.GH_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)"
|
||||
prompt: |
|
||||
You are a multilingual translation assistant. You need to respond to the following four types of GitHub Webhook events:
|
||||
|
||||
- issues
|
||||
- issue_comment
|
||||
- pull_request_review
|
||||
- pull_request_review_comment
|
||||
|
||||
Please complete the following tasks:
|
||||
|
||||
1. Retrieve complete information for the current event.
|
||||
|
||||
- If the current event is 'issues', get the issue information.
|
||||
- If the current event is 'issue_comment', get the comment information.
|
||||
- If the current event is 'pull_request_review', get the review information.
|
||||
- If the current event is 'pull_request_review_comment', get the comment information.
|
||||
|
||||
2. Intelligently detect content.
|
||||
|
||||
- If the retrieved information is already translated content following the format requirements, check if the translation matches the original content. If not, retranslate to match and follow the format requirements.
|
||||
- If the retrieved information is untranslated content, check its language. If not in English, translate to English.
|
||||
- If the retrieved information is partially translated to English, translate it completely to English.
|
||||
- If the retrieved information contains references to already translated content, clean the referenced content to contain only English. Referenced content should not include "This xxx was translated by Claude" and "Original Content" etc.
|
||||
- If the retrieved information contains other types of references (i.e., references to non-Claude translated content), keep them as-is without translation.
|
||||
- If the retrieved information is email reply content, place email content references at the end during translation. Include only the reply content itself in both original and translated content, without email content references.
|
||||
- If the retrieved information doesn't need any processing, skip the task.
|
||||
|
||||
3. Format requirements:
|
||||
|
||||
- Title: English translation (if non-English)
|
||||
- Content format:
|
||||
[Translated content]
|
||||
|
||||
---
|
||||
> This issue/comment/review was translated by Claude.
|
||||
|
||||
<details>
|
||||
<summary>Original Content</summary>
|
||||
[Original content]
|
||||
</details>
|
||||
|
||||
4. CRITICAL RULES to prevent hallucination and ensure accuracy:
|
||||
|
||||
- The "Original Content" section MUST contain the EXACT, UNMODIFIED original text byte-for-byte. NEVER add, remove, modify, or hallucinate ANY content in this section.
|
||||
- Code blocks, error logs, JSON structures, and other technical content MUST appear in BOTH the translated section AND the original content section WITHOUT ANY MODIFICATION.
|
||||
- When translating content with code/logs/JSON:
|
||||
* Copy the code/logs/JSON blocks identically to both sections
|
||||
* Only translate the natural language text (e.g., Chinese, Japanese) surrounding the code blocks
|
||||
* Keep all technical content (URLs, variable names, error messages in English) unchanged
|
||||
- ALWAYS verify the "Original Content" section matches the source text exactly before updating
|
||||
- If you detect any discrepancy, retrieve the original content again to ensure accuracy
|
||||
- Pay special attention to the end of comments - do not drop or hallucinate the last sentences
|
||||
|
||||
5. Update using gh tool:
|
||||
|
||||
- Choose the correct command based on the Event type in environment information:
|
||||
- If Event is 'issues': gh issue edit [ISSUE_NUMBER] --title "[English title]" --body "[Translated content + Original content]"
|
||||
- If Event is 'issue_comment': gh api -X PATCH /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }} -f body="[Translated content + Original content]"
|
||||
- If Event is 'pull_request_review': gh api -X PUT /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/${{ github.event.review.id }} -f body="[Translated content]"
|
||||
- If Event is 'pull_request_review_comment': gh api -X PATCH /repos/${{ github.repository }}/pulls/comments/${{ github.event.comment.id }} -f body="[Translated content + Original content]"
|
||||
|
||||
<environment_context>
|
||||
- Event: ${{ github.event_name }}
|
||||
- Issue Number: ${{ github.event.issue.number }}
|
||||
- Repository: ${{ github.repository }}
|
||||
- (Review) Comment ID: ${{ github.event.comment.id || 'N/A' }}
|
||||
- Pull Request Number: ${{ github.event.pull_request.number || 'N/A' }}
|
||||
- Review ID: ${{ github.event.review.id || 'N/A' }}
|
||||
</environment_context>
|
||||
|
||||
|
||||
Use the following command to get complete information:
|
||||
gh issue view ${{ github.event.issue.number }} --json title,body,comments
|
||||
@@ -18,8 +18,8 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
name: Code quality check
|
||||
# 添加 PR label 触发条件,只有添加了 Build Desktop 标签的 PR 才会触发构建
|
||||
if: contains(github.event.pull_request.labels.*.name, 'Build Desktop')
|
||||
# 添加 PR label 触发条件,只有添加了 trigger:build-desktop 标签的 PR 才会触发构建
|
||||
if: contains(github.event.pull_request.labels.*.name, 'trigger:build-desktop')
|
||||
runs-on: ubuntu-latest # 只在 ubuntu 上运行一次检查
|
||||
steps:
|
||||
- name: Checkout base
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ secrets.BUN_VERSION }}
|
||||
bun-version: 1.2.23
|
||||
|
||||
- name: Install deps
|
||||
run: bun i
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
version:
|
||||
name: Determine version
|
||||
# 与 test job 相同的触发条件
|
||||
if: contains(github.event.pull_request.labels.*.name, 'Build Desktop')
|
||||
if: contains(github.event.pull_request.labels.*.name, 'trigger:build-desktop')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
# 输出版本信息,供后续 job 使用
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, macos-13, windows-2025, ubuntu-latest]
|
||||
os: [macos-latest, macos-15-intel, windows-2025, ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -129,11 +129,11 @@ jobs:
|
||||
run: npm run desktop:build
|
||||
env:
|
||||
# 设置更新通道,PR构建为nightly,否则为stable
|
||||
UPDATE_CHANNEL: 'nightly'
|
||||
UPDATE_CHANNEL: "nightly"
|
||||
APP_URL: http://localhost:3015
|
||||
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
|
||||
DATABASE_URL: "postgresql://postgres@localhost:5432/postgres"
|
||||
# 默认添加一个加密 SECRET
|
||||
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
|
||||
KEY_VAULTS_SECRET: "oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE="
|
||||
# macOS 签名和公证配置
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -152,10 +152,10 @@ jobs:
|
||||
run: npm run desktop:build
|
||||
env:
|
||||
# 设置更新通道,PR构建为nightly,否则为stable
|
||||
UPDATE_CHANNEL: 'nightly'
|
||||
UPDATE_CHANNEL: "nightly"
|
||||
APP_URL: http://localhost:3015
|
||||
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
|
||||
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
|
||||
DATABASE_URL: "postgresql://postgres@localhost:5432/postgres"
|
||||
KEY_VAULTS_SECRET: "oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE="
|
||||
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }}
|
||||
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }}
|
||||
# 将 TEMP 和 TMP 目录设置到 C 盘
|
||||
@@ -168,10 +168,10 @@ jobs:
|
||||
run: npm run desktop:build
|
||||
env:
|
||||
# 设置更新通道,PR构建为nightly,否则为stable
|
||||
UPDATE_CHANNEL: 'nightly'
|
||||
UPDATE_CHANNEL: "nightly"
|
||||
APP_URL: http://localhost:3015
|
||||
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
|
||||
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
|
||||
DATABASE_URL: "postgresql://postgres@localhost:5432/postgres"
|
||||
KEY_VAULTS_SECRET: "oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE="
|
||||
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }}
|
||||
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }}
|
||||
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
else
|
||||
ARCH_SUFFIX="x64"
|
||||
fi
|
||||
|
||||
|
||||
mv latest-mac.yml "latest-mac-${ARCH_SUFFIX}.yml"
|
||||
echo "✅ Renamed latest-mac.yml to latest-mac-${ARCH_SUFFIX}.yml (detected: $SYSTEM_ARCH)"
|
||||
ls -la latest-mac-*.yml
|
||||
@@ -234,11 +234,11 @@ jobs:
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ secrets.BUN_VERSION }}
|
||||
bun-version: 1.2.23
|
||||
|
||||
# 下载所有平台的构建产物
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: release
|
||||
pattern: release-*
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
|
||||
# 下载合并后的构建产物
|
||||
- name: Download merged artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: merged-release-pr
|
||||
path: release
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
name: Publish Database Docker Image
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -20,7 +22,7 @@ jobs:
|
||||
# 添加 PR label 触发条件
|
||||
if: |
|
||||
(github.event_name == 'pull_request' &&
|
||||
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
|
||||
contains(github.event.pull_request.labels.*.name, 'trigger:build-docker')) ||
|
||||
github.event_name != 'pull_request'
|
||||
|
||||
strategy:
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digest-*
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
name: Publish Docker Pglite Image
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -20,7 +22,7 @@ jobs:
|
||||
# 添加 PR label 触发条件
|
||||
if: |
|
||||
(github.event_name == 'pull_request' &&
|
||||
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
|
||||
contains(github.event.pull_request.labels.*.name, 'trigger:build-docker')) ||
|
||||
github.event_name != 'pull_request'
|
||||
|
||||
strategy:
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digest-*
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
name: Publish Docker Image
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -20,7 +22,7 @@ jobs:
|
||||
# 添加 PR label 触发条件
|
||||
if: |
|
||||
(github.event_name == 'pull_request' &&
|
||||
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
|
||||
contains(github.event.pull_request.labels.*.name, 'trigger:build-docker')) ||
|
||||
github.event_name != 'pull_request'
|
||||
|
||||
strategy:
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digest-*
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
name: E2E CI
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: Test Web App
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.23
|
||||
|
||||
- name: Install dependencies (bun)
|
||||
run: bun install
|
||||
|
||||
- name: Install Playwright browsers (with system deps)
|
||||
run: bunx playwright install --with-deps chromium
|
||||
|
||||
- name: Run E2E tests
|
||||
env:
|
||||
PORT: 3010
|
||||
run: bun run e2e
|
||||
|
||||
- name: Upload Playwright HTML report (on failure)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload Playwright traces (on failure)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,30 @@
|
||||
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 }}
|
||||
@@ -28,8 +28,7 @@ jobs:
|
||||
👀 @{{ author }}
|
||||
|
||||
Thank you for raising an issue. We will investigate into the matter and get back to you as soon as possible.
|
||||
Please make sure you have given us as much context as possible.\
|
||||
非常感谢您提交 issue。我们会尽快调查此事,并尽快回复您。 请确保您已经提供了尽可能多的背景信息。
|
||||
Please make sure you have given us as much context as possible.
|
||||
- name: Auto Comment on Issues Closed
|
||||
uses: wow-actions/auto-comment@v1
|
||||
with:
|
||||
@@ -37,8 +36,7 @@ jobs:
|
||||
issuesClosed: |
|
||||
✅ @{{ author }}
|
||||
|
||||
This issue is closed, If you have any questions, you can comment and reply.\
|
||||
此问题已经关闭。如果您有任何问题,可以留言并回复。
|
||||
This issue is closed, If you have any questions, you can comment and reply.
|
||||
- name: Auto Comment on Pull Request Opened
|
||||
uses: wow-actions/auto-comment@v1
|
||||
with:
|
||||
@@ -48,9 +46,7 @@ jobs:
|
||||
|
||||
Thank you for raising your pull request and contributing to our Community
|
||||
Please make sure you have followed our contributing guidelines. We will review it as soon as possible.
|
||||
If you encounter any problems, please feel free to connect with us.\
|
||||
非常感谢您提出拉取请求并为我们的社区做出贡献,请确保您已经遵循了我们的贡献指南,我们会尽快审查它。
|
||||
如果您遇到任何问题,请随时与我们联系。
|
||||
If you encounter any problems, please feel free to connect with us.
|
||||
- name: Auto Comment on Pull Request Merged
|
||||
uses: actions-cool/pr-welcome@main
|
||||
if: github.event.pull_request.merged == true
|
||||
@@ -59,8 +55,7 @@ jobs:
|
||||
comment: |
|
||||
❤️ Great PR @${{ github.event.pull_request.user.login }} ❤️
|
||||
|
||||
The growth of project is inseparable from user feedback and contribution, thanks for your contribution! If you are interesting with the lobehub developer community, please join our [discord](https://discord.com/invite/AYFPHvv2jT) and then dm @arvinxx or @canisminor1990. They will invite you to our private developer channel. We are talking about the lobe-chat development or sharing ai newsletter around the world.\
|
||||
项目的成长离不开用户反馈和贡献,感谢您的贡献! 如果您对 LobeHub 开发者社区感兴趣,请加入我们的 [discord](https://discord.com/invite/AYFPHvv2jT),然后私信 @arvinxx 或 @canisminor1990。他们会邀请您加入我们的私密开发者频道。我们将会讨论关于 Lobe Chat 的开发,分享和讨论全球范围内的 AI 消息。
|
||||
The growth of project is inseparable from user feedback and contribution, thanks for your contribution! If you are interesting with the lobehub developer community, please join our [discord](https://discord.com/invite/AYFPHvv2jT) and then dm @arvinxx or @canisminor1990. They will invite you to our private developer channel. We are talking about the lobe-chat development or sharing ai newsletter around the world.
|
||||
emoji: 'hooray'
|
||||
pr-emoji: '+1, heart'
|
||||
- name: Remove inactive
|
||||
|
||||
@@ -38,8 +38,7 @@ jobs:
|
||||
body: |
|
||||
👋 @{{ author }}
|
||||
<br/>
|
||||
Since the issue was labeled with `✅ Fixed`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\
|
||||
由于该 issue 被标记为已修复,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
|
||||
Since the issue was labeled with `✅ Fixed`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.
|
||||
- name: need reproduce
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
@@ -50,8 +49,7 @@ jobs:
|
||||
body: |
|
||||
👋 @{{ author }}
|
||||
<br/>
|
||||
Since the issue was labeled with `🤔 Need Reproduce`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\
|
||||
由于该 issue 被标记为需要更多信息,却 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
|
||||
Since the issue was labeled with `🤔 Need Reproduce`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.
|
||||
- name: need reproduce
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
@@ -62,5 +60,4 @@ jobs:
|
||||
body: |
|
||||
👋 @{{ github.event.issue.user.login }}
|
||||
<br/>
|
||||
Since the issue was labeled with `🙅🏻♀️ WON'T DO`, and no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\
|
||||
由于该 issue 被标记为暂不处理,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
|
||||
Since the issue was labeled with `🙅🏻♀️ WON'T DO`, and no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
name: Issue Translate
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
BOT_GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
@@ -0,0 +1,26 @@
|
||||
name: "Lock Stale Issues"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
lock-closed-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Lock closed issues after 7 days of inactivity
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const lockScript = require('./.github/scripts/lock-closed-issues.js');
|
||||
await lockScript({ github, context });
|
||||
@@ -15,7 +15,7 @@ permissions: read-all
|
||||
jobs:
|
||||
test:
|
||||
name: Code quality check
|
||||
# 添加 PR label 触发条件,只有添加了 Build Desktop 标签的 PR 才会触发构建
|
||||
# 添加 PR label 触发条件,只有添加了 trigger:build-desktop 标签的 PR 才会触发构建
|
||||
runs-on: ubuntu-latest # 只在 ubuntu 上运行一次检查
|
||||
steps:
|
||||
- name: Checkout base
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ secrets.BUN_VERSION }}
|
||||
bun-version: 1.2.23
|
||||
|
||||
- name: Install deps
|
||||
run: bun i
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, macos-13, windows-2025, ubuntu-latest]
|
||||
os: [macos-latest, macos-15-intel, windows-2025, ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -116,9 +116,9 @@ jobs:
|
||||
run: npm run desktop:build
|
||||
env:
|
||||
APP_URL: http://localhost:3015
|
||||
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
|
||||
DATABASE_URL: "postgresql://postgres@localhost:5432/postgres"
|
||||
# 默认添加一个加密 SECRET
|
||||
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
|
||||
KEY_VAULTS_SECRET: "oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE="
|
||||
# macOS 签名和公证配置
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -137,8 +137,8 @@ jobs:
|
||||
run: npm run desktop:build
|
||||
env:
|
||||
APP_URL: http://localhost:3015
|
||||
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
|
||||
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
|
||||
DATABASE_URL: "postgresql://postgres@localhost:5432/postgres"
|
||||
KEY_VAULTS_SECRET: "oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE="
|
||||
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }}
|
||||
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }}
|
||||
|
||||
@@ -152,8 +152,8 @@ jobs:
|
||||
run: npm run desktop:build
|
||||
env:
|
||||
APP_URL: http://localhost:3015
|
||||
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
|
||||
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
|
||||
DATABASE_URL: "postgresql://postgres@localhost:5432/postgres"
|
||||
KEY_VAULTS_SECRET: "oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE="
|
||||
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }}
|
||||
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }}
|
||||
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
else
|
||||
ARCH_SUFFIX="x64"
|
||||
fi
|
||||
|
||||
|
||||
mv latest-mac.yml "latest-mac-${ARCH_SUFFIX}.yml"
|
||||
echo "✅ Renamed latest-mac.yml to latest-mac-${ARCH_SUFFIX}.yml (detected: $SYSTEM_ARCH)"
|
||||
ls -la latest-mac-*.yml
|
||||
@@ -216,11 +216,11 @@ jobs:
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ secrets.BUN_VERSION }}
|
||||
bun-version: 1.2.23
|
||||
|
||||
# 下载所有平台的构建产物
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: release
|
||||
pattern: release-*
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
steps:
|
||||
# 下载合并后的构建产物
|
||||
- name: Download merged artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: merged-release
|
||||
path: release
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
name: Release CI
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -35,7 +41,7 @@ jobs:
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ secrets.BUN_VERSION }}
|
||||
bun-version: 1.2.23
|
||||
|
||||
- name: Install deps
|
||||
run: bun i
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
name: Database Schema Visualization CI
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
@@ -50,5 +50,5 @@ jobs:
|
||||

|
||||
|
||||
[lobechat]: https://github.com/lobehub/lobe-chat
|
||||
[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
|
||||
[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
|
||||
|
||||
@@ -18,6 +18,7 @@ jobs:
|
||||
- web-crawler
|
||||
- electron-server-ipc
|
||||
- utils
|
||||
- python-interpreter
|
||||
- context-engine
|
||||
- agent-runtime
|
||||
|
||||
@@ -44,7 +45,7 @@ jobs:
|
||||
run: bun run --filter @lobechat/${{ matrix.package }} test:coverage
|
||||
|
||||
- name: Upload ${{ matrix.package }} coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/${{ matrix.package }}/coverage/lcov.info
|
||||
@@ -70,7 +71,7 @@ jobs:
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ secrets.BUN_VERSION }}
|
||||
bun-version: 1.2.23
|
||||
|
||||
- name: Install deps
|
||||
run: bun i
|
||||
@@ -79,7 +80,7 @@ jobs:
|
||||
run: bun run --filter ${{ matrix.package }} test:coverage
|
||||
|
||||
- name: Upload ${{ matrix.package }} coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/${{ matrix.package }}/coverage/lcov.info
|
||||
@@ -103,7 +104,7 @@ jobs:
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ secrets.BUN_VERSION }}
|
||||
bun-version: 1.2.23
|
||||
|
||||
- name: Install deps
|
||||
run: bun i
|
||||
@@ -112,7 +113,7 @@ jobs:
|
||||
run: bun run test-app:coverage
|
||||
|
||||
- name: Upload App Coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage/app/lcov.info
|
||||
@@ -147,7 +148,7 @@ jobs:
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ secrets.BUN_VERSION }}
|
||||
bun-version: 1.2.23
|
||||
|
||||
- name: Install deps
|
||||
run: bun i
|
||||
@@ -173,7 +174,7 @@ jobs:
|
||||
APP_URL: https://home.com
|
||||
|
||||
- name: Upload Database coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/database/coverage/lcov.info
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ module.exports = defineConfig({
|
||||
],
|
||||
temperature: 0,
|
||||
saveImmediately: true,
|
||||
modelName: 'gpt-4.1-mini',
|
||||
modelName: 'chatgpt-4o-latest',
|
||||
experimental: {
|
||||
jsonMode: true,
|
||||
},
|
||||
|
||||
@@ -4,8 +4,6 @@ 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*
|
||||
|
||||
Vendored
+2
-1
@@ -11,6 +11,7 @@
|
||||
{ "rule": "prettier/prettier", "severity": "off" },
|
||||
{ "rule": "react/jsx-sort-props", "severity": "off" },
|
||||
{ "rule": "sort-keys-fix/sort-keys-fix", "severity": "off" },
|
||||
{ "rule": "simple-import-sort/exports", "severity": "off" },
|
||||
{ "rule": "typescript-sort-keys/interface", "severity": "off" }
|
||||
],
|
||||
"eslint.validate": [
|
||||
@@ -81,7 +82,7 @@
|
||||
|
||||
"**/src/config/modelProviders/*.ts": "${filename} • provider",
|
||||
"**/packages/model-bank/src/aiModels/*.ts": "${filename} • model",
|
||||
"**/packages/model-runtime/src/*/index.ts": "${dirname} • runtime",
|
||||
"**/packages/model-runtime/src/providers/*/index.ts": "${dirname} • runtime",
|
||||
|
||||
"**/src/server/services/*/index.ts": "${dirname} • server/service",
|
||||
"**/src/server/routers/lambda/*.ts": "${filename} • lambda",
|
||||
|
||||
@@ -44,21 +44,7 @@ The project follows a well-organized monorepo structure:
|
||||
|
||||
#### TypeScript
|
||||
|
||||
- Follow strict TypeScript practices for type safety and code quality
|
||||
- Use proper type annotations
|
||||
- Prefer interfaces over types for object shapes
|
||||
- Use generics for reusable components
|
||||
|
||||
#### React Components
|
||||
|
||||
- Use functional components with hooks
|
||||
|
||||
#### Database Schema
|
||||
|
||||
- Follow Drizzle ORM naming conventions
|
||||
- Use plural snake_case for table names
|
||||
- Implement proper foreign key relationships
|
||||
- Follow the schema style guide
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
@@ -67,64 +53,57 @@ The project follows a well-organized monorepo structure:
|
||||
**Commands**:
|
||||
|
||||
- Web: `bunx vitest run --silent='passed-only' '[file-path-pattern]'`
|
||||
- Packages: `cd packages/[package-name] && bunx vitest run --silent='passed-only' '[file-path-pattern]'`
|
||||
- Packages: `cd packages/[package-name] && bunx vitest run --silent='passed-only' '[file-path-pattern]'` (each subpackage contains its own vitest.config.mts)
|
||||
|
||||
**Important Notes**:
|
||||
|
||||
- Wrap file paths in single quotes to avoid shell expansion
|
||||
- Never run `bun run test` - this runs all tests and takes ~10 minutes
|
||||
- If a test fails twice, stop and ask for help
|
||||
- Always add tests for new code
|
||||
- Never run `bun run test` - this runs all tests and takes \~10 minutes
|
||||
|
||||
### Type Checking
|
||||
|
||||
- Use `bun run type-check` to check for type errors
|
||||
- Ensure all TypeScript errors are resolved before committing
|
||||
|
||||
### Internationalization
|
||||
### i18n
|
||||
|
||||
- Add new keys to `src/locales/default/namespace.ts`
|
||||
- Translate at least `zh-CN` files for development preview
|
||||
- Use hierarchical nested objects, not flat keys
|
||||
- Don't run `pnpm i18n` manually (handled by CI)
|
||||
- **Keys**: Add to `src/locales/default/namespace.ts`
|
||||
- **Dev**: Translate `locales/zh-CN/namespace.json` locale file only for preview
|
||||
- DON'T run `pnpm i18n`, let CI auto handle it
|
||||
|
||||
## Available Development Rules
|
||||
## Project Rules Index
|
||||
|
||||
The project provides comprehensive rules in `.cursor/rules/` directory:
|
||||
All following rules are saved under `.cursor/rules/` directory:
|
||||
|
||||
### Core Development
|
||||
### Backend
|
||||
|
||||
- `backend-architecture.mdc` - Three-layer architecture and data flow
|
||||
- `react-component.mdc` - Component patterns and UI library usage
|
||||
- `drizzle-schema-style-guide.mdc` - Database schema conventions
|
||||
- `define-database-model.mdc` - Model templates and CRUD patterns
|
||||
- `i18n.mdc` - Internationalization workflow
|
||||
- `drizzle-schema-style-guide.mdc` – Style guide for defining Drizzle ORM schemas
|
||||
|
||||
### State Management & UI
|
||||
### Frontend
|
||||
|
||||
- `zustand-slice-organization.mdc` - Store organization patterns
|
||||
- `zustand-action-patterns.mdc` - Action implementation patterns
|
||||
- `packages/react-layout-kit.mdc` - Flex layout component usage
|
||||
- `react-component.mdc` – React component style guide and conventions
|
||||
- `i18n.mdc` – Internationalization guide using react-i18next
|
||||
- `typescript.mdc` – TypeScript code style guide
|
||||
- `packages/react-layout-kit.mdc` – Usage guide for react-layout-kit
|
||||
|
||||
### Testing & Quality
|
||||
### State Management
|
||||
|
||||
- `testing-guide/testing-guide.mdc` - Comprehensive testing strategy
|
||||
- `code-review.mdc` - Code review process and standards
|
||||
- `zustand-action-patterns.mdc` – Recommended patterns for organizing Zustand actions
|
||||
- `zustand-slice-organization.mdc` – Best practices for structuring Zustand slices
|
||||
|
||||
### Desktop (Electron)
|
||||
|
||||
- `desktop-feature-implementation.mdc` - Main/renderer process patterns
|
||||
- `desktop-local-tools-implement.mdc` - Tool integration workflow
|
||||
- `desktop-menu-configuration.mdc` - Menu system configuration
|
||||
- `desktop-window-management.mdc` - Window management patterns
|
||||
- `desktop-controller-tests.mdc` - Controller testing guide
|
||||
- `desktop-feature-implementation.mdc` – Implementing new Electron desktop features
|
||||
- `desktop-controller-tests.mdc` – Desktop controller unit testing guide
|
||||
- `desktop-local-tools-implement.mdc` – Workflow to add new desktop local tools
|
||||
- `desktop-menu-configuration.mdc` – Desktop menu configuration guide
|
||||
- `desktop-window-management.mdc` – Desktop window management guide
|
||||
|
||||
## Best Practices
|
||||
### Debugging
|
||||
|
||||
- **Conservative for existing code, modern approaches for new features**
|
||||
- **Code Language**: Use Chinese for files with existing Chinese comments, American English for new files
|
||||
- Always add tests for new functionality
|
||||
- Follow the established patterns in the codebase
|
||||
- Use proper error handling and logging
|
||||
- Implement proper accessibility features
|
||||
- Consider internationalization from the start
|
||||
- `debug-usage.mdc` – Using the debug package and namespace conventions
|
||||
|
||||
### Testing
|
||||
|
||||
- `testing-guide/testing-guide.mdc` – Comprehensive testing guide for Vitest
|
||||
- `testing-guide/electron-ipc-test.mdc` – Electron IPC interface testing strategy
|
||||
- `testing-guide/db-model-test.mdc` – Database Model testing guide
|
||||
|
||||
+1788
File diff suppressed because it is too large
Load Diff
@@ -31,28 +31,18 @@ This repository adopts a monorepo structure.
|
||||
|
||||
see @.cursor/rules/typescript.mdc
|
||||
|
||||
### Modify Code Rules
|
||||
|
||||
- **Code Language**:
|
||||
- For files with existing Chinese comments: Continue using Chinese to maintain consistency
|
||||
- For new files or files without Chinese comments: MUST use American English.
|
||||
- eg: new react tsx file and new test file
|
||||
- Conservative for existing code, modern approaches for new features
|
||||
|
||||
### Testing
|
||||
|
||||
Testing work follows the Rule-Aware Task Execution system above.
|
||||
|
||||
- **Required Rule**: `testing-guide/testing-guide.mdc`
|
||||
- **Required Rule**: read `@.cursor/rules/testing-guide/testing-guide.mdc` before writing tests
|
||||
- **Command**:
|
||||
- web: `bunx vitest run --silent='passed-only' '[file-path-pattern]'`
|
||||
- packages(eg: database): `cd packages/database && bunx vitest run --silent='passed-only' '[file-path-pattern]'`
|
||||
|
||||
**Important**:
|
||||
|
||||
- wrapped the file path in single quotes to avoid shell expansion
|
||||
- wrap the file path in single quotes to avoid shell expansion
|
||||
- Never run `bun run test` etc to run tests, this will run all tests and cost about 10mins
|
||||
- If try to fix the same test twice, but still failed, stop and ask for help.
|
||||
- If trying to fix the same test twice, but still failed, stop and ask for help.
|
||||
|
||||
### Typecheck
|
||||
|
||||
@@ -61,40 +51,9 @@ Testing work follows the Rule-Aware Task Execution system above.
|
||||
### i18n
|
||||
|
||||
- **Keys**: Add to `src/locales/default/namespace.ts`
|
||||
- **Dev**: Translate `locales/zh-CN/namespace.json` locale file only for preview
|
||||
- **Dev**: Translate `locales/zh-CN/namespace.json` and `locales/en-US/namespace.json` locales file only for dev preview
|
||||
- DON'T run `pnpm i18n`, let CI auto handle it
|
||||
|
||||
## Rules Index
|
||||
|
||||
Some useful rules of this project. Read them when needed.
|
||||
|
||||
**IMPORTANT**: All rule files referenced in this document are located in the `.cursor/rules/` directory. Throughout this document, rule files are referenced by their filename only for brevity.
|
||||
|
||||
### 📋 Complete Rule Files
|
||||
|
||||
**Core Development**
|
||||
|
||||
- `backend-architecture.mdc` - Three-layer architecture, data flow
|
||||
- `react-component.mdc` - antd-style, Lobe UI usage
|
||||
- `drizzle-schema-style-guide.mdc` - Schema naming, patterns
|
||||
- `define-database-model.mdc` - Model templates, CRUD patterns
|
||||
- `i18n.mdc` - Internationalization workflow
|
||||
|
||||
**State & UI**
|
||||
|
||||
- `zustand-slice-organization.mdc` - Store organization
|
||||
- `zustand-action-patterns.mdc` - Action patterns
|
||||
- `packages/react-layout-kit.mdc` - flex layout components usage
|
||||
|
||||
**Testing & Quality**
|
||||
|
||||
- `testing-guide/testing-guide.mdc` - Test strategy, mock patterns
|
||||
- `code-review.mdc` - Review process and standards
|
||||
|
||||
**Desktop (Electron)**
|
||||
|
||||
- `desktop-feature-implementation.mdc` - Main/renderer process patterns
|
||||
- `desktop-local-tools-implement.mdc` - Tool integration workflow
|
||||
- `desktop-menu-configuration.mdc` - App menu, context menu, tray menu
|
||||
- `desktop-window-management.mdc` - Window creation, state management, multi-window
|
||||
- `desktop-controller-tests.mdc` - Controller unit testing guide
|
||||
Some useful project rules are listed in @.cursor/rules/rules-index.mdc
|
||||
|
||||
+6
-2
@@ -142,7 +142,8 @@ ENV ACCESS_CODE="" \
|
||||
DEFAULT_AGENT_CONFIG="" \
|
||||
SYSTEM_AGENT="" \
|
||||
FEATURE_FLAGS="" \
|
||||
PROXY_URL=""
|
||||
PROXY_URL="" \
|
||||
ENABLE_AUTH_PROTECTION=""
|
||||
|
||||
# Model Variables
|
||||
ENV \
|
||||
@@ -164,6 +165,9 @@ ENV \
|
||||
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
|
||||
# Cohere
|
||||
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
|
||||
# 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
|
||||
@@ -255,7 +259,7 @@ ENV \
|
||||
# 302.AI
|
||||
AI302_API_KEY="" AI302_MODEL_LIST="" \
|
||||
# FAL
|
||||
FAL_API_KEY="" FAL_MODEL_LIST="" \
|
||||
ENABLED_FAL="" FAL_API_KEY="" FAL_MODEL_LIST="" \
|
||||
# BFL
|
||||
BFL_API_KEY="" BFL_MODEL_LIST="" \
|
||||
# Vercel AI Gateway
|
||||
|
||||
+21
-4
@@ -39,6 +39,8 @@ ARG USE_CN_MIRROR
|
||||
ARG NEXT_PUBLIC_BASE_PATH
|
||||
ARG NEXT_PUBLIC_SERVICE_MODE
|
||||
ARG NEXT_PUBLIC_ENABLE_NEXT_AUTH
|
||||
ARG NEXT_PUBLIC_ENABLE_CLERK_AUTH
|
||||
ARG NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
|
||||
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||
ARG NEXT_PUBLIC_ANALYTICS_POSTHOG
|
||||
ARG NEXT_PUBLIC_POSTHOG_HOST
|
||||
@@ -53,6 +55,9 @@ ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}" \
|
||||
|
||||
ENV NEXT_PUBLIC_SERVICE_MODE="${NEXT_PUBLIC_SERVICE_MODE:-server}" \
|
||||
NEXT_PUBLIC_ENABLE_NEXT_AUTH="${NEXT_PUBLIC_ENABLE_NEXT_AUTH:-1}" \
|
||||
NEXT_PUBLIC_ENABLE_CLERK_AUTH="${NEXT_PUBLIC_ENABLE_CLERK_AUTH:-0}" \
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}" \
|
||||
CLERK_WEBHOOK_SECRET="whsec_xxx" \
|
||||
APP_URL="http://app.com" \
|
||||
DATABASE_DRIVER="node" \
|
||||
DATABASE_URL="postgres://postgres:password@localhost:5432/postgres" \
|
||||
@@ -166,7 +171,8 @@ ENV ACCESS_CODE="" \
|
||||
DEFAULT_AGENT_CONFIG="" \
|
||||
SYSTEM_AGENT="" \
|
||||
FEATURE_FLAGS="" \
|
||||
PROXY_URL=""
|
||||
PROXY_URL="" \
|
||||
ENABLE_AUTH_PROTECTION=""
|
||||
|
||||
# Database
|
||||
ENV KEY_VAULTS_SECRET="" \
|
||||
@@ -178,13 +184,19 @@ ENV NEXT_AUTH_SECRET="" \
|
||||
NEXT_AUTH_SSO_PROVIDERS="" \
|
||||
NEXTAUTH_URL=""
|
||||
|
||||
# Clerk
|
||||
ENV CLERK_SECRET_KEY="" \
|
||||
CLERK_WEBHOOK_SECRET=""
|
||||
|
||||
# S3
|
||||
ENV NEXT_PUBLIC_S3_DOMAIN="" \
|
||||
S3_PUBLIC_DOMAIN="" \
|
||||
S3_ACCESS_KEY_ID="" \
|
||||
S3_BUCKET="" \
|
||||
S3_ENDPOINT="" \
|
||||
S3_SECRET_ACCESS_KEY=""
|
||||
S3_SECRET_ACCESS_KEY="" \
|
||||
S3_ENABLE_PATH_STYLE="" \
|
||||
S3_SET_ACL=""
|
||||
|
||||
# Model Variables
|
||||
ENV \
|
||||
@@ -206,6 +218,9 @@ ENV \
|
||||
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
|
||||
# Cohere
|
||||
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
|
||||
# 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
|
||||
@@ -297,11 +312,13 @@ ENV \
|
||||
# 302.AI
|
||||
AI302_API_KEY="" AI302_MODEL_LIST="" \
|
||||
# FAL
|
||||
FAL_API_KEY="" FAL_MODEL_LIST="" \
|
||||
ENABLED_FAL="" FAL_API_KEY="" FAL_MODEL_LIST="" \
|
||||
# BFL
|
||||
BFL_API_KEY="" BFL_MODEL_LIST="" \
|
||||
# Vercel AI Gateway
|
||||
VERCELAIGATEWAY_API_KEY="" VERCELAIGATEWAY_MODEL_LIST=""
|
||||
VERCELAIGATEWAY_API_KEY="" VERCELAIGATEWAY_MODEL_LIST="" \
|
||||
# Cerebras
|
||||
CEREBRAS_API_KEY="" CEREBRAS_MODEL_LIST=""
|
||||
|
||||
USER nextjs
|
||||
|
||||
|
||||
+6
-2
@@ -144,7 +144,8 @@ ENV ACCESS_CODE="" \
|
||||
DEFAULT_AGENT_CONFIG="" \
|
||||
SYSTEM_AGENT="" \
|
||||
FEATURE_FLAGS="" \
|
||||
PROXY_URL=""
|
||||
PROXY_URL="" \
|
||||
ENABLE_AUTH_PROTECTION=""
|
||||
|
||||
# Model Variables
|
||||
ENV \
|
||||
@@ -166,6 +167,9 @@ ENV \
|
||||
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
|
||||
# Cohere
|
||||
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
|
||||
# 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
|
||||
@@ -253,7 +257,7 @@ ENV \
|
||||
# 302.AI
|
||||
AI302_API_KEY="" AI302_MODEL_LIST="" \
|
||||
# FAL
|
||||
FAL_API_KEY="" FAL_MODEL_LIST="" \
|
||||
ENABLED_FAL="" FAL_API_KEY="" FAL_MODEL_LIST="" \
|
||||
# BFL
|
||||
BFL_API_KEY="" BFL_MODEL_LIST="" \
|
||||
# Vercel AI Gateway
|
||||
|
||||
@@ -382,14 +382,14 @@ In addition, these plugins are not limited to news aggregation, but can also ext
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
| Recent Submits | Description |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [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` |
|
||||
| [Tongyi wanxiang Image Generator](https://lobechat.com/discover/plugin/alps-tongyi-image)<br/><sup>By **YoungTx** on **2024-08-09**</sup> | This plugin uses Alibaba's Tongyi Wanxiang model to generate images based on text prompts.<br/>`image` `tongyi` `wanxiang` |
|
||||
| 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>**41**</kbd>](https://lobechat.com/discover/plugins)
|
||||
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
@@ -902,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/network/stargazers
|
||||
[github-stars-link]: https://github.com/lobehub/lobe-chat/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
|
||||
|
||||
+8
-8
@@ -375,14 +375,14 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
| 最近新增 | 描述 |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| [网页](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/>`网络` `搜索` |
|
||||
| [通义万象图像生成器](https://lobechat.com/discover/plugin/alps-tongyi-image)<br/><sup>By **YoungTx** on **2024-08-09**</sup> | 此插件使用阿里巴巴的通义万象模型根据文本提示生成图像。<br/>`图像` `通义` `万象` |
|
||||
| 最近新增 | 描述 |
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| [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>**41**</kbd>](https://lobechat.com/discover/plugins)
|
||||
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
@@ -923,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/network/stargazers
|
||||
[github-stars-link]: https://github.com/lobehub/lobe-chat/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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,7 @@
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -32,11 +34,50 @@ const getProtocolScheme = () => {
|
||||
|
||||
const protocolScheme = getProtocolScheme();
|
||||
|
||||
// Determine icon file based on version type
|
||||
const getIconFileName = () => {
|
||||
if (isNightly) return 'Icon-nightly';
|
||||
if (isBeta) return 'Icon-beta';
|
||||
return 'Icon';
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('electron-builder').Configuration}
|
||||
* @see https://www.electron.build/configuration
|
||||
*/
|
||||
const config = {
|
||||
/**
|
||||
* AfterPack hook to copy pre-generated Liquid Glass Assets.car for macOS 26+
|
||||
* @see https://github.com/electron-userland/electron-builder/issues/9254
|
||||
* @see https://github.com/MultiboxLabs/flow-browser/pull/159
|
||||
* @see https://github.com/electron/packager/pull/1806
|
||||
*/
|
||||
afterPack: async (context) => {
|
||||
// Only process macOS builds
|
||||
if (context.electronPlatformName !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
const iconFileName = getIconFileName();
|
||||
const assetsCarSource = path.join(__dirname, 'build', `${iconFileName}.Assets.car`);
|
||||
const resourcesPath = path.join(
|
||||
context.appOutDir,
|
||||
`${context.packager.appInfo.productFilename}.app`,
|
||||
'Contents',
|
||||
'Resources',
|
||||
);
|
||||
const assetsCarDest = path.join(resourcesPath, 'Assets.car');
|
||||
|
||||
try {
|
||||
await fs.access(assetsCarSource);
|
||||
await fs.copyFile(assetsCarSource, assetsCarDest);
|
||||
console.log(`✅ Copied Liquid Glass icon: ${iconFileName}.Assets.car`);
|
||||
} catch {
|
||||
// Non-critical: Assets.car not found or copy failed
|
||||
// App will use fallback .icns icon on all macOS versions
|
||||
console.log(`⏭️ Skipping Assets.car (not found or copy failed)`);
|
||||
}
|
||||
},
|
||||
appId: isNightly
|
||||
? 'com.lobehub.lobehub-desktop-nightly'
|
||||
: isBeta
|
||||
@@ -81,6 +122,7 @@ const config = {
|
||||
compression: 'maximum',
|
||||
entitlementsInherit: 'build/entitlements.mac.plist',
|
||||
extendInfo: {
|
||||
CFBundleIconName: 'AppIcon',
|
||||
CFBundleURLTypes: [
|
||||
{
|
||||
CFBundleURLName: 'LobeHub Protocol',
|
||||
|
||||
@@ -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": "^1.0.1",
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@lobechat/electron-client-ipc": "workspace:*",
|
||||
"@lobechat/electron-server-ipc": "workspace:*",
|
||||
|
||||
@@ -46,4 +46,55 @@ export const appBrowsers = {
|
||||
},
|
||||
} satisfies Record<string, BrowserWindowOpts>;
|
||||
|
||||
// Window templates for multi-instance windows
|
||||
export interface WindowTemplate {
|
||||
allowMultipleInstances: boolean;
|
||||
// Include common BrowserWindow options
|
||||
autoHideMenuBar?: boolean;
|
||||
baseIdentifier: string;
|
||||
basePath: string;
|
||||
devTools?: boolean;
|
||||
height?: number;
|
||||
keepAlive?: boolean;
|
||||
minWidth?: number;
|
||||
parentIdentifier?: string;
|
||||
showOnInit?: boolean;
|
||||
title?: string;
|
||||
titleBarStyle?: 'hidden' | 'default' | 'hiddenInset' | 'customButtonsOnHover';
|
||||
vibrancy?:
|
||||
| 'appearance-based'
|
||||
| 'content'
|
||||
| 'fullscreen-ui'
|
||||
| 'header'
|
||||
| 'hud'
|
||||
| 'menu'
|
||||
| 'popover'
|
||||
| 'selection'
|
||||
| 'sheet'
|
||||
| 'sidebar'
|
||||
| 'titlebar'
|
||||
| 'tooltip'
|
||||
| 'under-page'
|
||||
| 'under-window'
|
||||
| 'window';
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const windowTemplates = {
|
||||
chatSingle: {
|
||||
allowMultipleInstances: true,
|
||||
autoHideMenuBar: true,
|
||||
baseIdentifier: 'chatSingle',
|
||||
basePath: '/chat',
|
||||
height: 600,
|
||||
keepAlive: false, // Multi-instance windows don't need to stay alive
|
||||
minWidth: 400,
|
||||
parentIdentifier: 'chat',
|
||||
titleBarStyle: 'hidden',
|
||||
vibrancy: 'under-window',
|
||||
width: 900,
|
||||
},
|
||||
} satisfies Record<string, WindowTemplate>;
|
||||
|
||||
export type AppBrowsersIdentifiers = keyof typeof appBrowsers;
|
||||
export type WindowTemplateIdentifiers = keyof typeof windowTemplates;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
|
||||
import { extractSubPath, findMatchingRoute } from '~common/routes';
|
||||
|
||||
import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
|
||||
import { AppBrowsersIdentifiers, BrowsersIdentifiers, WindowTemplateIdentifiers } from '@/appBrowsers';
|
||||
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
||||
|
||||
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
||||
@@ -100,6 +100,77 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new multi-instance window
|
||||
*/
|
||||
@ipcClientEvent('createMultiInstanceWindow')
|
||||
async createMultiInstanceWindow(params: {
|
||||
templateId: WindowTemplateIdentifiers;
|
||||
path: string;
|
||||
uniqueId?: string;
|
||||
}) {
|
||||
try {
|
||||
console.log('[BrowserWindowsCtr] Creating multi-instance window:', params);
|
||||
|
||||
const result = this.app.browserManager.createMultiInstanceWindow(
|
||||
params.templateId,
|
||||
params.path,
|
||||
params.uniqueId,
|
||||
);
|
||||
|
||||
// Show the window
|
||||
result.browser.show();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
windowId: result.identifier,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[BrowserWindowsCtr] Failed to create multi-instance window:', error);
|
||||
return {
|
||||
error: error.message,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all windows by template
|
||||
*/
|
||||
@ipcClientEvent('getWindowsByTemplate')
|
||||
async getWindowsByTemplate(templateId: string) {
|
||||
try {
|
||||
const windowIds = this.app.browserManager.getWindowsByTemplate(templateId);
|
||||
return {
|
||||
success: true,
|
||||
windowIds,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[BrowserWindowsCtr] Failed to get windows by template:', error);
|
||||
return {
|
||||
error: error.message,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all windows by template
|
||||
*/
|
||||
@ipcClientEvent('closeWindowsByTemplate')
|
||||
async closeWindowsByTemplate(templateId: string) {
|
||||
try {
|
||||
this.app.browserManager.closeWindowsByTemplate(templateId);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[BrowserWindowsCtr] Failed to close windows by template:', error);
|
||||
return {
|
||||
error: error.message,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open target window and navigate to specified sub-path
|
||||
*/
|
||||
|
||||
@@ -77,6 +77,7 @@ 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 };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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, nextStandaloneDir } from '@/const/dir';
|
||||
import { buildDir, LOCAL_DATABASE_DIR, nextStandaloneDir } from '@/const/dir';
|
||||
import { isDev } from '@/const/env';
|
||||
import { IControlModule } from '@/controllers';
|
||||
import { IServiceModule } from '@/services';
|
||||
@@ -129,6 +130,9 @@ 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');
|
||||
@@ -371,6 +375,27 @@ 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({
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -336,7 +336,6 @@ export default class Browser {
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
contextIsolation: true,
|
||||
preload: join(preloadDir, 'index.js'),
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WebContents } from 'electron';
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { AppBrowsersIdentifiers, appBrowsers } from '../../appBrowsers';
|
||||
import { AppBrowsersIdentifiers, appBrowsers, WindowTemplate, WindowTemplateIdentifiers, windowTemplates } from '../../appBrowsers';
|
||||
import type { App } from '../App';
|
||||
import type { BrowserWindowOpts } from './Browser';
|
||||
import Browser from './Browser';
|
||||
@@ -14,9 +14,9 @@ const logger = createLogger('core:BrowserManager');
|
||||
export class BrowserManager {
|
||||
app: App;
|
||||
|
||||
browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
|
||||
browsers: Map<string, Browser> = new Map();
|
||||
|
||||
private webContentsMap = new Map<WebContents, AppBrowsersIdentifiers>();
|
||||
private webContentsMap = new Map<WebContents, string>();
|
||||
|
||||
constructor(app: App) {
|
||||
logger.debug('Initializing BrowserManager');
|
||||
@@ -51,12 +51,12 @@ export class BrowserManager {
|
||||
};
|
||||
|
||||
broadcastToWindow = <T extends MainBroadcastEventKey>(
|
||||
identifier: AppBrowsersIdentifiers,
|
||||
identifier: string,
|
||||
event: T,
|
||||
data: MainBroadcastParams<T>,
|
||||
) => {
|
||||
logger.debug(`Broadcasting event ${event} to window: ${identifier}`);
|
||||
this.browsers.get(identifier).broadcast(event, data);
|
||||
this.browsers.get(identifier)?.broadcast(event, data);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -87,13 +87,21 @@ export class BrowserManager {
|
||||
* @param identifier Window identifier
|
||||
* @param subPath Sub-path, such as 'agent', 'about', etc.
|
||||
*/
|
||||
async redirectToPage(identifier: AppBrowsersIdentifiers, subPath?: string) {
|
||||
async redirectToPage(identifier: string, subPath?: string) {
|
||||
try {
|
||||
// Ensure window is retrieved or created
|
||||
const browser = this.retrieveByIdentifier(identifier);
|
||||
browser.hide();
|
||||
|
||||
const baseRoute = appBrowsers[identifier].path;
|
||||
// Handle both static and dynamic windows
|
||||
let baseRoute: string;
|
||||
if (identifier in appBrowsers) {
|
||||
baseRoute = appBrowsers[identifier as AppBrowsersIdentifiers].path;
|
||||
} else {
|
||||
// For dynamic windows, extract base route from the browser options
|
||||
const browserOptions = browser.options;
|
||||
baseRoute = browserOptions.path;
|
||||
}
|
||||
|
||||
// Build complete URL path
|
||||
const fullPath = subPath ? `${baseRoute}/${subPath}` : baseRoute;
|
||||
@@ -114,13 +122,75 @@ export class BrowserManager {
|
||||
/**
|
||||
* get Browser by identifier
|
||||
*/
|
||||
retrieveByIdentifier(identifier: AppBrowsersIdentifiers) {
|
||||
retrieveByIdentifier(identifier: string) {
|
||||
const browser = this.browsers.get(identifier);
|
||||
|
||||
if (browser) return browser;
|
||||
|
||||
logger.debug(`Browser ${identifier} not found, initializing new instance`);
|
||||
return this.retrieveOrInitialize(appBrowsers[identifier]);
|
||||
// Check if it's a static browser
|
||||
if (identifier in appBrowsers) {
|
||||
logger.debug(`Browser ${identifier} not found, initializing new instance`);
|
||||
return this.retrieveOrInitialize(appBrowsers[identifier as AppBrowsersIdentifiers]);
|
||||
}
|
||||
|
||||
throw new Error(`Browser ${identifier} not found and is not a static browser`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a multi-instance window from template
|
||||
* @param templateId Template identifier
|
||||
* @param path Full path with query parameters
|
||||
* @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) {
|
||||
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).substr(2, 9)}`;
|
||||
|
||||
// Create browser options from template
|
||||
const browserOpts: BrowserWindowOpts = {
|
||||
...template,
|
||||
identifier: windowId,
|
||||
path: path,
|
||||
};
|
||||
|
||||
logger.debug(`Creating multi-instance window: ${windowId} with path: ${path}`);
|
||||
|
||||
const browser = this.retrieveOrInitialize(browserOpts);
|
||||
|
||||
return {
|
||||
identifier: windowId,
|
||||
browser: browser,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all windows based on template
|
||||
* @param templateId Template identifier
|
||||
* @returns Array of window identifiers matching the template
|
||||
*/
|
||||
getWindowsByTemplate(templateId: string): string[] {
|
||||
const prefix = `${templateId}_`;
|
||||
return Array.from(this.browsers.keys()).filter(id => id.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all windows based on template
|
||||
* @param templateId Template identifier
|
||||
*/
|
||||
closeWindowsByTemplate(templateId: string): void {
|
||||
const windowIds = this.getWindowsByTemplate(templateId);
|
||||
windowIds.forEach(id => {
|
||||
const browser = this.browsers.get(id);
|
||||
if (browser) {
|
||||
browser.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,7 +214,7 @@ export class BrowserManager {
|
||||
* @param options Browser window options
|
||||
*/
|
||||
private retrieveOrInitialize(options: BrowserWindowOpts) {
|
||||
let browser = this.browsers.get(options.identifier as AppBrowsersIdentifiers);
|
||||
let browser = this.browsers.get(options.identifier);
|
||||
if (browser) {
|
||||
logger.debug(`Retrieved existing browser: ${options.identifier}`);
|
||||
return browser;
|
||||
@@ -153,7 +223,7 @@ export class BrowserManager {
|
||||
logger.debug(`Creating new browser: ${options.identifier}`);
|
||||
browser = new Browser(options, this.app);
|
||||
|
||||
const identifier = options.identifier as AppBrowsersIdentifiers;
|
||||
const identifier = options.identifier;
|
||||
this.browsers.set(identifier, browser);
|
||||
|
||||
// 记录 WebContents 和 identifier 的映射
|
||||
@@ -166,32 +236,32 @@ export class BrowserManager {
|
||||
|
||||
browser.browserWindow.on('show', () => {
|
||||
if (browser.webContents)
|
||||
this.webContentsMap.set(browser.webContents, browser.identifier as AppBrowsersIdentifiers);
|
||||
this.webContentsMap.set(browser.webContents, browser.identifier);
|
||||
});
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
closeWindow(identifier: string) {
|
||||
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
||||
const browser = this.browsers.get(identifier);
|
||||
browser?.close();
|
||||
}
|
||||
|
||||
minimizeWindow(identifier: string) {
|
||||
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
||||
const browser = this.browsers.get(identifier);
|
||||
browser?.browserWindow.minimize();
|
||||
}
|
||||
|
||||
maximizeWindow(identifier: string) {
|
||||
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
||||
if (browser.browserWindow.isMaximized()) {
|
||||
const browser = this.browsers.get(identifier);
|
||||
if (browser?.browserWindow.isMaximized()) {
|
||||
browser?.browserWindow.unmaximize();
|
||||
} else {
|
||||
browser?.browserWindow.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
|
||||
getIdentifierByWebContents(webContents: WebContents): string | null {
|
||||
return this.webContentsMap.get(webContents) || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,469 @@
|
||||
[
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-21",
|
||||
"version": "1.141.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Refactor context engine."]
|
||||
},
|
||||
"date": "2025-10-21",
|
||||
"version": "1.141.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Add PDF export functionality to share modal."],
|
||||
"fixes": [
|
||||
"Ignore abort signal errors in TRPC client, slove when pwa user info have code cannot be viewed in full."
|
||||
],
|
||||
"improvements": [
|
||||
"Add knowledge base mansory layout [LOB-496], improve rich text link display."
|
||||
]
|
||||
},
|
||||
"date": "2025-10-21",
|
||||
"version": "1.141.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Add ComfyUI integration Phase1(RFC-128)."]
|
||||
},
|
||||
"date": "2025-10-21",
|
||||
"version": "1.140.0"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-21",
|
||||
"version": "1.139.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Pass threadId to messages in sendMessageInServer."]
|
||||
},
|
||||
"date": "2025-10-21",
|
||||
"version": "1.139.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Show message author in minimap."]
|
||||
},
|
||||
"date": "2025-10-21",
|
||||
"version": "1.139.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Solve when desktop the sider agent list too long."]
|
||||
},
|
||||
"date": "2025-10-20",
|
||||
"version": "1.139.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update i18n."]
|
||||
},
|
||||
"date": "2025-10-20",
|
||||
"version": "1.139.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Support image generation for siliconcloud."]
|
||||
},
|
||||
"date": "2025-10-19",
|
||||
"version": "1.139.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Refactor upload router into lambda and decide to remove it in V2."]
|
||||
},
|
||||
"date": "2025-10-18",
|
||||
"version": "1.138.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix response API tools calling issue."]
|
||||
},
|
||||
"date": "2025-10-18",
|
||||
"version": "1.138.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix topic fetch not correct in custom agent."]
|
||||
},
|
||||
"date": "2025-10-18",
|
||||
"version": "1.138.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Improve welcome message."]
|
||||
},
|
||||
"date": "2025-10-16",
|
||||
"version": "1.138.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Automatic topic creation switch does not work."]
|
||||
},
|
||||
"date": "2025-10-16",
|
||||
"version": "1.138.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Support Group Chat, Mention, and Multi-Agent Orchestration with feature flag."]
|
||||
},
|
||||
"date": "2025-10-16",
|
||||
"version": "1.138.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add Claude Haiku 4.5 model."]
|
||||
},
|
||||
"date": "2025-10-16",
|
||||
"version": "1.137.10"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Improve update notification."]
|
||||
},
|
||||
"date": "2025-10-15",
|
||||
"version": "1.137.9"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix duplicate tools id issue and fix link dialog issue."],
|
||||
"improvements": ["Add region support for Vertex AI provider."]
|
||||
},
|
||||
"date": "2025-10-15",
|
||||
"version": "1.137.8"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Use different favicon.ico in dev mode."]
|
||||
},
|
||||
"date": "2025-10-15",
|
||||
"version": "1.137.7"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Update Claude workflows to use oauth token, vertext ai create image."]
|
||||
},
|
||||
"date": "2025-10-14",
|
||||
"version": "1.137.6"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add imagen model to vertex ai."]
|
||||
},
|
||||
"date": "2025-10-14",
|
||||
"version": "1.137.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Prevent Vertex AI JSON credentials from being split by comma."]
|
||||
},
|
||||
"date": "2025-10-14",
|
||||
"version": "1.137.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": [
|
||||
"Fix mcp server connect issue and refactor web search implement, fix tools calling long name length >64 issue."
|
||||
]
|
||||
},
|
||||
"date": "2025-10-14",
|
||||
"version": "1.137.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix the Worker URL cross-origin issue."]
|
||||
},
|
||||
"date": "2025-10-14",
|
||||
"version": "1.137.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Change the user chatItem maxWidth should use flex 1."]
|
||||
},
|
||||
"date": "2025-10-14",
|
||||
"version": "1.137.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Add new setting for default image num."]
|
||||
},
|
||||
"date": "2025-10-12",
|
||||
"version": "1.137.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix input cannot send markdown."],
|
||||
"improvements": ["Optimize OpenRouter modelFetch endpoint, update i18n."]
|
||||
},
|
||||
"date": "2025-10-12",
|
||||
"version": "1.136.13"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add more AWS regions, Update infini-ai models."]
|
||||
},
|
||||
"date": "2025-10-11",
|
||||
"version": "1.136.12"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": [
|
||||
"Add capability inference for web search, image output and video recognition in model parsing and update UI form items to support search, imageOutput and video abilities."
|
||||
]
|
||||
},
|
||||
"date": "2025-10-11",
|
||||
"version": "1.136.11"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Improve search experience."]
|
||||
},
|
||||
"date": "2025-10-11",
|
||||
"version": "1.136.10"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add lab to support disable/enable rich text."]
|
||||
},
|
||||
"date": "2025-10-11",
|
||||
"version": "1.136.9"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-11",
|
||||
"version": "1.136.8"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Disable rich text in markdown editor."]
|
||||
},
|
||||
"date": "2025-10-11",
|
||||
"version": "1.136.7"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-11",
|
||||
"version": "1.136.6"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-11",
|
||||
"version": "1.136.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Add 'gemini-2.5-flash-image' to disabled models Thinking."]
|
||||
},
|
||||
"date": "2025-10-10",
|
||||
"version": "1.136.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add delete & regenerate hotkeys."]
|
||||
},
|
||||
"date": "2025-10-10",
|
||||
"version": "1.136.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update i18n."]
|
||||
},
|
||||
"date": "2025-10-10",
|
||||
"version": "1.136.2"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-09",
|
||||
"version": "1.136.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Add new provider Cerebras."],
|
||||
"fixes": ["Fix standalone plugin rerender issue."]
|
||||
},
|
||||
"date": "2025-10-09",
|
||||
"version": "1.136.0"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-08",
|
||||
"version": "1.135.6"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update i18n."]
|
||||
},
|
||||
"date": "2025-10-08",
|
||||
"version": "1.135.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add GPT-5 pro model."]
|
||||
},
|
||||
"date": "2025-10-07",
|
||||
"version": "1.135.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Improve Korean translate."]
|
||||
},
|
||||
"date": "2025-10-07",
|
||||
"version": "1.135.3"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-06",
|
||||
"version": "1.135.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Improve styles and fix tools calling condition."]
|
||||
},
|
||||
"date": "2025-10-06",
|
||||
"version": "1.135.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Huanyuan text-to-image 3."]
|
||||
},
|
||||
"date": "2025-10-06",
|
||||
"version": "1.135.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update i18n."]
|
||||
},
|
||||
"date": "2025-10-06",
|
||||
"version": "1.134.7"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-05",
|
||||
"version": "1.134.6"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-05",
|
||||
"version": "1.134.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add promptfoo to improve prompts quality."]
|
||||
},
|
||||
"date": "2025-10-05",
|
||||
"version": "1.134.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Type not preserved when model is sorted."]
|
||||
},
|
||||
"date": "2025-10-05",
|
||||
"version": "1.134.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Allow switching model type."]
|
||||
},
|
||||
"date": "2025-10-05",
|
||||
"version": "1.134.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update i18n."]
|
||||
},
|
||||
"date": "2025-10-05",
|
||||
"version": "1.134.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Support double-click to open multi agent window on the desktop."]
|
||||
},
|
||||
"date": "2025-10-04",
|
||||
"version": "1.134.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["type not preserved when model is disabled or sorted."],
|
||||
"improvements": ["Nano banana support aspect_ratio."]
|
||||
},
|
||||
"date": "2025-10-04",
|
||||
"version": "1.133.6"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Custom provider fails when client requests are enabled."],
|
||||
"improvements": ["Optimized extendParams UI, update i18n."]
|
||||
},
|
||||
"date": "2025-10-04",
|
||||
"version": "1.133.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["OllamaCloud error."],
|
||||
"improvements": ["Fix chat minimap overflow."]
|
||||
},
|
||||
"date": "2025-10-01",
|
||||
"version": "1.133.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Refactor a ssrf-safe-fetch module."],
|
||||
"fixes": ["Fix frontend random API key config not work."]
|
||||
},
|
||||
"date": "2025-10-01",
|
||||
"version": "1.133.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add minimap to chat list for quick navigation."]
|
||||
},
|
||||
"date": "2025-09-30",
|
||||
"version": "1.133.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update i18n."]
|
||||
},
|
||||
"date": "2025-09-30",
|
||||
"version": "1.133.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Add builtin Python plugin, add Claude Sonnet 4.5 model to AI chat models."]
|
||||
},
|
||||
"date": "2025-09-29",
|
||||
"version": "1.133.0"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-09-29",
|
||||
"version": "1.132.19"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Refactor tools-engine and fix search token count."],
|
||||
"improvements": ["Update i18n."]
|
||||
},
|
||||
"date": "2025-09-28",
|
||||
"version": "1.132.18"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix input empty group name."]
|
||||
},
|
||||
"date": "2025-09-27",
|
||||
"version": "1.132.17"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Resolve qwen-image-edit imageUrls conversion issue."]
|
||||
},
|
||||
"date": "2025-09-26",
|
||||
"version": "1.132.16"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Add proxyUrl configuration for NEW API provider."]
|
||||
|
||||
@@ -230,7 +230,7 @@ services:
|
||||
otel-tracing-test:
|
||||
profiles:
|
||||
- otel-test
|
||||
image: ghcr.io/grafana/xk6-client-tracing:v0.0.7
|
||||
image: ghcr.io/grafana/xk6-client-tracing:v0.0.9
|
||||
container_name: lobe-otel-tracing-test
|
||||
network_mode: 'service:network-service'
|
||||
restart: always
|
||||
|
||||
@@ -151,7 +151,7 @@ services:
|
||||
otel-tracing-test:
|
||||
profiles:
|
||||
- otel-test
|
||||
image: ghcr.io/grafana/xk6-client-tracing:v0.0.7
|
||||
image: ghcr.io/grafana/xk6-client-tracing:v0.0.9
|
||||
container_name: lobe-otel-tracing-test
|
||||
network_mode: 'service:network-service'
|
||||
restart: always
|
||||
|
||||
@@ -149,7 +149,7 @@ services:
|
||||
otel-tracing-test:
|
||||
profiles:
|
||||
- otel-test
|
||||
image: ghcr.io/grafana/xk6-client-tracing:v0.0.7
|
||||
image: ghcr.io/grafana/xk6-client-tracing:v0.0.9
|
||||
container_name: lobe-otel-tracing-test
|
||||
network_mode: 'service:network-service'
|
||||
restart: always
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,998 @@
|
||||
---
|
||||
title: ComfyUI 扩展开发指南
|
||||
description: 学习如何为 LobeChat ComfyUI 集成添加新模型、工作流和功能扩展
|
||||
tags:
|
||||
- ComfyUI
|
||||
- 开发指南
|
||||
- 模型扩展
|
||||
- 工作流开发
|
||||
---
|
||||
|
||||
# ComfyUI 扩展开发指南
|
||||
|
||||
本指南基于实际代码实现,帮助开发者扩展 LobeChat 的 ComfyUI 集成功能。
|
||||
|
||||
## 架构概览
|
||||
|
||||
LobeChat ComfyUI 集成采用四层服务架构,围绕 `LobeComfyUI` 主类构建:
|
||||
|
||||
```plaintext
|
||||
packages/model-runtime/src/providers/comfyui/
|
||||
├── index.ts # LobeComfyUI 主类入口
|
||||
├── services/ # 四大核心服务
|
||||
│ ├── comfyuiClient.ts # ComfyUIClientService - 客户端和认证
|
||||
│ ├── modelResolver.ts # ModelResolverService - 模型解析
|
||||
│ ├── workflowBuilder.ts # WorkflowBuilderService - 工作流构建
|
||||
│ └── imageService.ts # ImageService - 图像生成
|
||||
├── config/ # 配置系统
|
||||
│ ├── modelRegistry.ts # 主模型注册表(222个模型)
|
||||
│ ├── fluxModelRegistry.ts # 130个FLUX模型配置
|
||||
│ ├── sdModelRegistry.ts # 92个SD系列模型配置
|
||||
│ ├── systemComponents.ts # VAE/CLIP/T5/LoRA/ControlNet组件
|
||||
│ └── workflowRegistry.ts # 工作流路由配置
|
||||
├── workflows/ # 工作流实现
|
||||
│ ├── flux-dev.ts # FLUX Dev 20步工作流
|
||||
│ ├── flux-schnell.ts # FLUX Schnell 4步快速工作流
|
||||
│ ├── flux-kontext.ts # FLUX Kontext 填充工作流
|
||||
│ ├── sd35.ts # SD3.5 外部编码器工作流
|
||||
│ ├── simple-sd.ts # 通用SD工作流
|
||||
│ └── index.ts # 工作流导出
|
||||
├── utils/ # 工具层
|
||||
│ ├── staticModelLookup.ts # 模型查找函数
|
||||
│ ├── workflowDetector.ts # 模型架构检测
|
||||
│ ├── promptSplitter.ts # FLUX双提示词分割
|
||||
│ ├── seedGenerator.ts # 随机种子生成
|
||||
│ ├── cacheManager.ts # TTL缓存管理
|
||||
│ └── workflowUtils.ts # 工作流工具函数
|
||||
└── errors/ # 错误处理
|
||||
├── base.ts # 基础错误类
|
||||
├── modelResolverError.ts # 模型解析错误
|
||||
├── workflowError.ts # 工作流错误
|
||||
└── servicesError.ts # 服务错误
|
||||
|
||||
src/server/services/comfyui/ # 服务端实现
|
||||
├── core/ # 核心服务器服务
|
||||
│ ├── comfyUIAuthService.ts # 认证服务
|
||||
│ ├── comfyUIClientService.ts # 客户端服务
|
||||
│ ├── comfyUIConnectionService.ts # 连接服务
|
||||
│ ├── errorHandlerService.ts # 错误处理服务
|
||||
│ ├── imageService.ts # 图像生成服务
|
||||
│ ├── modelResolverService.ts # 模型解析服务
|
||||
│ └── workflowBuilderService.ts # 工作流构建服务
|
||||
├── config/ # 服务器端配置
|
||||
│ ├── constants.ts # 常量和默认值
|
||||
│ ├── modelRegistry.ts # 模型注册表
|
||||
│ ├── fluxModelRegistry.ts # FLUX模型
|
||||
│ ├── sdModelRegistry.ts # SD模型
|
||||
│ ├── systemComponents.ts # 系统组件
|
||||
│ └── workflowRegistry.ts # 工作流注册表
|
||||
├── workflows/ # 服务端工作流实现
|
||||
│ ├── flux-dev.ts # FLUX Dev 工作流
|
||||
│ ├── flux-schnell.ts # FLUX Schnell 工作流
|
||||
│ ├── flux-kontext.ts # FLUX Kontext 工作流
|
||||
│ ├── sd35.ts # SD3.5 工作流
|
||||
│ └── simple-sd.ts # Simple SD 工作流
|
||||
├── utils/ # 服务器工具
|
||||
│ ├── cacheManager.ts # 缓存管理
|
||||
│ ├── componentInfo.ts # 组件信息
|
||||
│ ├── imageResizer.ts # 图像调整
|
||||
│ ├── promptSplitter.ts # 提示词分割
|
||||
│ ├── staticModelLookup.ts # 模型查找
|
||||
│ ├── weightDType.ts # 权重数据类型工具
|
||||
│ ├── workflowDetector.ts # 工作流检测
|
||||
│ └── workflowUtils.ts # 工作流工具
|
||||
└── errors/ # 服务器错误处理
|
||||
├── base.ts # 基础错误类
|
||||
├── configError.ts # 配置错误
|
||||
├── modelResolverError.ts # 模型解析器错误
|
||||
├── servicesError.ts # 服务错误
|
||||
├── utilsError.ts # 工具错误
|
||||
└── workflowError.ts # 工作流错误
|
||||
|
||||
packages/model-runtime/src/utils/ # 共享工具
|
||||
└── comfyuiErrorParser.ts # 客户端/服务器统一错误解析器
|
||||
```
|
||||
|
||||
### 核心服务架构
|
||||
|
||||
`LobeComfyUI` 主类初始化四个核心服务:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/index.ts
|
||||
export class LobeComfyUI implements LobeRuntimeAI, AuthenticatedImageRuntime {
|
||||
constructor(options: ComfyUIKeyVault = {}) {
|
||||
// 1. 客户端服务 - 处理认证和API调用
|
||||
this.clientService = new ComfyUIClientService(options);
|
||||
|
||||
// 2. 模型解析服务 - 模型查找和组件选择
|
||||
const modelResolverService = new ModelResolverService(this.clientService);
|
||||
|
||||
// 3. 工作流构建服务 - 路由和构建工作流
|
||||
const workflowBuilderService = new WorkflowBuilderService({
|
||||
clientService: this.clientService,
|
||||
modelResolverService: modelResolverService,
|
||||
});
|
||||
|
||||
// 4. 图像服务 - 统一的图像生成入口
|
||||
this.imageService = new ImageService(
|
||||
this.clientService,
|
||||
modelResolverService,
|
||||
workflowBuilderService,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 认证系统
|
||||
|
||||
ComfyUI 集成支持四种认证方式,由 `ComfyUIClientService` 内的 `AuthManager` 处理:
|
||||
|
||||
### 支持的认证类型
|
||||
|
||||
```typescript
|
||||
interface ComfyUIKeyVault {
|
||||
baseURL: string;
|
||||
authType?: 'none' | 'basic' | 'bearer' | 'custom';
|
||||
// Basic Auth
|
||||
username?: string;
|
||||
password?: string;
|
||||
// Bearer Token
|
||||
apiKey?: string;
|
||||
// Custom Headers
|
||||
customHeaders?: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
### 认证配置示例
|
||||
|
||||
```typescript
|
||||
// 无认证
|
||||
const comfyUI = new LobeComfyUI({
|
||||
baseURL: 'http://localhost:8000',
|
||||
authType: 'none'
|
||||
});
|
||||
|
||||
// 基础认证
|
||||
const comfyUI = new LobeComfyUI({
|
||||
baseURL: 'https://your-comfyui-server.com',
|
||||
authType: 'basic',
|
||||
username: 'your-username',
|
||||
password: 'your-password'
|
||||
});
|
||||
|
||||
// Bearer Token
|
||||
const comfyUI = new LobeComfyUI({
|
||||
baseURL: 'https://your-comfyui-server.com',
|
||||
authType: 'bearer',
|
||||
apiKey: 'your-api-key'
|
||||
});
|
||||
|
||||
// 自定义头部
|
||||
const comfyUI = new LobeComfyUI({
|
||||
baseURL: 'https://your-comfyui-server.com',
|
||||
authType: 'custom',
|
||||
customHeaders: {
|
||||
'X-API-Key': 'your-custom-key',
|
||||
'Authorization': 'Custom your-token'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## WebAPI 路由
|
||||
|
||||
ComfyUI 提供了用于图像生成的 REST WebAPI 路由,支持常规认证和内部服务认证:
|
||||
|
||||
### 路由详情
|
||||
|
||||
```typescript
|
||||
// src/app/(backend)/webapi/create-image/comfyui/route.ts
|
||||
export const runtime = 'nodejs';
|
||||
export const maxDuration = 300; // 最长5分钟
|
||||
|
||||
// POST /api/create-image/comfyui
|
||||
{
|
||||
model: string; // 模型标识符
|
||||
params: { // 生成参数
|
||||
prompt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
// ... 其他参数
|
||||
};
|
||||
options?: { // 可选生成选项
|
||||
// ... 额外选项
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 认证中间件
|
||||
|
||||
WebAPI 路由使用 `checkAuth` 中间件进行认证:
|
||||
|
||||
```typescript
|
||||
import { checkAuth } from '@/app/(backend)/middleware/auth';
|
||||
|
||||
// 路由自动验证 JWT 令牌
|
||||
// 并将认证上下文传递给 tRPC 调用器
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
WebAPI 路由提供结构化的错误响应:
|
||||
|
||||
```typescript
|
||||
// 从 TRPCError 的 cause 中提取 AgentRuntimeError
|
||||
if (agentError && 'errorType' in agentError) {
|
||||
// 将 errorType 转换为适当的 HTTP 状态码
|
||||
// 401 对应 InvalidProviderAPIKey
|
||||
// 403 对应 PermissionDenied
|
||||
// 404 对应 NotFound
|
||||
// 500+ 对应服务器错误
|
||||
}
|
||||
```
|
||||
|
||||
## 添加新模型
|
||||
|
||||
### 1. 理解模型注册表结构
|
||||
|
||||
模型配置存储在配置文件中:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/config/modelRegistry.ts
|
||||
export interface ModelConfig {
|
||||
modelFamily: 'FLUX' | 'SD1' | 'SDXL' | 'SD3';
|
||||
priority: number; // 1=官方, 2=企业, 3=社区
|
||||
recommendedDtype?: 'default' | 'fp8_e4m3fn' | 'fp8_e5m2';
|
||||
variant: string; // 模型变体标识符
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 添加 FLUX 模型
|
||||
|
||||
在 `fluxModelRegistry.ts` 中添加新模型:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/config/fluxModelRegistry.ts
|
||||
export const FLUX_MODEL_REGISTRY: Record<string, ModelConfig> = {
|
||||
// 现有模型...
|
||||
|
||||
// 添加新的FLUX Dev模型
|
||||
'your-custom-flux-dev.safetensors': {
|
||||
modelFamily: 'FLUX',
|
||||
priority: 2, // 企业级模型
|
||||
variant: 'dev',
|
||||
recommendedDtype: 'default',
|
||||
},
|
||||
|
||||
// 添加量化版本
|
||||
'your-custom-flux-dev-fp8.safetensors': {
|
||||
modelFamily: 'FLUX',
|
||||
priority: 2,
|
||||
variant: 'dev',
|
||||
recommendedDtype: 'fp8_e4m3fn',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 添加 SD 系列模型
|
||||
|
||||
在 `sdModelRegistry.ts` 中添加:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/config/sdModelRegistry.ts
|
||||
export const SD_MODEL_REGISTRY: Record<string, ModelConfig> = {
|
||||
// 现有模型...
|
||||
|
||||
// 添加新的SD3.5模型
|
||||
'your-custom-sd35.safetensors': {
|
||||
modelFamily: 'SD3',
|
||||
priority: 2,
|
||||
variant: 'sd35',
|
||||
recommendedDtype: 'default',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 4. 更新模型 ID 映射(可选)
|
||||
|
||||
如果需要为前端提供友好的模型 ID,在 `modelRegistry.ts` 中添加映射:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/config/modelRegistry.ts
|
||||
export const MODEL_ID_VARIANT_MAP: Record<string, string> = {
|
||||
// 现有映射...
|
||||
|
||||
// 添加新模型的友好ID
|
||||
'my-custom-flux': 'dev', // 映射到dev变体
|
||||
'my-custom-sd35': 'sd35', // 映射到sd35变体
|
||||
};
|
||||
```
|
||||
|
||||
## 创建新工作流
|
||||
|
||||
### 工作流创建原理
|
||||
|
||||
**重要:工作流节点结构来自 ComfyUI 原生导出**
|
||||
|
||||
1. 在 ComfyUI 界面中设计工作流
|
||||
2. 使用 "Export (API Format)" 导出 JSON
|
||||
3. 将 JSON 结构复制到 TypeScript 文件
|
||||
4. 使用`PromptBuilder`包装并参数化
|
||||
|
||||
### 1. 从 ComfyUI 导出工作流
|
||||
|
||||
在 ComfyUI 界面中:
|
||||
|
||||
1. 拖拽节点构建所需工作流
|
||||
2. 连接各节点的输入输出
|
||||
3. 右键点击空白处 → "Export (API Format)"
|
||||
4. 复制生成的 JSON 结构
|
||||
|
||||
### 2. 工作流文件模板
|
||||
|
||||
创建新文件 `workflows/your-workflow.ts`:
|
||||
|
||||
```typescript
|
||||
import { PromptBuilder } from '@saintno/comfyui-sdk';
|
||||
|
||||
import type { WorkflowContext } from '../services/workflowBuilder';
|
||||
import { generateUniqueSeeds } from '../utils/seedGenerator';
|
||||
import { getWorkflowFilenamePrefix } from '../utils/workflowUtils';
|
||||
|
||||
/**
|
||||
* 构建自定义工作流
|
||||
* @param modelFileName - 模型文件名
|
||||
* @param params - 生成参数
|
||||
* @param context - 工作流上下文
|
||||
*/
|
||||
export async function buildYourCustomWorkflow(
|
||||
modelFileName: string,
|
||||
params: Record<string, any>,
|
||||
context: WorkflowContext,
|
||||
): Promise<PromptBuilder<any, any, any>> {
|
||||
|
||||
// 从ComfyUI "Export (API Format)" 获得的JSON结构
|
||||
const workflow = {
|
||||
'1': {
|
||||
_meta: { title: 'Load Checkpoint' },
|
||||
class_type: 'CheckpointLoaderSimple',
|
||||
inputs: {
|
||||
ckpt_name: modelFileName,
|
||||
},
|
||||
},
|
||||
'2': {
|
||||
_meta: { title: 'CLIP Text Encode' },
|
||||
class_type: 'CLIPTextEncode',
|
||||
inputs: {
|
||||
clip: ['1', 1], // 连接到节点1的CLIP输出
|
||||
text: params.prompt,
|
||||
},
|
||||
},
|
||||
'3': {
|
||||
_meta: { title: 'Empty Latent' },
|
||||
class_type: 'EmptyLatentImage',
|
||||
inputs: {
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
batch_size: 1,
|
||||
},
|
||||
},
|
||||
'4': {
|
||||
_meta: { title: 'KSampler' },
|
||||
class_type: 'KSampler',
|
||||
inputs: {
|
||||
model: ['1', 0], // 连接到节点1的MODEL输出
|
||||
positive: ['2', 0], // 连接到节点2的CONDITIONING输出
|
||||
negative: ['2', 0], // 可以配置负面提示词
|
||||
latent_image: ['3', 0],
|
||||
seed: params.seed ?? generateUniqueSeeds(1)[0],
|
||||
steps: params.steps,
|
||||
cfg: params.cfg,
|
||||
sampler_name: 'euler',
|
||||
scheduler: 'normal',
|
||||
denoise: 1.0,
|
||||
},
|
||||
},
|
||||
'5': {
|
||||
_meta: { title: 'VAE Decode' },
|
||||
class_type: 'VAEDecode',
|
||||
inputs: {
|
||||
samples: ['4', 0],
|
||||
vae: ['1', 2], // 连接到节点1的VAE输出
|
||||
},
|
||||
},
|
||||
'6': {
|
||||
_meta: { title: 'Save Image' },
|
||||
class_type: 'SaveImage',
|
||||
inputs: {
|
||||
filename_prefix: getWorkflowFilenamePrefix('buildYourCustomWorkflow', context.variant),
|
||||
images: ['5', 0],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 使用PromptBuilder包装静态JSON
|
||||
const builder = new PromptBuilder(
|
||||
workflow,
|
||||
['width', 'height', 'steps', 'cfg', 'seed'], // 输入参数
|
||||
['images'], // 输出参数
|
||||
);
|
||||
|
||||
// 设置输出节点
|
||||
builder.setOutputNode('images', '6');
|
||||
|
||||
// 设置输入节点路径
|
||||
builder.setInputNode('width', '3.inputs.width');
|
||||
builder.setInputNode('height', '3.inputs.height');
|
||||
builder.setInputNode('steps', '4.inputs.steps');
|
||||
builder.setInputNode('cfg', '4.inputs.cfg');
|
||||
builder.setInputNode('seed', '4.inputs.seed');
|
||||
|
||||
// 设置参数值
|
||||
builder
|
||||
.input('width', params.width)
|
||||
.input('height', params.height)
|
||||
.input('steps', params.steps)
|
||||
.input('cfg', params.cfg)
|
||||
.input('seed', params.seed ?? generateUniqueSeeds(1)[0]);
|
||||
|
||||
return builder;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 注册新工作流
|
||||
|
||||
在 `workflowRegistry.ts` 中添加工作流映射:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/config/workflowRegistry.ts
|
||||
import { buildYourCustomWorkflow } from '../workflows/your-workflow';
|
||||
|
||||
export const VARIANT_WORKFLOW_MAP: Record<string, WorkflowBuilder> = {
|
||||
// 现有映射...
|
||||
|
||||
// 添加新工作流
|
||||
'your-variant': buildYourCustomWorkflow,
|
||||
};
|
||||
```
|
||||
|
||||
### 4. 实际工作流示例
|
||||
|
||||
参考 `flux-dev.ts` 的真实实现:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/workflows/flux-dev.ts (简化版)
|
||||
export async function buildFluxDevWorkflow(
|
||||
modelFileName: string,
|
||||
params: Record<string, any>,
|
||||
context: WorkflowContext,
|
||||
): Promise<PromptBuilder<any, any, any>> {
|
||||
// 获取所需组件
|
||||
const selectedT5Model = await context.modelResolverService.getOptimalComponent('t5', 'FLUX');
|
||||
const selectedVAE = await context.modelResolverService.getOptimalComponent('vae', 'FLUX');
|
||||
const selectedCLIP = await context.modelResolverService.getOptimalComponent('clip', 'FLUX');
|
||||
|
||||
// 处理双提示词分割
|
||||
const { t5xxlPrompt, clipLPrompt } = splitPromptForDualCLIP(params.prompt);
|
||||
|
||||
// 静态工作流定义(来自ComfyUI导出)
|
||||
const workflow = {
|
||||
'1': {
|
||||
class_type: 'DualCLIPLoader',
|
||||
inputs: {
|
||||
clip_name1: selectedT5Model,
|
||||
clip_name2: selectedCLIP,
|
||||
type: 'flux',
|
||||
},
|
||||
},
|
||||
// ... 更多节点
|
||||
};
|
||||
|
||||
// 参数注入(必须在workflow文件内完成)
|
||||
workflow['5'].inputs.clip_l = clipLPrompt;
|
||||
workflow['5'].inputs.t5xxl = t5xxlPrompt;
|
||||
workflow['4'].inputs.width = params.width;
|
||||
workflow['4'].inputs.height = params.height;
|
||||
|
||||
// 创建并配置PromptBuilder
|
||||
const builder = new PromptBuilder(workflow, inputs, outputs);
|
||||
// 配置输入输出映射...
|
||||
|
||||
return builder;
|
||||
}
|
||||
```
|
||||
|
||||
## 系统组件管理
|
||||
|
||||
### 组件配置结构
|
||||
|
||||
所有系统组件(VAE、CLIP、T5、LoRA、ControlNet)统一配置在 `systemComponents.ts`:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/config/systemComponents.ts
|
||||
export interface ComponentConfig {
|
||||
modelFamily: string; // 模型家族
|
||||
priority: number; // 1=必需, 2=标准, 3=可选
|
||||
type: string; // 组件类型
|
||||
compatibleVariants?: string[]; // 兼容变体(LoRA/ControlNet)
|
||||
controlnetType?: string; // ControlNet类型
|
||||
}
|
||||
|
||||
export const SYSTEM_COMPONENTS: Record<string, ComponentConfig> = {
|
||||
// VAE组件
|
||||
'ae.safetensors': {
|
||||
modelFamily: 'FLUX',
|
||||
priority: 1,
|
||||
type: 'vae',
|
||||
},
|
||||
|
||||
// CLIP组件
|
||||
'clip_l.safetensors': {
|
||||
modelFamily: 'FLUX',
|
||||
priority: 1,
|
||||
type: 'clip',
|
||||
},
|
||||
|
||||
// T5编码器
|
||||
't5xxl_fp16.safetensors': {
|
||||
modelFamily: 'FLUX',
|
||||
priority: 1,
|
||||
type: 't5',
|
||||
},
|
||||
|
||||
// LoRA适配器
|
||||
'realism_lora.safetensors': {
|
||||
compatibleVariants: ['dev'],
|
||||
modelFamily: 'FLUX',
|
||||
priority: 1,
|
||||
type: 'lora',
|
||||
},
|
||||
|
||||
// ControlNet模型
|
||||
'flux-controlnet-canny-v3.safetensors': {
|
||||
compatibleVariants: ['dev'],
|
||||
controlnetType: 'canny',
|
||||
modelFamily: 'FLUX',
|
||||
priority: 1,
|
||||
type: 'controlnet',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 添加新组件
|
||||
|
||||
```typescript
|
||||
// 添加新的LoRA
|
||||
'your-custom-lora.safetensors': {
|
||||
compatibleVariants: ['dev', 'schnell'],
|
||||
modelFamily: 'FLUX',
|
||||
priority: 2,
|
||||
type: 'lora',
|
||||
},
|
||||
|
||||
// 添加新的ControlNet
|
||||
'your-controlnet-pose.safetensors': {
|
||||
compatibleVariants: ['dev'],
|
||||
controlnetType: 'pose',
|
||||
modelFamily: 'FLUX',
|
||||
priority: 2,
|
||||
type: 'controlnet',
|
||||
},
|
||||
```
|
||||
|
||||
### 组件查询 API
|
||||
|
||||
```typescript
|
||||
import { getAllComponentsWithNames, getOptimalComponent } from '../config/systemComponents';
|
||||
|
||||
// 获取最优组件
|
||||
const bestVAE = getOptimalComponent('vae', 'FLUX');
|
||||
const bestT5 = getOptimalComponent('t5', 'FLUX');
|
||||
|
||||
// 查询特定类型的组件
|
||||
const availableLoras = getAllComponentsWithNames({
|
||||
type: 'lora',
|
||||
modelFamily: 'FLUX',
|
||||
compatibleVariant: 'dev'
|
||||
});
|
||||
|
||||
// 查询ControlNet
|
||||
const cannyControlNets = getAllComponentsWithNames({
|
||||
type: 'controlnet',
|
||||
controlnetType: 'canny',
|
||||
modelFamily: 'FLUX'
|
||||
});
|
||||
```
|
||||
|
||||
## 模型解析和查找
|
||||
|
||||
### ModelResolverService 工作原理
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/services/modelResolver.ts
|
||||
export class ModelResolverService {
|
||||
async resolveModelFileName(modelId: string): Promise<string | undefined> {
|
||||
// 1. 清理模型ID
|
||||
const cleanId = modelId.replace(/^comfyui\//, '');
|
||||
|
||||
// 2. 检查模型ID映射
|
||||
const mappedVariant = MODEL_ID_VARIANT_MAP[cleanId];
|
||||
if (mappedVariant) {
|
||||
const prioritizedModels = getModelsByVariant(mappedVariant);
|
||||
const serverModels = await this.getAvailableModelFiles();
|
||||
|
||||
// 按优先级查找第一个可用模型
|
||||
for (const filename of prioritizedModels) {
|
||||
if (serverModels.includes(filename)) {
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 直接注册表查找
|
||||
if (MODEL_REGISTRY[cleanId]) {
|
||||
return cleanId;
|
||||
}
|
||||
|
||||
// 4. 检查服务器文件存在性
|
||||
if (isModelFile(cleanId)) {
|
||||
const serverModels = await this.getAvailableModelFiles();
|
||||
if (serverModels.includes(cleanId)) {
|
||||
return cleanId;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 模型查找示例
|
||||
|
||||
```typescript
|
||||
// 实际使用示例
|
||||
const resolver = new ModelResolverService(clientService);
|
||||
|
||||
// 友好ID查找
|
||||
const fluxDevFile = await resolver.resolveModelFileName('flux-dev');
|
||||
// 返回: 'flux1-dev.safetensors' (如果存在)
|
||||
|
||||
// 直接文件名查找
|
||||
const directFile = await resolver.resolveModelFileName('my-custom-model.safetensors');
|
||||
// 返回: 'my-custom-model.safetensors' (如果存在)
|
||||
|
||||
// 变体查找
|
||||
const devModels = getModelsByVariant('dev');
|
||||
console.log(devModels.slice(0, 3));
|
||||
// 输出: ['flux1-dev.safetensors', 'flux1-dev-fp8.safetensors', ...]
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 错误类型层次
|
||||
|
||||
```plaintext
|
||||
// packages/model-runtime/src/providers/comfyui/errors/
|
||||
ComfyUIInternalError // 基础错误
|
||||
├── ModelResolverError // 模型解析错误
|
||||
├── WorkflowError // 工作流错误
|
||||
├── ServicesError // 服务错误
|
||||
└── UtilsError // 工具错误
|
||||
```
|
||||
|
||||
### 错误处理示例
|
||||
|
||||
```typescript
|
||||
import { ModelResolverError, WorkflowError } from '../errors';
|
||||
|
||||
try {
|
||||
const result = await comfyUI.createImage({
|
||||
model: 'nonexistent-model',
|
||||
params: { prompt: '测试' }
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ModelResolverError) {
|
||||
console.log('模型解析失败:', error.message);
|
||||
console.log('错误原因:', error.reason);
|
||||
console.log('错误详情:', error.details);
|
||||
} else if (error instanceof WorkflowError) {
|
||||
console.log('工作流错误:', error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 统一错误解析器
|
||||
|
||||
客户端和服务器端错误处理可使用共享的错误解析器:
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/utils/comfyuiErrorParser.ts
|
||||
import { parseComfyUIErrorMessage, cleanComfyUIErrorMessage } from '../utils/comfyuiErrorParser';
|
||||
|
||||
// 解析错误消息并确定错误类型
|
||||
const { error, errorType } = parseComfyUIErrorMessage(rawError);
|
||||
|
||||
// 清理 ComfyUI 格式的错误消息
|
||||
const cleanMessage = cleanComfyUIErrorMessage(errorMessage);
|
||||
```
|
||||
|
||||
错误解析器处理:
|
||||
|
||||
- HTTP 状态码映射到错误类型
|
||||
- 服务器端错误增强
|
||||
- 模型文件缺失检测
|
||||
- 网络错误识别
|
||||
- 工作流验证错误
|
||||
|
||||
## 测试架构与开发
|
||||
|
||||
### 测试架构概述
|
||||
|
||||
ComfyUI 集成使用了统一的测试架构,确保测试的可维护性和定制友好性。该架构包括:
|
||||
|
||||
- **统一 Mock 系统**:集中管理所有外部依赖的模拟
|
||||
- **参数化测试**:自动适应新模型,无需修改现有测试
|
||||
- **夹具系统**:从配置文件中获取测试数据,确保准确性
|
||||
- **覆盖率目标**:ComfyUI 模块维持 97%+ 覆盖率
|
||||
|
||||
### 测试文件结构
|
||||
|
||||
```plaintext
|
||||
packages/model-runtime/src/providers/comfyui/__tests__/
|
||||
├── setup/
|
||||
│ └── unifiedMocks.ts # 统一Mock配置
|
||||
├── fixtures/
|
||||
│ ├── parameters.fixture.ts # 参数测试夹具
|
||||
│ └── workflow.fixture.ts # 工作流测试夹具
|
||||
├── integration/
|
||||
│ ├── parameterMapping.test.ts # 参数映射集成测试
|
||||
│ └── workflowBuilder.test.ts # 工作流构建测试
|
||||
├── services/ # 各服务单元测试
|
||||
└── workflows/ # 工作流单元测试
|
||||
```
|
||||
|
||||
### 添加新模型测试
|
||||
|
||||
当添加新模型时,测试会自动识别并运行相应的参数映射测试。你只需要:
|
||||
|
||||
#### 1. 在模型配置中添加参数架构
|
||||
|
||||
```typescript
|
||||
// packages/model-bank/src/aiModels/comfyui.ts
|
||||
export const myNewModelParamsSchema = {
|
||||
prompt: { type: 'string', required: true },
|
||||
steps: { type: 'number', default: 20, min: 1, max: 150 },
|
||||
cfg: { type: 'number', default: 7.0, min: 1.0, max: 30.0 }
|
||||
};
|
||||
```
|
||||
|
||||
#### 2. 创建工作流构建器
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/workflows/myNewModel.ts
|
||||
export async function buildMyNewModelWorkflow(
|
||||
modelName: string,
|
||||
params: MyNewModelParams,
|
||||
context: ComfyUIContext
|
||||
) {
|
||||
const workflow = { /* 工作流定义 */ };
|
||||
|
||||
// 参数注入
|
||||
workflow['1'].inputs.prompt = params.prompt;
|
||||
workflow['2'].inputs.steps = params.steps;
|
||||
|
||||
return workflow;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 在夹具中注册模型
|
||||
|
||||
```typescript
|
||||
// packages/model-runtime/src/providers/comfyui/__tests__/fixtures/parameters.fixture.ts
|
||||
import {
|
||||
myNewModelParamsSchema,
|
||||
// ... 其他架构
|
||||
} from '../../../../../model-bank/src/aiModels/comfyui';
|
||||
|
||||
export const parametersFixture = {
|
||||
models: {
|
||||
'my-new-model': {
|
||||
schema: myNewModelParamsSchema,
|
||||
defaults: {
|
||||
steps: myNewModelParamsSchema.steps.default,
|
||||
cfg: myNewModelParamsSchema.cfg.default,
|
||||
},
|
||||
boundaries: {
|
||||
min: { steps: myNewModelParamsSchema.steps.min },
|
||||
max: { steps: myNewModelParamsSchema.steps.max }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 测试最佳实践
|
||||
|
||||
#### 使用统一 Mock 系统
|
||||
|
||||
```typescript
|
||||
import { setupAllMocks } from '../setup/unifiedMocks';
|
||||
|
||||
describe('MyTest', () => {
|
||||
const mocks = setupAllMocks();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 编写参数映射测试
|
||||
|
||||
参数映射测试会自动运行,验证前端参数正确注入到工作流中:
|
||||
|
||||
```typescript
|
||||
// 测试会自动包含新注册的模型
|
||||
describe.each(
|
||||
Object.entries(models).filter(([name]) => workflowBuilders[name])
|
||||
)(
|
||||
'%s parameter mapping',
|
||||
(modelName, modelConfig) => {
|
||||
it('should map schema parameters to workflow', async () => {
|
||||
const params = {
|
||||
prompt: 'test prompt',
|
||||
...modelConfig.defaults,
|
||||
};
|
||||
|
||||
const workflow = await builder(`${modelName}.safetensors`, params, mockContext);
|
||||
expect(workflow).toBeDefined();
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### 定制友好的测试原则
|
||||
|
||||
- **不测试工作流结构**:工作流是 ComfyUI 官方格式,只测试参数映射
|
||||
- **使用配置驱动的数据**:测试数据来自模型配置文件,确保一致性
|
||||
- **避免脆性断言**:不检查具体的节点 ID 或内部结构
|
||||
- **支持扩展**:新增模型应该只影响覆盖率,不破坏现有测试
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行 ComfyUI 相关测试
|
||||
cd packages/model-runtime
|
||||
bunx vitest run --silent='passed-only' 'src/comfyui'
|
||||
|
||||
# 查看覆盖率
|
||||
bunx vitest run --coverage 'src/comfyui'
|
||||
|
||||
# 运行特定测试文件
|
||||
bunx vitest run 'src/comfyui/__tests__/integration/parameterMapping.test.ts'
|
||||
```
|
||||
|
||||
### 覆盖率目标
|
||||
|
||||
- **整体覆盖率**:ComfyUI 模块维持 97%+ 覆盖率
|
||||
- **核心功能**:100% 分支覆盖率
|
||||
- **新增功能**:保持或提升现有覆盖率水平
|
||||
|
||||
## 开发和测试
|
||||
|
||||
### 1. 本地开发设置
|
||||
|
||||
```bash
|
||||
# 启动ComfyUI调试模式
|
||||
DEBUG=lobe-image:* pnpm dev
|
||||
```
|
||||
|
||||
### 2. 测试新功能
|
||||
|
||||
```typescript
|
||||
// 创建测试文件
|
||||
import { buildYourCustomWorkflow } from './your-workflow';
|
||||
|
||||
describe('Custom Workflow', () => {
|
||||
test('should build workflow correctly', async () => {
|
||||
const mockContext = {
|
||||
clientService: mockClientService,
|
||||
modelResolverService: mockModelResolver,
|
||||
};
|
||||
|
||||
const workflow = await buildYourCustomWorkflow(
|
||||
'test-model.safetensors',
|
||||
{ prompt: '测试', width: 512, height: 512 },
|
||||
mockContext
|
||||
);
|
||||
|
||||
expect(workflow).toBeDefined();
|
||||
// 验证工作流结构...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 模型配置测试
|
||||
|
||||
```typescript
|
||||
import { getModelConfig, getAllModelNames } from '../config/modelRegistry';
|
||||
|
||||
describe('Model Registry', () => {
|
||||
test('should find new model', () => {
|
||||
const config = getModelConfig('your-new-model.safetensors');
|
||||
expect(config).toBeDefined();
|
||||
expect(config?.variant).toBe('dev');
|
||||
expect(config?.modelFamily).toBe('FLUX');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 完整使用示例
|
||||
|
||||
### 基础图像生成
|
||||
|
||||
```typescript
|
||||
import { LobeComfyUI } from '@/libs/model-runtime/comfyui';
|
||||
|
||||
const comfyUI = new LobeComfyUI({
|
||||
baseURL: 'http://localhost:8000',
|
||||
authType: 'none'
|
||||
});
|
||||
|
||||
// FLUX Dev模型生成
|
||||
const result = await comfyUI.createImage({
|
||||
model: 'flux-dev',
|
||||
params: {
|
||||
prompt: '美丽的风景画,高质量,详细',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
steps: 20,
|
||||
cfg: 3.5,
|
||||
seed: -1
|
||||
}
|
||||
});
|
||||
|
||||
console.log('生成图像URL:', result.imageUrl);
|
||||
```
|
||||
|
||||
### SD3.5 模型使用
|
||||
|
||||
```typescript
|
||||
// SD3.5会自动检测可用编码器
|
||||
const sd35Result = await comfyUI.createImage({
|
||||
model: 'stable-diffusion-35',
|
||||
params: {
|
||||
prompt: '未来主义城市景观',
|
||||
width: 1344,
|
||||
height: 768,
|
||||
steps: 28,
|
||||
cfg: 4.5
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 企业优化模型
|
||||
|
||||
```typescript
|
||||
// 系统会自动选择最佳可用变体(如FP8量化版本)
|
||||
const optimizedResult = await comfyUI.createImage({
|
||||
model: 'flux-dev',
|
||||
params: {
|
||||
prompt: '专业商务肖像',
|
||||
width: 768,
|
||||
height: 1024,
|
||||
steps: 15 // FP8模型可以用更少步数
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 确保 ComfyUI 服务正常运行并可访问
|
||||
- 检查所有必需的模型文件是否已正确安装
|
||||
- 注意模型文件的命名规范和路径配置
|
||||
- 定期检查和更新工作流配置以支持新功能
|
||||
- 注意不同模型系列的参数差异和兼容性
|
||||
- 添加新模型时,请遵循测试架构指南确保测试完整性
|
||||
- 在提交代码前务必运行相关测试确保覆盖率达标
|
||||
|
||||
通过遵循这些指南,开发者可以有效地在 LobeChat 中使用和扩展 ComfyUI 功能,为用户提供强大的图像生成和处理能力。
|
||||
@@ -5,7 +5,9 @@ LobeChat is built on the Next.js framework and uses TypeScript as the primary de
|
||||
1. Routing: Define routes (`src/app`).
|
||||
2. Data Structure: Define data structures (`src/types`).
|
||||
3. Business Logic Implementation: Zustand store (`src/store`).
|
||||
4. Page Display: Write static components/pages (`src/app/<new-page>/features/<new-feature>.tsx`).
|
||||
4. Page Display: Write static components/pages. Create features in:
|
||||
- `src/features/<feature-name>/` for **shared global features** (used across multiple pages)
|
||||
- `src/app/<new-page>/features/<feature-name>/` for **page-specific features** (only used in this page)
|
||||
5. Function Binding: Bind the store with page triggers (`const [state, function] = useNewStore(s => [s.state, s.function])`).
|
||||
|
||||
Taking the "Chat Messages" feature as an example, here are the brief steps to implement this feature:
|
||||
@@ -60,7 +62,8 @@ export const useChatStore = create<ChatState>((set) => ({
|
||||
In `src/app/<new-page>/features/<new-feature>.tsx`, we need to create a new page or component to display "Chat Messages". In this file, we can use the Zustand Store created earlier and Ant Design components to build the UI:
|
||||
|
||||
```jsx
|
||||
// src/features/chat/index.tsx
|
||||
// src/app/chat/features/ChatPage/index.tsx
|
||||
// Note: Use src/app/<page>/features/ for page-specific components
|
||||
import { List, Typography } from 'antd';
|
||||
import { useChatStore } from 'src/store/chatStore';
|
||||
|
||||
@@ -82,6 +85,12 @@ const ChatPage = () => {
|
||||
export default ChatPage;
|
||||
```
|
||||
|
||||
> **Note on Feature Organization**: LobeChat uses two patterns for organizing features:
|
||||
> - **Global features** (`src/features/`): Shared components like `ChatInput`, `Conversation` used across the app
|
||||
> - **Page-specific features** (`src/app/<page>/features/`): Components used only within a specific page route
|
||||
>
|
||||
> Choose based on reusability. If unsure, start with page-specific and refactor to global if needed elsewhere.
|
||||
|
||||
## 5. Function Binding
|
||||
|
||||
In a page or component, we need to bind the Zustand Store's state and methods to the UI. In the example above, we have already bound the `messages` state to the `dataSource` property of the list. Now, we also need a method to add new messages. We can define this method in the Zustand Store and then use it in the page or component:
|
||||
|
||||
@@ -5,7 +5,9 @@ LobeChat 基于 Next.js 框架构建,使用 TypeScript 作为主要开发语
|
||||
1. 路由:定义路由 (`src/app`)
|
||||
2. 数据结构: 定义数据结构 ( `src/types` )
|
||||
3. 业务功能实现: zustand store (`src/store`)
|
||||
4. 页面展示:书写静态组件 / 页面 (`src/app/<new-page>/features/<new-feature>.tsx`)
|
||||
4. 页面展示:书写静态组件 / 页面。根据以下方式创建功能组件:
|
||||
- `src/features/<feature-name>/` 用于 **全局共享功能**(跨多个页面使用)
|
||||
- `src/app/<new-page>/features/<feature-name>/` 用于 **页面专属功能**(仅在当前页面使用)
|
||||
5. 功能绑定:绑定 store 与页面的触发 (`const [state,function]= useNewStore(s=>[s.state,s.function])`)
|
||||
|
||||
我们以 "会话消息" 功能为例,以下是实现这个功能的简要步骤:
|
||||
@@ -42,7 +44,7 @@ export type ChatMessage = {
|
||||
|
||||
```ts
|
||||
// src/store/chatStore.ts
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
|
||||
type ChatState = {
|
||||
messages: ChatMessage[];
|
||||
@@ -60,7 +62,8 @@ export const useChatStore = create<ChatState>((set) => ({
|
||||
在 `src/app/<new-page>/features/<new-feature>.tsx` 中,我们需要创建一个新的页面或组件来显示 "会话消息"。在这个文件中,我们可以使用上面创建的 Zustand Store,以及 Ant Design 的组件来构建 UI:
|
||||
|
||||
```jsx
|
||||
// src/features/chat/index.tsx
|
||||
// src/app/chat/features/ChatPage/index.tsx
|
||||
// 注意:使用 src/app/<page>/features/ 放置页面专属组件
|
||||
import { List, Typography } from 'antd';
|
||||
import { useChatStore } from 'src/store/chatStore';
|
||||
|
||||
@@ -82,6 +85,12 @@ const ChatPage = () => {
|
||||
export default ChatPage;
|
||||
```
|
||||
|
||||
> **关于功能组件组织方式的说明**:LobeChat 使用两种模式来组织功能组件:
|
||||
> - **全局功能**(`src/features/`):跨应用共享的组件,如 `ChatInput`、`Conversation` 等
|
||||
> - **页面专属功能**(`src/app/<page>/features/`):仅在特定页面路由中使用的组件
|
||||
>
|
||||
> 根据可复用性选择合适的方式。如果不确定,可以先放在页面专属位置,需要时再重构为全局共享。
|
||||
|
||||
## 5. 功能绑定
|
||||
|
||||
在页面或组件中,我们需要将 Zustand Store 的状态和方法绑定到 UI 上。在上面的示例中,我们已经将 `messages` 状态绑定到了列表的 `dataSource` 属性上。现在,我们还需要一个方法来添加新的消息。我们可以在 Zustand Store 中定义这个方法,然后在页面或组件中使用它:
|
||||
|
||||
@@ -4,37 +4,88 @@ The directory structure of LobeChat is as follows:
|
||||
|
||||
```bash
|
||||
src
|
||||
├── app # Main logic and state management related code for the application
|
||||
├── app # Next.js App Router implementation with route groups and API routes
|
||||
├── components # Reusable UI components
|
||||
├── config # Application configuration files, including client-side and server-side environment variables
|
||||
├── const # Used to define constants, such as action types, route names, etc.
|
||||
├── features # Function modules related to business functions, such as agent settings, plugin development pop-ups, etc.
|
||||
├── hooks # Custom utility hooks reused throughout the application
|
||||
├── layout # Application layout components, such as navigation bars, sidebars, etc.
|
||||
├── libs # Third-party integrations (analytics, OIDC, etc.)
|
||||
├── locales # Internationalization language files
|
||||
├── server # Server-side modules and services
|
||||
├── services # Encapsulated backend service interfaces, such as HTTP requests
|
||||
├── store # Zustand store for state management
|
||||
├── styles # Global styles and CSS-in-JS configurations
|
||||
├── types # TypeScript type definition files
|
||||
└── utils # Common utility functions
|
||||
```
|
||||
|
||||
## app
|
||||
|
||||
In the `app` folder, we organize each route page according to the app router's [Route Groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups) to separately handle the implementation of desktop and mobile code. Taking the file structure of the `welcome` page as an example:
|
||||
The `app` directory follows Next.js 13+ App Router conventions with a sophisticated architecture using [Route Groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups) to organize backend services, platform variants, and application routes:
|
||||
|
||||
```bash
|
||||
welcome
|
||||
├── (desktop) # Desktop implementation
|
||||
│ ├── features # Desktop-specific features
|
||||
│ ├── index.tsx # Main entry file for desktop
|
||||
│ └── layout.desktop.tsx # Desktop layout component
|
||||
├── (mobile) # Mobile implementation
|
||||
│ ├── features # Mobile-specific features
|
||||
│ ├── index.tsx # Main entry file for mobile
|
||||
│ └── layout.mobile.tsx # Mobile layout component
|
||||
├── features # This folder contains features code shared by both desktop and mobile, such as the Banner component
|
||||
│ └── Banner
|
||||
└── page.tsx # This is the main entry file for the page, used to load desktop or mobile code based on the device type
|
||||
app
|
||||
├── (backend)/ # Backend API routes and services
|
||||
│ ├── api/ # REST API endpoints
|
||||
│ │ ├── auth/ # Authentication routes
|
||||
│ │ └── webhooks/ # Webhook handlers
|
||||
│ ├── middleware/ # Request middleware
|
||||
│ ├── oidc/ # OpenID Connect routes
|
||||
│ ├── trpc/ # tRPC API endpoints
|
||||
│ │ ├── async/ # Async tRPC routes
|
||||
│ │ ├── desktop/ # Desktop-specific tRPC routes
|
||||
│ │ ├── edge/ # Edge runtime tRPC routes
|
||||
│ │ ├── lambda/ # Lambda tRPC routes
|
||||
│ │ └── tools/ # Tools tRPC routes
|
||||
│ └── webapi/ # Web API endpoints
|
||||
│ ├── chat/ # Chat-related APIs
|
||||
│ ├── models/ # Model management APIs
|
||||
│ ├── tts/ # Text-to-speech APIs
|
||||
│ └── ...
|
||||
├── [variants]/ # Platform and device variants
|
||||
│ ├── (auth)/ # Authentication pages
|
||||
│ │ ├── login/
|
||||
│ │ ├── signup/
|
||||
│ │ └── next-auth/
|
||||
│ ├── (main)/ # Main application routes
|
||||
│ │ ├── (mobile)/ # Mobile-specific routes
|
||||
│ │ │ └── me/ # Mobile profile pages
|
||||
│ │ ├── _layout/ # Layout components
|
||||
│ │ ├── chat/ # Chat interface
|
||||
│ │ ├── discover/ # Discovery pages
|
||||
│ │ ├── files/ # File management
|
||||
│ │ ├── image/ # Image generation
|
||||
│ │ ├── profile/ # User profile
|
||||
│ │ ├── repos/ # Repository management
|
||||
│ │ └── settings/ # Application settings
|
||||
│ └── @modal/ # Parallel modal routes
|
||||
│ ├── (.)changelog/
|
||||
│ └── _layout/
|
||||
├── desktop/ # Desktop-specific routes
|
||||
│ └── devtools/
|
||||
├── manifest.ts # PWA manifest
|
||||
├── robots.tsx # Robots.txt generation
|
||||
├── sitemap.tsx # Sitemap generation
|
||||
└── sw.ts # Service worker
|
||||
```
|
||||
|
||||
In this way, we can clearly distinguish and manage desktop and mobile code, while also easily reusing code required on both devices, thereby improving development efficiency and maintaining code cleanliness and maintainability.
|
||||
### Architecture Explanation
|
||||
|
||||
**Route Groups:**
|
||||
- `(backend)` - Contains all server-side API routes, middleware, and backend services
|
||||
- `[variants]` - Dynamic route group handling different platform variants and main application pages
|
||||
- `@modal` - Parallel routes for modal dialogs using Next.js parallel routing
|
||||
|
||||
**Platform Organization:**
|
||||
- The architecture supports multiple platforms (web, desktop, mobile) through route organization
|
||||
- Desktop-specific routes are in the `desktop/` directory
|
||||
- Mobile-specific routes are organized under `(main)/(mobile)/`
|
||||
- Shared layouts and components are in `_layout/` directories
|
||||
|
||||
**API Architecture:**
|
||||
- REST APIs in `(backend)/api/` and `(backend)/webapi/`
|
||||
- tRPC endpoints organized by runtime environment (edge, lambda, async, desktop)
|
||||
- Authentication and OIDC handling in dedicated route groups
|
||||
|
||||
This architecture provides clear separation of concerns while maintaining flexibility for different deployment targets and runtime environments.
|
||||
|
||||
@@ -4,37 +4,88 @@ LobeChat 的文件夹目录架构如下:
|
||||
|
||||
```bash
|
||||
src
|
||||
├── app # 应用主要逻辑和状态管理相关的代码
|
||||
├── app # Next.js App Router 实现,包含路由组和 API 路由
|
||||
├── components # 可复用的 UI 组件
|
||||
├── config # 应用的配置文件,包含客户端环境变量与服务端环境变量
|
||||
├── const # 用于定义常量,如 action 类型、路由名等
|
||||
├── features # 与业务功能相关的功能模块,如 Agent 设置、插件开发弹窗等
|
||||
├── hooks # 全应用复用自定义的工具 Hooks
|
||||
├── layout # 应用的布局组件,如导航栏、侧边栏等
|
||||
├── libs # 第三方集成(分析、OIDC 等)
|
||||
├── locales # 国际化的语言文件
|
||||
├── server # 服务端模块和服务
|
||||
├── services # 封装的后端服务接口,如 HTTP 请求
|
||||
├── store # 用于状态管理的 zustand store
|
||||
├── styles # 全局样式和 CSS-in-JS 配置
|
||||
├── types # TypeScript 的类型定义文件
|
||||
└── utils # 通用的工具函数
|
||||
```
|
||||
|
||||
## app
|
||||
|
||||
在 `app` 文件夹中,我们将每个路由页面按照 app router 的 [Route Groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups) 进行组织,以此来分别处理桌面端和移动端的代码实现。以 `welcome` 页面的文件结构为例:
|
||||
`app` 目录遵循 Next.js 13+ App Router 约定,采用复杂的架构,使用 [路由组](https://nextjs.org/docs/app/building-your-application/routing/route-groups) 来组织后端服务、平台变体和应用路由:
|
||||
|
||||
```bash
|
||||
welcome
|
||||
├── (desktop) # 桌面端实现
|
||||
│ ├── features # 桌面端特有的功能
|
||||
│ ├── index.tsx # 桌面端的主入口文件
|
||||
│ └── layout.desktop.tsx # 桌面端的布局组件
|
||||
├── (mobile) # 移动端实现
|
||||
│ ├── features # 移动端特有的功能
|
||||
│ ├── index.tsx # 移动端的主入口文件
|
||||
│ └── layout.mobile.tsx # 移动端的布局组件
|
||||
├── features # 此文件夹包含双端共享的特性代码,如 Banner 组件
|
||||
│ └── Banner
|
||||
└── page.tsx # 此为页面的主入口文件,用于根据设备类型选择加载桌面端或移动端的代码
|
||||
app
|
||||
├── (backend)/ # 后端 API 路由和服务
|
||||
│ ├── api/ # REST API 端点
|
||||
│ │ ├── auth/ # 身份验证路由
|
||||
│ │ └── webhooks/ # Webhook 处理器
|
||||
│ ├── middleware/ # 请求中间件
|
||||
│ ├── oidc/ # OpenID Connect 路由
|
||||
│ ├── trpc/ # tRPC API 端点
|
||||
│ │ ├── async/ # 异步 tRPC 路由
|
||||
│ │ ├── desktop/ # 桌面端专用 tRPC 路由
|
||||
│ │ ├── edge/ # Edge 运行时 tRPC 路由
|
||||
│ │ ├── lambda/ # Lambda tRPC 路由
|
||||
│ │ └── tools/ # 工具 tRPC 路由
|
||||
│ └── webapi/ # Web API 端点
|
||||
│ ├── chat/ # 聊天相关 API
|
||||
│ ├── models/ # 模型管理 API
|
||||
│ ├── tts/ # 文本转语音 API
|
||||
│ └── ...
|
||||
├── [variants]/ # 平台和设备变体
|
||||
│ ├── (auth)/ # 身份验证页面
|
||||
│ │ ├── login/
|
||||
│ │ ├── signup/
|
||||
│ │ └── next-auth/
|
||||
│ ├── (main)/ # 主应用路由
|
||||
│ │ ├── (mobile)/ # 移动端专用路由
|
||||
│ │ │ └── me/ # 移动端个人资料页面
|
||||
│ │ ├── _layout/ # 布局组件
|
||||
│ │ ├── chat/ # 聊天界面
|
||||
│ │ ├── discover/ # 发现页面
|
||||
│ │ ├── files/ # 文件管理
|
||||
│ │ ├── image/ # 图像生成
|
||||
│ │ ├── profile/ # 用户资料
|
||||
│ │ ├── repos/ # 仓库管理
|
||||
│ │ └── settings/ # 应用设置
|
||||
│ └── @modal/ # 并行模态框路由
|
||||
│ ├── (.)changelog/
|
||||
│ └── _layout/
|
||||
├── desktop/ # 桌面端专用路由
|
||||
│ └── devtools/
|
||||
├── manifest.ts # PWA 清单
|
||||
├── robots.tsx # Robots.txt 生成
|
||||
├── sitemap.tsx # 站点地图生成
|
||||
└── sw.ts # Service Worker
|
||||
```
|
||||
|
||||
通过这种方式,我们可以清晰地区分和管理桌面端和移动端的代码,同时也能方便地复用在两种设备上都需要的代码,从而提高开发效率并保持代码的整洁和可维护性。
|
||||
### 架构说明
|
||||
|
||||
**路由组:**
|
||||
- `(backend)` - 包含所有服务端 API 路由、中间件和后端服务
|
||||
- `[variants]` - 处理不同平台变体和主应用页面的动态路由组
|
||||
- `@modal` - 使用 Next.js 并行路由的模态框对话框并行路由
|
||||
|
||||
**平台组织:**
|
||||
- 架构通过路由组织支持多个平台(Web、桌面端、移动端)
|
||||
- 桌面端专用路由位于 `desktop/` 目录中
|
||||
- 移动端专用路由组织在 `(main)/(mobile)/` 下
|
||||
- 共享布局和组件位于 `_layout/` 目录中
|
||||
|
||||
**API 架构:**
|
||||
- `(backend)/api/` 和 `(backend)/webapi/` 中的 REST API
|
||||
- 按运行时环境组织的 tRPC 端点(edge、lambda、async、desktop)
|
||||
- 专用路由组中的身份验证和 OIDC 处理
|
||||
|
||||
这种架构在保持不同部署目标和运行时环境灵活性的同时,提供了清晰的关注点分离。
|
||||
|
||||
@@ -11,13 +11,13 @@ But here is the easier approach that can reduce your pain.
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
First, copy the example environment file to create your development configuration:
|
||||
First, copy the example environment file to create your Docker Compose configuration:
|
||||
|
||||
```bash
|
||||
cp .env.example.development .env.development
|
||||
cp docker-compose/local/.env.example docker-compose/local/.env
|
||||
```
|
||||
|
||||
This file contains all necessary environment variables for server-side database mode and configures:
|
||||
Edit `docker-compose/local/.env` as needed for your development setup. This file contains all necessary environment variables for the Docker services and configures:
|
||||
|
||||
- **Service Mode**: `NEXT_PUBLIC_SERVICE_MODE=server`
|
||||
- **Database**: PostgreSQL with connection string
|
||||
@@ -72,7 +72,7 @@ When working with image generation features (text-to-image, image-to-image), the
|
||||
|
||||
### Image Generation Configuration
|
||||
|
||||
The existing Docker Compose configuration already includes MinIO storage service and all necessary environment variables in `.env.example.development`. No additional setup is required.
|
||||
The existing Docker Compose configuration already includes MinIO storage service and all necessary environment variables in `docker-compose/local/.env.example`. No additional setup is required.
|
||||
|
||||
### Image Generation Architecture
|
||||
|
||||
@@ -84,7 +84,7 @@ The image generation feature requires:
|
||||
|
||||
### Storage Configuration
|
||||
|
||||
The `.env.example.development` file includes all necessary S3 environment variables:
|
||||
The `docker-compose/local/.env.example` file includes all necessary S3 environment variables:
|
||||
|
||||
```bash
|
||||
# S3 Storage Configuration (MinIO for local development)
|
||||
|
||||
@@ -11,13 +11,13 @@ LobeChat 提供了内置的客户端数据库体验。
|
||||
|
||||
### 环境配置
|
||||
|
||||
首先,复制示例环境文件来创建你的开发配置:
|
||||
首先,复制示例环境文件来创建你的 Docker Compose 配置:
|
||||
|
||||
```bash
|
||||
cp .env.example.development .env.development
|
||||
cp docker-compose/local/.env.example docker-compose/local/.env
|
||||
```
|
||||
|
||||
此文件包含服务端数据库模式所需的所有环境变量,配置了:
|
||||
根据需要编辑 `docker-compose/local/.env` 文件以适应你的开发设置。此文件包含 Docker 服务所需的所有环境变量,配置了:
|
||||
|
||||
- **服务模式**: `NEXT_PUBLIC_SERVICE_MODE=server`
|
||||
- **数据库**: 带连接字符串的 PostgreSQL
|
||||
@@ -72,7 +72,7 @@ docker-compose -f docker-compose.development.yml ps
|
||||
|
||||
### 图像生成配置
|
||||
|
||||
现有的 Docker Compose 配置已经包含了 MinIO 存储服务以及 `.env.example.development` 中的所有必要环境变量。无需额外配置。
|
||||
现有的 Docker Compose 配置已经包含了 MinIO 存储服务以及 `docker-compose/local/.env.example` 中的所有必要环境变量。无需额外配置。
|
||||
|
||||
### 图像生成架构
|
||||
|
||||
@@ -84,7 +84,7 @@ docker-compose -f docker-compose.development.yml ps
|
||||
|
||||
### 存储配置
|
||||
|
||||
`.env.example.development` 文件包含所有必要的 S3 环境变量:
|
||||
`docker-compose/local/.env.example` 文件包含所有必要的 S3 环境变量:
|
||||
|
||||
```bash
|
||||
# S3 存储配置(本地开发使用 MinIO)
|
||||
|
||||
@@ -16,6 +16,7 @@ table agents {
|
||||
provider text
|
||||
system_role text
|
||||
tts jsonb
|
||||
virtual boolean [default: false]
|
||||
opening_message text
|
||||
opening_questions text[] [default: `[]`]
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
@@ -317,6 +318,24 @@ table message_chunks {
|
||||
}
|
||||
}
|
||||
|
||||
table message_groups {
|
||||
id varchar(255) [pk, not null]
|
||||
topic_id text
|
||||
user_id text [not null]
|
||||
parent_group_id varchar(255)
|
||||
parent_message_id text
|
||||
title varchar(255)
|
||||
description text
|
||||
client_id varchar(255)
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(client_id, user_id) [name: 'message_groups_client_id_user_id_unique', unique]
|
||||
}
|
||||
}
|
||||
|
||||
table message_plugins {
|
||||
id text [pk, not null]
|
||||
tool_call_id text
|
||||
@@ -410,6 +429,7 @@ table messages {
|
||||
agent_id text
|
||||
group_id text
|
||||
target_id text
|
||||
message_group_id varchar(255)
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
@@ -925,6 +945,7 @@ table user_settings {
|
||||
system_agent jsonb
|
||||
default_agent jsonb
|
||||
tool jsonb
|
||||
image jsonb
|
||||
}
|
||||
|
||||
table users {
|
||||
@@ -945,6 +966,127 @@ table users {
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
}
|
||||
|
||||
table user_memories {
|
||||
id varchar(255) [pk, not null]
|
||||
user_id text
|
||||
memory_category varchar(255)
|
||||
memory_layer varchar(255)
|
||||
memory_type varchar(255)
|
||||
title varchar(255)
|
||||
summary text
|
||||
summary_vector_1024 vector(1024)
|
||||
details text
|
||||
details_vector_1024 vector(1024)
|
||||
status varchar(255)
|
||||
accessed_count bigint [default: 0]
|
||||
last_accessed_at "timestamp with time zone" [not null]
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
summary_vector_1024 [name: 'user_memories_summary_vector_1024_index']
|
||||
details_vector_1024 [name: 'user_memories_details_vector_1024_index']
|
||||
}
|
||||
}
|
||||
|
||||
table user_memories_contexts {
|
||||
id varchar(255) [pk, not null]
|
||||
user_memory_ids jsonb
|
||||
labels jsonb
|
||||
extracted_labels jsonb
|
||||
associated_objects jsonb
|
||||
associated_subjects jsonb
|
||||
title text
|
||||
title_vector vector(1024)
|
||||
description text
|
||||
description_vector vector(1024)
|
||||
type varchar(255)
|
||||
current_status text
|
||||
score_impact numeric [default: 0]
|
||||
score_urgency numeric [default: 0]
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
title_vector [name: 'user_memories_contexts_title_vector_index']
|
||||
description_vector [name: 'user_memories_contexts_description_vector_index']
|
||||
type [name: 'user_memories_contexts_type_index']
|
||||
}
|
||||
}
|
||||
|
||||
table user_memories_experiences {
|
||||
id varchar(255) [pk, not null]
|
||||
user_memory_id text
|
||||
labels jsonb
|
||||
extracted_labels jsonb
|
||||
type varchar(255)
|
||||
situation text
|
||||
situation_vector vector(1024)
|
||||
reasoning text
|
||||
possible_outcome text
|
||||
action text
|
||||
action_vector vector(1024)
|
||||
key_learning text
|
||||
key_learning_vector vector(1024)
|
||||
metadata jsonb
|
||||
score_confidence real [default: 0]
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
situation_vector [name: 'user_memories_experiences_situation_vector_index']
|
||||
action_vector [name: 'user_memories_experiences_action_vector_index']
|
||||
key_learning_vector [name: 'user_memories_experiences_key_learning_vector_index']
|
||||
type [name: 'user_memories_experiences_type_index']
|
||||
}
|
||||
}
|
||||
|
||||
table user_memories_identities {
|
||||
current_focuses text
|
||||
description text
|
||||
description_vector vector(1024)
|
||||
experience text
|
||||
extracted_labels jsonb
|
||||
id varchar(255) [pk, not null]
|
||||
labels jsonb
|
||||
relationship text
|
||||
role text
|
||||
type varchar(255)
|
||||
user_memory_id text
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
description_vector [name: 'user_memories_identities_description_vector_index']
|
||||
type [name: 'user_memories_identities_type_index']
|
||||
}
|
||||
}
|
||||
|
||||
table user_memories_preferences {
|
||||
id varchar(255) [pk, not null]
|
||||
context_id varchar(255)
|
||||
user_memory_id varchar(255)
|
||||
labels jsonb
|
||||
extracted_labels jsonb
|
||||
extracted_scopes jsonb
|
||||
conclusion_directives text
|
||||
conclusion_directives_vector vector(1024)
|
||||
type varchar(255)
|
||||
suggestions text
|
||||
score_priority numeric [default: 0]
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
conclusion_directives_vector [name: 'user_memories_preferences_conclusion_directives_vector_index']
|
||||
}
|
||||
}
|
||||
|
||||
ref: agents_files.file_id > files.id
|
||||
|
||||
ref: agents_files.agent_id > agents.id
|
||||
@@ -995,6 +1137,12 @@ ref: generations.generation_batch_id > generation_batches.id
|
||||
|
||||
ref: generations.async_task_id - async_tasks.id
|
||||
|
||||
ref: message_groups.user_id - users.id
|
||||
|
||||
ref: message_groups.topic_id - topics.id
|
||||
|
||||
ref: message_groups.parent_group_id > message_groups.id
|
||||
|
||||
ref: messages_files.file_id > files.id
|
||||
|
||||
ref: messages_files.message_id > messages.id
|
||||
@@ -1007,6 +1155,8 @@ ref: messages.topic_id - topics.id
|
||||
|
||||
ref: threads.source_message_id - messages.id
|
||||
|
||||
ref: messages.message_group_id > message_groups.id
|
||||
|
||||
ref: sessions.group_id - session_groups.id
|
||||
|
||||
ref: topic_documents.document_id > documents.id
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
# 集成测试指南
|
||||
|
||||
## 概述
|
||||
|
||||
集成测试验证多个模块协同工作的正确性,确保完整的调用链路(Router → Service → Model → Database)正常运行。
|
||||
|
||||
## 为什么需要集成测试?
|
||||
|
||||
即使单元测试覆盖率很高(80%+),仍可能出现集成问题:
|
||||
|
||||
### 常见问题示例
|
||||
|
||||
```typescript
|
||||
// ❌ 问题:参数在调用链中丢失
|
||||
// Router 层
|
||||
const messageId = await messageModel.create({
|
||||
content: 'test',
|
||||
sessionId: 'xxx',
|
||||
topicId: 'yyy', // ← 传入了 topicId
|
||||
});
|
||||
|
||||
// Model 层(假设实现有问题)
|
||||
async create(data) {
|
||||
return this.db.insert(messages).values({
|
||||
content: data.content,
|
||||
sessionId: data.sessionId,
|
||||
// ❌ 忘记传递 topicId
|
||||
});
|
||||
}
|
||||
|
||||
// 结果:单元测试通过(因为 mock 了 Model),但实际运行时 topicId 丢失
|
||||
```
|
||||
|
||||
### 集成测试能发现的问题
|
||||
|
||||
1. **参数传递遗漏**: containerId、threadId、topicId 等在调用链中丢失
|
||||
2. **数据库约束**: 外键关系、级联删除等在 mock 中无法验证
|
||||
3. **事务完整性**: 跨表操作的原子性
|
||||
4. **权限验证**: 跨用户访问控制
|
||||
5. **真实场景**: 模拟用户的完整操作流程
|
||||
|
||||
## 运行集成测试
|
||||
|
||||
```bash
|
||||
# 运行所有集成测试
|
||||
pnpm test:integration
|
||||
|
||||
# 运行特定文件
|
||||
pnpm vitest tests/integration/routers/message.integration.test.ts
|
||||
|
||||
# 监听模式
|
||||
pnpm vitest tests/integration --watch
|
||||
|
||||
# 生成覆盖率报告
|
||||
pnpm test:integration --coverage
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
tests/integration/
|
||||
├── README.md # 集成测试说明
|
||||
├── setup.ts # 通用设置和工具函数
|
||||
└── routers/ # Router 层集成测试
|
||||
├── message.integration.test.ts # Message Router 测试
|
||||
├── session.integration.test.ts # Session Router 测试
|
||||
├── topic.integration.test.ts # Topic Router 测试
|
||||
└── chat-flow.integration.test.ts # 完整聊天流程测试
|
||||
```
|
||||
|
||||
## 编写集成测试
|
||||
|
||||
### 基本模板
|
||||
|
||||
```typescript
|
||||
// @vitest-environment node
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { getTestDB } from '@/database/models/__tests__/_util';
|
||||
import { messages, sessions, users } from '@/database/schemas';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
import { messageRouter } from '@/server/routers/lambda/message';
|
||||
|
||||
import { cleanupTestUser, createTestContext, createTestUser } from '../setup';
|
||||
|
||||
describe('Your Feature Integration Tests', () => {
|
||||
let serverDB: LobeChatDatabase;
|
||||
let userId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// 1. 获取测试数据库
|
||||
serverDB = await getTestDB();
|
||||
|
||||
// 2. 创建测试用户
|
||||
userId = await createTestUser(serverDB);
|
||||
|
||||
// 3. 准备其他测试数据
|
||||
// ...
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestUser(serverDB, userId);
|
||||
});
|
||||
|
||||
it('should do something', async () => {
|
||||
// 1. 创建 tRPC caller
|
||||
const caller = messageRouter.createCaller(createTestContext(userId));
|
||||
|
||||
// 2. 执行操作
|
||||
const result = await caller.someMethod({
|
||||
/* params */
|
||||
});
|
||||
|
||||
// 3. 验证结果
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// 4. 🔥 关键:从数据库验证
|
||||
const [dbRecord] = await serverDB.select().from(messages).where(eq(messages.id, result));
|
||||
|
||||
expect(dbRecord).toMatchObject({
|
||||
// 验证所有关键字段
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 最佳实践
|
||||
|
||||
#### 1. 测试完整的调用链路
|
||||
|
||||
```typescript
|
||||
it('should create message with correct associations', async () => {
|
||||
const caller = messageRouter.createCaller(createTestContext(userId));
|
||||
|
||||
// 执行操作
|
||||
const messageId = await caller.createMessage({
|
||||
content: 'Test',
|
||||
sessionId: testSessionId,
|
||||
topicId: testTopicId,
|
||||
});
|
||||
|
||||
// ✅ 从数据库验证,而不是只验证返回值
|
||||
const [message] = await serverDB.select().from(messages).where(eq(messages.id, messageId));
|
||||
|
||||
expect(message.sessionId).toBe(testSessionId);
|
||||
expect(message.topicId).toBe(testTopicId);
|
||||
expect(message.userId).toBe(userId);
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. 测试级联操作
|
||||
|
||||
```typescript
|
||||
it('should cascade delete messages when session is deleted', async () => {
|
||||
const sessionCaller = sessionRouter.createCaller(createTestContext(userId));
|
||||
const messageCaller = messageRouter.createCaller(createTestContext(userId));
|
||||
|
||||
// 创建 session 和 messages
|
||||
const sessionId = await sessionCaller.createSession({
|
||||
/* ... */
|
||||
});
|
||||
await messageCaller.createMessage({ sessionId /* ... */ });
|
||||
|
||||
// 删除 session
|
||||
await sessionCaller.removeSession({ id: sessionId });
|
||||
|
||||
// ✅ 验证相关消息也被删除
|
||||
const remainingMessages = await serverDB
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(eq(messages.sessionId, sessionId));
|
||||
|
||||
expect(remainingMessages).toHaveLength(0);
|
||||
});
|
||||
```
|
||||
|
||||
#### 3. 测试跨 Router 协作
|
||||
|
||||
```typescript
|
||||
it('should handle complete chat flow', async () => {
|
||||
const sessionCaller = sessionRouter.createCaller(createTestContext(userId));
|
||||
const topicCaller = topicRouter.createCaller(createTestContext(userId));
|
||||
const messageCaller = messageRouter.createCaller(createTestContext(userId));
|
||||
|
||||
// 1. 创建 session
|
||||
const sessionId = await sessionCaller.createSession({
|
||||
/* ... */
|
||||
});
|
||||
|
||||
// 2. 创建 topic
|
||||
const topicId = await topicCaller.createTopic({ sessionId /* ... */ });
|
||||
|
||||
// 3. 创建 message
|
||||
const messageId = await messageCaller.createMessage({
|
||||
sessionId,
|
||||
topicId,
|
||||
/* ... */
|
||||
});
|
||||
|
||||
// ✅ 验证完整的关联关系
|
||||
const [message] = await serverDB.select().from(messages).where(eq(messages.id, messageId));
|
||||
|
||||
expect(message.sessionId).toBe(sessionId);
|
||||
expect(message.topicId).toBe(topicId);
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. 测试错误场景
|
||||
|
||||
```typescript
|
||||
it('should prevent cross-user access', async () => {
|
||||
// 用户 A 创建 session
|
||||
const sessionId = await sessionRouter.createCaller(createTestContext(userA)).createSession({
|
||||
/* ... */
|
||||
});
|
||||
|
||||
// 用户 B 尝试访问
|
||||
const callerB = messageRouter.createCaller(createTestContext(userB));
|
||||
|
||||
// ✅ 应该抛出错误
|
||||
await expect(
|
||||
callerB.createMessage({
|
||||
sessionId,
|
||||
content: 'Unauthorized',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
```
|
||||
|
||||
#### 5. 测试并发场景
|
||||
|
||||
```typescript
|
||||
it('should handle concurrent operations', async () => {
|
||||
const caller = messageRouter.createCaller(createTestContext(userId));
|
||||
|
||||
// 并发创建多个消息
|
||||
const promises = Array.from({ length: 10 }, (_, i) =>
|
||||
caller.createMessage({
|
||||
content: `Message ${i}`,
|
||||
sessionId: testSessionId,
|
||||
}),
|
||||
);
|
||||
|
||||
const messageIds = await Promise.all(promises);
|
||||
|
||||
// ✅ 验证所有消息都创建成功且唯一
|
||||
expect(messageIds).toHaveLength(10);
|
||||
expect(new Set(messageIds).size).toBe(10);
|
||||
});
|
||||
```
|
||||
|
||||
### 数据隔离
|
||||
|
||||
每个测试用例应该独立,不依赖其他测试:
|
||||
|
||||
```typescript
|
||||
beforeEach(async () => {
|
||||
// 为每个测试创建新的数据
|
||||
userId = await createTestUser(serverDB);
|
||||
testSessionId = await createTestSession(serverDB, userId);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// 清理测试数据
|
||||
await cleanupTestUser(serverDB, userId);
|
||||
});
|
||||
```
|
||||
|
||||
### 测试命名
|
||||
|
||||
使用清晰的命名描述测试意图:
|
||||
|
||||
```typescript
|
||||
// ✅ 好的命名
|
||||
it('should create message with correct sessionId and topicId');
|
||||
it('should cascade delete messages when session is deleted');
|
||||
it('should prevent cross-user access to messages');
|
||||
|
||||
// ❌ 不好的命名
|
||||
it('test message creation');
|
||||
it('test delete');
|
||||
```
|
||||
|
||||
## 与单元测试的区别
|
||||
|
||||
| 维度 | 单元测试 | 集成测试 |
|
||||
| ------- | --------- | ------- |
|
||||
| **范围** | 单个函数 / 类 | 多个模块协作 |
|
||||
| **依赖** | Mock 外部依赖 | 使用真实依赖 |
|
||||
| **数据库** | Mock | 真实测试数据库 |
|
||||
| **速度** | 快(毫秒级) | 慢(秒级) |
|
||||
| **数量** | 多(60%) | 少(30%) |
|
||||
| **目的** | 验证逻辑正确性 | 验证集成正确性 |
|
||||
|
||||
## 测试金字塔
|
||||
|
||||
```
|
||||
/\
|
||||
/E2E\ ← 10% (关键业务流程)
|
||||
/------\
|
||||
/ 集成 \ ← 30% (API 集成测试) ⭐ 本指南重点
|
||||
/----------\
|
||||
/ 单元测试 \ ← 60% (已有 80%+)
|
||||
/--------------\
|
||||
```
|
||||
|
||||
## 覆盖目标
|
||||
|
||||
### 优先级 P0(必须覆盖)
|
||||
|
||||
- ✅ 跨层级的 ID 传递(sessionId、topicId、containerId、threadId)
|
||||
- ✅ 权限验证(用户只能访问自己的资源)
|
||||
- ✅ 级联删除(删除 session 时相关数据也删除)
|
||||
- ✅ 外键约束(不能创建不存在的关联)
|
||||
|
||||
### 优先级 P1(应该覆盖)
|
||||
|
||||
- 并发场景(多个请求同时操作)
|
||||
- 分页查询(正确的数据分页)
|
||||
- 搜索功能(关键词搜索)
|
||||
- 批量操作(批量创建 / 删除)
|
||||
|
||||
### 优先级 P2(可以覆盖)
|
||||
|
||||
- 统计功能(计数、排名)
|
||||
- 复杂查询(多条件筛选)
|
||||
- 性能测试(大量数据场景)
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 查看测试数据库状态
|
||||
|
||||
```typescript
|
||||
it('debug test', async () => {
|
||||
// 执行操作
|
||||
await caller.createMessage({
|
||||
/* ... */
|
||||
});
|
||||
|
||||
// 打印数据库状态
|
||||
const allMessages = await serverDB.select().from(messages);
|
||||
console.log('All messages:', allMessages);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 使用 Drizzle Studio
|
||||
|
||||
```bash
|
||||
# 启动 Drizzle Studio 查看测试数据库
|
||||
pnpm db:studio
|
||||
```
|
||||
|
||||
### 3. 保留测试数据
|
||||
|
||||
```typescript
|
||||
afterEach(async () => {
|
||||
// 临时注释掉清理代码,保留数据用于调试
|
||||
// await cleanupTestUser(serverDB, userId);
|
||||
});
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 集成测试很慢怎么办?
|
||||
|
||||
A:
|
||||
|
||||
1. 只测试关键路径,不要过度测试
|
||||
2. 使用 `test.concurrent` 并行执行独立的测试
|
||||
3. 优化测试数据准备,避免重复创建
|
||||
|
||||
### Q: 测试之间相互影响怎么办?
|
||||
|
||||
A:
|
||||
|
||||
1. 确保每个测试使用独立的 userId
|
||||
2. 在 `afterEach` 中彻底清理数据
|
||||
3. 使用事务隔离(如果数据库支持)
|
||||
|
||||
### Q: 如何测试需要认证的 API?
|
||||
|
||||
A: 使用 `createTestContext(userId)` 创建带认证信息的上下文:
|
||||
|
||||
```typescript
|
||||
const caller = messageRouter.createCaller(createTestContext(userId));
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Vitest 文档](https://vitest.dev/)
|
||||
- [Drizzle ORM 文档](https://orm.drizzle.team/)
|
||||
- [tRPC 测试指南](https://trpc.io/docs/server/testing)
|
||||
- [测试金字塔](https://martinfowler.com/articles/practical-test-pyramid.html)
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎补充更多集成测试用例!请参考现有测试文件的风格。
|
||||
@@ -31,20 +31,30 @@ You can achieve various feature combinations using the above configuration synta
|
||||
their default values).
|
||||
</Callout>
|
||||
|
||||
| Configuration Item | Description | Default Value |
|
||||
| ------------------------- | ----------------------------------------------- | ------------- |
|
||||
| `webrtc_sync` | Enables WebRTC sync functionality. | Disabled |
|
||||
| `language_model_settings` | Enables language model settings. | Enabled |
|
||||
| `openai_api_key` | Allows users to customize the OpenAI API Key. | Enabled |
|
||||
| `openai_proxy_url` | Allows users to customize the OpenAI proxy URL. | Enabled |
|
||||
| `create_session` | Allows users to create sessions. | Enabled |
|
||||
| `edit_agent` | Allows users to edit assistants. | Enabled |
|
||||
| `dalle` | Enables the DALL-E functionality. | Enabled |
|
||||
| `check_updates` | Allows checking for updates. | Enabled |
|
||||
| `welcome_suggest` | Displays welcome suggestions. | Enabled |
|
||||
| `market` | Enables the assistant market functionality. | Enabled |
|
||||
| `speech_to_text` | Enables speech-to-text functionality. | Enabled |
|
||||
| `knowledge_base` | Enables the knowledge base functionality. | Enabled |
|
||||
| `clerk_sign_up` | Enables the Clerk SignUp functionality. | Enabled |
|
||||
| Configuration Item | Description | Default Value |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `check_updates` | Allows checking for updates. | Enabled |
|
||||
| `pin_list` | Controls pinned agent list display in sidebar. | Disabled |
|
||||
| `language_model_settings` | Enables language model settings. | Enabled |
|
||||
| `provider_settings` | Controls model provider settings display. | Enabled |
|
||||
| `openai_api_key` | Allows users to customize the OpenAI API Key. | Enabled |
|
||||
| `openai_proxy_url` | Allows users to customize the OpenAI proxy URL. | Enabled |
|
||||
| `api_key_manage` | Controls access to API key management page (/profile/apikey). | Disabled |
|
||||
| `create_session` | Allows users to create sessions. | Enabled |
|
||||
| `edit_agent` | Allows users to edit assistants. | Enabled |
|
||||
| `plugins` | Controls plugin functionality in chat and agent settings. | Enabled |
|
||||
| `dalle` | Enables the DALL-E functionality. | Enabled |
|
||||
| `ai_image` | Controls AI image generation feature and page (/image). | Enabled |
|
||||
| `speech_to_text` | Enables speech-to-text functionality. | Enabled |
|
||||
| `token_counter` | Reserved for token counter display. | Enabled |
|
||||
| `welcome_suggest` | Displays welcome suggestions. | Enabled |
|
||||
| `changelog` | Controls changelog modal/page display. | Enabled |
|
||||
| `clerk_sign_up` | Enables the Clerk SignUp functionality. | Enabled |
|
||||
| `market` | Enables the assistant market functionality. | Enabled |
|
||||
| `knowledge_base` | Enables the knowledge base functionality. | Enabled |
|
||||
| `rag_eval` | Controls RAG evaluation feature (/repos/\[id]/evals). | Disabled |
|
||||
| `cloud_promotion` | Controls cloud service promotion link display in user menu. | Disabled |
|
||||
| `commercial_hide_github` | Hides GitHub-related links in settings footer (requires commercial license). | Disabled |
|
||||
| `commercial_hide_docs` | Hides documentation and help menu including changelog, docs, and feedback (requires commercial license). | Disabled |
|
||||
|
||||
You can always check the [featureFlags](https://github.com/lobehub/lobe-chat/blob/main/src/config/featureFlags/schema.ts) to get the latest list of feature flags.
|
||||
|
||||
@@ -28,20 +28,30 @@ tags:
|
||||
关键字,你需要手动控制所有的功能标志(否则它们会采用对应的默认值)。
|
||||
</Callout>
|
||||
|
||||
| 配置项 | 解释 | 默认值 |
|
||||
| ------------------------- | ----------------------- | --- |
|
||||
| `webrtc_sync` | 启用 WebRTC 同步功能。 | 关闭 |
|
||||
| `language_model_settings` | 启用语言模型设置。 | 开启 |
|
||||
| `openai_api_key` | 允许用户自定义 OpenAI API Key。 | 开启 |
|
||||
| `openai_proxy_url` | 允许用户自定义 OpenAI 代理 URL。 | 开启 |
|
||||
| `create_session` | 允许用户创建会话。 | 开启 |
|
||||
| `edit_agent` | 允许用户编辑助手。 | 开启 |
|
||||
| `dalle` | 启用 DALL-E 功能。 | 开启 |
|
||||
| `check_updates` | 允许检查更新。 | 开启 |
|
||||
| `welcome_suggest` | 显示欢迎建议。 | 开启 |
|
||||
| `market` | 启用助手市场功能。 | 开启 |
|
||||
| `speech_to_text` | 启用语音转文本功能。 | 开启 |
|
||||
| `knowledge_base` | 启用知识库功能。 | 开启 |
|
||||
| `clerk_sign_up` | 启用 Clerk 注册功能。 | 开启 |
|
||||
| 配置项 | 解释 | 默认值 |
|
||||
| ------------------------- | ------------------------------------ | --- |
|
||||
| `check_updates` | 允许检查更新。 | 开启 |
|
||||
| `pin_list` | 控制侧边栏中置顶助手列表的显示。 | 关闭 |
|
||||
| `language_model_settings` | 启用语言模型设置。 | 开启 |
|
||||
| `provider_settings` | 控制模型供应商设置的显示。 | 开启 |
|
||||
| `openai_api_key` | 允许用户自定义 OpenAI API Key。 | 开启 |
|
||||
| `openai_proxy_url` | 允许用户自定义 OpenAI 代理 URL。 | 开启 |
|
||||
| `api_key_manage` | 控制 API 密钥管理页面 (/profile/apikey) 的访问。 | 关闭 |
|
||||
| `create_session` | 允许用户创建会话。 | 开启 |
|
||||
| `edit_agent` | 允许用户编辑助手。 | 开启 |
|
||||
| `plugins` | 控制聊天和助手设置中的插件功能。 | 开启 |
|
||||
| `dalle` | 启用 DALL-E 功能。 | 开启 |
|
||||
| `ai_image` | 控制 AI 图像生成功能和页面 (/image)。 | 开启 |
|
||||
| `speech_to_text` | 启用语音转文本功能。 | 开启 |
|
||||
| `token_counter` | 保留用于令牌计数器显示。 | 开启 |
|
||||
| `welcome_suggest` | 显示欢迎建议。 | 开启 |
|
||||
| `changelog` | 控制更新日志弹窗 / 页面的显示。 | 开启 |
|
||||
| `clerk_sign_up` | 启用 Clerk 注册功能。 | 开启 |
|
||||
| `market` | 启用助手市场功能。 | 开启 |
|
||||
| `knowledge_base` | 启用知识库功能。 | 开启 |
|
||||
| `rag_eval` | 控制 RAG 评估功能 (/repos/\[id]/evals)。 | 关闭 |
|
||||
| `cloud_promotion` | 控制用户菜单中云服务推广链接的显示。 | 关闭 |
|
||||
| `commercial_hide_github` | 隐藏设置页面底部的 GitHub 相关链接(需要商业授权)。 | 关闭 |
|
||||
| `commercial_hide_docs` | 隐藏文档和帮助菜单,包括更新日志、文档和反馈(需要商业授权)。 | 关闭 |
|
||||
|
||||
你可以随时检查 [featureFlags](https://github.com/lobehub/lobe-chat/blob/main/src/config/featureFlags/schema.ts) 以获取最新的特性标志列表。
|
||||
|
||||
@@ -3,6 +3,7 @@ title: LobeChat Authentication Service Environment Variables
|
||||
description: >-
|
||||
Explore the essential environment variables for configuring authentication services in LobeChat, including OAuth SSO, NextAuth settings, and provider-specific details.
|
||||
|
||||
|
||||
tags:
|
||||
- Authentication Service
|
||||
- OAuth SSO
|
||||
@@ -279,6 +280,22 @@ LobeChat provides a complete authentication service capability when deployed. Th
|
||||
- Default: `-`
|
||||
- Example: `https://your-instance.okta.com`
|
||||
|
||||
### Feishu
|
||||
|
||||
#### `AUTH_FEISHU_APP_ID`
|
||||
|
||||
- Type: Required
|
||||
- Description: App ID of the Feishu application.
|
||||
- Default: `-`
|
||||
- Example: `cli_9f7b1e1e1e1e1e1e`
|
||||
|
||||
#### `AUTH_FEISHU_APP_SECRET`
|
||||
|
||||
- Type: Required
|
||||
- Description: App Secret of the Feishu application.
|
||||
- Default: `-`
|
||||
- Example: `AlHxxX1e1e1e1e1e1e1e1e1e1e1e1e1e`
|
||||
|
||||
### Generic OIDC
|
||||
|
||||
#### `AUTH_GENERIC_OIDC_ID`
|
||||
|
||||
@@ -152,6 +152,18 @@ For specific content, please refer to the [Feature Flags](/docs/self-hosting/adv
|
||||
- Default: -
|
||||
- Example: `https://cdn.example.com`
|
||||
|
||||
## AI Image
|
||||
|
||||
### `AI_IMAGE_DEFAULT_IMAGE_NUM`
|
||||
|
||||
- Type: Optional
|
||||
- Description: Sets the default number of images to generate for AI image generation. Users can still override this value in their settings.
|
||||
- Default: `4`
|
||||
- Example: `6`
|
||||
- Range: `1-20`
|
||||
|
||||
This environment variable allows administrators to customize the default image generation count for their deployment. The value must be between 1 and 20. If not set, it defaults to 4. Users can still adjust this value in their personal settings.
|
||||
|
||||
## Plugin Service
|
||||
|
||||
### `PLUGINS_INDEX_URL`
|
||||
|
||||
@@ -148,6 +148,18 @@ LobeChat 在部署时提供了一些额外的配置项,你可以使用环境
|
||||
- 默认值:-
|
||||
- 示例:`https://cdn.example.com`
|
||||
|
||||
## AI 图像
|
||||
|
||||
### `AI_IMAGE_DEFAULT_IMAGE_NUM`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:设置 AI 图像生成的默认图片数量。用户仍可在个人设置中覆盖此值。
|
||||
- 默认值:`4`
|
||||
- 示例:`6`
|
||||
- 范围:`1-20`
|
||||
|
||||
此环境变量允许管理员为其部署自定义默认图片生成数量。值必须在 1 到 20 之间。如果未设置,默认为 4。用户仍可在个人设置中调整此值。
|
||||
|
||||
## 插件服务
|
||||
|
||||
### `PLUGINS_INDEX_URL`
|
||||
|
||||
@@ -651,6 +651,58 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
|
||||
|
||||
The above example disables all models first, then enables `fal-ai/flux/schnell` and `fal-ai/flux-pro/kontext` (displayed as `FLUX.1 Kontext [pro]`).
|
||||
|
||||
## ComfyUI
|
||||
|
||||
### `COMFYUI_BASE_URL`
|
||||
|
||||
- Type: Optional
|
||||
- Description: The base URL address of the ComfyUI service
|
||||
- Default: `http://localhost:8188`
|
||||
- Example: `http://192.168.1.100:8188` or `https://my-comfyui-server.com`
|
||||
|
||||
### `COMFYUI_AUTH_TYPE`
|
||||
|
||||
- Type: Optional
|
||||
- Description: The authentication type for ComfyUI, supporting 4 authentication methods
|
||||
- `none`: No authentication (default)
|
||||
- `basic`: Basic authentication (username + password)
|
||||
- `bearer`: Bearer Token authentication (API key)
|
||||
- `custom`: Custom request header authentication
|
||||
- Default: `none`
|
||||
- Example: `basic`
|
||||
|
||||
### `COMFYUI_API_KEY`
|
||||
|
||||
- Type: Optional
|
||||
- Description: The API key used when the authentication type is `bearer`
|
||||
- Default: -
|
||||
- Example: `sk-xxxxxx...xxxxxx`
|
||||
|
||||
### `COMFYUI_USERNAME`
|
||||
|
||||
- Type: Optional
|
||||
- Description: The username used when the authentication type is `basic`
|
||||
- Default: -
|
||||
- Example: `admin`
|
||||
|
||||
### `COMFYUI_PASSWORD`
|
||||
|
||||
- Type: Optional
|
||||
- Description: The password used when the authentication type is `basic`
|
||||
- Default: -
|
||||
- Example: `password123`
|
||||
|
||||
### `COMFYUI_CUSTOM_HEADERS`
|
||||
|
||||
- Type: Optional
|
||||
- Description: Custom request headers used when the authentication type is `custom`, requires JSON format string
|
||||
- Default: -
|
||||
- Example: `{"X-Auth-Token": "your-token", "X-Custom-Header": "value"}`
|
||||
|
||||
<Callout type={'info'}>
|
||||
ComfyUI supports multiple authentication methods. Please choose the appropriate authentication type and corresponding authentication parameters according to your ComfyUI service configuration. If your ComfyUI service has no authentication set up, you can skip configuring authentication-related environment variables.
|
||||
</Callout>
|
||||
|
||||
## BFL
|
||||
|
||||
### `ENABLED_BFL`
|
||||
@@ -717,4 +769,20 @@ NewAPI is a multi-provider model aggregation service that supports automatic mod
|
||||
- Default: `-`
|
||||
- Example: `-all,+vercel-model-1,+vercel-model-2=vercel-special`
|
||||
|
||||
## Cerebras
|
||||
|
||||
### `CEREBRAS_API_KEY`
|
||||
|
||||
- Type: Required
|
||||
- Description: This is the API key you applied for in the Cerebras service.
|
||||
- Default: -
|
||||
- Example: `csk-xxxxxx...xxxxxx`
|
||||
|
||||
### `CEREBRAS_MODEL_LIST`
|
||||
|
||||
- Type: Optional
|
||||
- Description: Used to control the Cerebras model list. Use `+` to add a model, `-` to hide a model, and `model_name=display_name` to customize the display name of a model. Separate multiple entries with commas. The definition syntax follows the same rules as other providers' model lists.
|
||||
- Default: `-`
|
||||
- Example: `-all,+cerebras-model-1,+cerebras-model-2=cerebras-special`
|
||||
|
||||
[model-list]: /docs/self-hosting/advanced/model-list
|
||||
|
||||
@@ -165,6 +165,58 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
|
||||
- 默认值:`us-east-1`
|
||||
- 示例:`us-east-1`
|
||||
|
||||
## ComfyUI
|
||||
|
||||
### `COMFYUI_BASE_URL`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:ComfyUI 服务的基础 URL 地址
|
||||
- 默认值:`http://localhost:8000`
|
||||
- 示例:`http://192.168.1.100:8000` 或 `https://my-comfyui-server.com`
|
||||
|
||||
### `COMFYUI_AUTH_TYPE`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:ComfyUI 的认证类型,支持 4 种认证方式
|
||||
- `none`: 无认证(默认)
|
||||
- `basic`: 基础认证(用户名 + 密码)
|
||||
- `bearer`: Bearer Token 认证(API 密钥)
|
||||
- `custom`: 自定义请求头认证
|
||||
- 默认值:`none`
|
||||
- 示例:`basic`
|
||||
|
||||
### `COMFYUI_API_KEY`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:当认证类型为 `bearer` 时使用的 API 密钥
|
||||
- 默认值:-
|
||||
- 示例:`sk-xxxxxx...xxxxxx`
|
||||
|
||||
### `COMFYUI_USERNAME`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:当认证类型为 `basic` 时使用的用户名
|
||||
- 默认值:-
|
||||
- 示例:`admin`
|
||||
|
||||
### `COMFYUI_PASSWORD`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:当认证类型为 `basic` 时使用的密码
|
||||
- 默认值:-
|
||||
- 示例:`password123`
|
||||
|
||||
### `COMFYUI_CUSTOM_HEADERS`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:当认证类型为 `custom` 时使用的自定义请求头,需要使用 JSON 格式字符串
|
||||
- 默认值:-
|
||||
- 示例:`{"X-Auth-Token": "your-token", "X-Custom-Header": "value"}`
|
||||
|
||||
<Callout type={'info'}>
|
||||
ComfyUI 支持多种认证方式,请根据您的 ComfyUI 服务配置选择合适的认证类型和相应的认证参数。如果您的 ComfyUI 服务没有设置认证,可以不配置认证相关的环境变量。
|
||||
</Callout>
|
||||
|
||||
## DeepSeek AI
|
||||
|
||||
### `DEEPSEEK_PROXY_URL`
|
||||
@@ -720,4 +772,20 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
|
||||
- 默认值:`-`
|
||||
- 示例:`-all,+vercel-model-1,+vercel-model-2=vercel-special`
|
||||
|
||||
## Cerebras
|
||||
|
||||
### `CEREBRAS_API_KEY`
|
||||
|
||||
- 类型:必选
|
||||
- 描述:这是你在 Cerebras 服务中申请的 API 密钥
|
||||
- 默认值:-
|
||||
- 示例:`csk-xxxxxx...xxxxxx`
|
||||
|
||||
### `CEREBRAS_MODEL_LIST`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:用来控制 Cerebras 模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则与其他 provider 保持一致。
|
||||
- 默认值:`-`
|
||||
- 示例:`-all,+cerebras-model-1,+cerebras-model-2=cerebras-special`
|
||||
|
||||
[model-list]: /zh/docs/self-hosting/advanced/model-list
|
||||
|
||||
@@ -29,11 +29,6 @@ LobeChat integrates `next-auth`, a flexible and powerful identity verification l
|
||||
- **Social Login**: Support quick login via various social platforms.
|
||||
- **Data Security**: Ensure the security and privacy of user data.
|
||||
|
||||
<Callout type={'warning'}>
|
||||
Due to workload constraints, integration of next-auth with a server-side database has not been
|
||||
implemented yet. If you need to use a server-side database, please use Clerk.
|
||||
</Callout>
|
||||
|
||||
<Callout type={'info'}>
|
||||
For information on using Next-Auth, you can refer to [Authentication Services - Next
|
||||
Auth](/docs/self-hosting/advanced/authentication#next-auth).
|
||||
|
||||
@@ -25,11 +25,6 @@ LobeChat 集成了 `next-auth`,一个灵活且强大的身份验证库,支
|
||||
- **社交登录**:支持多种社交平台的快捷登录。
|
||||
- **数据安全**:保障用户数据的安全性和隐私性。
|
||||
|
||||
<Callout type={'warning'}>
|
||||
由于工作量原因,目前还没有实现 next-auth 与服务端数据库的集成,如果需要使用服务端数据库,请使用
|
||||
Clerk 。
|
||||
</Callout>
|
||||
|
||||
<Callout type={'info'}>
|
||||
关于 Next-Auth 的使用,可以查阅 [身份验证服务 - Next
|
||||
Auth](/zh/docs/self-hosting/advanced/authentication#next-auth)。
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Group Chat
|
||||
description: Turn any conversation into a team effort. Multiple AI agents collaborate naturally to give you richer, more insightful responses.
|
||||
tags:
|
||||
- Group Chat
|
||||
- Multi-Agent
|
||||
- AI Orchestration
|
||||
- Agent Coordination
|
||||
---
|
||||
|
||||
# Group Chat
|
||||
|
||||
<Image alt={'Group Chat'} cover src={'https://github.com/user-attachments/assets/6e5599f6-1e39-4b14-a218-4f5ffa2c306a'} />
|
||||
|
||||
Sometimes one perspective isn't enough. Group Chat brings together multiple AI agents, each with their own expertise, to collaborate on your conversations. Richer discussions, diverse viewpoints, and solutions you wouldn't get from any single agent.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Multiple assistants with specialized knowledge work together, each contributing their strengths
|
||||
- A built-in host ensures the group chat runs smoothly and organized
|
||||
- Private messaging allows seamless coordination between assistants
|
||||
- You get comprehensive answers from multiple perspectives
|
||||
- Ready to use with your own assistants or rich group chat team templates
|
||||
|
||||
## Use Cases
|
||||
|
||||
**Learning and Research**: Different assistants gather different materials through different tools, then come together for spontaneous discussion.
|
||||
|
||||
**Entertainment**: Multiplayer language games like Werewolf, Model United Nations, and Turtle Soup.
|
||||
|
||||
**Brainstorming**: Diverse perspectives spark better ideas.
|
||||
|
||||
**Problem Solving**: Benefit from insights across different professional fields, with different tools and MCPs, group chat allows you to have the perfect AI team.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Click the "Create Group Chat" button, you can choose to create directly from preset group chat templates, or select your own assistants to form a group chat.
|
||||
|
||||
You can use @ to mention a group member in the group chat, or click their avatar to send them a private message. Everything works just like in a real chat room.
|
||||
|
||||
### Interrupt and Resume Group Chat
|
||||
|
||||
You can interrupt the host's thinking at any time, and the group chat will "pause" after interruption. You can start the moderator at any time, and the group chat will continue.
|
||||
|
||||
Of course, conversations may also stop naturally.
|
||||
|
||||
### Advanced Options
|
||||
|
||||
- Group Chat Speed: Customize the response speed of the group chat
|
||||
- Custom Moderator: Guide the moderator's behavior according to specific needs
|
||||
|
||||
Group Chat transforms how you interact with AI. Instead of getting one answer, you participate in a conversation—complete with different viewpoints, collaborative problem-solving, and profound insights that emerge when AI agents work together.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: 群聊
|
||||
description: 让对话变成团队协作。多个 AI 智能体自然配合,为你提供更丰富、更有洞察力的回答。
|
||||
tags:
|
||||
- 群组对话
|
||||
- 多智能体
|
||||
- AI 编排
|
||||
- 智能体协调
|
||||
---
|
||||
|
||||
# 群聊
|
||||
|
||||
<Image alt={'群聊'} cover src={'https://github.com/user-attachments/assets/6e5599f6-1e39-4b14-a218-4f5ffa2c306a'} />
|
||||
|
||||
有时候,一个视角远远不够。群聊功能让多个拥有不同知识和技能的 AI 智能体聚在一起,协作参与你的对话。更丰富的讨论、多元的观点,以及任何单一智能体都无法提供的解决方案。
|
||||
|
||||
## 亮点
|
||||
|
||||
- 多个具有专业知识的助手协同工作,各取所长
|
||||
- 群聊内置的主持人将确保整个群聊有条不紊的进行
|
||||
- 私信功能让助手间无缝协调配合
|
||||
- 你将从多个视角获得全面的答案
|
||||
- 开箱即用,由你自己的助手组成,或者使用丰富的群聊团队模版
|
||||
|
||||
## 适用场景
|
||||
|
||||
**学习研究**:不同的助手通过不同的工具收集不同的资料,汇聚一处并自发讨论。
|
||||
|
||||
**娱乐**:狼人杀、模拟联合国、海龟汤等多人语聊游戏。
|
||||
|
||||
**头脑风暴**:多元观点激发更好的想法。
|
||||
|
||||
**问题解决**:受益于不同专业领域的见解,以及不同的工具和 MCP,群聊允许你拥有完美的 AI 团队。
|
||||
|
||||
## 快速开始
|
||||
|
||||
点击「创建群聊」按钮,你可以选择直接从预设的群聊模版创建,或选择你自己的助手组成群聊。
|
||||
|
||||
你可以在群聊中输入 @ 来提及某个群成员,或者点击它们的头像对其私聊。一切就像是在真实的聊天室中一样。
|
||||
|
||||
### 中断与继续群聊
|
||||
|
||||
你可以随时打断主持人的思考,打断后群聊将会「暂停」。你可以随时开启主持人,群聊便会继续。
|
||||
|
||||
当然,对话也有可能自然地停止。
|
||||
|
||||
### 高级选项
|
||||
|
||||
- 群聊速度:自定义群聊的回答速度
|
||||
- 自定义主持人:根据特定需要指导主持人的行为
|
||||
|
||||
群聊改变了你与 AI 的互动方式。不再是获得一个答案,而是参与一场对话 —— 包含不同观点、协作解决问题,以及 AI 智能体协同工作时产生的深刻洞察。
|
||||
@@ -0,0 +1,816 @@
|
||||
---
|
||||
title: Using ComfyUI for Image Generation in LobeChat
|
||||
description: Learn how to configure and use ComfyUI service in LobeChat, supporting FLUX series models for high-quality image generation and editing features
|
||||
tags:
|
||||
- ComfyUI
|
||||
- FLUX
|
||||
- Text-to-Image
|
||||
- Image Editing
|
||||
- AI Image Generation
|
||||
---
|
||||
|
||||
# Using ComfyUI in LobeChat
|
||||
|
||||
<Image alt={'Using ComfyUI in LobeChat'} cover src={'https://github.com/lobehub/lobe-chat/assets/17870709/c9e5eafc-ca22-496b-a88d-cc0ae53bf720'} />
|
||||
|
||||
This documentation will guide you on how to use [ComfyUI](https://github.com/comfyanonymous/ComfyUI) in LobeChat for high-quality AI image generation and editing.
|
||||
|
||||
## ComfyUI Overview
|
||||
|
||||
ComfyUI is a powerful stable diffusion and flow diffusion GUI that provides a node-based workflow interface. LobeChat integrates with ComfyUI, supporting complete FLUX series models, including text-to-image generation and image editing capabilities.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Extensive Model Support**: Supports 223 models, including FLUX series (130) and SD series (93)
|
||||
- **Configuration-Driven Architecture**: Registry system provides intelligent model selection
|
||||
- **Multi-Format Support**: Supports .safetensors and .gguf formats with various quantization levels
|
||||
- **Dynamic Precision Selection**: Supports default, fp8\_e4m3fn, fp8\_e5m2, fp8\_e4m3fn\_fast precision
|
||||
- **Multiple Authentication Methods**: Supports no authentication, basic authentication, Bearer Token, and custom authentication
|
||||
- **Intelligent Component Selection**: Automatically selects optimal T5, CLIP, VAE encoder combinations
|
||||
- **Enterprise-Grade Optimization**: Includes NF4, SVDQuant, TorchAO, MFLUX optimization variants
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Configure ComfyUI in LobeChat
|
||||
|
||||
#### 1. Open Settings Interface
|
||||
|
||||
- Access LobeChat's `Settings` interface
|
||||
- Find the `ComfyUI` setting item under `AI Providers`
|
||||
|
||||
<Image alt={'ComfyUI Settings Interface'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/3f31bc33-509f-4ad2-ba81-280c2a6ec5fa'} />
|
||||
|
||||
#### 2. Configure Connection Parameters
|
||||
|
||||
**Basic Configuration**:
|
||||
|
||||
- **Server Address**: Enter ComfyUI server address, e.g., `http://localhost:8188`
|
||||
- **Authentication Type**: Select appropriate authentication method (default: no authentication)
|
||||
|
||||
### Step 2: Select Model and Start Generating Images
|
||||
|
||||
#### 1. Select FLUX Model
|
||||
|
||||
In the conversation interface:
|
||||
|
||||
- Click the model selection button
|
||||
- Select the desired FLUX model from the ComfyUI category
|
||||
|
||||
<Image alt={'Select FLUX Model'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/ff7ebacf-27f0-42d7-810b-00314499a084'} />
|
||||
|
||||
#### 2. Text-to-Image Generation
|
||||
|
||||
**Using FLUX Schnell (Fast Generation)**:
|
||||
|
||||
```plaintext
|
||||
Generate an image: A cute orange cat sitting on a sunny windowsill, warm lighting, detailed fur texture
|
||||
```
|
||||
|
||||
**Using FLUX Dev (High Quality Generation)**:
|
||||
|
||||
```plaintext
|
||||
Generate high quality image: City skyline at sunset, cyberpunk style, neon lights, 4K high resolution, detailed architecture
|
||||
```
|
||||
|
||||
#### 3. Image Editing
|
||||
|
||||
**Using FLUX Kontext-dev for Image Editing**:
|
||||
|
||||
```plaintext
|
||||
Edit this image: Change the background to a starry night sky, keep the main subject, cosmic atmosphere
|
||||
```
|
||||
|
||||
Then upload the original image you want to edit.
|
||||
|
||||
<Callout type={'info'}>
|
||||
Image editing functionality requires uploading the original image first, then describing the modifications you want to make.
|
||||
</Callout>
|
||||
|
||||
## Authentication Configuration Guide
|
||||
|
||||
ComfyUI supports four authentication methods. Choose the appropriate method based on your server configuration and security requirements:
|
||||
|
||||
### No Authentication (none)
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- Local development environment (localhost)
|
||||
- Internal network with trusted users
|
||||
- Personal single-machine deployment
|
||||
|
||||
**Configuration**:
|
||||
|
||||
```yaml
|
||||
Authentication Type: None
|
||||
Server Address: http://localhost:8188
|
||||
```
|
||||
|
||||
### Basic Authentication (basic)
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- Deployments using Nginx reverse proxy
|
||||
- Team internal use requiring basic access control
|
||||
|
||||
**Configuration**:
|
||||
|
||||
1. **Create User Password**:
|
||||
|
||||
```bash
|
||||
# Install apache2-utils
|
||||
sudo apt-get install apache2-utils
|
||||
|
||||
# Create user 'admin'
|
||||
sudo htpasswd -c /etc/nginx/.htpasswd admin
|
||||
```
|
||||
|
||||
2. **LobeChat Configuration**:
|
||||
|
||||
```yaml
|
||||
Authentication Type: Basic Authentication
|
||||
Server Address: http://your-domain.com
|
||||
Username: admin
|
||||
Password: your_secure_password
|
||||
```
|
||||
|
||||
### Bearer Token (bearer)
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- API-driven application integration
|
||||
- Enterprise environments requiring Token authentication
|
||||
|
||||
**Generate Token**:
|
||||
|
||||
```python
|
||||
import jwt
|
||||
import datetime
|
||||
|
||||
payload = {
|
||||
'user': 'admin',
|
||||
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=30)
|
||||
}
|
||||
|
||||
secret_key = "your-secret-key"
|
||||
token = jwt.encode(payload, secret_key, algorithm='HS256')
|
||||
print(f"Bearer Token: {token}")
|
||||
```
|
||||
|
||||
**LobeChat Configuration**:
|
||||
|
||||
```yaml
|
||||
Authentication Type: Bearer Token
|
||||
Server Address: http://your-server:8188
|
||||
API Key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
### Custom Authentication (custom)
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- Integration with existing enterprise authentication systems
|
||||
- Systems requiring multiple authentication headers
|
||||
|
||||
**LobeChat Configuration**:
|
||||
|
||||
```yaml
|
||||
Authentication Type: Custom
|
||||
Server Address: http://your-server:8188
|
||||
Custom Headers:
|
||||
{
|
||||
"X-API-Key": "your_api_key",
|
||||
"X-Client-ID": "lobechat"
|
||||
}
|
||||
```
|
||||
|
||||
## Common Issues Resolution
|
||||
|
||||
### 1. How to Install Comfy-Manager
|
||||
|
||||
Comfy-Manager is ComfyUI's extension manager that allows you to easily install and manage various nodes, models, and extensions.
|
||||
|
||||
<details>
|
||||
<summary><b>📦 Install Comfy-Manager Steps</b></summary>
|
||||
|
||||
#### Method 1: Manual Installation (Recommended)
|
||||
|
||||
```bash
|
||||
# Navigate to ComfyUI's custom_nodes directory
|
||||
cd ComfyUI/custom_nodes
|
||||
|
||||
# Clone Comfy-Manager repository
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager.git
|
||||
|
||||
# Restart ComfyUI server
|
||||
# After restart, you'll see the Manager button in the UI
|
||||
```
|
||||
|
||||
#### Method 2: One-Click Installation Script
|
||||
|
||||
```bash
|
||||
# Execute in ComfyUI root directory
|
||||
curl -fsSL https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/install.sh | bash
|
||||
```
|
||||
|
||||
#### Verify Installation
|
||||
|
||||
1. Restart ComfyUI server
|
||||
2. Visit `http://localhost:8188`
|
||||
3. You should see the "Manager" button in the bottom-right corner
|
||||
|
||||
#### Using Comfy-Manager
|
||||
|
||||
**Install Models**:
|
||||
|
||||
1. Click "Manager" button
|
||||
2. Select "Install Models"
|
||||
3. Search for needed models (e.g., FLUX, SD3.5)
|
||||
4. Click "Install" to automatically download to correct directory
|
||||
|
||||
**Install Node Extensions**:
|
||||
|
||||
1. Click "Manager" button
|
||||
2. Select "Install Custom Nodes"
|
||||
3. Search for needed nodes (e.g., ControlNet, AnimateDiff)
|
||||
4. Click "Install" and restart server
|
||||
|
||||
**Manage Installed Content**:
|
||||
|
||||
1. Click "Manager" button
|
||||
2. Select "Installed" to view installed extensions
|
||||
3. Update, disable, or uninstall extensions
|
||||
</details>
|
||||
|
||||
### 2. How to Handle "Model not found" Errors
|
||||
|
||||
When you see errors like `Model not found: flux1-dev.safetensors, flux1-krea-dev.safetensors, flux1-schnell.safetensors`, it means the required model files are missing from the server.
|
||||
|
||||
<details>
|
||||
<summary><b>🔧 Resolve Model not found Errors</b></summary>
|
||||
|
||||
#### Error Example
|
||||
|
||||
```plaintext
|
||||
Model not found: flux1-dev.safetensors, flux1-krea-dev.safetensors, flux1-schnell.safetensors
|
||||
```
|
||||
|
||||
This error indicates the system expects to find these model files but couldn't locate them on the server.
|
||||
|
||||
#### Resolution Methods
|
||||
|
||||
**Method 1: Download using Comfy-Manager (Recommended)**
|
||||
|
||||
1. Open ComfyUI interface
|
||||
2. Click "Manager" → "Install Models"
|
||||
3. Search for the model name from the error (e.g., "flux1-dev")
|
||||
4. Click "Install" to automatically download
|
||||
|
||||
**Method 2: Manual Model Download**
|
||||
|
||||
1. **Download Model Files**:
|
||||
- Visit [Hugging Face](https://huggingface.co/black-forest-labs/FLUX.1-dev) or other model sources
|
||||
- Download the files mentioned in the error (e.g., `flux1-dev.safetensors`)
|
||||
|
||||
2. **Place in Correct Directory**:
|
||||
```bash
|
||||
# FLUX and SD3.5 main models go to
|
||||
ComfyUI/models/diffusion_models/flux1-dev.safetensors
|
||||
|
||||
# SD1.5 and SDXL models go to
|
||||
ComfyUI/models/checkpoints/
|
||||
```
|
||||
|
||||
3. **Verify Files**:
|
||||
```bash
|
||||
# Check if file exists
|
||||
ls -la ComfyUI/models/diffusion_models/flux1-dev.safetensors
|
||||
|
||||
# Check file integrity (optional)
|
||||
sha256sum flux1-dev.safetensors
|
||||
```
|
||||
|
||||
4. **Restart ComfyUI Server**
|
||||
|
||||
**Method 3: Direct Download with wget/curl**
|
||||
|
||||
```bash
|
||||
# Navigate to models directory
|
||||
cd ComfyUI/models/diffusion_models/
|
||||
|
||||
# Download using wget (replace with actual download link)
|
||||
wget https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors
|
||||
|
||||
# Or use curl
|
||||
curl -L -o flux1-dev.safetensors https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors
|
||||
```
|
||||
|
||||
#### Common Model Download Sources
|
||||
|
||||
- **Hugging Face**: [https://huggingface.co/models](https://huggingface.co/models)
|
||||
- **Civitai**: [https://civitai.com/models](https://civitai.com/models)
|
||||
- **Official Sources**:
|
||||
- FLUX: [https://huggingface.co/black-forest-labs](https://huggingface.co/black-forest-labs)
|
||||
- SD3.5: [https://huggingface.co/stabilityai](https://huggingface.co/stabilityai)
|
||||
|
||||
#### Prevention Measures
|
||||
|
||||
1. **Basic Model Package**: Download at least one base model
|
||||
- FLUX: `flux1-schnell.safetensors` (fast) or `flux1-dev.safetensors` (high quality)
|
||||
- SD3.5: `sd3.5_large.safetensors`
|
||||
|
||||
2. **Check Disk Space**:
|
||||
```bash
|
||||
# Check available space
|
||||
df -h ComfyUI/models/
|
||||
```
|
||||
|
||||
3. **Set Model Path** (optional):
|
||||
If your models are stored elsewhere, create symbolic links:
|
||||
```bash
|
||||
ln -s /path/to/your/models ComfyUI/models/diffusion_models/
|
||||
```
|
||||
</details>
|
||||
|
||||
### 3. How to Handle Missing System Component Errors
|
||||
|
||||
When you see errors like `Missing VAE encoder: ae.safetensors` or other component files missing, you need to download the corresponding system components.
|
||||
|
||||
<details>
|
||||
<summary><b>🛠️ Resolve Missing System Component Errors</b></summary>
|
||||
|
||||
#### Common Component Errors
|
||||
|
||||
```plaintext
|
||||
Missing VAE encoder: ae.safetensors. Please download and place it in the models/vae folder.
|
||||
Missing CLIP encoder: clip_l.safetensors. Please download and place it in the models/clip folder.
|
||||
Missing T5 encoder: t5xxl_fp16.safetensors. Please download and place it in the models/clip folder.
|
||||
```
|
||||
|
||||
#### Component Types Description
|
||||
|
||||
| Component Type | Example Filename | Purpose | Storage Directory |
|
||||
| -------------- | ------------------------------ | ----------------------- | ------------------ |
|
||||
| **VAE** | ae.safetensors | Image encoding/decoding | models/vae/ |
|
||||
| **CLIP** | clip\_l.safetensors | Text encoding (CLIP) | models/clip/ |
|
||||
| **T5** | t5xxl\_fp16.safetensors | Text encoding (T5) | models/clip/ |
|
||||
| **ControlNet** | flux-controlnet-\*.safetensors | Control networks | models/controlnet/ |
|
||||
|
||||
#### Resolution Methods
|
||||
|
||||
**Method 1: Use Comfy-Manager (Recommended)**
|
||||
|
||||
1. Click "Manager" → "Install Models"
|
||||
2. Select component type in "Filter" (VAE/CLIP/T5)
|
||||
3. Download corresponding component files
|
||||
|
||||
**Method 2: Manual Component Download**
|
||||
|
||||
##### FLUX Required Components
|
||||
|
||||
```bash
|
||||
# 1. VAE Encoder
|
||||
cd ComfyUI/models/vae/
|
||||
wget https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/ae.safetensors
|
||||
|
||||
# 2. CLIP-L Encoder
|
||||
cd ComfyUI/models/clip/
|
||||
wget https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors
|
||||
|
||||
# 3. T5-XXL Encoder (choose different precisions)
|
||||
# FP16 version (recommended, balanced performance)
|
||||
wget https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors
|
||||
|
||||
# Or FP8 version (saves VRAM)
|
||||
wget https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors
|
||||
```
|
||||
|
||||
##### SD3.5 Required Components
|
||||
|
||||
```bash
|
||||
# SD3.5 uses different encoders
|
||||
cd ComfyUI/models/clip/
|
||||
|
||||
# CLIP-G Encoder
|
||||
wget https://huggingface.co/stabilityai/stable-diffusion-3.5-large/resolve/main/text_encoders/clip_g.safetensors
|
||||
|
||||
# CLIP-L Encoder
|
||||
wget https://huggingface.co/stabilityai/stable-diffusion-3.5-large/resolve/main/text_encoders/clip_l.safetensors
|
||||
|
||||
# T5-XXL Encoder
|
||||
wget https://huggingface.co/stabilityai/stable-diffusion-3.5-large/resolve/main/text_encoders/t5xxl_fp16.safetensors
|
||||
```
|
||||
|
||||
##### SDXL Required Components
|
||||
|
||||
```bash
|
||||
# SDXL VAE
|
||||
cd ComfyUI/models/vae/
|
||||
wget https://huggingface.co/stabilityai/sdxl-vae/resolve/main/sdxl_vae.safetensors
|
||||
|
||||
# SDXL uses built-in CLIP encoders, usually no separate download needed
|
||||
```
|
||||
|
||||
#### Component Compatibility Matrix
|
||||
|
||||
| Model Series | Required VAE | Required CLIP | Required T5 | Optional Components |
|
||||
| ------------ | -------------- | ------------------- | ----------------------- | ------------------- |
|
||||
| **FLUX** | ae.safetensors | clip\_l.safetensors | t5xxl\_fp16.safetensors | ControlNet |
|
||||
| **SD3.5** | Built-in | clip\_g + clip\_l | t5xxl\_fp16 | - |
|
||||
| **SDXL** | sdxl\_vae | Built-in | - | Refiner |
|
||||
| **SD1.5** | vae-ft-mse | Built-in | - | ControlNet |
|
||||
|
||||
#### Precision Selection Recommendations
|
||||
|
||||
**T5 Encoder Precision Selection**:
|
||||
|
||||
| VRAM Capacity | Recommended Version | Filename |
|
||||
| ------------- | ------------------- | ------------------------------ |
|
||||
| \< 12GB | FP8 Quantized | t5xxl\_fp8\_e4m3fn.safetensors |
|
||||
| 12-16GB | FP16 | t5xxl\_fp16.safetensors |
|
||||
| > 16GB | FP32 | t5xxl.safetensors |
|
||||
|
||||
#### Verify Component Installation
|
||||
|
||||
```bash
|
||||
# Check all required components
|
||||
echo "=== VAE Components ==="
|
||||
ls -la ComfyUI/models/vae/
|
||||
|
||||
echo "=== CLIP/T5 Components ==="
|
||||
ls -la ComfyUI/models/clip/
|
||||
|
||||
echo "=== ControlNet Components ==="
|
||||
ls -la ComfyUI/models/controlnet/
|
||||
```
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
**Issue: Still getting errors after download**
|
||||
|
||||
1. **Check File Permissions**:
|
||||
```bash
|
||||
chmod 644 ComfyUI/models/vae/*.safetensors
|
||||
chmod 644 ComfyUI/models/clip/*.safetensors
|
||||
```
|
||||
|
||||
2. **Clear Cache**:
|
||||
```bash
|
||||
# Clear ComfyUI cache
|
||||
rm -rf ComfyUI/temp/*
|
||||
rm -rf ComfyUI/__pycache__/*
|
||||
```
|
||||
|
||||
3. **Restart Server**:
|
||||
```bash
|
||||
# Fully restart ComfyUI
|
||||
pkill -f "python main.py"
|
||||
python main.py --listen 0.0.0.0 --port 8188
|
||||
```
|
||||
|
||||
**Issue: Insufficient VRAM**
|
||||
|
||||
Use quantized component versions:
|
||||
|
||||
- T5: Use `t5xxl_fp8_e4m3fn.safetensors` instead of FP16/FP32
|
||||
- VAE: Some models support FP16 VAE versions
|
||||
|
||||
**Issue: Slow Downloads**
|
||||
|
||||
1. Use mirror sources (if applicable)
|
||||
2. Use download tools (like aria2c) with resume support:
|
||||
```bash
|
||||
aria2c -x 16 -s 16 -k 1M [download_link]
|
||||
```
|
||||
</details>
|
||||
|
||||
## ComfyUI Server Installation
|
||||
|
||||
<details>
|
||||
<summary><b>🚀 Install and Configure ComfyUI Server</b></summary>
|
||||
|
||||
### 1. Install ComfyUI
|
||||
|
||||
```bash
|
||||
# Clone ComfyUI repository
|
||||
git clone https://github.com/comfyanonymous/ComfyUI.git
|
||||
cd ComfyUI
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Optional: Install JWT support (for Token authentication)
|
||||
pip install PyJWT
|
||||
|
||||
# Start ComfyUI server
|
||||
python main.py --listen 0.0.0.0 --port 8188
|
||||
```
|
||||
|
||||
### 2. Download Model Files
|
||||
|
||||
**Recommended Basic Configuration** (Minimal installation):
|
||||
|
||||
**Main Models** (place in `models/diffusion_models/` directory):
|
||||
|
||||
- `flux1-schnell.safetensors` - Fast generation (4 steps)
|
||||
- `flux1-dev.safetensors` - High-quality creation (20 steps)
|
||||
|
||||
**Required Components** (place in respective directories):
|
||||
|
||||
- `models/vae/ae.safetensors` - VAE encoder
|
||||
- `models/clip/clip_l.safetensors` - CLIP text encoder
|
||||
- `models/clip/t5xxl_fp16.safetensors` - T5 text encoder
|
||||
|
||||
### 3. Verify Server Running
|
||||
|
||||
Visit `http://localhost:8188` to confirm ComfyUI interface loads properly.
|
||||
|
||||
<Callout type={'info'}>
|
||||
**Smart Model Selection**: LobeChat will automatically select the best model based on available model files on the server. You don't need to download all models; the system will automatically choose from available models by priority (Official > Enterprise > Community).
|
||||
</Callout>
|
||||
</details>
|
||||
|
||||
## Supported Models
|
||||
|
||||
LobeChat's ComfyUI integration uses a configuration-driven architecture, supporting **223 models**, providing complete coverage from official models to community-optimized versions.
|
||||
|
||||
### FLUX Series Recommended Parameters
|
||||
|
||||
| Model Type | Recommended Steps | CFG Scale | Resolution Range |
|
||||
| ----------- | ----------------- | --------- | -------------------- |
|
||||
| **Schnell** | 4 steps | - | 512×512 to 1536×1536 |
|
||||
| **Dev** | 20 steps | 3.5 | 512×512 to 2048×2048 |
|
||||
| **Kontext** | 20 steps | 3.5 | 512×512 to 2048×2048 |
|
||||
| **Krea** | 20 steps | 4.5 | 512×512 to 2048×2048 |
|
||||
|
||||
### SD3.5 Series Parameters
|
||||
|
||||
| Model Type | Recommended Steps | CFG Scale | Resolution Range |
|
||||
| --------------- | ----------------- | --------- | -------------------- |
|
||||
| **Large** | 25 steps | 7.0 | 512×512 to 2048×2048 |
|
||||
| **Large Turbo** | 8 steps | 3.5 | 512×512 to 1536×1536 |
|
||||
| **Medium** | 20 steps | 6.0 | 512×512 to 1536×1536 |
|
||||
|
||||
<details>
|
||||
<summary><b>📋 Complete Supported Model List</b></summary>
|
||||
|
||||
### Model Classification System
|
||||
|
||||
#### Priority 1: Official Core Models
|
||||
|
||||
**FLUX.1 Official Series**:
|
||||
|
||||
- `flux1-dev.safetensors` - High-quality creation model
|
||||
- `flux1-schnell.safetensors` - Fast generation model
|
||||
- `flux1-kontext-dev.safetensors` - Image editing model
|
||||
- `flux1-krea-dev.safetensors` - Safety-enhanced model
|
||||
|
||||
**SD3.5 Official Series**:
|
||||
|
||||
- `sd3.5_large.safetensors` - SD3.5 large base model
|
||||
- `sd3.5_large_turbo.safetensors` - Fast generation version
|
||||
- `sd3.5_medium.safetensors` - Medium-scale model
|
||||
|
||||
#### Priority 2: Enterprise Optimized Models (106 FLUX)
|
||||
|
||||
**Quantization Optimization Series**:
|
||||
|
||||
- **GGUF Quantization**: Each variant supports 11 quantization levels (F16, Q8\_0, Q6\_K, Q5\_K\_M, Q5\_K\_S, Q4\_K\_M, Q4\_K\_S, Q4\_0, Q3\_K\_M, Q3\_K\_S, Q2\_K)
|
||||
- **FP8 Precision**: fp8\_e4m3fn, fp8\_e5m2 optimized versions
|
||||
- **Enterprise Lightweight**: FLUX.1-lite-8B series
|
||||
- **Technical Experiments**: NF4, SVDQuant, TorchAO, optimum-quanto, MFLUX optimized versions
|
||||
|
||||
#### Priority 3: Community Fine-tuned Models (48 FLUX)
|
||||
|
||||
**Community Optimization Series**:
|
||||
|
||||
- **Jib Mix Flux** Series: High-quality mixed models
|
||||
- **Real Dream FLUX** Series: Realism style
|
||||
- **Vision Realistic** Series: Visual realism
|
||||
- **PixelWave FLUX** Series: Pixel art optimization
|
||||
- **Fluxmania** Series: Diverse style support
|
||||
|
||||
### SD Series Model Support (93 models)
|
||||
|
||||
**SD3.5 Series**: 5 models
|
||||
**SD1.5 Series**: 37 models (including official, quantized, and community versions)
|
||||
**SDXL Series**: 50 models (including base, Refiner, and Playground models)
|
||||
|
||||
### Workflow Support
|
||||
|
||||
System supports **6 workflows**:
|
||||
|
||||
- **flux-dev**: High-quality creation workflow
|
||||
- **flux-schnell**: Fast generation workflow
|
||||
- **flux-kontext**: Image editing workflow
|
||||
- **sd35**: SD3.5 dedicated workflow
|
||||
- **simple-sd**: Simple SD workflow
|
||||
- **index**: Workflow entry point
|
||||
</details>
|
||||
|
||||
## Performance Optimization Recommendations
|
||||
|
||||
### Hardware Requirements
|
||||
|
||||
**Minimum Configuration** (GGUF quantized models):
|
||||
|
||||
- GPU: 6GB VRAM (using Q4 quantization)
|
||||
- RAM: 12GB
|
||||
- Storage: 30GB available space
|
||||
|
||||
**Recommended Configuration** (standard models):
|
||||
|
||||
- GPU: 12GB+ VRAM (RTX 4070 Ti or higher)
|
||||
- RAM: 24GB+
|
||||
- Storage: SSD 100GB+ available space
|
||||
|
||||
### VRAM Optimization Strategy
|
||||
|
||||
| VRAM Capacity | Recommended Quantization | Model Example | Performance Characteristics |
|
||||
| ------------- | ------------------------ | ---------------------------------- | --------------------------- |
|
||||
| **6-8GB** | Q4\_0, Q4\_K\_S | `flux1-dev-Q4_0.gguf` | Minimal VRAM usage |
|
||||
| **10-12GB** | Q6\_K, Q8\_0 | `flux1-dev-Q6_K.gguf` | Balance performance/quality |
|
||||
| **16GB+** | FP8, FP16 | `flux1-dev-fp8-e4m3fn.safetensors` | Near-original quality |
|
||||
| **24GB+** | Full model | `flux1-dev.safetensors` | Best quality |
|
||||
|
||||
## Custom Model Usage
|
||||
|
||||
<details>
|
||||
<summary><b>🎨 Configure Custom SD Models</b></summary>
|
||||
|
||||
LobeChat supports using custom Stable Diffusion models. The system uses fixed filenames to identify custom models.
|
||||
|
||||
### 1. Model File Preparation
|
||||
|
||||
**Required Files**:
|
||||
|
||||
- **Main Model File**: `custom_sd_lobe.safetensors`
|
||||
- **VAE File (Optional)**: `custom_sd_vae_lobe.safetensors`
|
||||
|
||||
### 2. Add Custom Model
|
||||
|
||||
**Method 1: Rename Existing Model**
|
||||
|
||||
```bash
|
||||
# Rename your model to fixed filename
|
||||
mv your_custom_model.safetensors custom_sd_lobe.safetensors
|
||||
|
||||
# Move to correct directory
|
||||
mv custom_sd_lobe.safetensors ComfyUI/models/diffusion_models/
|
||||
```
|
||||
|
||||
**Method 2: Create Symbolic Link (Recommended)**
|
||||
|
||||
```bash
|
||||
# Create soft link for easy model switching
|
||||
ln -s /path/to/your_model.safetensors ComfyUI/models/diffusion_models/custom_sd_lobe.safetensors
|
||||
```
|
||||
|
||||
### 3. Use Custom Model
|
||||
|
||||
In LobeChat, custom models will appear as:
|
||||
|
||||
- **stable-diffusion-custom**: Standard custom model
|
||||
- **stable-diffusion-custom-refiner**: Refiner custom model
|
||||
|
||||
### Custom Model Parameter Recommendations
|
||||
|
||||
| Parameter | SD 1.5 Models | SDXL Models |
|
||||
| ---------- | ------------- | ----------- |
|
||||
| **steps** | 20-30 | 25-40 |
|
||||
| **cfg** | 7.0 | 6.0-8.0 |
|
||||
| **width** | 512 | 1024 |
|
||||
| **height** | 512 | 1024 |
|
||||
</details>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Smart Error Diagnosis System
|
||||
|
||||
LobeChat integrates a smart error handling system that can automatically diagnose and provide targeted solutions.
|
||||
|
||||
#### Error Types and Solutions
|
||||
|
||||
| Error Type | User Prompt | Automatic Diagnosis |
|
||||
| ------------------ | ---------------------------------- | --------------------------------------------------- |
|
||||
| **Connection** | "Cannot connect to ComfyUI server" | Auto-detect server status and connectivity |
|
||||
| **Authentication** | "API key invalid or expired" | Auto-verify authentication credentials |
|
||||
| **Permissions** | "Access permissions insufficient" | Auto-check user permissions and file access |
|
||||
| **Model Issues** | "Cannot find specified model file" | Auto-scan available models and suggest alternatives |
|
||||
| **Configuration** | "Configuration file error" | Auto-verify config completeness and syntax |
|
||||
|
||||
<details>
|
||||
<summary><b>🔍 Traditional Troubleshooting Methods</b></summary>
|
||||
|
||||
#### 1. Connection Failure
|
||||
|
||||
**Issue**: Cannot connect to ComfyUI server
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Confirm server running
|
||||
curl http://localhost:8188/system_stats
|
||||
|
||||
# Check port
|
||||
netstat -tulpn | grep 8188
|
||||
```
|
||||
|
||||
#### 2. Out of Memory
|
||||
|
||||
**Issue**: Memory errors during generation
|
||||
|
||||
**Solution**:
|
||||
|
||||
- Lower image resolution
|
||||
- Reduce generation steps
|
||||
- Use quantized models
|
||||
|
||||
#### 3. Authentication Failure
|
||||
|
||||
**Issue**: 401 or 403 errors
|
||||
|
||||
**Solution**:
|
||||
|
||||
- Verify authentication configuration
|
||||
- Check if Token is expired
|
||||
- Confirm user permissions
|
||||
</details>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Prompt Writing
|
||||
|
||||
1. **Detailed Description**: Provide clear, detailed image descriptions
|
||||
2. **Style Specification**: Clearly specify artistic style, color style, etc.
|
||||
3. **Quality Keywords**: Add "4K", "high quality", "detailed" keywords
|
||||
4. **Avoid Contradictions**: Ensure description content is logically consistent
|
||||
|
||||
**Example**:
|
||||
|
||||
```plaintext
|
||||
A young woman with flowing long hair, wearing an elegant blue dress, standing in a cherry blossom park,
|
||||
sunlight filtering through leaves, warm atmosphere, cinematic lighting, 4K high resolution, detailed, photorealistic
|
||||
```
|
||||
|
||||
### Parameter Optimization
|
||||
|
||||
1. **FLUX Schnell**: Suitable for quick previews, use 4-step generation
|
||||
2. **FLUX Dev**: Balance quality and speed, CFG 3.5, 20 steps
|
||||
3. **FLUX Krea-dev**: Safe creation, CFG 4.5, note content filtering
|
||||
4. **FLUX Kontext-dev**: Image editing, strength 0.6-0.9
|
||||
|
||||
<Callout type={'warning'}>
|
||||
Please note during use:
|
||||
|
||||
- FLUX Dev, Krea-dev, Kontext-dev models are for non-commercial use only
|
||||
- Generated content must comply with relevant laws and platform policies
|
||||
- Large model generation may take considerable time, please be patient
|
||||
</Callout>
|
||||
|
||||
## API Reference
|
||||
|
||||
<details>
|
||||
<summary><b>📚 API Documentation</b></summary>
|
||||
|
||||
### Request Format
|
||||
|
||||
```typescript
|
||||
interface ComfyUIRequest {
|
||||
model: string; // Model ID, e.g., 'flux-schnell'
|
||||
prompt: string; // Text prompt
|
||||
width: number; // Image width
|
||||
height: number; // Image height
|
||||
steps: number; // Generation steps
|
||||
seed: number; // Random seed
|
||||
cfg?: number; // CFG Scale (Dev/Krea/Kontext specific)
|
||||
strength?: number; // Edit strength (Kontext specific)
|
||||
imageUrl?: string; // Input image (Kontext specific)
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
```typescript
|
||||
interface ComfyUIResponse {
|
||||
images: Array<{
|
||||
url: string; // Generated image URL
|
||||
filename: string; // Filename
|
||||
subfolder: string; // Subdirectory
|
||||
type: string; // File type
|
||||
}>;
|
||||
prompt_id: string; // Prompt ID
|
||||
}
|
||||
```
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Error Code | Description | Resolution Suggestions |
|
||||
| ---------- | ------------------------ | -------------------------------- |
|
||||
| `400` | Invalid parameters | Check parameter format and range |
|
||||
| `401` | Authentication failed | Verify API key and auth config |
|
||||
| `403` | Insufficient permissions | Check user permissions |
|
||||
| `404` | Model not found | Confirm model file exists |
|
||||
| `500` | Server error | Check ComfyUI logs |
|
||||
</details>
|
||||
|
||||
You can now use ComfyUI in LobeChat for high-quality AI image generation and editing. If you encounter issues, please refer to the troubleshooting section or consult the [ComfyUI official documentation](https://github.com/comfyanonymous/ComfyUI).
|
||||
@@ -0,0 +1,816 @@
|
||||
---
|
||||
title: 在 LobeChat 中使用 ComfyUI 生成图像
|
||||
description: 学习如何在 LobeChat 中配置和使用 ComfyUI 服务,支持 FLUX 系列模型的高质量图像生成和编辑功能
|
||||
tags:
|
||||
- ComfyUI
|
||||
- FLUX
|
||||
- 文生图
|
||||
- 图像编辑
|
||||
- AI 图像生成
|
||||
---
|
||||
|
||||
# 在 LobeChat 中使用 ComfyUI
|
||||
|
||||
<Image alt={'在 LobeChat 中使用 ComfyUI'} cover src={'https://github.com/lobehub/lobe-chat/assets/17870709/c9e5eafc-ca22-496b-a88d-cc0ae53bf720'} />
|
||||
|
||||
本文档将指导你如何在 LobeChat 中使用 [ComfyUI](https://github.com/comfyanonymous/ComfyUI) 进行高质量的 AI 图像生成和编辑。
|
||||
|
||||
## ComfyUI 简介
|
||||
|
||||
ComfyUI 是一个功能强大的稳定扩散和流扩散 GUI,提供基于节点的工作流界面。LobeChat 集成了 ComfyUI,支持完整的 FLUX 系列模型,包括文本生成图像和图像编辑功能。
|
||||
|
||||
### 主要特性
|
||||
|
||||
- **广泛模型支持**:支持 223 个模型,包含 FLUX 系列(130 个)和 SD 系列(93 个)
|
||||
- **配置驱动架构**:注册表系统提供智能模型选择
|
||||
- **多格式支持**:支持 .safetensors 和 .gguf 格式,包含多种量化级别
|
||||
- **动态精度选择**:支持 default、fp8\_e4m3fn、fp8\_e5m2、fp8\_e4m3fn\_fast 精度
|
||||
- **多种认证方式**:支持无认证、基本认证、Bearer Token 和自定义认证
|
||||
- **智能组件选择**:自动选择最优的 T5、CLIP、VAE 编码器组合
|
||||
- **企业级优化**:包含 NF4、SVDQuant、TorchAO、MFLUX 等优化变体
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 步骤一:在 LobeChat 中配置 ComfyUI
|
||||
|
||||
#### 1. 打开设置界面
|
||||
|
||||
- 访问 LobeChat 的 `设置` 界面
|
||||
- 在 `AI 服务商` 下找到 `ComfyUI` 的设置项
|
||||
|
||||
<Image alt={'ComfyUI 设置界面'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/3f31bc33-509f-4ad2-ba81-280c2a6ec5fa'} />
|
||||
|
||||
#### 2. 配置连接参数
|
||||
|
||||
**基本配置**:
|
||||
|
||||
- **服务器地址**:输入 ComfyUI 服务器地址,如 `http://localhost:8000`
|
||||
- **认证类型**:选择合适的认证方式(默认无认证)
|
||||
|
||||
### 步骤二:选择模型并开始生成图像
|
||||
|
||||
#### 1. 选择 FLUX 模型
|
||||
|
||||
在对话界面中:
|
||||
|
||||
- 点击模型选择按钮
|
||||
- 从 ComfyUI 分类中选择所需的 FLUX 模型
|
||||
|
||||
<Image alt={'选择 FLUX 模型'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/ff7ebacf-27f0-42d7-810b-00314499a084'} />
|
||||
|
||||
#### 2. 文本生成图像
|
||||
|
||||
**使用 FLUX Schnell(快速生成)**:
|
||||
|
||||
```plaintext
|
||||
Generate an image: A cute orange cat sitting on a sunny windowsill, warm lighting, detailed fur texture
|
||||
```
|
||||
|
||||
**使用 FLUX Dev(高质量生成)**:
|
||||
|
||||
```plaintext
|
||||
Generate high quality image: City skyline at sunset, cyberpunk style, neon lights, 4K high resolution, detailed architecture
|
||||
```
|
||||
|
||||
#### 3. 图像编辑
|
||||
|
||||
**使用 FLUX Kontext-dev 编辑图像**:
|
||||
|
||||
```plaintext
|
||||
Edit this image: Change the background to a starry night sky, keep the main subject, cosmic atmosphere
|
||||
```
|
||||
|
||||
然后上传需要编辑的原始图像。
|
||||
|
||||
<Callout type={'info'}>
|
||||
图像编辑功能需要先上传原始图像,然后描述你希望进行的修改。
|
||||
</Callout>
|
||||
|
||||
## 认证配置指南
|
||||
|
||||
ComfyUI 支持四种认证方式,请根据你的服务器配置和安全需求选择合适的认证方式:
|
||||
|
||||
### 无认证 (none)
|
||||
|
||||
**适用场景**:
|
||||
|
||||
- 本地开发环境(localhost)
|
||||
- 内网环境且信任所有用户
|
||||
- 个人使用的单机部署
|
||||
|
||||
**配置方法**:
|
||||
|
||||
```yaml
|
||||
认证类型:无认证
|
||||
服务器地址:http://localhost:8000
|
||||
```
|
||||
|
||||
### 基本认证 (basic)
|
||||
|
||||
**适用场景**:
|
||||
|
||||
- 使用 Nginx 反向代理的部署
|
||||
- 团队内部使用且需要基础访问控制
|
||||
|
||||
**配置方法**:
|
||||
|
||||
1. **创建用户密码**:
|
||||
|
||||
```bash
|
||||
# 安装 apache2-utils
|
||||
sudo apt-get install apache2-utils
|
||||
|
||||
# 创建用户 'admin'
|
||||
sudo htpasswd -c /etc/nginx/.htpasswd admin
|
||||
```
|
||||
|
||||
2. **LobeChat 配置**:
|
||||
|
||||
```yaml
|
||||
认证类型:基本认证
|
||||
服务器地址:https://your-domain.com
|
||||
用户名:admin
|
||||
密码:your_secure_password
|
||||
```
|
||||
|
||||
### Bearer Token (bearer)
|
||||
|
||||
**适用场景**:
|
||||
|
||||
- API 驱动的应用集成
|
||||
- 需要 Token 认证的企业环境
|
||||
|
||||
**生成 Token**:
|
||||
|
||||
```python
|
||||
import jwt
|
||||
import datetime
|
||||
|
||||
payload = {
|
||||
'user': 'admin',
|
||||
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=30)
|
||||
}
|
||||
|
||||
secret_key = "your-secret-key"
|
||||
token = jwt.encode(payload, secret_key, algorithm='HS256')
|
||||
print(f"Bearer Token: {token}")
|
||||
```
|
||||
|
||||
**LobeChat 配置**:
|
||||
|
||||
```yaml
|
||||
认证类型:Bearer Token
|
||||
服务器地址:https://your-domain.com
|
||||
API 密钥:example-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
### 自定义认证 (custom)
|
||||
|
||||
**适用场景**:
|
||||
|
||||
- 集成现有企业认证系统
|
||||
- 需要多重认证头的系统
|
||||
|
||||
**LobeChat 配置**:
|
||||
|
||||
```yaml
|
||||
认证类型:自定义
|
||||
服务器地址:https://your-domain.com
|
||||
自定义请求头:
|
||||
{
|
||||
"X-API-Key": "your_api_key",
|
||||
"X-Client-ID": "lobechat"
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题处理
|
||||
|
||||
### 1. 如何安装 Comfy-Manager
|
||||
|
||||
Comfy-Manager 是 ComfyUI 的扩展管理器,让你能够轻松安装和管理各种节点、模型和扩展。
|
||||
|
||||
<details>
|
||||
<summary><b>📦 安装 Comfy-Manager 步骤</b></summary>
|
||||
|
||||
#### 方法一:手动安装(推荐)
|
||||
|
||||
```bash
|
||||
# 进入 ComfyUI 的 custom_nodes 目录
|
||||
cd ComfyUI/custom_nodes
|
||||
|
||||
# 克隆 Comfy-Manager 仓库
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager.git
|
||||
|
||||
# 重启 ComfyUI 服务器
|
||||
# 重新启动后,你会在 UI 中看到 Manager 按钮
|
||||
```
|
||||
|
||||
#### 方法二:使用一键安装脚本
|
||||
|
||||
```bash
|
||||
# 在 ComfyUI 根目录下执行
|
||||
curl -fsSL https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/install.sh | bash
|
||||
```
|
||||
|
||||
#### 验证安装
|
||||
|
||||
1. 重启 ComfyUI 服务器
|
||||
2. 访问 `http://localhost:8000`
|
||||
3. 你应该能在界面右下角看到 "Manager" 按钮
|
||||
|
||||
#### 使用 Comfy-Manager
|
||||
|
||||
**安装模型**:
|
||||
|
||||
1. 点击 "Manager" 按钮
|
||||
2. 选择 "Install Models"
|
||||
3. 搜索需要的模型(如 FLUX、SD3.5)
|
||||
4. 点击 "Install" 自动下载到正确目录
|
||||
|
||||
**安装节点扩展**:
|
||||
|
||||
1. 点击 "Manager" 按钮
|
||||
2. 选择 "Install Custom Nodes"
|
||||
3. 搜索需要的节点(如 ControlNet、AnimateDiff)
|
||||
4. 点击 "Install" 并重启服务器
|
||||
|
||||
**管理已安装内容**:
|
||||
|
||||
1. 点击 "Manager" 按钮
|
||||
2. 选择 "Installed" 查看已安装的扩展
|
||||
3. 可以更新、禁用或卸载扩展
|
||||
</details>
|
||||
|
||||
### 2. 如何处理 "Model not found" 错误
|
||||
|
||||
当你看到类似 `Model not found: flux1-dev.safetensors, please install one first.` 的错误时,说明服务器上缺少所需的模型文件。
|
||||
|
||||
<details>
|
||||
<summary><b>🔧 解决 Model not found 错误</b></summary>
|
||||
|
||||
#### 错误示例
|
||||
|
||||
```plaintext
|
||||
Model not found: flux1-dev.safetensors, please install one first.
|
||||
```
|
||||
|
||||
这个错误表示系统期望找到 `flux1-dev.safetensors` 模型文件,但在服务器上没有找到。
|
||||
|
||||
#### 解决方法
|
||||
|
||||
**方法一:使用 Comfy-Manager 下载(推荐)**
|
||||
|
||||
1. 打开 ComfyUI 界面
|
||||
2. 点击 "Manager" → "Install Models"
|
||||
3. 搜索错误提示中的模型名(如 "flux1-dev")
|
||||
4. 点击 "Install" 自动下载
|
||||
|
||||
**方法二:手动下载模型**
|
||||
|
||||
1. **下载模型文件**:
|
||||
- 访问 [Hugging Face](https://huggingface.co/black-forest-labs/FLUX.1-dev) 或其他模型源
|
||||
- 下载错误提示中的文件(如 `flux1-dev.safetensors`)
|
||||
|
||||
2. **放置到正确目录**:
|
||||
```bash
|
||||
# FLUX 和 SD3.5 主模型放入
|
||||
ComfyUI/models/diffusion_models/flux1-dev.safetensors
|
||||
|
||||
# SD1.5 和 SDXL 模型放入
|
||||
ComfyUI/models/checkpoints/
|
||||
```
|
||||
|
||||
3. **验证文件**:
|
||||
```bash
|
||||
# 检查文件是否存在
|
||||
ls -la ComfyUI/models/diffusion_models/flux1-dev.safetensors
|
||||
|
||||
# 检查文件完整性(可选)
|
||||
sha256sum flux1-dev.safetensors
|
||||
```
|
||||
|
||||
4. **重启 ComfyUI 服务器**
|
||||
|
||||
**方法三:使用 wget/curl 直接下载**
|
||||
|
||||
```bash
|
||||
# 进入模型目录
|
||||
cd ComfyUI/models/diffusion_models/
|
||||
|
||||
# 使用 wget 下载(替换为实际下载链接)
|
||||
wget https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors
|
||||
|
||||
# 或使用 curl
|
||||
curl -L -o flux1-dev.safetensors https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors
|
||||
```
|
||||
|
||||
#### 常见模型下载源
|
||||
|
||||
- **Hugging Face**:[https://huggingface.co/models](https://huggingface.co/models)
|
||||
- **Civitai**:[https://civitai.com/models](https://civitai.com/models)
|
||||
- **官方源**:
|
||||
- FLUX: [https://huggingface.co/black-forest-labs](https://huggingface.co/black-forest-labs)
|
||||
- SD3.5: [https://huggingface.co/stabilityai](https://huggingface.co/stabilityai)
|
||||
|
||||
#### 预防措施
|
||||
|
||||
1. **基础模型包**:至少下载一个基础模型
|
||||
- FLUX: `flux1-schnell.safetensors`(快速)或 `flux1-dev.safetensors`(高质量)
|
||||
- SD3.5: `sd3.5_large.safetensors`
|
||||
|
||||
2. **检查磁盘空间**:
|
||||
```bash
|
||||
# 检查可用空间
|
||||
df -h ComfyUI/models/
|
||||
```
|
||||
|
||||
3. **设置模型路径**(可选):
|
||||
如果你的模型存储在其他位置,可以创建符号链接:
|
||||
```bash
|
||||
ln -s /path/to/your/models ComfyUI/models/diffusion_models/
|
||||
```
|
||||
</details>
|
||||
|
||||
### 3. 如何处理缺少 System Component 错误
|
||||
|
||||
当你看到类似 `Missing VAE encoder: ae.safetensors` 或其他组件文件缺失的错误时,需要下载相应的系统组件。
|
||||
|
||||
<details>
|
||||
<summary><b>🛠️ 解决缺少 System Component 错误</b></summary>
|
||||
|
||||
#### 常见组件错误
|
||||
|
||||
```plaintext
|
||||
Missing VAE encoder: ae.safetensors. Please download and place it in the models/vae folder.
|
||||
Missing CLIP encoder: clip_l.safetensors. Please download and place it in the models/clip folder.
|
||||
Missing T5 encoder: t5xxl_fp16.safetensors. Please download and place it in the models/clip folder.
|
||||
```
|
||||
|
||||
#### 组件类型说明
|
||||
|
||||
| 组件类型 | 文件名示例 | 用途 | 存放目录 |
|
||||
| -------------- | ------------------------------ | ---------- | ------------------ |
|
||||
| **VAE** | ae.safetensors | 图像编码 / 解码 | models/vae/ |
|
||||
| **CLIP** | clip\_l.safetensors | 文本编码(CLIP) | models/clip/ |
|
||||
| **T5** | t5xxl\_fp16.safetensors | 文本编码(T5) | models/clip/ |
|
||||
| **ControlNet** | flux-controlnet-\*.safetensors | 控制网络 | models/controlnet/ |
|
||||
|
||||
#### 解决方法
|
||||
|
||||
**方法一:使用 Comfy-Manager(推荐)**
|
||||
|
||||
1. 点击 "Manager" → "Install Models"
|
||||
2. 在 "Filter" 中选择组件类型(VAE/CLIP/T5)
|
||||
3. 下载对应的组件文件
|
||||
|
||||
**方法二:手动下载必需组件**
|
||||
|
||||
##### FLUX 必需组件
|
||||
|
||||
```bash
|
||||
# 1. VAE 编码器
|
||||
cd ComfyUI/models/vae/
|
||||
wget https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/ae.safetensors
|
||||
|
||||
# 2. CLIP-L 编码器
|
||||
cd ComfyUI/models/clip/
|
||||
wget https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors
|
||||
|
||||
# 3. T5-XXL 编码器(可选择不同精度)
|
||||
# FP16 版本(推荐,平衡性能)
|
||||
wget https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors
|
||||
|
||||
# 或 FP8 版本(节省显存)
|
||||
wget https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors
|
||||
```
|
||||
|
||||
##### SD3.5 必需组件
|
||||
|
||||
```bash
|
||||
# SD3.5 使用不同的编码器
|
||||
cd ComfyUI/models/clip/
|
||||
|
||||
# CLIP-G 编码器
|
||||
wget https://huggingface.co/stabilityai/stable-diffusion-3.5-large/resolve/main/text_encoders/clip_g.safetensors
|
||||
|
||||
# CLIP-L 编码器
|
||||
wget https://huggingface.co/stabilityai/stable-diffusion-3.5-large/resolve/main/text_encoders/clip_l.safetensors
|
||||
|
||||
# T5-XXL 编码器
|
||||
wget https://huggingface.co/stabilityai/stable-diffusion-3.5-large/resolve/main/text_encoders/t5xxl_fp16.safetensors
|
||||
```
|
||||
|
||||
##### SDXL 必需组件
|
||||
|
||||
```bash
|
||||
# SDXL VAE
|
||||
cd ComfyUI/models/vae/
|
||||
wget https://huggingface.co/stabilityai/sdxl-vae/resolve/main/sdxl_vae.safetensors
|
||||
|
||||
# SDXL 使用内置的 CLIP 编码器,通常不需要单独下载
|
||||
```
|
||||
|
||||
#### 组件兼容性矩阵
|
||||
|
||||
| 模型系列 | 必需 VAE | 必需 CLIP | 必需 T5 | 可选组件 |
|
||||
| --------- | -------------- | ------------------- | ----------------------- | ---------- |
|
||||
| **FLUX** | ae.safetensors | clip\_l.safetensors | t5xxl\_fp16.safetensors | ControlNet |
|
||||
| **SD3.5** | 内置 | clip\_g + clip\_l | t5xxl\_fp16 | - |
|
||||
| **SDXL** | sdxl\_vae | 内置 | - | Refiner |
|
||||
| **SD1.5** | vae-ft-mse | 内置 | - | ControlNet |
|
||||
|
||||
#### 精度选择建议
|
||||
|
||||
**T5 编码器精度选择**:
|
||||
|
||||
| 显存容量 | 推荐版本 | 文件名 |
|
||||
| ------- | ------ | ------------------------------ |
|
||||
| \< 12GB | FP8 量化 | t5xxl\_fp8\_e4m3fn.safetensors |
|
||||
| 12-16GB | FP16 | t5xxl\_fp16.safetensors |
|
||||
| > 16GB | FP32 | t5xxl.safetensors |
|
||||
|
||||
#### 验证组件安装
|
||||
|
||||
```bash
|
||||
# 检查所有必需组件
|
||||
echo "=== VAE Components ==="
|
||||
ls -la ComfyUI/models/vae/
|
||||
|
||||
echo "=== CLIP/T5 Components ==="
|
||||
ls -la ComfyUI/models/clip/
|
||||
|
||||
echo "=== ControlNet Components ==="
|
||||
ls -la ComfyUI/models/controlnet/
|
||||
```
|
||||
|
||||
#### 故障排除
|
||||
|
||||
**问题:下载后仍然报错**
|
||||
|
||||
1. **检查文件权限**:
|
||||
```bash
|
||||
chmod 644 ComfyUI/models/vae/*.safetensors
|
||||
chmod 644 ComfyUI/models/clip/*.safetensors
|
||||
```
|
||||
|
||||
2. **清除缓存**:
|
||||
```bash
|
||||
# 清除 ComfyUI 缓存
|
||||
rm -rf ComfyUI/temp/*
|
||||
rm -rf ComfyUI/__pycache__/*
|
||||
```
|
||||
|
||||
3. **重启服务器**:
|
||||
```bash
|
||||
# 完全重启 ComfyUI
|
||||
pkill -f "python main.py"
|
||||
python main.py --listen 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
**问题:显存不足**
|
||||
|
||||
使用量化版本的组件:
|
||||
|
||||
- T5: 使用 `t5xxl_fp8_e4m3fn.safetensors` 而不是 FP16/FP32
|
||||
- VAE: 某些模型支持 FP16 VAE 版本
|
||||
|
||||
**问题:下载速度慢**
|
||||
|
||||
1. 使用镜像源(如适用)
|
||||
2. 使用下载工具(如 aria2c)支持断点续传:
|
||||
```bash
|
||||
aria2c -x 16 -s 16 -k 1M [下载链接]
|
||||
```
|
||||
</details>
|
||||
|
||||
## ComfyUI 服务器安装
|
||||
|
||||
<details>
|
||||
<summary><b>🚀 安装和配置 ComfyUI 服务器</b></summary>
|
||||
|
||||
### 1. 安装 ComfyUI
|
||||
|
||||
```bash
|
||||
# 克隆 ComfyUI 仓库
|
||||
git clone https://github.com/comfyanonymous/ComfyUI.git
|
||||
cd ComfyUI
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 可选:安装JWT支持(用于Token认证)
|
||||
pip install PyJWT
|
||||
|
||||
# 启动 ComfyUI 服务器
|
||||
python main.py --listen 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### 2. 下载模型文件
|
||||
|
||||
**推荐基础配置** (最小化安装):
|
||||
|
||||
**主模型** (放入 `models/diffusion_models/` 目录):
|
||||
|
||||
- `flux1-schnell.safetensors` - 快速生成(4 步)
|
||||
- `flux1-dev.safetensors` - 高质量创作(20 步)
|
||||
|
||||
**必需组件** (放入相应目录):
|
||||
|
||||
- `models/vae/ae.safetensors` - VAE 编码器
|
||||
- `models/clip/clip_l.safetensors` - CLIP 文本编码器
|
||||
- `models/clip/t5xxl_fp16.safetensors` - T5 文本编码器
|
||||
|
||||
### 3. 验证服务器运行
|
||||
|
||||
访问 `http://localhost:8000` 确认 ComfyUI 界面正常加载。
|
||||
|
||||
<Callout type={'info'}>
|
||||
**智能模型选择**:LobeChat 会根据服务器上可用的模型文件自动选择最佳模型。您无需下载所有模型,系统会在可用模型中按优先级(官方 > 企业 > 社区)自动选择。
|
||||
</Callout>
|
||||
</details>
|
||||
|
||||
## 支持的模型
|
||||
|
||||
LobeChat ComfyUI 集成采用配置驱动的架构,支持 **223 个模型**,提供从官方模型到社区优化版本的全覆盖。
|
||||
|
||||
### FLUX 系列推荐参数
|
||||
|
||||
| 模型类型 | 推荐步数 | CFG Scale | 分辨率范围 |
|
||||
| ----------- | ---- | --------- | ------------------- |
|
||||
| **Schnell** | 4 步 | - | 512×512 至 1536×1536 |
|
||||
| **Dev** | 20 步 | 3.5 | 512×512 至 2048×2048 |
|
||||
| **Kontext** | 20 步 | 3.5 | 512×512 至 2048×2048 |
|
||||
| **Krea** | 20 步 | 4.5 | 512×512 至 2048×2048 |
|
||||
|
||||
### SD3.5 系列参数
|
||||
|
||||
| 模型类型 | 推荐步数 | CFG Scale | 分辨率范围 |
|
||||
| --------------- | ---- | --------- | ------------------- |
|
||||
| **Large** | 25 步 | 7.0 | 512×512 至 2048×2048 |
|
||||
| **Large Turbo** | 8 步 | 3.5 | 512×512 至 1536×1536 |
|
||||
| **Medium** | 20 步 | 6.0 | 512×512 至 1536×1536 |
|
||||
|
||||
<details>
|
||||
<summary><b>📋 当前完整支持的模型列表</b></summary>
|
||||
|
||||
### 模型分类体系
|
||||
|
||||
#### 优先级 1:官方核心模型
|
||||
|
||||
**FLUX.1 Official 系列**:
|
||||
|
||||
- `flux1-dev.safetensors` - 高质量创作模型
|
||||
- `flux1-schnell.safetensors` - 快速生成模型
|
||||
- `flux1-kontext-dev.safetensors` - 图像编辑模型
|
||||
- `flux1-krea-dev.safetensors` - 安全增强模型
|
||||
|
||||
**SD3.5 Official 系列**:
|
||||
|
||||
- `sd3.5_large.safetensors` - SD3.5 大型基础模型
|
||||
- `sd3.5_large_turbo.safetensors` - 快速生成版本
|
||||
- `sd3.5_medium.safetensors` - 中等规模模型
|
||||
|
||||
#### 优先级 2:企业优化模型(106 个 FLUX)
|
||||
|
||||
**量化优化系列**:
|
||||
|
||||
- **GGUF 量化**:每个变体支持 11 种量化级别(F16, Q8\_0, Q6\_K, Q5\_K\_M, Q5\_K\_S, Q4\_K\_M, Q4\_K\_S, Q4\_0, Q3\_K\_M, Q3\_K\_S, Q2\_K)
|
||||
- **FP8 精度**:fp8\_e4m3fn、fp8\_e5m2 优化版本
|
||||
- **企业轻量级**:FLUX.1-lite-8B 系列
|
||||
- **技术实验**:NF4、SVDQuant、TorchAO、optimum-quanto、MFLUX 优化版本
|
||||
|
||||
#### 优先级 3:社区精调模型(48 个 FLUX)
|
||||
|
||||
**社区优化系列**:
|
||||
|
||||
- **Jib Mix Flux** 系列:高质量混合模型
|
||||
- **Real Dream FLUX** 系列:现实主义风格
|
||||
- **Vision Realistic** 系列:视觉现实化
|
||||
- **PixelWave FLUX** 系列:像素艺术优化
|
||||
- **Fluxmania** 系列:多样化风格支持
|
||||
|
||||
### SD 系列模型支持(93 个)
|
||||
|
||||
**SD3.5 系列**:5 个模型
|
||||
**SD1.5 系列**:37 个模型(包括官方、量化和社区版本)
|
||||
**SDXL 系列**:50 个模型(包括基础、Refiner 和 Playground 模型)
|
||||
|
||||
### 工作流支持
|
||||
|
||||
系统支持 **6 种工作流**:
|
||||
|
||||
- **flux-dev**:高质量创作工作流
|
||||
- **flux-schnell**:快速生成工作流
|
||||
- **flux-kontext**:图像编辑工作流
|
||||
- **sd35**:SD3.5 专用工作流
|
||||
- **simple-sd**:简单 SD 工作流
|
||||
- **index**:工作流入口
|
||||
</details>
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 硬件要求
|
||||
|
||||
**最低配置** (GGUF 量化模型):
|
||||
|
||||
- GPU:6GB VRAM (使用 Q4 量化)
|
||||
- RAM:12GB
|
||||
- 存储:30GB 可用空间
|
||||
|
||||
**推荐配置** (标准模型):
|
||||
|
||||
- GPU:12GB+ VRAM (RTX 4070 Ti 或更高)
|
||||
- RAM:24GB+
|
||||
- 存储:SSD 100GB+ 可用空间
|
||||
|
||||
### 显存优化策略
|
||||
|
||||
| 显存容量 | 推荐量化 | 模型示例 | 性能特点 |
|
||||
| ----------- | --------------- | ---------------------------------- | ------- |
|
||||
| **6-8GB** | Q4\_0, Q4\_K\_S | `flux1-dev-Q4_0.gguf` | 最小显存占用 |
|
||||
| **10-12GB** | Q6\_K, Q8\_0 | `flux1-dev-Q6_K.gguf` | 平衡性能与质量 |
|
||||
| **16GB+** | FP8, FP16 | `flux1-dev-fp8-e4m3fn.safetensors` | 接近原始质量 |
|
||||
| **24GB+** | 完整模型 | `flux1-dev.safetensors` | 最佳质量 |
|
||||
|
||||
## 自定义模型使用
|
||||
|
||||
<details>
|
||||
<summary><b>🎨 配置自定义 SD 模型</b></summary>
|
||||
|
||||
LobeChat 支持使用自定义的 Stable Diffusion 模型。系统使用固定的文件名来识别自定义模型。
|
||||
|
||||
### 1. 模型文件准备
|
||||
|
||||
**必需文件**:
|
||||
|
||||
- **主模型文件**:`custom_sd_lobe.safetensors`
|
||||
- **VAE 文件(可选)**:`custom_sd_vae_lobe.safetensors`
|
||||
|
||||
### 2. 添加自定义模型
|
||||
|
||||
**方法一:重命名现有模型**
|
||||
|
||||
```bash
|
||||
# 将您的模型重命名为固定文件名
|
||||
mv your_custom_model.safetensors custom_sd_lobe.safetensors
|
||||
|
||||
# 移动到正确目录
|
||||
mv custom_sd_lobe.safetensors ComfyUI/models/diffusion_models/
|
||||
```
|
||||
|
||||
**方法二:创建符号链接(推荐)**
|
||||
|
||||
```bash
|
||||
# 创建软链接,方便切换不同模型
|
||||
ln -s /path/to/your_model.safetensors ComfyUI/models/diffusion_models/custom_sd_lobe.safetensors
|
||||
```
|
||||
|
||||
### 3. 使用自定义模型
|
||||
|
||||
在 LobeChat 中,自定义模型会显示为:
|
||||
|
||||
- **stable-diffusion-custom**:标准自定义模型
|
||||
- **stable-diffusion-custom-refiner**:Refiner 自定义模型
|
||||
|
||||
### 自定义模型参数建议
|
||||
|
||||
| 参数 | SD 1.5 模型 | SDXL 模型 |
|
||||
| ---------- | --------- | ------- |
|
||||
| **steps** | 20-30 | 25-40 |
|
||||
| **cfg** | 7.0 | 6.0-8.0 |
|
||||
| **width** | 512 | 1024 |
|
||||
| **height** | 512 | 1024 |
|
||||
</details>
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 智能错误诊断系统
|
||||
|
||||
LobeChat 集成了智能错误处理系统,能够自动诊断并提供针对性的解决方案。
|
||||
|
||||
#### 错误类型与解决方案
|
||||
|
||||
| 错误类型 | 用户提示 | 自动诊断 |
|
||||
| -------- | ------------------- | --------------- |
|
||||
| **连接问题** | "无法连接到 ComfyUI 服务器" | 自动检测服务器状态和网络连通性 |
|
||||
| **认证问题** | "API 密钥无效或已过期" | 自动验证认证凭据有效性 |
|
||||
| **权限问题** | "访问权限不足" | 自动检查用户权限和文件访问权限 |
|
||||
| **模型问题** | "找不到指定的模型文件" | 自动扫描可用模型并建议替代方案 |
|
||||
| **配置问题** | "配置文件存在错误" | 自动验证配置完整性和语法正确性 |
|
||||
|
||||
<details>
|
||||
<summary><b>🔍 传统故障排除方法</b></summary>
|
||||
|
||||
#### 1. 连接失败
|
||||
|
||||
**问题**:无法连接到 ComfyUI 服务器
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 确认服务器运行
|
||||
curl http://localhost:8000/system_stats
|
||||
|
||||
# 检查端口
|
||||
netstat -tulpn | grep 8000
|
||||
```
|
||||
|
||||
#### 2. 内存不足
|
||||
|
||||
**问题**:生成过程中出现内存错误
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- 降低图像分辨率
|
||||
- 减少生成步数
|
||||
- 使用量化模型
|
||||
|
||||
#### 3. 认证失败
|
||||
|
||||
**问题**:401 或 403 错误
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- 验证认证配置
|
||||
- 检查 Token 是否过期
|
||||
- 确认用户权限
|
||||
</details>
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 提示词编写
|
||||
|
||||
1. **详细描述**:提供清晰、详细的图像描述
|
||||
2. **风格指定**:明确指定艺术风格、色彩风格等
|
||||
3. **质量关键词**:添加 "4K", "high quality", "detailed" 等关键词
|
||||
4. **避免矛盾**:确保描述内容逻辑一致
|
||||
|
||||
**示例**:
|
||||
|
||||
```plaintext
|
||||
A young woman with flowing long hair, wearing an elegant blue dress, standing in a cherry blossom park,
|
||||
sunlight filtering through leaves, warm atmosphere, cinematic lighting, 4K high resolution, detailed, photorealistic
|
||||
```
|
||||
|
||||
### 参数调优
|
||||
|
||||
1. **FLUX Schnell**:适合快速预览,使用 4 步生成
|
||||
2. **FLUX Dev**:平衡质量和速度,CFG 3.5,步数 20
|
||||
3. **FLUX Krea-dev**:安全创作,CFG 4.5,注意内容过滤
|
||||
4. **FLUX Kontext-dev**:图像编辑,strength 0.6-0.9
|
||||
|
||||
<Callout type={'warning'}>
|
||||
在使用过程中请注意:
|
||||
|
||||
- FLUX Dev、Krea-dev、Kontext-dev 模型仅限非商业使用
|
||||
- 生成内容请遵守相关法律法规和平台政策
|
||||
- 大型模型生成可能需要较长时间,请耐心等待
|
||||
</Callout>
|
||||
|
||||
## API 参考
|
||||
|
||||
<details>
|
||||
<summary><b>📚 API 文档</b></summary>
|
||||
|
||||
### 请求格式
|
||||
|
||||
```typescript
|
||||
interface ComfyUIRequest {
|
||||
model: string; // 模型 ID,如 'flux-schnell'
|
||||
prompt: string; // 文本提示词
|
||||
width: number; // 图像宽度
|
||||
height: number; // 图像高度
|
||||
steps: number; // 生成步数
|
||||
seed: number; // 随机种子
|
||||
cfg?: number; // CFG Scale (Dev/Krea/Kontext 专用)
|
||||
strength?: number; // 编辑强度 (Kontext 专用)
|
||||
imageUrl?: string; // 输入图像 (Kontext 专用)
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```typescript
|
||||
interface ComfyUIResponse {
|
||||
images: Array<{
|
||||
url: string; // 生成的图像 URL
|
||||
filename: string; // 文件名
|
||||
subfolder: string; // 子目录
|
||||
type: string; // 文件类型
|
||||
}>;
|
||||
prompt_id: string; // 提示 ID
|
||||
}
|
||||
```
|
||||
|
||||
### 错误代码
|
||||
|
||||
| 错误代码 | 描述 | 解决建议 |
|
||||
| ----- | ------ | -------------- |
|
||||
| `400` | 请求参数无效 | 检查参数格式和范围 |
|
||||
| `401` | 认证失败 | 验证 API 密钥和认证配置 |
|
||||
| `403` | 权限不足 | 检查用户权限 |
|
||||
| `404` | 模型未找到 | 确认模型文件存在 |
|
||||
| `500` | 服务器错误 | 检查 ComfyUI 日志 |
|
||||
</details>
|
||||
|
||||
至此你已经可以在 LobeChat 中使用 ComfyUI 进行高质量的 AI 图像生成和编辑了。如果遇到问题,请参考故障排除部分或查阅 [ComfyUI 官方文档](https://github.com/comfyanonymous/ComfyUI)。
|
||||
@@ -0,0 +1,73 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
// 覆盖核心可访问路径(含重定向来源)
|
||||
const baseRoutes: string[] = [
|
||||
'/',
|
||||
'/chat',
|
||||
'/discover',
|
||||
'/image',
|
||||
'/files',
|
||||
'/repos', // next.config.ts -> /files
|
||||
'/changelog',
|
||||
];
|
||||
|
||||
// settings 路由改为通过 query 参数控制 active tab
|
||||
// 参考 SettingsTabs: about, agent, common, hotkey, llm, provider, proxy, storage, system-agent, tts
|
||||
const settingsTabs = [
|
||||
'common',
|
||||
'llm',
|
||||
'provider',
|
||||
'about',
|
||||
'hotkey',
|
||||
'proxy',
|
||||
'storage',
|
||||
'tts',
|
||||
'system-agent',
|
||||
'agent',
|
||||
];
|
||||
|
||||
const routes: string[] = [...baseRoutes, ...settingsTabs.map((key) => `/settings?active=${key}`)];
|
||||
|
||||
// CI 环境下跳过容易不稳定或受特性开关影响的路由
|
||||
const ciSkipPaths = new Set<string>([
|
||||
'/image',
|
||||
'/changelog',
|
||||
'/settings?active=common',
|
||||
'/settings?active=llm',
|
||||
]);
|
||||
|
||||
// @ts-ignore
|
||||
async function assertNoPageErrors(page: Parameters<typeof test>[0]['page']) {
|
||||
const pageErrors: Error[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
|
||||
page.on('pageerror', (err: Error) => pageErrors.push(err));
|
||||
page.on('console', (msg: any) => {
|
||||
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
||||
});
|
||||
|
||||
// 仅校验页面级错误,忽略控制台 error 以提升稳定性
|
||||
expect
|
||||
.soft(pageErrors, `page errors: ${pageErrors.map((e) => e.message).join('\n')}`)
|
||||
.toHaveLength(0);
|
||||
}
|
||||
|
||||
test.describe('Smoke: core routes', () => {
|
||||
for (const path of routes) {
|
||||
test(`should open ${path} without error`, async ({ page }) => {
|
||||
if (process.env.CI && ciSkipPaths.has(path)) test.skip(true, 'skip flaky route on CI');
|
||||
const response = await page.goto(path, { waitUntil: 'commit' });
|
||||
// 2xx 或 3xx 视为可接受(允许中间件/重定向)
|
||||
const status = response?.status() ?? 0;
|
||||
expect(status, `unexpected status for ${path}: ${status}`).toBeLessThan(400);
|
||||
|
||||
// 一般错误标题防御
|
||||
await expect(page).not.toHaveTitle(/not found|error/i);
|
||||
|
||||
// body 可见
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
|
||||
await assertNoPageErrors(page);
|
||||
});
|
||||
}
|
||||
});
|
||||
+117
-2
@@ -2,9 +2,10 @@
|
||||
"ModelSwitch": {
|
||||
"title": "النموذج"
|
||||
},
|
||||
"active": "نشط",
|
||||
"agentDefaultMessage": "مرحبًا، أنا **{{name}}**، يمكنك بدء المحادثة معي على الفور، أو يمكنك الذهاب إلى [إعدادات المساعد]({{url}}) لإكمال معلوماتي.",
|
||||
"agentDefaultMessageWithSystemRole": "مرحبًا، أنا **{{name}}**، {{systemRole}}، دعنا نبدأ الدردشة!",
|
||||
"agentDefaultMessageWithoutEdit": "مرحبًا، أنا **{{name}}**، دعنا نبدأ المحادثة!",
|
||||
"agentDefaultMessageWithSystemRole": "مرحبًا، أنا **{{name}}**، كيف يمكنني مساعدتك؟",
|
||||
"agentDefaultMessageWithoutEdit": "مرحبًا، أنا **{{name}}**، كيف يمكنني مساعدتك؟",
|
||||
"agents": "مساعد",
|
||||
"artifact": {
|
||||
"generating": "جاري الإنشاء",
|
||||
@@ -13,17 +14,28 @@
|
||||
"thought": "عملية التفكير",
|
||||
"unknownTitle": "عمل غير مسمى"
|
||||
},
|
||||
"availableAgents": "المساعدون المتاحون",
|
||||
"backToBottom": "العودة إلى الأسفل",
|
||||
"chatList": {
|
||||
"longMessageDetail": "عرض التفاصيل"
|
||||
},
|
||||
"clearCurrentMessages": "مسح رسائل الجلسة الحالية",
|
||||
"confirmClearCurrentMessages": "سيتم مسح رسائل الجلسة الحالية قريبًا، وبمجرد المسح لن يمكن استعادتها، يرجى تأكيد الإجراء الخاص بك",
|
||||
"confirmRemoveChatGroupItemAlert": "سيتم حذف هذه المحادثة الجماعية، ولن يتأثر أعضاء المجموعة، يرجى تأكيد العملية الخاصة بك",
|
||||
"confirmRemoveGroupItemAlert": "سيتم حذف هذه المجموعة قريبًا. بعد الحذف، سيُنتقل المساعدون في هذه المجموعة إلى القائمة الافتراضية. يرجى تأكيد إجراء الحذف.",
|
||||
"confirmRemoveGroupSuccess": "تم حذف الدردشة الجماعية بنجاح",
|
||||
"confirmRemoveSessionItemAlert": "سيتم حذف هذا المساعد قريبًا، وبمجرد الحذف لن يمكن استعادته، يرجى تأكيد الإجراء الخاص بك",
|
||||
"confirmRemoveSessionSuccess": "تم حذف المساعد بنجاح",
|
||||
"defaultAgent": "المساعد الافتراضي",
|
||||
"defaultGroupChat": "المحادثة الجماعية",
|
||||
"defaultList": "القائمة الافتراضية",
|
||||
"defaultSession": "المساعد الافتراضي",
|
||||
"dm": {
|
||||
"placeholder": "ستظهر رسائلك الخاصة مع {{agentTitle}} هنا.",
|
||||
"tooltip": "أرسل رسالة خاصة",
|
||||
"visibleTo": "مرئي فقط لـ {{target}}",
|
||||
"you": "أنت"
|
||||
},
|
||||
"duplicateSession": {
|
||||
"loading": "جاري النسخ...",
|
||||
"success": "تم النسخ بنجاح",
|
||||
@@ -58,11 +70,56 @@
|
||||
"title": "استخراج محتوى رابط الويب"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"desc": "التعاون مع عدة مساعدين للذكاء الاصطناعي في مساحة محادثة مشتركة.",
|
||||
"memberTooltip": "هناك {{count}} عضوًا في المجموعة",
|
||||
"orchestratorThinking": "المُنسق يفكر...",
|
||||
"removeMember": "إزالة عضو",
|
||||
"title": "مجموعة"
|
||||
},
|
||||
"groupDescription": "وصف المجموعة",
|
||||
"groupSidebar": {
|
||||
"members": {
|
||||
"addMember": "إضافة عضو",
|
||||
"memberSettings": "إعدادات العضو",
|
||||
"orchestrator": "المُنسق",
|
||||
"orchestratorThinking": "المُنسق يفكر...",
|
||||
"removeMember": "إزالة عضو",
|
||||
"stopOrchestrator": "إيقاف التفكير",
|
||||
"triggerOrchestrator": "بدء المحادثة الجماعية"
|
||||
},
|
||||
"tabs": {
|
||||
"host": "المضيف",
|
||||
"members": "الأعضاء",
|
||||
"role": "الإعداد"
|
||||
}
|
||||
},
|
||||
"groupWizard": {
|
||||
"chooseMembers": "اختر المساعدين الحاليين...",
|
||||
"createGroup": "إنشاء دردشة جماعية",
|
||||
"existingMembers": "الأعضاء الحاليون",
|
||||
"groupMembers": "سيتم أيضًا إضافة هؤلاء المساعدين إلى قائمتك",
|
||||
"host": {
|
||||
"description": "لجعل الدردشة الجماعية تعمل تلقائيًا",
|
||||
"title": "المضيف",
|
||||
"tooltip": "إذا تم تعطيل مضيف الدردشة الجماعية، فستحتاج إلى الإشارة يدويًا إلى الأعضاء باستخدام @ لكي يتمكنوا من الرد"
|
||||
},
|
||||
"memberCount": "{{count}} عضو",
|
||||
"noMatchingTemplates": "لا توجد قوالب مطابقة",
|
||||
"noSelectedTemplates": "لم يتم اختيار أي قالب",
|
||||
"noTemplateMembers": "لا يوجد أعضاء في القالب",
|
||||
"noTemplates": "لا توجد قوالب متاحة",
|
||||
"searchTemplates": "ابحث في القوالب...",
|
||||
"title": "إنشاء دردشة جماعية",
|
||||
"useTemplate": "استخدام القالب"
|
||||
},
|
||||
"hideForYou": "تم إخفاء محتوى الرسائل الخاصة، يرجى تفعيل خيار 【عرض محتوى الرسائل الخاصة】 في الإعدادات للعرض",
|
||||
"history": {
|
||||
"title": "سيتذكر المساعد آخر {{count}} رسالة فقط"
|
||||
},
|
||||
"historyRange": "نطاق التاريخ",
|
||||
"historySummary": "ملخص الرسائل التاريخية",
|
||||
"inactive": "غير نشط",
|
||||
"inbox": {
|
||||
"desc": "قم بتشغيل مجموعة الدماغ وأشعل شرارة التفكير. مساعدك الذكي، هنا حيث يمكنك التواصل بكل شيء",
|
||||
"title": "دردشة عشوائية"
|
||||
@@ -83,6 +140,7 @@
|
||||
"intentUnderstanding": {
|
||||
"title": "جارٍ فهم وتحليل نواياك..."
|
||||
},
|
||||
"inviteMembers": "دعوة الأعضاء",
|
||||
"knowledgeBase": {
|
||||
"all": "جميع المحتويات",
|
||||
"allFiles": "جميع الملفات",
|
||||
@@ -101,12 +159,29 @@
|
||||
"uploadGuide": "يمكنك عرض الملفات التي تم تحميلها في «قاعدة المعرفة»",
|
||||
"viewMore": "عرض المزيد"
|
||||
},
|
||||
"memberSelection": {
|
||||
"addMember": "إضافة عضو",
|
||||
"allMembers": "جميع الأعضاء",
|
||||
"createGroup": "إنشاء محادثة جماعية",
|
||||
"noAvailableAgents": "لا يوجد مساعدين متاحين للدعوة",
|
||||
"noSelectedAgents": "لم يتم اختيار مساعدين بعد",
|
||||
"searchAgents": "البحث عن مساعدين...",
|
||||
"setInitialMembers": "اختيار أعضاء المجموعة"
|
||||
},
|
||||
"members": "الأعضاء",
|
||||
"mention": {
|
||||
"title": "الإشارة إلى الأعضاء"
|
||||
},
|
||||
"messageAction": {
|
||||
"delAndRegenerate": "حذف وإعادة الإنشاء",
|
||||
"deleteDisabledByThreads": "يوجد موضوعات فرعية، لا يمكن الحذف",
|
||||
"regenerate": "إعادة الإنشاء"
|
||||
},
|
||||
"messages": {
|
||||
"dm": {
|
||||
"sentTo": "مرئي فقط لـ {{name}}",
|
||||
"title": "الرسائل الخاصة"
|
||||
},
|
||||
"modelCard": {
|
||||
"credit": "نقاط",
|
||||
"creditPricing": "التسعير",
|
||||
@@ -150,7 +225,21 @@
|
||||
"total": "الإجمالي المستهلك"
|
||||
}
|
||||
},
|
||||
"minimap": {
|
||||
"jumpToMessage": "الانتقال إلى الرسالة رقم {{index}}",
|
||||
"nextMessage": "الرسالة التالية",
|
||||
"previousMessage": "الرسالة السابقة",
|
||||
"senderAssistant": "المساعد",
|
||||
"senderUser": "أنت"
|
||||
},
|
||||
"newAgent": "مساعد جديد",
|
||||
"newGroupChat": "إنشاء دردشة جماعية جديدة",
|
||||
"noAgentsYet": "لا يوجد أي مساعد في هذه المجموعة بعد. انقر على زر + لدعوة مساعد.",
|
||||
"noAvailableAgents": "لا يوجد مساعدون متاحون للدعوة",
|
||||
"noMatchingAgents": "لا يوجد مساعد متطابق",
|
||||
"noMembersYet": "لا يوجد أعضاء في هذه المجموعة بعد. انقر على زر + لدعوة المساعدين.",
|
||||
"noSelectedAgents": "لم يتم اختيار أي مساعد بعد",
|
||||
"owner": "مالك المجموعة",
|
||||
"pin": "تثبيت",
|
||||
"pinOff": "إلغاء التثبيت",
|
||||
"rag": {
|
||||
@@ -191,12 +280,16 @@
|
||||
"title": "بحث عبر الإنترنت"
|
||||
},
|
||||
"searchAgentPlaceholder": "مساعد البحث...",
|
||||
"searchAgents": "مساعد البحث...",
|
||||
"selectedAgents": "المساعدون المختارون",
|
||||
"sendPlaceholder": "أدخل محتوى الدردشة...",
|
||||
"sessionGroup": {
|
||||
"config": "إدارة المجموعات",
|
||||
"confirmRemoveGroupAlert": "سيتم حذف هذه المجموعة قريبًا، وبعد الحذف، سيتم نقل مساعدي هذه المجموعة إلى القائمة الافتراضية، يرجى تأكيد إجراءك",
|
||||
"createAgentSuccess": "تم إنشاء المساعد بنجاح",
|
||||
"createGroup": "إضافة مجموعة جديدة",
|
||||
"createGroupFailed": "فشل إنشاء المحادثة الجماعية",
|
||||
"createGroupSuccess": "تم إنشاء المحادثة الجماعية بنجاح",
|
||||
"createSuccess": "تم الإنشاء بنجاح",
|
||||
"creatingAgent": "جاري إنشاء المساعد...",
|
||||
"inputPlaceholder": "الرجاء إدخال اسم المجموعة...",
|
||||
@@ -211,11 +304,24 @@
|
||||
"shareModal": {
|
||||
"copy": "نسخ",
|
||||
"download": "تحميل اللقطة",
|
||||
"downloadError": "فشل التنزيل",
|
||||
"downloadFile": "تحميل الملف",
|
||||
"downloadPdf": "تنزيل PDF",
|
||||
"downloadSuccess": "تم التنزيل بنجاح",
|
||||
"exportPdf": "تصدير إلى PDF",
|
||||
"exportTitle": "العنوان الافتراضي",
|
||||
"generatePdf": "إنشاء ملف PDF",
|
||||
"generatingPdf": "جارٍ إنشاء PDF...",
|
||||
"imageType": "نوع الصورة",
|
||||
"includeTool": "تضمين رسالة الأداة",
|
||||
"includeUser": "تضمين رسالة المستخدم",
|
||||
"loadingPdf": "جارٍ تحميل ملف PDF...",
|
||||
"noPdfData": "لا توجد بيانات PDF",
|
||||
"pdf": "PDF",
|
||||
"pdfErrorDescription": "حدث خطأ أثناء إنشاء PDF، يرجى المحاولة مرة أخرى",
|
||||
"pdfGenerationError": "فشل إنشاء PDF",
|
||||
"pdfReady": "تم تجهيز PDF",
|
||||
"regeneratePdf": "إعادة إنشاء ملف PDF",
|
||||
"screenshot": "لقطة شاشة",
|
||||
"settings": "إعدادات التصدير",
|
||||
"text": "نص",
|
||||
@@ -230,6 +336,12 @@
|
||||
"loading": "جارٍ التعرف...",
|
||||
"prettifying": "جارٍ التجميل..."
|
||||
},
|
||||
"supervisor": {
|
||||
"todoList": {
|
||||
"allComplete": "تم إنجاز جميع المهام",
|
||||
"title": "المهام المنجزة"
|
||||
}
|
||||
},
|
||||
"thread": {
|
||||
"divider": "موضوع فرعي",
|
||||
"threadMessageCount": "{{messageCount}} رسالة",
|
||||
@@ -243,6 +355,7 @@
|
||||
"chats": "رسائل المحادثة",
|
||||
"historySummary": "ملخص التاريخ",
|
||||
"rest": "المتبقي",
|
||||
"supervisor": "مُنسق المجموعة",
|
||||
"systemRole": "تعيين الدور",
|
||||
"title": "تفاصيل الرمز",
|
||||
"tools": "تعيين الإضافات",
|
||||
@@ -268,6 +381,7 @@
|
||||
"action": "قراءة صوتية",
|
||||
"clear": "مسح الصوت"
|
||||
},
|
||||
"untitledAgent": "مساعد بدون اسم",
|
||||
"updateAgent": "تحديث معلومات المساعد",
|
||||
"upload": {
|
||||
"action": {
|
||||
@@ -295,5 +409,6 @@
|
||||
"videoSizeExceeded": "لا يمكن أن يتجاوز حجم ملف الفيديو 20 ميغابايت، حجم الملف الحالي هو {{actualSize}}"
|
||||
}
|
||||
},
|
||||
"you": "أنت",
|
||||
"zenMode": "وضع التركيز"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user