Compare commits

...

78 Commits

Author SHA1 Message Date
arvinxx 862f61f379 update info 2026-02-15 22:30:00 +08:00
arvinxx 8127c1b9e7 update 2026-02-15 22:14:23 +08:00
arvinxx 8bff2097e0 🐛 fix: scroll ChatInput into view when starter mode activates
When clicking Create Agent/Group/Write, the SuggestQuestions panel
renders below the ChatInput and pushes total content beyond the
viewport, causing the ChatInput to scroll out of view. This adds
scrollIntoView + focus on mode change so the editor stays visible
and ready for input. Also improves E2E test to target contenteditable
inside ChatInput directly and wait for animation to settle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:14:16 +08:00
Arvin Xu d2a042cd95 🐛 fix: scroll ChatInput into view when starter mode activates (#12334)
* 🐛 fix: scroll ChatInput into view when starter mode activates

When clicking Create Agent/Group/Write, the SuggestQuestions panel
renders below the ChatInput and pushes total content beyond the
viewport, causing the ChatInput to scroll out of view. This adds
scrollIntoView + focus on mode change so the editor stays visible
and ready for input. Also improves E2E test to target contenteditable
inside ChatInput directly and wait for animation to settle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:06:14 +08:00
lobehubbot c1916a1996 Merge remote-tracking branch 'origin/main' into canary 2026-02-15 13:28:56 +00:00
sxjeru 502d94bd4c 💄 style: add new MiniMax-M2.5 model (#12289)
* feat: 添加多个新模型及其定价信息,更新模型解析配置

* fix: 更新多个模型的导入语法,添加新模型GLM-5及其属性

* feat: 添加多个Doubao模型及其定价信息,优化payload处理逻辑
2026-02-15 21:28:16 +08:00
YuTengjing f4bd332d11 🐛 fix: use batch config for computePriceParams and pass latency (#12348)
## Summary
- Pass latency (task submission → webhook callback) to
`chargeAfterGenerate` for video generation metrics
- Use `batch.config` instead of webhook `result` for
`computePriceParams` (generateAudio), ensuring pricing uses
user-submitted config
- Rename `generateAudio` top-level param to `computePriceParams: {
generateAudio }` for better structure

## Test plan
- [ ] Submit a video generation task with generateAudio enabled, verify
charge uses correct pricing
- [ ] Submit a video generation task with generateAudio disabled, verify
charge reflects the difference
- [ ] Verify latency is recorded in the charge metrics

## Summary by Sourcery

Adjust video generation charging and metadata to rely on batch config
and record end-to-end latency.

Bug Fixes:
- Ensure generateAudio pricing uses the original batch configuration
instead of the webhook result payload.

Enhancements:
- Pass task submission-to-webhook latency into video charging for
improved metrics.
- Rename chargeAfterGenerate pricing input to computePriceParams for
clearer structure and future extensibility.
- Name generated video files using the batch prompt prefix when
available to improve asset identification.

Tests:
- Add unit tests for the Volcengine video provider request/response
handling, including payload mapping, client config, and error cases.
- Add initial test scaffolding for video standard parameters, cost
computation, single-price resolution, and Volcengine video webhook
handling.
2026-02-15 20:46:29 +08:00
YuTengjing 7df81ffaa1 🐛 fix: add sanitizeFileName alias to vitest config for test resolution 2026-02-15 20:32:24 +08:00
YuTengjing ed076b3cf5 test: add unit tests for sanitizeFileName utility 2026-02-15 20:13:22 +08:00
YuTengjing 0abde1623d 🐛 fix: extract sanitizeFileName util to prevent path traversal in generated file names 2026-02-15 20:03:04 +08:00
YuTengjing 481e5c0066 🐛 fix: sanitize prompt for video file name to prevent path traversal 2026-02-15 20:00:41 +08:00
YuTengjing 8719354282 🐛 fix: use batch config for computePriceParams instead of webhook result 2026-02-15 19:47:10 +08:00
YuTengjing 5957bd4578 🐛 fix: use prompt as video file name instead of generation id 2026-02-15 19:34:14 +08:00
YuTengjing 335e246ac5 test: add unit tests for video generation feature
Cover resolveVideoSinglePrice, computeVideoCost, handleCreateVideoWebhook,
createVolcengineVideo, and video standard-parameters (63 cases total).
2026-02-15 19:34:14 +08:00
YuTengjing 918d048b3d feat: pass latency to chargeAfterGenerate for video generation metrics 2026-02-15 19:32:19 +08:00
Arvin Xu 398d8b7f3c Sync main branch to canary branch (#12339)
Automatic sync from main to canary. Merge conflicts detected.

**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260214-22020422229
git merge origin/main
# Resolve conflicts
git add -A && git commit
git push
```

> Do NOT merge canary into a main-based branch — always merge main INTO
the canary-based branch to keep a clean commit graph.
2026-02-15 18:52:05 +08:00
Innei 1529f31dff ci: sync workflow for main to canary branch
- Added concurrency control to prevent overlapping sync jobs.
- Improved logic for detecting changes between main and canary branches.
- Streamlined handling of fast-forward merges and conflict resolution.
- Updated PR creation process for manual conflict resolution with detailed instructions.

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-15 16:36:16 +08:00
Innei b4bd5b288c Merge remote-tracking branch 'origin/main' into sync/main-to-canary-20260214-22020422229 2026-02-15 16:03:47 +08:00
YuTengjing 448cfb2cfd 🐛 fix: prevent stale topic ID from persisting in URL on remount (#12341)
## Summary
- Fix bidirectional sync race condition in `TopicUrlSync` where stale
`activeGenerationTopicId` from zustand store overwrites the URL when
navigating back to image/video pages without a `?topic=` param
- Replace `createStoreUpdater` (useEffect-based) with `useLayoutEffect`
for URL → store sync, ensuring it runs before the store → URL
subscription
- Generate latest i18n locales

## Test plan
- [ ] Visit `/video?topic=<id>`, navigate to home, click `/video` —
topic should not persist
- [ ] Visit `/image?topic=<id>`, navigate to home, click `/image` —
topic should not persist
- [ ] Click a topic in the sidebar — URL should update with
`?topic=<id>`
- [ ] Refresh page with `?topic=<id>` — correct topic should be selected

## Summary by Sourcery

Fix URL and store synchronization for generation topics and refresh
localized video page translations.

Bug Fixes:
- Prevent stale activeGenerationTopicId values in the store from
overwriting the URL when remounting image or video pages without a topic
query parameter.

Documentation:
- Regenerate and update i18n locale JSON files across all supported
languages, including new video translation files.
2026-02-15 11:11:47 +08:00
YuTengjing d8c3ef3232 🌐 chore: generate latest i18n locales 2026-02-15 11:10:47 +08:00
YuTengjing 2a23fb9a10 🐛 fix: prevent stale topic ID from persisting in URL on remount 2026-02-15 11:03:06 +08:00
YuTengjing 82f9cb4486 🔨 chore: add video generation feature (#12312)
## Summary
- Add complete video generation feature including UI pages, store
management, server routes, and webhook handling
- Support Volcengine video generation provider with text-to-video and
image-to-video capabilities
- Add video generation topic management, config panel, generation feed,
and prompt input components
- Include database migration for video generation schema
- Refactor GenerationTopicList/TopicPanel as shared components for both
image and video generation

## Test plan
- [ ] Verify video generation page renders correctly
- [ ] Test text-to-video generation flow end-to-end
- [ ] Test image-to-video generation with reference frames
- [ ] Verify video generation topic CRUD operations
- [ ] Confirm webhook handling for async video generation results
- [ ] Check video generation config panel model selection and parameter
controls
2026-02-15 10:13:43 +08:00
YuTengjing 6419fd32b1 🔧 chore: revert some lint config 2026-02-15 09:46:02 +08:00
Arvin Xu 927fe3fd22 ci: fix sync workflow by using PAT for checkout (#12338)
The GITHUB_TOKEN cannot push changes to .github/workflows/ files due to
GitHub's security restrictions. The 'workflows' permission key added in
the previous commit is not a valid workflow permission scope.

Fix: Use secrets.GH_TOKEN (PAT with workflow scope) in the checkout step
so that git push has the necessary credentials to push branches that
contain workflow file changes (e.g. from merge conflicts).

Also reverts the invalid 'workflows: write' permission.
2026-02-15 00:13:13 +08:00
Arvin Xu 03bda41c07 chore(ci): add workflows permission to sync-main-to-canary (#12337)
When merge conflicts involve .github/workflows/ files, GitHub requires
the `workflows: write` permission to push branches containing workflow
file changes. Without this permission, the sync branch push is rejected
with 'refusing to allow a GitHub App to create or update workflow without
workflows permission'.
2026-02-14 23:56:07 +08:00
LobeHub Bot e804773a7e 🌐 chore: translate non-English comments to English in lobehub-skill-store (#12316)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-14 22:22:40 +08:00
YuTengjing 67875bd60a feat: add skeleton loading screen for video topic switching 2026-02-14 21:07:01 +08:00
YuTengjing 916d4841f4 🔒 chore: hide video nav entry behind enableBusinessFeatures flag 2026-02-14 20:52:03 +08:00
YuTengjing d5b1ff20e0 🔧 chore: revert locales/ changes and remove unused locale key
- Revert locales/ directory to canary state (CI auto-generates translations)
- Remove unused `video.topic.empty` key from default locale
2026-02-14 20:16:26 +08:00
YuTengjing 212348eafe 🐛 fix: refund precharge when video task submission fails 2026-02-14 20:16:26 +08:00
YuTengjing 43820eeb2e 🐛 fix: improve video generation security and type safety
- Reject webhook requests when token is missing instead of skipping verification
- Use proper union type for generation assets instead of blanket VideoGenerationAsset cast
- Replace sync js-sha256 with Node.js crypto.createHash for video hashing
- Add 500MB size limit and 5-minute timeout for video downloads
- Add displayName to VideoLoading component
2026-02-14 20:16:26 +08:00
YuTengjing 7397d6f8c1 🐛 fix: add type assertion for prechargeResult in video webhook route 2026-02-14 20:16:26 +08:00
YuTengjing 85d5bc9e08 ♻️ refactor: add model parameter to video free quota query 2026-02-14 20:16:26 +08:00
YuTengjing 3fbc46b23a 🔒 feat: add webhook token verification for video generation callbacks
Generate a one-time crypto-random token per video task, store it in
asyncTask metadata, and append it to the callback URL. The webhook
endpoint verifies the token using timing-safe comparison before
processing, returning 401 on mismatch. Old tasks without a token
are allowed through for backward compatibility.
2026-02-14 20:16:26 +08:00
YuTengjing 6e2ef05270 feat: support new badge on sidebar nav items and enable for video 2026-02-14 20:16:26 +08:00
YuTengjing 06d65e9ce5 🙈 chore: hide Seedance 2.0 entry from home starter list
Temporarily comment out the Seedance 2.0 video button until it's ready for launch. Seedance 1.5 will be released first.
2026-02-14 20:16:26 +08:00
YuTengjing 6002863c17 feat: add video free quota query endpoint and UI stub
Add getVideoFreeQuota TRPC query and business stub for displaying
daily free video generation quota in PromptInput.
2026-02-14 20:16:26 +08:00
YuTengjing 8f2e72d1b8 🐛 fix: update generationBatch tests to match filesToDelete refactor 2026-02-14 20:16:26 +08:00
YuTengjing fcf2444fa8 feat: add eval method to RedisClient for Lua script execution 2026-02-14 20:16:25 +08:00
YuTengjing 661f1a80b4 ♻️ refactor: improve video generation webhook and type safety
- Make AsyncTaskModel.findByInferenceId static to avoid empty userId
- Extract GenerationTopicType for type-safe topic type narrowing
- Hoist batch query to eliminate duplicate DB call in webhook handler
- Add missing i18n keys for video error actions and status
- Fix comment accuracy in generationBatch deletion
2026-02-14 20:16:25 +08:00
YuTengjing 57772d1f3b 🐛 fix: add videoGeneration pricing unit and clean up all asset files on deletion
- Add videoGeneration to PricingGroup maps to fix TS2741 type errors
- Collect url and coverUrl in addition to thumbnailUrl when deleting topics/batches
- Fix createTopic test assertion to match new optional type parameter
2026-02-14 20:16:25 +08:00
YuTengjing abe4c969a5 feat: add video generation feature 2026-02-14 20:16:25 +08:00
lobehubbot b767a66d38 🐛 chore(hotfix): bump version to v2.1.30 [skip ci] 2026-02-14 11:03:50 +00:00
Innei 53e4228ea7 🐛 fix: hotfix v2.1.30 (#12321)
*  chore: enhance release workflow to include conditional release body handling

- Added environment variable `RELEASE_BODY` to capture release notes from the GitHub event.
- Updated the workflow to use this variable, ensuring proper handling of release body content during manual dispatch events.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: simplify GitHub release workflow by removing hotfix-specific logic

- Consolidated the GitHub release creation step to handle both regular and hotfix releases under a single condition.
- Removed the separate hotfix release creation step to streamline the workflow.

Signed-off-by: Innei <tukon479@gmail.com>

*  fix: replace UserPanel popover

- Introduced `PanelContentSkeleton` for better user experience during loading states in the UserPanel.
- Updated `UserPanel` to use the new skeleton and adjusted popover content handling.
- Refactored `PanelContent` to use `FC` type for better type safety.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: bump @lobehub/ui dependency to version 4.38.1

- Updated the @lobehub/ui package to the latest version for improved features and bug fixes.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: remove inset prop from Popover in UserPanel

- Cleaned up the Popover component in UserPanel by removing the unnecessary inset prop for improved clarity and maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-14 19:03:00 +08:00
Innei 7efcdd2f7c 🐛 fix: resolve tooltip z-index stacking context in ModelSwitchPanel (#12324)
🐛 fix: move TooltipGroup to panel root to fix z-index stacking context
2026-02-14 18:49:23 +08:00
Neko bde1503309 🔨 chore(memory-user-memory): support effort & tool permission for configuring memory (#12311) 2026-02-14 18:17:15 +08:00
Neko 487713361a 🔨 chore(userMemories): support to auto calculate the timeout of the memory analysis task (#12325) 2026-02-14 17:36:58 +08:00
LiJian 7bad876259 🐛 fix: slove the execAgent task run error & parse crash problem (#12318)
fix: slove the execAgent task run error & parse crash problem
2026-02-14 14:22:33 +08:00
lobehubbot 229200853a 🐛 chore(hotfix): bump version to v2.1.29 [skip ci] 2026-02-14 02:09:17 +00:00
Innei 5ec89941f3 🐛 fix: bump lobehub/ui and fix workflow (#12313)
## 🩹 Hotfix v2.1.29

This PR starts a hotfix release from `main`.

### Release Process
1.  Hotfix branch created from main
2.  Pushed to remote
3. 🔄 Waiting for PR review and merge
4.  Auto tag + GitHub Release will be created after merge

---
Created by hotfix script

## Summary by Sourcery

Improve main-to-canary sync workflow robustness and tighten hotfix
auto-tagging criteria for release automation.

Enhancements:
- Make the main-to-canary sync workflow attempt direct merges to canary,
falling back to PR creation only when necessary or when conflicts occur,
and handle existing sync PRs more gracefully.
- Refine hotfix detection in the auto-tag workflow by requiring both a
hotfix branch prefix and a valid conventional commit-style PR title
prefix before tagging.
- Update the @lobehub/ui dependency to the latest patch version.

Build:
- Adjust release auto-tag workflow logic to gate hotfix tagging by both
branch naming and PR title format.

CI:
- Enhance GitHub Actions workflow for syncing main to canary with
conflict handling, direct-push optimization, and automated PR
management.
2026-02-14 10:08:31 +08:00
Innei f46916a74d feat(desktop): integrate electron-liquid-glass for macOS Tahoe (#12277)
*  feat(desktop): integrate electron-liquid-glass for macOS Tahoe

Add native liquid glass visual effect on macOS 26+ (Tahoe), replacing
vibrancy with Apple's NSGlassEffectView API via electron-liquid-glass.

- Centralize all platform visual effects in WindowThemeManager
- Strip platform props from BrowserWindow options to prevent config leaking
- Remove vibrancy from appBrowsers/WindowTemplate (managed by ThemeManager)
- Add isMacTahoe detection in env.ts and preload
- Fix applyVisualEffects to handle macOS platform symmetrically

* fix(tests): add isMacTahoe detection in Browser test environment

Introduce isMacTahoe flag in the test environment to support macOS Tahoe-specific features. This change enhances the test suite's ability to simulate and validate platform-specific behavior.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(theme): update liquid glass variant and adjust background color mix for desktop themes

- Changed liquid glass variant from 2 to 15 for improved visual effects.
- Adjusted background color mix percentages for dark and light themes on desktop to enhance visual consistency.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(theme): adjust background color mix for dark theme on desktop

- Updated the background color mix percentage for the dark theme on desktop from 70% to 90% for improved visual effect consistency.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-14 00:31:16 +08:00
Arvin Xu 2ee46b8693 Sync main branch to canary branch (#12308)
Automatic sync

## Summary by Sourcery

Extend database schemas and migrations to support async task inference
tracking and typed generation topics.

New Features:
- Add an inferenceId field to async tasks with a dedicated index for
lookup by inference ID.
- Add a typed generation topic field to distinguish between image and
video topics with a default of image.

Enhancements:
- Update database schema metadata and documentation snapshots to reflect
the new async task and generation topic fields.

Tests:
- Adjust async task and generation topic tests to cover the new
inferenceId and topic type fields.
2026-02-13 23:44:28 +08:00
Innei baf0b56f64 🔧 ci: optimize sync-main-to-canary to merge directly when no conflicts (#12306)
## Summary
- Optimize the main-to-canary sync workflow to directly merge and push
when there are no conflicts, avoiding unnecessary PR creation
- When merge conflicts exist, fall back to creating a PR for manual
resolution
- Add duplicate PR detection to prevent multiple PRs on the same day

## Test plan
- [ ] Push to main with no conflicts on canary → should auto-merge
without PR
- [ ] Push to main with conflicts on canary → should create PR
- [ ] Trigger workflow twice on same day with conflicts → should not
create duplicate PRs

## Summary by Sourcery

CI:
- Update the sync-main-to-canary workflow to merge main into canary
directly on no-conflict updates, only creating a PR when merge conflicts
occur and avoiding duplicate PRs for the same sync date.
2026-02-13 17:39:19 +08:00
YuTengjing 12dc7f90be 👷 build: add video generation schema changes (#12293) 2026-02-13 17:13:16 +08:00
Innei 1b905ede31 Sync main branch to canary branch (#12297)
Automatic sync
2026-02-13 15:50:44 +08:00
Innei cfaa911153 🔧 ci: add commit prefix gate for hotfix auto-tag (#12304)
* 🔧 ci: add commit prefix gate for hotfix auto-tag

* 🔧 chore: update ESLint suppressions and dependencies

- Added new ESLint suppressions for various files to address linting issues, including `no-console` and `object-shorthand`.
- Updated ESLint version to 10.0.0 in both root and desktop package.json files.
- Adjusted linting scripts for improved performance and consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: add ESLint support for YAML files in package.json

- Included ESLint fix command for YAML files (*.yml, *.yaml) in the linting scripts section.
- Ensured consistent formatting by adding a newline at the end of the file.

This update enhances linting capabilities for YAML configuration files.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: remove ESLint configuration file

- Deleted the .eslintrc.js file, which contained custom ESLint rules and overrides.
- This change simplifies the project by relying on default ESLint configurations.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-13 15:49:33 +08:00
Innei e17bf4b0cc Merge remote-tracking branch 'origin/main' into canary
# Conflicts:
#	locales/ja-JP/suggestQuestions.json
#	locales/vi-VN/models.json
2026-02-13 15:43:49 +08:00
LiJian 58cf27dcf8 🐛 fix: add the Agent Meta info back into agent advance model (#12302)
fix: add the Agent Meta info back into agent advance model
2026-02-13 15:01:50 +08:00
YuTengjing f12d9fbd22 🔒 fix: upgrade next-mdx-remote to v6 for CVE-2026-0969 (#12296)
Upgrade next-mdx-remote from v5.0.0 to v6.0.0 to fix CVE-2026-0969,
an arbitrary code execution vulnerability in MDX content processing.
2026-02-13 13:36:41 +08:00
YuTengjing d4f72eb752 🐛 fix: add missing inferenceId and type fields in test mocks 2026-02-13 12:34:19 +08:00
YuTengjing e112cd6f7f 🗃️ db: add video generation schema changes
- async_tasks: add inference_id column with index
- generation_topics: add type column (default 'image')
2026-02-13 12:09:18 +08:00
Innei 9a9147ca7e feat: support image upload in editor with desktop file picker (#12285)
- Add handleShowOpenDialog and handlePickFile IPC methods for Electron
- Create useImageUpload hook for editor image upload with progress
- Refactor ReactImagePlugin config to support handleUpload and onPickFile
- Simplify slash command image insertion by delegating upload to plugin
- Upgrade @lobehub/editor to ^3.16.1
2026-02-13 01:27:22 +08:00
Innei 2d1eec4482 feat(desktop): configure DMG background image (#12284)
*  feat: configure DMG background image for macOS installer

*  feat(desktop): set DMG window size, icon positions, and retina DPI

* 🔧 chore(desktop): resize DMG background to 600x400 and adjust window/icon positions

* chore: update remote
2026-02-13 01:06:43 +08:00
Innei c11d6de7db 🔧 build: add canary desktop release workflow (#12286)
🔧 build: add canary desktop release workflow and channel support

Add automated canary build pipeline triggered by build/fix/style commits
on canary branch, with concurrency control to cancel stale builds.
2026-02-13 00:37:03 +08:00
René Wang bbbe3a8d09 feat: add banner (#12258)
* feat: add banner

* fix: type error
2026-02-12 21:22:21 +08:00
Innei e51fbba881 feat: redesign Copilot ChatInput with compact action bar layout (#12279)
* add

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance ChatInput with customizable action bar properties

- Added `actionSize` to `ActionBarContextValue` for flexible action sizing.
- Updated `Action` component to utilize `actionSize` for dynamic sizing.
- Introduced `actionBarStyle` and `leftContent` props in `DesktopChatInput` for custom styling and content.
- Enhanced `SendButton` to accept size from `sendButtonProps`.
- Updated `ChatInput` to support new props for improved layout customization.
- Refactored `Conversation` component to implement compact action bar style and size.

This update improves the flexibility and customization of the ChatInput feature, allowing for better user experience.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance NavHeader and Copilot Toolbar with layout improvements

- Added `allowShrink` property to Flexbox components in NavHeader for better responsiveness.
- Updated Text component in Copilot Toolbar to include tooltip support for ellipsis overflow, improving user experience.

These changes enhance the layout flexibility and visual consistency across the application.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: update Conversation and AgentSelectorAction components for improved styling

- Adjusted padding in the compact action bar style for better alignment.
- Enhanced AgentSelectorAction styles with additional border radius for a refined look.
- Simplified title translation in AgentSelectorAction for consistency.

These changes enhance the visual appeal and maintainability of the components within the Copilot feature.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: update ModelSwitchPanel to improve DropdownMenuPositioner prop usage

- Changed the `hoverTrigger` prop in `DropdownMenuPositioner` from a boolean to a destructured assignment for better clarity and consistency in the component's API.

This update enhances the readability and maintainability of the ModelSwitchPanel component.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-12 20:09:39 +08:00
Arvin Xu 1bfeeea6f4 🔧 chore: always run E2E tests on main and canary branches (#12268)
Skip duplicate check only applies to development branches now.
Main and canary branches will always execute E2E tests on every commit.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:42:56 +08:00
Rdmclin2 12d0ec21d0 fix: setting response error and other bugs (#12265)
* fix: setting response error and other bugs

* chore: remove  popover arrow
2026-02-12 18:20:28 +08:00
Rdmclin2 2bf0a08919 feat: support model detail dropdown (#12275)
* feat: support model detail dropdown

# Conflicts:
#	src/features/ChatInput/ActionBar/Model/index.tsx

* chore: fix test cases

* fix: type error

* fix: e2e tests
2026-02-12 17:51:52 +08:00
Innei 79e146f1a3 🐛 fix: improve RunCommand copy button visibility and ActionBar border radius (#12280)
Closes LOBE-4402
2026-02-12 16:01:44 +08:00
Innei 0e42ca5ca2 🐛 fix: improve GitHub Copilot auth retry logic (#12250)
* 🐛 fix: improve GitHub Copilot auth retry logic

Simplify auth refresh tracking from counter to boolean flag and clear
cached bearer token on 401 to ensure fresh token exchange.

* 🔧 fix: update package.json formatting and import statements in GitHub Copilot provider

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: refine GitHub Copilot auth refresh logic to check for exchange credential

Update the 401 error handling to refresh the token only if an exchange credential is available, improving the authentication flow.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-12 13:09:38 +08:00
LobeHub Bot 823aa29c67 Sync main branch to canary branch (#12267)
* 🔧 chore(release): bump version to v2.1.27 [skip ci]

* chore: update sync main to canary workflow

* 🐛 fix: update @lobehub/ui version and refactor dynamic import handling (#12260)

*  feat: add hotfix workflow and script for automated hotfix management

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 fix: refactor PR creation command to use execFileSync for improved reliability

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui version and refactor dynamic import handling

- Bump @lobehub/ui dependency from ^4.35.0 to ^4.36.2 in package.json.
- Refactor settingsContentToStatic.mts to simplify dynamic import processing by removing business feature checks.
- Add initialize.ts to enable immer's map set functionality.
- Correct import path in layout.tsx from 'initiallize' to 'initialize'.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @types/react version in package.json

- Bump @types/react dependency from ^19.2.9 to 19.2.14.
- Add @types/react version to overrides section for consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: enhance auto-tag-release workflow for strict semver validation

- Updated regex to match strict semantic versioning format, allowing for optional prerelease and build metadata.
- Added validation step to ensure the version is a valid semver before proceeding with the release process.

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ chore: remove defaultSecurityBlacklist test file

- Deleted the test file for DEFAULT_SECURITY_BLACKLIST as it is no longer needed.
- This cleanup helps maintain a more streamlined test suite.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update localization files for multiple languages

- Improved translations in Arabic, Bulgarian, German, English, and Spanish for chat and tool-related strings.
- Enhanced descriptions for various parameters and added new keys for file handling and security warnings.
- Adjusted phrasing for clarity and consistency across languages.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update PR comment script to include Actions Artifacts link

- Modified the PR comment generation script to accept an additional artifactsUrl parameter.
- Updated the comment format to include both Release download and Actions Artifacts links for better accessibility.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 chore(hotfix): bump version to v2.1.28 [skip ci]

* chore: update secrets token

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: rdmclin2 <rdmclin2@gmail.com>
Co-authored-by: Arvin Xu <arvinx@foxmail.com>
Co-authored-by: Innei <i@innei.in>
2026-02-11 23:51:35 +08:00
rdmclin2 d225da96df chore: update secrets token 2026-02-11 23:45:07 +08:00
lobehubbot 0acaf01f9a 🐛 chore(hotfix): bump version to v2.1.28 [skip ci] 2026-02-11 15:34:52 +00:00
Innei 5a8911b72d 🐛 fix: update @lobehub/ui version and refactor dynamic import handling (#12260)
*  feat: add hotfix workflow and script for automated hotfix management

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 fix: refactor PR creation command to use execFileSync for improved reliability

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui version and refactor dynamic import handling

- Bump @lobehub/ui dependency from ^4.35.0 to ^4.36.2 in package.json.
- Refactor settingsContentToStatic.mts to simplify dynamic import processing by removing business feature checks.
- Add initialize.ts to enable immer's map set functionality.
- Correct import path in layout.tsx from 'initiallize' to 'initialize'.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @types/react version in package.json

- Bump @types/react dependency from ^19.2.9 to 19.2.14.
- Add @types/react version to overrides section for consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: enhance auto-tag-release workflow for strict semver validation

- Updated regex to match strict semantic versioning format, allowing for optional prerelease and build metadata.
- Added validation step to ensure the version is a valid semver before proceeding with the release process.

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ chore: remove defaultSecurityBlacklist test file

- Deleted the test file for DEFAULT_SECURITY_BLACKLIST as it is no longer needed.
- This cleanup helps maintain a more streamlined test suite.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update localization files for multiple languages

- Improved translations in Arabic, Bulgarian, German, English, and Spanish for chat and tool-related strings.
- Enhanced descriptions for various parameters and added new keys for file handling and security warnings.
- Adjusted phrasing for clarity and consistency across languages.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update PR comment script to include Actions Artifacts link

- Modified the PR comment generation script to accept an additional artifactsUrl parameter.
- Updated the comment format to include both Release download and Actions Artifacts links for better accessibility.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-11 23:33:44 +08:00
Arvin Xu e6596e94a5 🔨 chore: update sync main to canary script (#12264)
#### 💻 Change Type

<!-- For change type, change [ ] to [x]. -->

- [ ]  feat
- [ ] 🐛 fix
- [ ] ♻️ refactor
- [ ] 💄 style
- [ ] 👷 build
- [ ] ️ perf
- [ ]  test
- [ ] 📝 docs
- [x] 🔨 chore

#### 🔗 Related Issue

<!-- Link to the issue that is fixed by this PR -->

<!-- Example: Fixes #xxx, Closes #xxx, Related to #xxx -->

#### 🔀 Description of Change

<!-- Thank you for your Pull Request. Please provide a description
above. -->

#### 🧪 How to Test

<!-- Please describe how you tested your changes -->

<!-- For AI features, please include test prompts or scenarios -->

- [ ] Tested locally
- [ ] Added/updated tests
- [ ] No tests needed

#### 📸 Screenshots / Videos

<!-- If this PR includes UI changes, please provide screenshots or
videos -->

| Before | After |
| ------ | ----- |
| ...    | ...   |

#### 📝 Additional Information

<!-- Add any other context about the Pull Request here. -->

<!-- Breaking changes? Migration guide? Performance impact? -->

## Summary by Sourcery

CI:
- Introduce a new sync-main-to-canary GitHub Actions workflow that
creates an automatic PR from main to canary on pushes to main and remove
the previous sync-main-to-dev workflow.
2026-02-11 23:18:27 +08:00
rdmclin2 9c09160154 chore: update sync main to canary workflow 2026-02-11 23:09:00 +08:00
lobehubbot d7d186df1a 🔧 chore(release): bump version to v2.1.27 [skip ci] 2026-02-11 09:33:15 +00:00
580 changed files with 27312 additions and 3451 deletions
@@ -38,13 +38,3 @@ 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:
```bash
bun run db:generate:client
```
This updates the hash in `packages/database/src/core/migrations.json`.
-63
View File
@@ -1,63 +0,0 @@
const config = require('@lobehub/lint').eslint;
config.root = true;
config.extends.push('plugin:@next/next/recommended-legacy');
config.rules['unicorn/no-negated-condition'] = 0;
config.rules['unicorn/prefer-type-error'] = 0;
config.rules['unicorn/prefer-logical-operator-over-ternary'] = 0;
config.rules['unicorn/no-null'] = 0;
config.rules['unicorn/no-typeof-undefined'] = 0;
config.rules['unicorn/explicit-length-check'] = 0;
config.rules['unicorn/prefer-code-point'] = 0;
config.rules['no-extra-boolean-cast'] = 0;
config.rules['unicorn/no-useless-undefined'] = 0;
config.rules['react/no-unknown-property'] = 0;
config.rules['unicorn/prefer-ternary'] = 0;
config.rules['unicorn/prefer-spread'] = 0;
config.rules['unicorn/catch-error-name'] = 0;
config.rules['unicorn/no-array-for-each'] = 0;
config.rules['unicorn/prefer-number-properties'] = 0;
config.rules['unicorn/prefer-query-selector'] = 0;
config.rules['unicorn/no-array-callback-reference'] = 0;
config.rules['unicorn/text-encoding-identifier-case'] = 0;
config.rules['@typescript-eslint/no-use-before-define'] = 0;
// FIXME: Linting error in src/app/[variants]/(main)/chat/features/Migration/DBReader.ts, the fundamental solution should be upgrading typescript-eslint
config.rules['@typescript-eslint/no-useless-constructor'] = 0;
config.rules['@next/next/no-img-element'] = 0;
config.overrides = [
{
extends: ['plugin:mdx/recommended'],
files: ['*.mdx'],
rules: {
'@typescript-eslint/no-unused-vars': 1,
'micromark-extension-mdx-jsx': 0,
'no-undef': 0,
'react/jsx-no-undef': 0,
'react/no-unescaped-entities': 0,
},
settings: {
'mdx/code-blocks': false,
},
},
{
files: ['src/store/image/**/*', 'src/types/generation/**/*'],
rules: {
'@typescript-eslint/no-empty-interface': 0,
'sort-keys-fix/sort-keys-fix': 0,
'typescript-sort-keys/interface': 0,
'typescript-sort-keys/string-enum': 0,
},
},
// CLI scripts legitimately use process.exit() and async IIFE patterns
{
files: ['scripts/**/*'],
rules: {
'unicorn/no-process-exit': 0,
'unicorn/prefer-top-level-await': 0,
},
},
];
module.exports = config;
+8 -13
View File
@@ -2,8 +2,7 @@
* Generate PR comment with download links for desktop builds
* and handle comment creation/update logic
*/
module.exports = async ({ github, context, releaseUrl, version, tag }) => {
// 用于识别构建评论的标识符
const prComment = async ({ github, context, releaseUrl, artifactsUrl, version, tag }) => {
const COMMENT_IDENTIFIER = '<!-- DESKTOP-BUILD-COMMENT -->';
/**
@@ -69,7 +68,7 @@ module.exports = async ({ github, context, releaseUrl, version, tag }) => {
**Version**: \`${version}\`
**Build Time**: \`${new Date().toISOString()}\`
📦 [View All Build Artifacts](${releaseUrl})
📦 [Release Download](${releaseUrl}) · 📥 [Actions Artifacts](${artifactsUrl || `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`})
## Build Artifacts
@@ -88,7 +87,7 @@ ${assetTable}
**Version**: \`${version}\`
**Build Time**: \`${new Date().toISOString()}\`
## 📦 [View All Build Artifacts](${releaseUrl})
📦 [Release Download](${releaseUrl}) · 📥 [Actions Artifacts](${artifactsUrl || `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`})
> Note: This is a temporary build for testing purposes only.
`;
@@ -96,45 +95,41 @@ ${assetTable}
};
/**
* 查找并更新或创建PR评论
* Find and update or create the PR comment
*/
const updateOrCreateComment = async () => {
// 生成评论内容
const body = await generateCommentBody();
// 查找我们之前可能创建的评论
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
// 查找包含我们标识符的评论
const buildComment = comments.find((comment) => comment.body.includes(COMMENT_IDENTIFIER));
if (buildComment) {
// 如果找到现有评论,则更新它
await github.rest.issues.updateComment({
comment_id: buildComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
console.log(`已更新现有评论 ID: ${buildComment.id}`);
console.log(`Updated existing comment ID: ${buildComment.id}`);
return { updated: true, id: buildComment.id };
} else {
// 如果没有找到现有评论,则创建新评论
const result = await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
console.log(`已创建新评论 ID: ${result.data.id}`);
console.log(`Created new comment ID: ${result.data.id}`);
return { updated: false, id: result.data.id };
}
};
// 执行评论更新或创建
return await updateOrCreateComment();
};
module.exports = prComment;
+135 -24
View File
@@ -24,28 +24,91 @@ jobs:
# Fetch full history for proper tagging
fetch-depth: 0
- name: Check and extract version from PR title
id: extract-version
- name: Detect release PR (version from title)
id: release
run: |
PR_TITLE="${{ github.event.pull_request.title }}"
echo "PR Title: $PR_TITLE"
# Match "🚀 release: v{x.x.x}" format
if [[ "$PR_TITLE" =~ ^🚀[[:space:]]+release:[[:space:]]*v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
# Match "🚀 release: v{x.x.x}" format (strict semver: x.y.z with optional -prerelease or +build)
if [[ "$PR_TITLE" =~ ^🚀[[:space:]]+release:[[:space:]]*v([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?)$ ]]; then
VERSION="${BASH_REMATCH[1]}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "should_tag=true" >> $GITHUB_OUTPUT
echo "✅ Detected release PR, version: v$VERSION"
else
echo "should_tag=false" >> $GITHUB_OUTPUT
echo "⏭️ Not a release PR, skipping tag creation"
echo "⏭️ Not a release PR"
fi
- name: Detect hotfix PR (branch first, title fallback)
id: hotfix
if: steps.release.outputs.should_tag != 'true'
run: |
HEAD_REF="${{ github.event.pull_request.head.ref }}"
PR_TITLE="${{ github.event.pull_request.title }}"
echo "Head ref: $HEAD_REF"
echo "PR Title: $PR_TITLE"
# Priority 1: hotfix/* branch always counts as hotfix, ignore PR title gate.
if [[ "$HEAD_REF" == hotfix/* ]]; then
echo "should_tag=true" >> $GITHUB_OUTPUT
echo "✅ Detected hotfix PR from hotfix/* branch (title gate bypassed)"
exit 0
fi
# Priority 2: fallback to PR title prefix gate (legacy behavior).
if echo "$PR_TITLE" | grep -qiE '^(💄[[:space:]]*)?style(\(.+\))?:|^(✨[[:space:]]*)?feat(\(.+\))?:|^(🐛[[:space:]]*)?fix(\(.+\))?:|^(♻️[[:space:]]*)?refactor(\(.+\))?:|^((🐛|🩹)[[:space:]]*)?hotfix(\(.+\))?:'; then
echo "should_tag=true" >> $GITHUB_OUTPUT
echo "✅ Detected hotfix PR from title prefix gate"
else
echo "should_tag=false" >> $GITHUB_OUTPUT
echo "⏭️ Not a hotfix PR (neither hotfix/* branch nor style/feat/fix/refactor/hotfix title prefix)"
fi
- name: Prepare main branch
if: steps.release.outputs.should_tag == 'true' || steps.hotfix.outputs.should_tag == 'true'
run: |
git checkout main
git pull --rebase origin main
- name: Resolve hotfix version (patch bump)
id: hotfix-version
if: steps.hotfix.outputs.should_tag == 'true'
run: |
CURRENT_VERSION="$(node -p "require('./package.json').version")"
echo "Current version: ${CURRENT_VERSION}"
# Coerce to stable base (e.g. 2.0.0-beta.1 -> 2.0.0), then bump patch (-> 2.0.1)
BASE_VERSION="$(npx -y semver@7 "${CURRENT_VERSION}" -c)"
if [ -z "${BASE_VERSION}" ]; then
echo "❌ Invalid version in package.json: ${CURRENT_VERSION}"
exit 1
fi
NEXT_VERSION="$(npx -y semver@7 -i patch "${BASE_VERSION}")"
echo "📦 Hotfix version: ${NEXT_VERSION}"
echo "version=${NEXT_VERSION}" >> "$GITHUB_OUTPUT"
- name: Set context (release)
if: steps.release.outputs.should_tag == 'true'
run: |
echo "SHOULD_TAG=true" >> $GITHUB_ENV
echo "KIND=release" >> $GITHUB_ENV
echo "VERSION=${{ steps.release.outputs.version }}" >> $GITHUB_ENV
- name: Set context (hotfix)
if: steps.hotfix.outputs.should_tag == 'true'
run: |
echo "SHOULD_TAG=true" >> $GITHUB_ENV
echo "KIND=hotfix" >> $GITHUB_ENV
echo "VERSION=${{ steps.hotfix-version.outputs.version }}" >> $GITHUB_ENV
- name: Check if tag already exists
if: steps.extract-version.outputs.should_tag == 'true'
if: env.SHOULD_TAG == 'true'
id: check-tag
run: |
VERSION="${{ steps.extract-version.outputs.version }}"
VERSION="${{ env.VERSION }}"
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "⚠️ Tag v$VERSION already exists"
@@ -54,21 +117,69 @@ jobs:
echo "✅ Tag v$VERSION does not exist, can create"
fi
- name: Create Tag
if: steps.extract-version.outputs.should_tag == 'true' && steps.check-tag.outputs.exists == 'false'
- name: Bump package.json version (before tagging)
if: env.SHOULD_TAG == 'true' && steps.check-tag.outputs.exists == 'false'
id: bump-version
run: |
VERSION="${{ steps.extract-version.outputs.version }}"
echo "🏷️ Creating tag: v$VERSION"
VERSION="${{ env.VERSION }}"
KIND="${{ env.KIND }}"
echo "📝 Bumping package.json version to: $VERSION"
# Validate VERSION is strict semver before writing
if ! npx -y semver@7 "$VERSION" >/dev/null 2>&1; then
echo "❌ Invalid semver version: $VERSION"
exit 1
fi
# Configure git
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "lobehubbot"
git config --global user.email "i@lobehub.com"
# Get PR merge commit SHA
MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}"
# Update package.json using Node.js
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const target = '$VERSION';
if (pkg.version === target) {
console.log('✅ package.json already at version', target);
process.exit(0);
}
pkg.version = target;
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n');
console.log('✅ package.json updated to', target);
"
# Commit changes (if any) and push
git add package.json
if [ "$KIND" == "hotfix" ]; then
COMMIT_MSG="🐛 chore(hotfix): bump version to v$VERSION [skip ci]"
else
COMMIT_MSG="🔧 chore(release): bump version to v$VERSION [skip ci]"
fi
git commit -m "$COMMIT_MSG" || echo "Nothing to commit"
git push origin HEAD:main
# Output the SHA we will tag
echo "tag_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Create Tag
if: env.SHOULD_TAG == 'true' && steps.check-tag.outputs.exists == 'false'
run: |
VERSION="${{ env.VERSION }}"
KIND="${{ env.KIND }}"
echo "🏷️ Creating tag: v$VERSION"
# Tag the bumped version commit SHA (not the PR merge commit SHA)
TAG_SHA="${{ steps.bump-version.outputs.tag_sha }}"
if [ "$KIND" == "hotfix" ]; then
PREFIX="🐛 hotfix"
else
PREFIX="🚀 release"
fi
# Create annotated tag with single line message
git tag -a "v$VERSION" "$MERGE_SHA" -m "🚀 release: v$VERSION | PR #${{ github.event.pull_request.number }} | Author: ${{ github.event.pull_request.user.login }}"
git tag -a "v$VERSION" "$TAG_SHA" -m "$PREFIX: v$VERSION | PR #${{ github.event.pull_request.number }} | Author: ${{ github.event.pull_request.user.login }}"
# Push tag
git push origin "v$VERSION"
@@ -76,13 +187,13 @@ jobs:
echo "✅ Tag v$VERSION created successfully!"
- name: Create GitHub Release
if: steps.extract-version.outputs.should_tag == 'true' && steps.check-tag.outputs.exists == 'false'
if: env.SHOULD_TAG == 'true' && steps.check-tag.outputs.exists == 'false'
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ steps.extract-version.outputs.version }}
name: 🚀 Release v${{ steps.extract-version.outputs.version }}
tag_name: v${{ env.VERSION }}
name: 🚀 Release v${{ env.VERSION }}
body: |
## 📦 Release v${{ steps.extract-version.outputs.version }}
## 📦 Release v${{ env.VERSION }}
This release was automatically published from PR #${{ github.event.pull_request.number }}.
@@ -97,12 +208,12 @@ jobs:
- name: Output result
run: |
if [ "${{ steps.extract-version.outputs.should_tag }}" == "true" ]; then
if [ "${{ env.SHOULD_TAG }}" == "true" ]; then
if [ "${{ steps.check-tag.outputs.exists }}" == "true" ]; then
echo "⚠️ Result: Tag v${{ steps.extract-version.outputs.version }} already exists, skipping creation"
echo "⚠️ Result: Tag v${{ env.VERSION }} already exists, skipping creation"
else
echo "✅ Result: Tag v${{ steps.extract-version.outputs.version }} created successfully!"
echo "✅ Result: Tag v${{ env.VERSION }} created successfully!"
fi
else
echo "️ Result: Not a release PR, no tag created"
echo "️ Result: Not a release/hotfix PR, no tag created"
fi
+4 -1
View File
@@ -39,7 +39,10 @@ jobs:
e2e:
needs: check-duplicate-run
if: needs.check-duplicate-run.outputs.should_skip != 'true'
if: >-
github.ref == 'refs/heads/main' ||
github.ref == 'refs/heads/canary' ||
needs.check-duplicate-run.outputs.should_skip != 'true'
name: Test Web App
runs-on: ubuntu-latest
services:
+4 -2
View File
@@ -340,21 +340,23 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 在 PR 上添加评论,包含构建信息和下载链接
# Post comment on PR with build info, release download link, and Actions artifacts link
- name: Comment on PR
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const releaseUrl = "${{ steps.create_release.outputs.url }}";
const artifactsUrl = "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}";
const prCommentGenerator = require('${{ github.workspace }}/.github/scripts/pr-comment.js');
const result = await prCommentGenerator({
github,
context,
releaseUrl,
artifactsUrl,
version: "${{ needs.version.outputs.version }}",
tag: "v${{ needs.version.outputs.version }}"
});
console.log(`评论状态: ${result.updated ? '已更新' : '已创建'}, ID: ${result.id}`);
console.log(`Comment ${result.updated ? 'updated' : 'created'}, ID: ${result.id}`);
@@ -0,0 +1,392 @@
name: Release Desktop Canary
# ============================================
# Canary 自动发版工作流
# ============================================
# 触发条件:
# 1. canary 分支有 push (合入 PR) 且 commit 前缀为 style/feat/fix/refactor
# 2. 手动触发 (workflow_dispatch)
#
# 并发策略:
# 同一 workflow 仅保留最新一次运行,自动取消排队中的旧 build
#
# 版本策略:
# 基于最新 stable tag 的 minor+1, 格式: X.(Y+1).0-canary.YYYYMMDDHHMM
# 例: 当前 tag v2.1.28 → v2.2.0-canary.202602121400
# ============================================
on:
push:
branches:
- canary
workflow_dispatch:
inputs:
force:
description: 'Force build (skip commit message check)'
required: false
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
permissions: read-all
env:
NODE_VERSION: '24.11.1'
jobs:
# ============================================
# 检查 commit 前缀并计算版本号
# ============================================
calculate-version:
name: Calculate Canary Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
should_build: ${{ steps.check.outputs.should_build }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- name: Check commit message prefix
id: check
run: |
# 手动触发 + force 时跳过检查
if [ "${{ inputs.force }}" == "true" ]; then
echo "should_build=true" >> $GITHUB_OUTPUT
echo "🔧 Force build requested, skipping commit check"
exit 0
fi
# 手动触发 (无 force) 也直接构建
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "should_build=true" >> $GITHUB_OUTPUT
echo "🔧 Manual trigger, proceeding with build"
exit 0
fi
# 获取本次 push 的 head commit message
commit_msg=$(git log -1 --pretty=%s HEAD)
echo "📝 Head commit: $commit_msg"
# 检查是否匹配 style/feat/fix/refactor 前缀 (支持 gitmoji 前缀)
if echo "$commit_msg" | grep -qiE '^(💄\s*)?style(\(.+\))?:|^(✨\s*)?feat(\(.+\))?:|^(🐛\s*)?fix(\(.+\))?:|^(♻️\s*)?refactor(\(.+\))?:'; then
echo "should_build=true" >> $GITHUB_OUTPUT
echo "✅ Commit matches canary build trigger: $commit_msg"
else
echo "should_build=false" >> $GITHUB_OUTPUT
echo "⏭️ Commit does not match style/feat/fix/refactor prefix, skipping: $commit_msg"
fi
- name: Calculate canary version
if: steps.check.outputs.should_build == 'true'
id: version
run: |
# 获取最新的 stable tag (排除 nightly/canary/beta 等)
latest_tag=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
if [ -z "$latest_tag" ]; then
echo "❌ No stable tag found"
exit 1
fi
echo "📌 Latest stable tag: $latest_tag"
# 去掉 v 前缀
base_version="${latest_tag#v}"
# 解析 major.minor.patch
IFS='.' read -r major minor patch <<< "$base_version"
# minor + 1, patch 归零
new_minor=$((minor + 1))
timestamp=$(date -u +"%Y%m%d%H%M")
version="${major}.${new_minor}.0-canary.${timestamp}"
tag="v${version}"
echo "version=${version}" >> $GITHUB_OUTPUT
echo "tag=${tag}" >> $GITHUB_OUTPUT
echo "✅ Canary version: ${version}"
echo "🏷️ Tag: ${tag}"
# ============================================
# 代码质量检查
# ============================================
test:
name: Code quality check
needs: [calculate-version]
if: needs.calculate-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout base
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install deps
run: bun i
- name: Lint
run: bun run lint
# ============================================
# 多平台构建
# ============================================
build:
needs: [calculate-version, test]
if: needs.calculate-version.outputs.should_build == 'true'
name: Build Desktop App
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-15, macos-15-intel, windows-2025, ubuntu-latest]
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup build environment
uses: ./.github/actions/desktop-build-setup
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set package version
run: npm run workflow:set-desktop-version ${{ needs.calculate-version.outputs.version }} canary
# macOS 构建前清理 (修复 hdiutil 问题)
- name: Clean previous build artifacts (macOS)
if: runner.os == 'macOS'
run: |
sudo rm -rf apps/desktop/release || true
sudo rm -rf apps/desktop/dist || true
sudo rm -rf /tmp/electron-builder* || true
# macOS 构建
- name: Build artifact on macOS
if: runner.os == 'macOS'
run: npm run desktop:package:app
env:
UPDATE_CHANNEL: canary
APP_URL: http://localhost:3015
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }}
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }}
# Windows 构建
- name: Build artifact on Windows
if: runner.os == 'Windows'
run: npm run desktop:package:app
env:
UPDATE_CHANNEL: canary
APP_URL: http://localhost:3015
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 }}
TEMP: C:\temp
TMP: C:\temp
# Linux 构建
- name: Build artifact on Linux
if: runner.os == 'Linux'
run: npm run desktop:package:app
env:
UPDATE_CHANNEL: canary
APP_URL: http://localhost:3015
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 }}
- name: Upload artifacts
uses: ./.github/actions/desktop-upload-artifacts
with:
artifact-name: release-${{ matrix.os }}
retention-days: 3
# ============================================
# 合并 macOS 多架构 latest-mac.yml 文件
# ============================================
merge-mac-files:
needs: [build]
name: Merge macOS Release Files
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Download artifacts
uses: actions/download-artifact@v7
with:
path: release
pattern: release-*
merge-multiple: true
- name: List downloaded artifacts
run: ls -R release
- name: Install yaml only for merge step
run: |
cd scripts/electronWorkflow
if [ ! -f package.json ]; then
echo '{"name":"merge-mac-release","private":true}' > package.json
fi
bun add --no-save yaml@2.8.1
- name: Merge latest-mac.yml files
run: bun run scripts/electronWorkflow/mergeMacReleaseFiles.js
- name: Upload artifacts with merged macOS files
uses: actions/upload-artifact@v6
with:
name: merged-release
path: release/
retention-days: 1
# ============================================
# 创建 Canary Release
# ============================================
publish-release:
needs: [merge-mac-files, calculate-version]
name: Publish Canary Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download merged artifacts
uses: actions/download-artifact@v7
with:
name: merged-release
path: release
- name: List final artifacts
run: ls -R release
- name: Create Canary Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.calculate-version.outputs.tag }}
name: 'Desktop Canary ${{ needs.calculate-version.outputs.tag }}'
prerelease: true
body: |
## 🐤 Canary Build — ${{ needs.calculate-version.outputs.tag }}
> Automated canary build from `canary` branch.
### ⚠️ Important Notes
- **This is an automated canary build and is NOT intended for production use.**
- Canary builds are triggered by `build`/`fix`/`style` commits on the `canary` branch.
- May contain **unstable or incomplete changes**. **Use at your own risk.**
- It is strongly recommended to **back up your data** before using a canary build.
### 📦 Installation
Download the appropriate installer for your platform from the assets below.
| Platform | File |
|----------|------|
| macOS (Apple Silicon) | `.dmg` (arm64) |
| macOS (Intel) | `.dmg` (x64) |
| Windows | `.exe` |
| Linux | `.AppImage` / `.deb` |
files: |
release/latest*
release/*.dmg*
release/*.zip*
release/*.exe*
release/*.AppImage
release/*.deb*
release/*.snap*
release/*.rpm*
release/*.tar.gz*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ============================================
# 清理旧的 Canary Releases (保留最近 7 个)
# ============================================
cleanup-old-canaries:
needs: [publish-release]
name: Cleanup Old Canary Releases
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Delete old canary releases
uses: actions/github-script@v7
with:
script: |
const { data: releases } = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const canaryReleases = releases
.filter(r => r.tag_name.includes('-canary.'))
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
const toDelete = canaryReleases.slice(7);
for (const release of toDelete) {
console.log(`🗑️ Deleting old canary release: ${release.tag_name}`);
// Delete the release
await github.rest.repos.deleteRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.id,
});
// Delete the tag
try {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${release.tag_name}`,
});
} catch (e) {
console.log(`⚠️ Could not delete tag ${release.tag_name}: ${e.message}`);
}
}
console.log(`✅ Cleanup complete. Kept ${Math.min(canaryReleases.length, 7)} canary releases, deleted ${toDelete.length}.`);
+3 -1
View File
@@ -84,6 +84,8 @@ jobs:
steps:
- name: Check release info
id: check
env:
RELEASE_BODY: ${{ github.event.release.body || '' }}
run: |
# 判断触发方式
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
@@ -100,7 +102,7 @@ jobs:
version="${version#v}"
echo "is_manual=false" >> $GITHUB_OUTPUT
echo "version=${version}" >> $GITHUB_OUTPUT
release_body="${{ github.event.release.body }}"
release_body="${RELEASE_BODY:-}"
{
echo "release_notes<<EOF"
printf '%s\n' "$release_body"
+9 -15
View File
@@ -73,28 +73,22 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "📦 Release version: v$VERSION"
- name: Update package.json version
- name: Verify package.json version matches tag
run: |
VERSION="${{ steps.get-version.outputs.version }}"
echo "📝 Updating package.json version to: $VERSION"
# Update package.json using Node.js
echo "🔎 Checking package.json version equals tag: $VERSION"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
pkg.version = '$VERSION';
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\\n');
console.log('✅ package.json updated');
const expected = '$VERSION';
const actual = pkg.version;
if (actual !== expected) {
console.error('❌ Version mismatch: package.json=' + actual + ' tag=' + expected);
process.exit(1);
}
console.log('✅ Version OK:', actual);
"
# Commit changes
git config --global user.name "lobehubbot"
git config --global user.email "i@lobehub.com"
git add package.json
git commit -m "🔧 chore(release): bump version to v$VERSION [skip ci]" || echo "Nothing to commit"
git push origin HEAD:main
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Release
run: bun run release
env:
+128
View File
@@ -0,0 +1,128 @@
name: 🔄 Branch Synchronization
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
concurrency:
group: sync-main-to-canary
cancel-in-progress: true
jobs:
sync-branches:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GH_TOKEN }}
- name: Set up Git
run: |
git config --global user.name 'lobehubbot'
git config --global user.email 'i@lobehub.com'
- name: Sync main to canary
if: github.ref == 'refs/heads/main'
run: |
# Find existing open sync PR by head branch prefix
EXISTING_PR=$(gh pr list --base canary --state open --json number,headRefName \
--jq '[.[] | select(.headRefName | startswith("sync/main-to-canary-"))][0] // empty')
EXISTING_PR_NUMBER=$(echo "$EXISTING_PR" | jq -r '.number // empty' 2>/dev/null)
# Refresh remote refs to avoid stale comparisons
git fetch origin canary main
# Check if there are actual changes to sync
if [ "$(git rev-parse origin/main)" = "$(git rev-parse origin/canary)" ]; then
echo "No changes to sync"
exit 0
fi
close_stale_pr() {
if [ -n "$EXISTING_PR_NUMBER" ]; then
gh pr close "$EXISTING_PR_NUMBER" --comment "Superseded by $1." --delete-branch || true
fi
}
# 1) Fast-forward: canary is ancestor of main → just move canary pointer
if git merge-base --is-ancestor origin/canary origin/main; then
echo "canary is ancestor of main, fast-forwarding"
git checkout canary
git reset --hard origin/main
if git push origin canary; then
close_stale_pr "fast-forward push"
exit 0
fi
echo "Fast-forward push failed, falling back to merge"
fi
# 2) Merge: canary has unique commits but no conflicts
git checkout canary
git reset --hard origin/canary
if git merge origin/main --no-edit; then
echo "Merge succeeded, pushing directly"
if git push origin canary; then
close_stale_pr "direct merge push"
exit 0
fi
# Direct push failed (e.g. non-fast-forward race), fall back to PR
echo "Direct push failed, falling back to PR"
SYNC_BRANCH="sync/main-to-canary-$(date +'%Y%m%d')-${GITHUB_RUN_ID}"
git checkout -B "$SYNC_BRANCH"
git push origin "$SYNC_BRANCH" -f
gh pr create \
--base canary \
--head "$SYNC_BRANCH" \
--title "Sync main branch to canary branch" \
--body "Automatic sync from main to canary. Direct push failed, please merge this PR." || true
exit 0
fi
# 3) Conflicts: create or update PR for manual resolution
echo "Merge conflicts detected, creating PR"
git merge --abort
if [ -n "$EXISTING_PR_NUMBER" ]; then
gh pr comment "$EXISTING_PR_NUMBER" --body "New commits on \`main\`. Please pull latest \`origin/main\` into this branch to include them."
echo "Commented on existing PR #$EXISTING_PR_NUMBER"
else
SYNC_BRANCH="sync/main-to-canary-$(date +'%Y%m%d')-${GITHUB_RUN_ID}"
git checkout -B "$SYNC_BRANCH" origin/canary
if ! git merge origin/main --no-edit; then
git add -A
git commit --no-edit -m "chore: merge main into canary (has conflicts to resolve)"
fi
git push origin "$SYNC_BRANCH" -f
printf '%s\n' \
'Automatic sync from main to canary. Merge conflicts detected.' \
'' \
'**Resolution steps:**' \
'```bash' \
'git fetch origin' \
"git checkout $SYNC_BRANCH" \
'git merge origin/main' \
'# Resolve conflicts' \
'git add -A && git commit' \
'git push' \
'```' \
'' \
'> Do NOT merge canary into a main-based branch — always merge main INTO the canary-based branch to keep a clean commit graph.' \
> /tmp/pr-body.md
gh pr create \
--base canary \
--head "$SYNC_BRANCH" \
--title "Sync main branch to canary branch" \
--body-file /tmp/pr-body.md
fi
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
-42
View File
@@ -1,42 +0,0 @@
name: 🔄 Branch Synchronization
on:
push:
branches:
- main
jobs:
sync-branches:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Git
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
- name: Prepare sync branch
id: branch
run: |
echo "SYNC_BRANCH_MAIN_DEV=sync/main-to-dev-$(date +'%Y%m%d')" >> $GITHUB_ENV
- name: Sync main to dev
if: github.ref == 'refs/heads/main'
run: |
# Sync main to dev
git checkout main
SYNC_BRANCH_DEV=${{ env.SYNC_BRANCH_MAIN_DEV }}
git checkout -B $SYNC_BRANCH_DEV
DIFF=$(git diff origin/dev...)
if [ -z "$DIFF" ]; then
echo "No changes to sync"
exit 0
fi
git push origin $SYNC_BRANCH_DEV -f
gh pr create --base dev --head $SYNC_BRANCH_DEV --title "Sync main branch to dev branch" --body "Automatic sync" || exit 0
env:
GH_TOKEN: ${{ github.token }}
+2 -1
View File
@@ -53,7 +53,8 @@
"typescriptreact"
],
"typescript.tsdk": "node_modules/typescript/lib",
"vitest.maximumConfigs": 20,
"vitest.disableWorkspaceWarning": true,
"vitest.maximumConfigs": 10,
"workbench.editor.customLabels.patterns": {
"**/app/**/[[]*[]]/[[]*[]]/page.tsx": "${dirname(2)}/${dirname(1)}/${dirname} • page component",
"**/app/**/[[]*[]]/page.tsx": "${dirname(1)}/${dirname} • page component",
+24 -14
View File
@@ -1,9 +1,10 @@
import dotenv from 'dotenv';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
import {
copyNativeModules,
copyNativeModulesToSource,
@@ -27,9 +28,11 @@ const updateServerUrl = process.env.UPDATE_SERVER_URL;
console.log(`🚄 Build Version ${packageJSON.version}, Channel: ${channel}`);
console.log(`🏗️ Building for architecture: ${arch}`);
// Channel identity derived solely from UPDATE_CHANNEL env var.
// Adding a new channel won't break stable detection.
const isStable = !channel || channel === 'stable';
const isNightly = channel === 'nightly';
const isBeta = packageJSON.name.includes('beta');
const isStable = !isNightly && !isBeta;
const isBeta = channel === 'beta';
// 根据 channel 配置不同的 publish provider
// - Stable + UPDATE_SERVER_URL: 使用 generic (自定义 HTTP 服务器)
@@ -80,9 +83,10 @@ const protocolScheme = getProtocolScheme();
// Determine icon file based on version type
const getIconFileName = () => {
if (isNightly) return 'Icon-nightly';
if (isStable) return 'Icon';
if (isBeta) return 'Icon-beta';
return 'Icon';
// nightly, canary, and any future pre-release channels share nightly icon
return 'Icon-nightly';
};
/**
@@ -196,6 +200,16 @@ const config = {
dmg: {
artifactName: '${productName}-${version}-${arch}.${ext}',
background: 'resources/dmg.png',
contents: [
{ type: 'file', x: 150, y: 240 },
{ type: 'link', path: '/Applications', x: 450, y: 240 },
],
iconSize: 80,
window: {
height: 400,
width: 600,
},
},
electronDownload: {
@@ -208,6 +222,7 @@ const config = {
// Ensure Next export assets are packaged
'dist/next/**/*',
'!resources/locales',
'!resources/dmg.png',
'!dist/next/docs',
'!dist/next/packages',
'!dist/next/.next/server/app/sitemap',
@@ -249,15 +264,10 @@ const config = {
hardenedRuntime: hasAppleCertificate,
notarize: hasAppleCertificate,
...(hasAppleCertificate ? {} : { identity: null }),
target:
// 降低构建时间,nightly 只打 dmg
// 根据当前机器架构只构建对应架构的包
isNightly
? [{ arch: [arch === 'arm64' ? 'arm64' : 'x64'], target: 'dmg' }]
: [
{ arch: [arch === 'arm64' ? 'arm64' : 'x64'], target: 'dmg' },
{ arch: [arch === 'arm64' ? 'arm64' : 'x64'], target: 'zip' },
],
target: [
{ arch: [arch === 'arm64' ? 'arm64' : 'x64'], target: 'dmg' },
{ arch: [arch === 'arm64' ? 'arm64' : 'x64'], target: 'zip' },
],
},
npmRebuild: true,
nsis: {
+1 -1
View File
@@ -33,7 +33,7 @@ const isDarwin = getTargetPlatform() === 'darwin';
*/
export const nativeModules = [
// macOS-only native modules
...(isDarwin ? ['node-mac-permissions'] : []),
...(isDarwin ? ['node-mac-permissions', 'electron-liquid-glass'] : []),
'@napi-rs/canvas',
// Add more native modules here as needed
];
+3 -2
View File
@@ -41,6 +41,7 @@
},
"dependencies": {
"@napi-rs/canvas": "^0.1.70",
"electron-liquid-glass": "^1.1.1",
"electron-updater": "^6.6.2",
"electron-window-state": "^5.0.3",
"fetch-socks": "^1.3.2",
@@ -77,7 +78,7 @@
"electron-store": "^8.2.0",
"electron-vite": "^4.0.1",
"es-toolkit": "^1.43.0",
"eslint": "^9.18.0",
"eslint": "10.0.0",
"execa": "^9.6.1",
"fast-glob": "^3.3.3",
"fix-path": "^5.0.0",
@@ -111,4 +112,4 @@
"node-mac-permissions"
]
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+2 -20
View File
@@ -18,7 +18,6 @@ export const appBrowsers = {
path: '/',
showOnInit: true,
titleBarStyle: 'hidden',
vibrancy: 'under-window',
width: 1200,
},
devtools: {
@@ -31,7 +30,6 @@ export const appBrowsers = {
parentIdentifier: 'app',
path: '/desktop/devtools',
titleBarStyle: 'hiddenInset',
vibrancy: 'under-window',
width: 1000,
},
} satisfies Record<string, BrowserWindowOpts>;
@@ -39,7 +37,6 @@ export const appBrowsers = {
// Window templates for multi-instance windows
export interface WindowTemplate {
allowMultipleInstances: boolean;
// Include common BrowserWindow options
autoHideMenuBar?: boolean;
baseIdentifier: string;
basePath: string;
@@ -51,22 +48,8 @@ export interface WindowTemplate {
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';
// Note: vibrancy / visualEffectState / transparent are intentionally omitted.
// Platform visual effects are managed exclusively by WindowThemeManager.
width?: number;
}
@@ -81,7 +64,6 @@ export const windowTemplates = {
minWidth: 400,
parentIdentifier: 'app',
titleBarStyle: 'hidden',
vibrancy: 'under-window',
width: 900,
},
} satisfies Record<string, WindowTemplate>;
+9
View File
@@ -11,6 +11,15 @@ export const isMac = macOS();
export const isWindows = windows();
export const isLinux = linux();
function getIsMacTahoe(): boolean {
if (!isMac) return false;
// macOS 26 (Tahoe) corresponds to Darwin kernel 25.x
const darwinMajor = parseInt(os.release().split('.')[0], 10);
return darwinMajor >= 25;
}
export const isMacTahoe = getIsMacTahoe();
function getIsWindows11() {
if (!isWindows) return false;
// Get OS version (e.g., "10.0.22621")
@@ -1,33 +1,37 @@
/* eslint-disable unicorn/no-array-push-push */
import {
EditLocalFileParams,
EditLocalFileResult,
GlobFilesParams,
GlobFilesResult,
GrepContentParams,
GrepContentResult,
ListLocalFileParams,
LocalMoveFilesResultItem,
LocalReadFileParams,
LocalReadFileResult,
LocalReadFilesParams,
LocalSearchFilesParams,
MoveLocalFilesParams,
OpenLocalFileParams,
OpenLocalFolderParams,
RenameLocalFileResult,
ShowSaveDialogParams,
ShowSaveDialogResult,
WriteLocalFileParams,
} from '@lobechat/electron-client-ipc';
import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
import { createPatch } from 'diff';
import { dialog, shell } from 'electron';
import { constants } from 'node:fs';
import { access, mkdir, readFile, readdir, rename, stat, writeFile } from 'node:fs/promises';
import { access, mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
import * as path from 'node:path';
import { FileResult, SearchOptions } from '@/modules/fileSearch';
import {
type EditLocalFileParams,
type EditLocalFileResult,
type GlobFilesParams,
type GlobFilesResult,
type GrepContentParams,
type GrepContentResult,
type ListLocalFileParams,
type LocalMoveFilesResultItem,
type LocalReadFileParams,
type LocalReadFileResult,
type LocalReadFilesParams,
type LocalSearchFilesParams,
type MoveLocalFilesParams,
type OpenLocalFileParams,
type OpenLocalFolderParams,
type PickFileParams,
type PickFileResult,
type RenameLocalFileResult,
type ShowOpenDialogParams,
type ShowOpenDialogResult,
type ShowSaveDialogParams,
type ShowSaveDialogResult,
type WriteLocalFileParams,
} from '@lobechat/electron-client-ipc';
import { loadFile, SYSTEM_FILES_TO_IGNORE } from '@lobechat/file-loaders';
import { createPatch } from 'diff';
import { dialog, shell } from 'electron';
import { type FileResult, type SearchOptions } from '@/modules/fileSearch';
import ContentSearchService from '@/services/contentSearchSrv';
import FileSearchService from '@/services/fileSearchSrv';
import { makeSureDirExist } from '@/utils/file-system';
@@ -85,6 +89,67 @@ export default class LocalFileCtr extends ControllerModule {
}
}
@IpcMethod()
async handleShowOpenDialog({
filters,
multiple,
title,
}: ShowOpenDialogParams): Promise<ShowOpenDialogResult> {
logger.debug('Showing open dialog:', { filters, multiple, title });
const result = await dialog.showOpenDialog({
filters,
properties: multiple ? ['openFile', 'multiSelections'] : ['openFile'],
title,
});
logger.debug('Open dialog result:', { canceled: result.canceled, filePaths: result.filePaths });
return {
canceled: result.canceled,
filePaths: result.filePaths,
};
}
@IpcMethod()
async handlePickFile({ filters, title }: PickFileParams): Promise<PickFileResult> {
logger.debug('Picking file:', { filters, title });
const result = await dialog.showOpenDialog({
filters,
properties: ['openFile'],
title,
});
if (result.canceled || result.filePaths.length === 0) {
return { canceled: true };
}
const filePath = result.filePaths[0];
const data = await readFile(filePath);
const name = path.basename(filePath);
const ext = path.extname(filePath).toLowerCase().slice(1);
const MIME_MAP: Record<string, string> = {
avif: 'image/avif',
gif: 'image/gif',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
png: 'image/png',
svg: 'image/svg+xml',
webp: 'image/webp',
};
return {
canceled: false,
file: {
data: new Uint8Array(data),
mimeType: MIME_MAP[ext] || 'application/octet-stream',
name,
},
};
}
@IpcMethod()
async handleShowSaveDialog({
defaultPath,
+14 -5
View File
@@ -110,7 +110,18 @@ export default class Browser {
// ==================== Window Creation ====================
private createBrowserWindow(): BrowserWindow {
const { title, width, height, ...rest } = this.options;
const {
title,
width,
height,
// Strip platform visual effect props — these are managed exclusively
// by WindowThemeManager.getPlatformConfig() to prevent config leaking
// from appBrowsers/windowTemplates into the BrowserWindow constructor.
vibrancy: _vibrancy,
visualEffectState: _visualEffectState,
transparent: _transparent,
...rest
} = this.options;
const resolvedState = this.stateManager.resolveState({ height, width });
logger.info(`Creating new BrowserWindow instance: ${this.identifier}`);
@@ -125,9 +136,6 @@ export default class Browser {
height: resolvedState.height,
show: false,
title,
vibrancy: 'sidebar',
visualEffectState: 'active',
webPreferences: {
backgroundThrottling: false,
contextIsolation: true,
@@ -138,6 +146,7 @@ export default class Browser {
width: resolvedState.width,
x: resolvedState.x,
y: resolvedState.y,
// Platform visual config is the SOLE source of vibrancy / transparency / titleBarOverlay.
...this.themeManager.getPlatformConfig(),
});
}
@@ -145,7 +154,7 @@ export default class Browser {
private setupWindow(browserWindow: BrowserWindow): void {
logger.debug(`[${this.identifier}] BrowserWindow instance created.`);
// Setup theme management
// Setup theme management (includes liquid glass lifecycle on macOS Tahoe)
this.themeManager.attach(browserWindow);
// Setup network interceptors
@@ -1,9 +1,12 @@
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
import { BrowserWindow, BrowserWindowConstructorOptions, nativeTheme } from 'electron';
import { join } from 'node:path';
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
import { type BrowserWindow, type BrowserWindowConstructorOptions, nativeTheme } from 'electron';
import { buildDir } from '@/const/dir';
import { isDev, isMac, isWindows } from '@/const/env';
import { isDev, isMac, isMacTahoe, isWindows } from '@/const/env';
import { createLogger } from '@/utils/logger';
import {
BACKGROUND_DARK,
BACKGROUND_LIGHT,
@@ -11,7 +14,6 @@ import {
SYMBOL_COLOR_LIGHT,
THEME_CHANGE_DELAY,
} from '../../const/theme';
import { createLogger } from '@/utils/logger';
const logger = createLogger('core:WindowThemeManager');
@@ -26,6 +28,18 @@ interface WindowsThemeConfig {
titleBarStyle: 'hidden';
}
// Lazy-load liquid glass only on macOS Tahoe to avoid import errors on other platforms.
// Dynamic require is intentional: native .node addons cannot be loaded via
// async import() and must be synchronously required at module init time.
let liquidGlass: typeof import('electron-liquid-glass').default | undefined;
if (isMacTahoe) {
try {
liquidGlass = require('electron-liquid-glass');
} catch {
// Native module not available (e.g. wrong architecture or missing binary)
}
}
/**
* Manages window theme configuration and visual effects
*/
@@ -34,6 +48,7 @@ export class WindowThemeManager {
private browserWindow?: BrowserWindow;
private listenerSetup = false;
private boundHandleThemeChange: () => void;
private liquidGlassViewId?: number;
constructor(identifier: string) {
this.identifier = identifier;
@@ -52,12 +67,21 @@ export class WindowThemeManager {
// ==================== Lifecycle ====================
/**
* Attach to a browser window and setup theme handling
* Attach to a browser window and setup theme handling.
* Owns the full visual effect lifecycle including liquid glass on macOS Tahoe.
*/
attach(browserWindow: BrowserWindow): void {
this.browserWindow = browserWindow;
this.setupThemeListener();
this.applyVisualEffects();
// Liquid glass must be applied after window content loads (native view needs
// a rendered surface). The effect persists across subsequent in-window navigations.
if (this.useLiquidGlass) {
browserWindow.webContents.once('did-finish-load', () => {
this.applyLiquidGlass();
});
}
}
/**
@@ -69,6 +93,7 @@ export class WindowThemeManager {
this.listenerSetup = false;
logger.debug(`[${this.identifier}] Theme listener cleaned up.`);
}
this.liquidGlassViewId = undefined;
this.browserWindow = undefined;
}
@@ -81,6 +106,13 @@ export class WindowThemeManager {
return nativeTheme.shouldUseDarkColors;
}
/**
* Whether liquid glass is available and should be used
*/
get useLiquidGlass(): boolean {
return isMacTahoe && !!liquidGlass;
}
/**
* Get platform-specific theme configuration for window creation
*/
@@ -92,8 +124,19 @@ export class WindowThemeManager {
// Calculate traffic light position to center vertically in title bar
// Traffic light buttons are approximately 12px tall
const trafficLightY = Math.round((TITLE_BAR_HEIGHT - 12) / 2);
if (this.useLiquidGlass) {
// Liquid glass requires transparent window and must NOT use vibrancy — they conflict.
return {
trafficLightPosition: { x: 12, y: trafficLightY },
transparent: true,
};
}
return {
trafficLightPosition: { x: 12, y: trafficLightY },
vibrancy: 'sidebar',
visualEffectState: 'active',
};
}
return {};
@@ -135,58 +178,37 @@ export class WindowThemeManager {
logger.debug(`[${this.identifier}] App theme mode changed, reapplying visual effects.`);
setTimeout(() => {
this.applyVisualEffects();
this.applyWindowsTitleBarOverlay();
}, THEME_CHANGE_DELAY);
}
// ==================== Visual Effects ====================
private resolveWindowsIsDarkModeFromElectron(): boolean {
/**
* Resolve dark mode from Electron theme source for runtime visual effect updates.
* Checks explicit themeSource first to handle app-level theme overrides correctly.
*/
private resolveIsDarkMode(): boolean {
if (nativeTheme.themeSource === 'dark') return true;
if (nativeTheme.themeSource === 'light') return false;
return nativeTheme.shouldUseDarkColors;
}
/**
* Apply Windows title bar overlay based on Electron theme mode.
* Mirror the structure of `applyVisualEffects`, but only updates title bar overlay.
*/
private applyWindowsTitleBarOverlay(): void {
if (!this.browserWindow || this.browserWindow.isDestroyed()) return;
logger.debug(`[${this.identifier}] Applying Windows title bar overlay`);
const isDarkMode = this.resolveWindowsIsDarkModeFromElectron();
try {
if (!isWindows) return;
this.browserWindow.setTitleBarOverlay(this.getWindowsTitleBarOverlay(isDarkMode));
logger.debug(
`[${this.identifier}] Windows title bar overlay applied successfully (dark mode: ${isDarkMode})`,
);
} catch (error) {
logger.error(`[${this.identifier}] Failed to apply Windows title bar overlay:`, error);
}
}
/**
* Apply visual effects based on current theme
* Apply visual effects based on current theme.
* Single entry point for ALL platform visual effects.
*/
applyVisualEffects(): void {
if (!this.browserWindow || this.browserWindow.isDestroyed()) return;
logger.debug(`[${this.identifier}] Applying visual effects for platform`);
const isDarkMode = this.isDarkMode;
const isDarkMode = this.resolveIsDarkMode();
logger.debug(`[${this.identifier}] Applying visual effects (dark: ${isDarkMode})`);
try {
if (isWindows) {
this.applyWindowsVisualEffects(isDarkMode);
} else if (isMac) {
this.applyMacVisualEffects();
}
logger.debug(
`[${this.identifier}] Visual effects applied successfully (dark mode: ${isDarkMode})`,
);
} catch (error) {
logger.error(`[${this.identifier}] Failed to apply visual effects:`, error);
}
@@ -207,4 +229,44 @@ export class WindowThemeManager {
this.browserWindow.setBackgroundColor(config.backgroundColor);
this.browserWindow.setTitleBarOverlay(config.titleBarOverlay);
}
/**
* Apply macOS visual effects.
* - Tahoe+: liquid glass auto-adapts to dark mode; ensure it's applied if not yet.
* - Pre-Tahoe: vibrancy is managed natively by Electron, no runtime action needed.
*/
private applyMacVisualEffects(): void {
if (!this.browserWindow) return;
if (this.useLiquidGlass) {
// Attempt apply if not yet done (e.g. initial load failed, or window recreated)
this.applyLiquidGlass();
}
}
// ==================== Liquid Glass ====================
/**
* Apply liquid glass native view to the window.
* Idempotent — guards against double-application via `liquidGlassViewId`.
*/
applyLiquidGlass(): void {
if (!this.useLiquidGlass || !liquidGlass) return;
if (!this.browserWindow || this.browserWindow.isDestroyed()) return;
if (this.liquidGlassViewId !== undefined) return;
try {
// Ensure traffic light buttons remain visible with transparent window
this.browserWindow.setWindowButtonVisibility(true);
const handle = this.browserWindow.getNativeWindowHandle();
this.liquidGlassViewId = liquidGlass.addView(handle);
liquidGlass.unstable_setVariant(this.liquidGlassViewId, 15);
logger.info(`[${this.identifier}] Liquid glass applied (viewId: ${this.liquidGlassViewId})`);
} catch (error) {
logger.error(`[${this.identifier}] Failed to apply liquid glass:`, error);
}
}
}
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { App as AppCore } from '../../App';
import Browser, { BrowserWindowOpts } from '../Browser';
import { type App as AppCore } from '../../App';
import Browser, { type BrowserWindowOpts } from '../Browser';
// Use vi.hoisted to define mocks before hoisting
const { mockBrowserWindow, mockNativeTheme, mockIpcMain, mockScreen, MockBrowserWindow } =
@@ -100,6 +100,7 @@ vi.mock('@/const/dir', () => ({
vi.mock('@/const/env', () => ({
isDev: false,
isMac: false,
isMacTahoe: false,
isWindows: true,
}));
@@ -605,9 +606,9 @@ describe('Browser', () => {
const keepAliveBrowser = new Browser(keepAliveOptions, mockApp);
// Get the new close handler
const keepAliveCloseHandler = mockBrowserWindow.on.mock.calls
.filter((call) => call[0] === 'close')
.pop()?.[1];
const keepAliveCloseHandler = mockBrowserWindow.on.mock.calls.findLast(
(call) => call[0] === 'close',
)?.[1];
const mockEvent = { preventDefault: vi.fn() };
keepAliveCloseHandler(mockEvent);
@@ -13,6 +13,7 @@ const { mockNativeTheme, mockBrowserWindow } = vi.hoisted(() => ({
off: vi.fn(),
on: vi.fn(),
shouldUseDarkColors: false,
themeSource: 'system' as string,
},
}));
@@ -35,6 +36,8 @@ vi.mock('@/const/dir', () => ({
vi.mock('@/const/env', () => ({
isDev: false,
isMac: false,
isMacTahoe: false,
isWindows: true,
}));
@@ -58,6 +61,7 @@ describe('WindowThemeManager', () => {
vi.useFakeTimers();
mockNativeTheme.shouldUseDarkColors = false;
mockNativeTheme.themeSource = 'system';
mockBrowserWindow.isDestroyed.mockReturnValue(false);
manager = new WindowThemeManager('test-window');
+4 -1
View File
@@ -51,7 +51,7 @@ describe('setupElectronApi', () => {
});
});
it('should expose lobeEnv with darwinMajorVersion', () => {
it('should expose lobeEnv with darwinMajorVersion and isMacTahoe', () => {
setupElectronApi();
const call = mockContextBridgeExposeInMainWorld.mock.calls.find((i) => i[0] === 'lobeEnv');
@@ -63,6 +63,9 @@ describe('setupElectronApi', () => {
exposedEnv.darwinMajorVersion === undefined ||
typeof exposedEnv.darwinMajorVersion === 'number',
).toBe(true);
expect(Object.prototype.hasOwnProperty.call(exposedEnv, 'isMacTahoe')).toBe(true);
expect(typeof exposedEnv.isMacTahoe).toBe('boolean');
});
it('should expose both APIs in correct order', () => {
+3 -2
View File
@@ -22,9 +22,10 @@ export const setupElectronApi = () => {
const os = require('node:os');
const osInfo = os.release();
const darwinMajorVersion = osInfo.split('.')[0];
const darwinMajorVersion = Number(osInfo.split('.')[0]);
contextBridge.exposeInMainWorld('lobeEnv', {
darwinMajorVersion: Number(darwinMajorVersion),
darwinMajorVersion,
isMacTahoe: process.platform === 'darwin' && darwinMajorVersion >= 25,
});
};
+3
View File
@@ -201,6 +201,7 @@ table async_tasks {
type text
status text
error jsonb
inference_id text
user_id text [not null]
duration integer
parent_id uuid
@@ -213,6 +214,7 @@ table async_tasks {
user_id [name: 'async_tasks_user_id_idx']
parent_id [name: 'async_tasks_parent_id_idx']
(type, status) [name: 'async_tasks_type_status_idx']
inference_id [name: 'async_tasks_inference_id_idx']
metadata [name: 'async_tasks_metadata_idx']
}
}
@@ -479,6 +481,7 @@ table generation_topics {
user_id text [not null]
title text
cover_url text
type varchar(32) [not null, default: 'image']
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()`]
+85 -85
View File
@@ -11,14 +11,14 @@
import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// Given Steps
// ============================================
Given('用户已有一个对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建一个对话...');
console.info(' 📍 Step: 创建一个对话...');
// Send a message to create a conversation
const chatInputs = this.page.locator('[data-testid="chat-input"]');
@@ -45,13 +45,13 @@ Given('用户已有一个对话', async function (this: CustomWorld) {
// Store the current conversation title for later reference
const topicItems = this.page.locator('.ant-menu-item, [class*="NavItem"]');
const topicCount = await topicItems.count();
console.log(` 📍 Found ${topicCount} topic items after creating conversation`);
console.info(` 📍 Found ${topicCount} topic items after creating conversation`);
console.log(' ✅ 已创建一个对话');
console.info(' ✅ 已创建一个对话');
});
Given('用户有多个对话历史', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建多个对话...');
console.info(' 📍 Step: 创建多个对话...');
// Create first conversation
const chatInputs = this.page.locator('[data-testid="chat-input"]');
@@ -77,7 +77,7 @@ Given('用户有多个对话历史', async function (this: CustomWorld) {
this.testContext.firstConversation = 'first';
// Create new topic and second conversation
console.log(' 📍 Creating second conversation...');
console.info(' 📍 Creating second conversation...');
const addTopicButton = this.page.locator('svg.lucide-message-square-plus').locator('..');
if ((await addTopicButton.count()) > 0) {
await addTopicButton.first().click();
@@ -91,7 +91,7 @@ Given('用户有多个对话历史', async function (this: CustomWorld) {
await this.page.waitForTimeout(2000);
}
console.log(' ✅ 已创建多个对话');
console.info(' ✅ 已创建多个对话');
});
// ============================================
@@ -99,20 +99,20 @@ Given('用户有多个对话历史', async function (this: CustomWorld) {
// ============================================
When('用户点击新建对话按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击新建对话按钮...');
console.info(' 📍 Step: 点击新建对话按钮...');
// The add topic button uses MessageSquarePlusIcon from lucide-react
const addTopicButton = this.page.locator('svg.lucide-message-square-plus').locator('..');
if ((await addTopicButton.count()) > 0) {
await addTopicButton.first().click();
console.log(' ✅ 已点击新建对话按钮');
console.info(' ✅ 已点击新建对话按钮');
} else {
// Fallback: look for button with "新建" or "add" in title
const addButton = this.page.locator('button[title*="新建"], button[title*="add"]');
if ((await addButton.count()) > 0) {
await addButton.first().click();
console.log(' ✅ 已点击新建对话按钮 (fallback)');
console.info(' ✅ 已点击新建对话按钮 (fallback)');
} else {
throw new Error('New topic button not found');
}
@@ -122,24 +122,24 @@ When('用户点击新建对话按钮', async function (this: CustomWorld) {
});
When('用户点击另一个对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击另一个对话...');
console.info(' 📍 Step: 点击另一个对话...');
// Check if we're on the home page (has Recent Topics section)
const recentTopicsSection = this.page.locator('text=Recent Topics');
const isOnHomePage = (await recentTopicsSection.count()) > 0;
console.log(` 📍 Is on home page: ${isOnHomePage}`);
console.info(` 📍 Is on home page: ${isOnHomePage}`);
if (isOnHomePage) {
// Click the second topic card in Recent Topics section
// Cards are wrapped in Link components and contain "Hello! I am a mock AI" text from the mock
const recentTopicCards = this.page.locator('a[href*="topic="]');
const cardCount = await recentTopicCards.count();
console.log(` 📍 Found ${cardCount} recent topic cards (by href)`);
console.info(` 📍 Found ${cardCount} recent topic cards (by href)`);
if (cardCount >= 2) {
// Click the second card (different from current topic)
await recentTopicCards.nth(1).click();
console.log(' ✅ 已点击首页 Recent Topics 中的另一个对话');
console.info(' ✅ 已点击首页 Recent Topics 中的另一个对话');
await this.page.waitForTimeout(2000);
return;
}
@@ -147,11 +147,11 @@ When('用户点击另一个对话', async function (this: CustomWorld) {
// Fallback: try to find by text content
const topicTextCards = this.page.locator('text=Hello! I am a mock AI');
const textCardCount = await topicTextCards.count();
console.log(` 📍 Found ${textCardCount} topic cards by text`);
console.info(` 📍 Found ${textCardCount} topic cards by text`);
if (textCardCount >= 2) {
await topicTextCards.nth(1).click();
console.log(' ✅ 已点击首页 Recent Topics 中的另一个对话 (by text)');
console.info(' ✅ 已点击首页 Recent Topics 中的另一个对话 (by text)');
await this.page.waitForTimeout(2000);
return;
}
@@ -161,18 +161,18 @@ When('用户点击另一个对话', async function (this: CustomWorld) {
// Topics are displayed with star icons (lucide-star) in the left sidebar
const sidebarTopics = this.page.locator('svg.lucide-star').locator('..').locator('..');
let topicCount = await sidebarTopics.count();
console.log(` 📍 Found ${topicCount} topics with star icons`);
console.info(` 📍 Found ${topicCount} topics with star icons`);
// If not found by star, try finding by topic list structure
if (topicCount < 2) {
// Topics might be in a list container - look for items in sidebar with specific text
const topicItems = this.page.locator('[class*="nav-item"], [class*="NavItem"]');
topicCount = await topicItems.count();
console.log(` 📍 Found ${topicCount} nav items`);
console.info(` 📍 Found ${topicCount} nav items`);
if (topicCount >= 2) {
await topicItems.nth(1).click();
console.log(' ✅ 已点击另一个对话');
console.info(' ✅ 已点击另一个对话');
await this.page.waitForTimeout(500);
return;
}
@@ -181,7 +181,7 @@ When('用户点击另一个对话', async function (this: CustomWorld) {
// Click the second topic (first one is current/active)
if (topicCount >= 2) {
await sidebarTopics.nth(1).click();
console.log(' ✅ 已点击另一个对话');
console.info(' ✅ 已点击另一个对话');
} else {
throw new Error('Not enough topics to switch');
}
@@ -190,17 +190,17 @@ When('用户点击另一个对话', async function (this: CustomWorld) {
});
When('用户右键点击对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击对话...');
console.info(' 📍 Step: 右键点击对话...');
// Find topic items by their star icon - each saved topic has a star
const sidebarTopics = this.page.locator('svg.lucide-star').locator('..').locator('..');
let topicCount = await sidebarTopics.count();
console.log(` 📍 Found ${topicCount} topics with star icons`);
const topicCount = await sidebarTopics.count();
console.info(` 📍 Found ${topicCount} topics with star icons`);
if (topicCount > 0) {
// Right-click the first saved topic
await sidebarTopics.first().click({ button: 'right' });
console.log(' ✅ 已右键点击对话');
console.info(' ✅ 已右键点击对话');
} else {
throw new Error('No topics found to right-click');
}
@@ -209,19 +209,19 @@ When('用户右键点击对话', async function (this: CustomWorld) {
});
When('用户右键点击一个对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击一个对话...');
console.info(' 📍 Step: 右键点击一个对话...');
// Find topic items by their star icon
const sidebarTopics = this.page.locator('svg.lucide-star').locator('..').locator('..');
let topicCount = await sidebarTopics.count();
console.log(` 📍 Found ${topicCount} topics with star icons`);
const topicCount = await sidebarTopics.count();
console.info(` 📍 Found ${topicCount} topics with star icons`);
// Store the topic text for later verification
if (topicCount > 0) {
const topicText = await sidebarTopics.first().textContent();
this.testContext.deletedTopicTitle = topicText?.slice(0, 30);
await sidebarTopics.first().click({ button: 'right' });
console.log(` ✅ 已右键点击对话: "${topicText?.slice(0, 30)}..."`);
console.info(` ✅ 已右键点击对话: "${topicText?.slice(0, 30)}..."`);
} else {
throw new Error('No topics found to right-click');
}
@@ -230,7 +230,7 @@ When('用户右键点击一个对话', async function (this: CustomWorld) {
});
When('用户选择重命名选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择重命名选项...');
console.info(' 📍 Step: 选择重命名选项...');
// First, close any open context menu by clicking elsewhere
await this.page.click('body', { position: { x: 500, y: 300 } });
@@ -240,46 +240,46 @@ When('用户选择重命名选项', async function (this: CustomWorld) {
// which appears when hovering over a topic item
const topicItems = this.page.locator('svg.lucide-star').locator('..').locator('..');
const topicCount = await topicItems.count();
console.log(` 📍 Found ${topicCount} topic items`);
console.info(` 📍 Found ${topicCount} topic items`);
if (topicCount > 0) {
// Hover on the first topic to reveal the "..." action button
const firstTopic = topicItems.first();
await firstTopic.hover();
console.log(' 📍 Hovering on topic item...');
console.info(' 📍 Hovering on topic item...');
await this.page.waitForTimeout(500);
// The "..." button should now be visible INSIDE the topic item
// Important: we must find the icon WITHIN the hovered topic, not the global one
// The topic item has a specific structure with nav-item-actions
const moreButtonInTopic = firstTopic.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal');
let moreButtonCount = await moreButtonInTopic.count();
console.log(` 📍 Found ${moreButtonCount} more buttons inside topic`);
const moreButtonCount = await moreButtonInTopic.count();
console.info(` 📍 Found ${moreButtonCount} more buttons inside topic`);
if (moreButtonCount > 0) {
// Click the "..." button to open dropdown menu
await moreButtonInTopic.first().click();
console.log(' 📍 Clicked ... button inside topic');
console.info(' 📍 Clicked ... button inside topic');
await this.page.waitForTimeout(500);
} else {
// Fallback: try to find it by looking at the actions container
console.log(' 📍 Trying alternative: looking for actions container...');
console.info(' 📍 Trying alternative: looking for actions container...');
// Debug: print the topic item HTML structure
const topicHTML = await firstTopic.evaluate((el) => el.outerHTML.slice(0, 500));
console.log(` 📍 Topic HTML: ${topicHTML}`);
console.info(` 📍 Topic HTML: ${topicHTML}`);
// The actions might be in a sibling or parent element
// Try finding any ellipsis icon that's near the topic
const allEllipsis = this.page.locator('svg.lucide-ellipsis');
const ellipsisCount = await allEllipsis.count();
console.log(` 📍 Total ellipsis icons on page: ${ellipsisCount}`);
console.info(` 📍 Total ellipsis icons on page: ${ellipsisCount}`);
// Skip the first one (which is the global topic list menu)
// and click the second one (which should be in the topic item)
if (ellipsisCount > 1) {
await allEllipsis.nth(1).click();
console.log(' 📍 Clicked second ellipsis icon');
console.info(' 📍 Clicked second ellipsis icon');
await this.page.waitForTimeout(500);
}
}
@@ -289,24 +289,24 @@ When('用户选择重命名选项', async function (this: CustomWorld) {
const renameOption = this.page.getByRole('menuitem', { exact: true, name: /^(Rename|重命名)$/ });
await expect(renameOption).toBeVisible({ timeout: 5000 });
console.log(' 📍 Found rename menu item');
console.info(' 📍 Found rename menu item');
// Click the rename option
await renameOption.click();
console.log(' 📍 Clicked rename menu item');
console.info(' 📍 Clicked rename menu item');
// Wait for the popover/input to appear
await this.page.waitForTimeout(500);
// Check if input appeared
const inputCount = await this.page.locator('input').count();
console.log(` 📍 After click: ${inputCount} inputs on page`);
console.info(` 📍 After click: ${inputCount} inputs on page`);
console.log(' ✅ 已选择重命名选项');
console.info(' ✅ 已选择重命名选项');
});
When('用户输入新的对话名称 {string}', async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}"...`);
console.info(` 📍 Step: 输入新名称 "${newName}"...`);
// Debug: check what's on the page
const debugInfo = await this.page.evaluate(() => {
@@ -326,7 +326,7 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
popoverCount: allPopovers.length,
};
});
console.log(' 📍 Debug info:', JSON.stringify(debugInfo, null, 2));
console.info(' 📍 Debug info:', JSON.stringify(debugInfo, null, 2));
// Wait a short moment for the popover to render
await this.page.waitForTimeout(300);
@@ -350,7 +350,7 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
const locator = this.page.locator(selector).first();
await locator.waitFor({ state: 'visible', timeout: 2000 });
renameInput = locator;
console.log(` 📍 Found input with selector: ${selector}`);
console.info(` 📍 Found input with selector: ${selector}`);
break;
} catch {
// Try next selector
@@ -359,10 +359,10 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
if (!renameInput) {
// Fallback: find any visible input that's not the search or chat input
console.log(' 📍 Trying fallback: finding any visible input...');
console.info(' 📍 Trying fallback: finding any visible input...');
const allInputs = this.page.locator('input:visible');
const count = await allInputs.count();
console.log(` 📍 Found ${count} visible inputs`);
console.info(` 📍 Found ${count} visible inputs`);
for (let i = 0; i < count; i++) {
const input = allInputs.nth(i);
@@ -380,7 +380,7 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
if (isInPopover || count === 1) {
renameInput = input;
console.log(` 📍 Found candidate input at index ${i}`);
console.info(` 📍 Found candidate input at index ${i}`);
break;
}
}
@@ -391,20 +391,20 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
await renameInput.click();
await renameInput.clear();
await renameInput.fill(newName);
console.log(` 📍 Filled input with "${newName}"`);
console.info(` 📍 Filled input with "${newName}"`);
// Press Enter to confirm
await renameInput.press('Enter');
console.log(` ✅ 已输入新名称 "${newName}"`);
console.info(` ✅ 已输入新名称 "${newName}"`);
} else {
// Last resort: the input should have autoFocus, so keyboard should work
console.log(' ⚠️ Could not find rename input element, using keyboard fallback...');
console.info(' ⚠️ Could not find rename input element, using keyboard fallback...');
// Select all and replace
await this.page.keyboard.press('Meta+A');
await this.page.waitForTimeout(50);
await this.page.keyboard.type(newName, { delay: 20 });
await this.page.keyboard.press('Enter');
console.log(` ✅ 已通过键盘输入新名称 "${newName}"`);
console.info(` ✅ 已通过键盘输入新名称 "${newName}"`);
}
// Wait for the rename to be saved
@@ -412,7 +412,7 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
});
When('用户选择删除选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择删除选项...');
console.info(' 📍 Step: 选择删除选项...');
// The context menu should be visible with "delete" option
// Support both English and Chinese
@@ -421,12 +421,12 @@ When('用户选择删除选项', async function (this: CustomWorld) {
await expect(deleteOption).toBeVisible({ timeout: 5000 });
await deleteOption.click();
console.log(' ✅ 已选择删除选项');
console.info(' ✅ 已选择删除选项');
await this.page.waitForTimeout(300);
});
When('用户确认删除', async function (this: CustomWorld) {
console.log(' 📍 Step: 确认删除...');
console.info(' 📍 Step: 确认删除...');
// A confirmation modal should appear
const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous');
@@ -435,12 +435,12 @@ When('用户确认删除', async function (this: CustomWorld) {
await expect(confirmButton).toBeVisible({ timeout: 5000 });
await confirmButton.click();
console.log(' ✅ 已确认删除');
console.info(' ✅ 已确认删除');
await this.page.waitForTimeout(500);
});
When('用户在搜索框中输入 {string}', async function (this: CustomWorld, searchText: string) {
console.log(` 📍 Step: 在搜索框中输入 "${searchText}"...`);
console.info(` 📍 Step: 在搜索框中输入 "${searchText}"...`);
// Find the search input in the sidebar
// Support both English and Chinese placeholders
@@ -463,7 +463,7 @@ When('用户在搜索框中输入 {string}', async function (this: CustomWorld,
}
}
console.log(` ✅ 已输入搜索内容 "${searchText}"`);
console.info(` ✅ 已输入搜索内容 "${searchText}"`);
await this.page.waitForTimeout(500);
});
@@ -472,7 +472,7 @@ When('用户在搜索框中输入 {string}', async function (this: CustomWorld,
// ============================================
Then('应该创建一个新的空白对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证新对话已创建...');
console.info(' 📍 Step: 验证新对话已创建...');
// The chat area should be empty or show welcome message
// Check that there are no user/assistant messages
@@ -482,17 +482,17 @@ Then('应该创建一个新的空白对话', async function (this: CustomWorld)
const userCount = await userMessages.count();
const assistantCount = await assistantMessages.count();
console.log(` 📍 用户消息数量: ${userCount}, 助手消息数量: ${assistantCount}`);
console.info(` 📍 用户消息数量: ${userCount}, 助手消息数量: ${assistantCount}`);
// New conversation should have no messages
expect(userCount).toBe(0);
expect(assistantCount).toBe(0);
console.log(' ✅ 新对话已创建');
console.info(' ✅ 新对话已创建');
});
Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面显示欢迎界面...');
console.info(' 📍 Step: 验证页面显示欢迎界面...');
// Wait for the page to update
await this.page.waitForTimeout(500);
@@ -508,7 +508,7 @@ Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
const box = await elem.boundingBox();
if (box && box.width > 0 && box.height > 0) {
foundVisible = true;
console.log(` 📍 Found visible chat-input at index ${i}`);
console.info(` 📍 Found visible chat-input at index ${i}`);
break;
}
}
@@ -518,27 +518,27 @@ Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
// Fallback: just verify we're still on the chat page
const currentUrl = this.page.url();
expect(currentUrl).toContain('/chat');
console.log(' 📍 Fallback: verified we are on chat page');
console.info(' 📍 Fallback: verified we are on chat page');
}
console.log(' ✅ 欢迎界面已显示');
console.info(' ✅ 欢迎界面已显示');
});
Then('应该切换到该对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证已切换对话...');
console.info(' 📍 Step: 验证已切换对话...');
// The URL or active state should change
// For now, just verify the page is responsive
await this.page.waitForTimeout(500);
console.log(' ✅ 已切换到该对话');
console.info(' ✅ 已切换到该对话');
});
Then('显示该对话的历史消息', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示历史消息...');
console.info(' 📍 Step: 验证显示历史消息...');
// Wait for the loading to finish - the messages need time to load after switching topics
console.log(' 📍 等待消息加载...');
console.info(' 📍 等待消息加载...');
await this.page.waitForTimeout(2000);
// Wait for the message wrapper to appear (ChatItem component uses message-wrapper class)
@@ -546,23 +546,23 @@ Then('显示该对话的历史消息', async function (this: CustomWorld) {
try {
await this.page.waitForSelector(messageSelector, { timeout: 10_000 });
} catch {
console.log(' ⚠️ 等待消息选择器超时,尝试备用选择器...');
console.info(' ⚠️ 等待消息选择器超时,尝试备用选择器...');
}
// There should be messages in the chat area
const messages = this.page.locator(messageSelector);
const messageCount = await messages.count();
console.log(` 📍 找到 ${messageCount} 条消息`);
console.info(` 📍 找到 ${messageCount} 条消息`);
// At least some messages should be visible
expect(messageCount).toBeGreaterThan(0);
console.log(' ✅ 历史消息已显示');
console.info(' ✅ 历史消息已显示');
});
Then('对话名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
console.log(` 📍 Step: 验证对话名称为 "${expectedName}"...`);
console.info(` 📍 Step: 验证对话名称为 "${expectedName}"...`);
// Wait for the rename to take effect
await this.page.waitForTimeout(1000);
@@ -574,20 +574,20 @@ Then('对话名称应该更新为 {string}', async function (this: CustomWorld,
await expect(renamedTopic).toBeVisible({ timeout: 5000 });
console.log(` ✅ 对话名称已更新为 "${expectedName}"`);
console.info(` ✅ 对话名称已更新为 "${expectedName}"`);
});
Then('该对话应该被删除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证对话已删除...');
console.info(' 📍 Step: 验证对话已删除...');
// Wait for deletion to take effect
await this.page.waitForTimeout(500);
console.log(' ✅ 对话已删除');
console.info(' ✅ 对话已删除');
});
Then('对话列表中不再显示该对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证对话列表中不再显示该对话...');
console.info(' 📍 Step: 验证对话列表中不再显示该对话...');
// Wait for UI to update
await this.page.waitForTimeout(500);
@@ -599,14 +599,14 @@ Then('对话列表中不再显示该对话', async function (this: CustomWorld)
);
const count = await deletedTopic.count();
expect(count).toBe(0);
console.log(` ✅ 对话 "${this.testContext.deletedTopicTitle}" 已从列表中移除`);
console.info(` ✅ 对话 "${this.testContext.deletedTopicTitle}" 已从列表中移除`);
} else {
console.log(' ✅ 对话已从列表中移除');
console.info(' ✅ 对话已从列表中移除');
}
});
Then('应该显示包含 {string} 的对话', async function (this: CustomWorld, searchText: string) {
console.log(` 📍 Step: 验证搜索结果包含 "${searchText}"...`);
console.info(` 📍 Step: 验证搜索结果包含 "${searchText}"...`);
// Wait for search results to load (search opens a modal dialog)
await this.page.waitForTimeout(2000);
@@ -615,7 +615,7 @@ Then('应该显示包含 {string} 的对话', async function (this: CustomWorld,
// Look for the search modal and check for matching results
const searchModal = this.page.locator('.ant-modal, [role="dialog"]');
const hasModal = (await searchModal.count()) > 0;
console.log(` 📍 搜索模态框: ${hasModal}`);
console.info(` 📍 搜索模态框: ${hasModal}`);
// Find matching items in the search results (either in modal or in sidebar if filtered)
const matchingInModal = searchModal.getByText(searchText);
@@ -624,20 +624,20 @@ Then('应该显示包含 {string} 的对话', async function (this: CustomWorld,
const modalMatchCount = await matchingInModal.count();
const pageMatchCount = await matchingInPage.count();
console.log(` 📍 模态框中找到 ${modalMatchCount} 个匹配, 页面中找到 ${pageMatchCount} 个匹配`);
console.info(` 📍 模态框中找到 ${modalMatchCount} 个匹配, 页面中找到 ${pageMatchCount} 个匹配`);
// At least one match should be found (either in search input or results)
expect(modalMatchCount + pageMatchCount).toBeGreaterThan(0);
console.log(` ✅ 搜索结果显示包含 "${searchText}" 的对话`);
console.info(` ✅ 搜索结果显示包含 "${searchText}" 的对话`);
});
Then('不相关的对话应该被过滤', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证不相关对话已被过滤...');
console.info(' 📍 Step: 验证不相关对话已被过滤...');
// This would require checking that non-matching topics are hidden
// For now, just verify the search is active
await this.page.waitForTimeout(300);
console.log(' ✅ 不相关对话已被过滤');
console.info(' ✅ 不相关对话已被过滤');
});
+23 -23
View File
@@ -7,7 +7,7 @@ import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { llmMockManager, presetResponses } from '../../mocks/llm';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// ============================================
// Given Steps
@@ -21,29 +21,29 @@ Given('用户已登录系统', async function (this: CustomWorld) {
});
Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 设置 LLM mock...');
console.info(' 📍 Step: 设置 LLM mock...');
// Setup LLM mock before navigation
llmMockManager.setResponse('hello', presetResponses.greeting);
await llmMockManager.setup(this.page);
console.log(' 📍 Step: 导航到首页...');
console.info(' 📍 Step: 导航到首页...');
// Navigate to home page first
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: WAIT_TIMEOUT });
console.log(' 📍 Step: 查找 Lobe AI...');
console.info(' 📍 Step: 查找 Lobe AI...');
// Find and click on "Lobe AI" agent in the sidebar/home
const lobeAIAgent = this.page.locator('text=Lobe AI').first();
await expect(lobeAIAgent).toBeVisible({ timeout: WAIT_TIMEOUT });
console.log(' 📍 Step: 点击 Lobe AI...');
console.info(' 📍 Step: 点击 Lobe AI...');
await lobeAIAgent.click();
console.log(' 📍 Step: 等待聊天界面加载...');
console.info(' 📍 Step: 等待聊天界面加载...');
// Wait for the chat interface to be ready
await this.page.waitForLoadState('networkidle', { timeout: WAIT_TIMEOUT });
console.log(' 📍 Step: 查找输入框...');
console.info(' 📍 Step: 查找输入框...');
// The input is a rich text editor with contenteditable
// There are 2 ChatInput components (desktop & mobile), find the visible one
@@ -53,7 +53,7 @@ Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
// Find all chat-input elements and get the visible one
const chatInputs = this.page.locator('[data-testid="chat-input"]');
const count = await chatInputs.count();
console.log(` 📍 Found ${count} chat-input elements`);
console.info(` 📍 Found ${count} chat-input elements`);
// Find the first visible one or just use the first one
let chatInputContainer = chatInputs.first();
@@ -62,19 +62,19 @@ Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
const box = await elem.boundingBox();
if (box && box.width > 0 && box.height > 0) {
chatInputContainer = elem;
console.log(` ✓ Using chat-input element ${i} (has bounding box)`);
console.info(` ✓ Using chat-input element ${i} (has bounding box)`);
break;
}
}
// Click the container to focus the editor
await chatInputContainer.click();
console.log(' ✓ Clicked on chat input container');
console.info(' ✓ Clicked on chat input container');
// Wait for any animations to complete
await this.page.waitForTimeout(300);
console.log(' ✅ 已进入 Lobe AI 对话页面');
console.info(' ✅ 已进入 Lobe AI 对话页面');
});
// ============================================
@@ -86,7 +86,7 @@ Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
* This sends a message and waits for the AI response
*/
Given('用户已发送消息 {string}', async function (this: CustomWorld, message: string) {
console.log(` 📍 Step: 发送消息 "${message}" 并等待回复...`);
console.info(` 📍 Step: 发送消息 "${message}" 并等待回复...`);
// Find visible chat input container first
const chatInputs = this.page.locator('[data-testid="chat-input"]');
@@ -118,7 +118,7 @@ Given('用户已发送消息 {string}', async function (this: CustomWorld, messa
// Wait for the assistant response to appear
// Assistant messages are left-aligned .message-wrapper elements that contain "Lobe AI" title
console.log(' 📍 Step: 等待助手回复...');
console.info(' 📍 Step: 等待助手回复...');
// Wait for any new message wrapper to appear (there should be at least 2 - user + assistant)
const messageWrappers = this.page.locator('.message-wrapper');
@@ -126,7 +126,7 @@ Given('用户已发送消息 {string}', async function (this: CustomWorld, messa
.toHaveCount(2, { timeout: 15_000 })
.catch(() => {
// Fallback: just wait for at least one message wrapper
console.log(' 📍 Fallback: checking for any message wrapper');
console.info(' 📍 Fallback: checking for any message wrapper');
});
// Verify the assistant message contains expected content
@@ -136,16 +136,16 @@ Given('用户已发送消息 {string}', async function (this: CustomWorld, messa
await expect(assistantMessage).toBeVisible({ timeout: 5000 });
this.testContext.lastMessage = message;
console.log(` ✅ 消息已发送并收到回复`);
console.info(` ✅ 消息已发送并收到回复`);
});
When('用户发送消息 {string}', async function (this: CustomWorld, message: string) {
console.log(` 📍 Step: 查找输入框...`);
console.info(` 📍 Step: 查找输入框...`);
// Find visible chat input container first
const chatInputs = this.page.locator('[data-testid="chat-input"]');
const count = await chatInputs.count();
console.log(` 📍 Found ${count} chat-input containers`);
console.info(` 📍 Found ${count} chat-input containers`);
let chatInputContainer = chatInputs.first();
for (let i = 0; i < count; i++) {
@@ -153,28 +153,28 @@ When('用户发送消息 {string}', async function (this: CustomWorld, message:
const box = await elem.boundingBox();
if (box && box.width > 0 && box.height > 0) {
chatInputContainer = elem;
console.log(` 📍 Using container ${i}`);
console.info(` 📍 Using container ${i}`);
break;
}
}
// Click the container to ensure focus is on the input area
console.log(` 📍 Step: 点击输入区域...`);
console.info(` 📍 Step: 点击输入区域...`);
await chatInputContainer.click();
await this.page.waitForTimeout(500);
console.log(` 📍 Step: 输入消息 "${message}"...`);
console.info(` 📍 Step: 输入消息 "${message}"...`);
// Just type via keyboard - the input should be focused after clicking
await this.page.keyboard.type(message, { delay: 30 });
await this.page.waitForTimeout(300);
console.log(` 📍 Step: 发送消息 (按 Enter)...`);
console.info(` 📍 Step: 发送消息 (按 Enter)...`);
await this.page.keyboard.press('Enter');
// Wait for the message to be sent and processed
await this.page.waitForTimeout(1000);
console.log(` ✅ 消息已发送`);
console.info(` ✅ 消息已发送`);
this.testContext.lastMessage = message;
});
@@ -207,5 +207,5 @@ Then('回复内容应该可见', async function (this: CustomWorld) {
expect(text).toBeTruthy();
expect(text!.length).toBeGreaterThan(0);
console.log(` ✅ Assistant replied: "${text?.slice(0, 50)}..."`);
console.info(` ✅ Assistant replied: "${text?.slice(0, 50)}..."`);
});
+53 -53
View File
@@ -10,7 +10,7 @@
import { Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// When Steps
@@ -20,7 +20,7 @@ import { CustomWorld } from '../../support/world';
async function findAssistantMessage(page: CustomWorld['page']) {
const messageWrappers = page.locator('.message-wrapper');
const wrapperCount = await messageWrappers.count();
console.log(` 📍 Found ${wrapperCount} message wrappers`);
console.info(` 📍 Found ${wrapperCount} message wrappers`);
// Find the assistant message by looking for the one with "Lobe AI" or "AI" in title
for (let i = wrapperCount - 1; i >= 0; i--) {
@@ -31,7 +31,7 @@ async function findAssistantMessage(page: CustomWorld['page']) {
.catch(() => '');
if (titleText?.includes('Lobe AI') || titleText?.includes('AI')) {
console.log(` 📍 Found assistant message at index ${i}`);
console.info(` 📍 Found assistant message at index ${i}`);
return wrapper;
}
}
@@ -41,7 +41,7 @@ async function findAssistantMessage(page: CustomWorld['page']) {
}
When('用户点击消息的复制按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击复制按钮...');
console.info(' 📍 Step: 点击复制按钮...');
// Find the assistant message wrapper
const assistantMessage = await findAssistantMessage(this.page);
@@ -52,8 +52,8 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
// First try: find copy button directly by its icon (lucide-copy)
const copyButtonByIcon = this.page.locator('svg.lucide-copy').locator('..');
let copyButtonCount = await copyButtonByIcon.count();
console.log(` 📍 Found ${copyButtonCount} buttons with copy icon`);
const copyButtonCount = await copyButtonByIcon.count();
console.info(` 📍 Found ${copyButtonCount} buttons with copy icon`);
if (copyButtonCount > 0) {
// Click the visible copy button
@@ -62,7 +62,7 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
const box = await btn.boundingBox();
if (box && box.width > 0 && box.height > 0) {
await btn.click();
console.log(' ✅ 已点击复制按钮');
console.info(' ✅ 已点击复制按钮');
await this.page.waitForTimeout(500);
return;
}
@@ -70,7 +70,7 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
}
// Fallback: Look for action bar within message and open more menu
console.log(' 📍 Fallback: Looking for copy in more menu...');
console.info(' 📍 Fallback: Looking for copy in more menu...');
const actionBar = assistantMessage.locator('[role="menubar"]');
if ((await actionBar.count()) > 0) {
const moreButton = actionBar.locator('button').last();
@@ -80,7 +80,7 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
const copyMenuItem = this.page.getByRole('menuitem', { name: /复制/ });
if ((await copyMenuItem.count()) > 0) {
await copyMenuItem.click();
console.log(' ✅ 已从菜单中点击复制');
console.info(' ✅ 已从菜单中点击复制');
await this.page.waitForTimeout(500);
return;
}
@@ -94,14 +94,14 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
const copyMenuItem = this.page.getByRole('menuitem', { name: /复制/ });
await copyMenuItem.click();
console.log(' ✅ 已从更多菜单中点击复制');
console.info(' ✅ 已从更多菜单中点击复制');
}
await this.page.waitForTimeout(500);
});
When('用户点击助手消息的编辑按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击编辑按钮...');
console.info(' 📍 Step: 点击编辑按钮...');
// Find the assistant message wrapper
const assistantMessage = await findAssistantMessage(this.page);
@@ -112,8 +112,8 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
// First try: find edit button directly by its icon (lucide-pencil)
const editButtonByIcon = this.page.locator('svg.lucide-pencil').locator('..');
let editButtonCount = await editButtonByIcon.count();
console.log(` 📍 Found ${editButtonCount} buttons with pencil icon`);
const editButtonCount = await editButtonByIcon.count();
console.info(` 📍 Found ${editButtonCount} buttons with pencil icon`);
if (editButtonCount > 0) {
for (let i = 0; i < editButtonCount; i++) {
@@ -121,7 +121,7 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
const box = await btn.boundingBox();
if (box && box.width > 0 && box.height > 0) {
await btn.click();
console.log(' ✅ 已点击编辑按钮');
console.info(' ✅ 已点击编辑按钮');
await this.page.waitForTimeout(500);
return;
}
@@ -129,7 +129,7 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
}
// Fallback: Look for edit in more menu
console.log(' 📍 Fallback: Looking for edit in more menu...');
console.info(' 📍 Fallback: Looking for edit in more menu...');
const moreButtonByIcon = this.page.locator('svg.lucide-more-horizontal').locator('..');
if ((await moreButtonByIcon.count()) > 0) {
await moreButtonByIcon.first().click();
@@ -138,7 +138,7 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
const editMenuItem = this.page.getByRole('menuitem', { name: /编辑/ });
if ((await editMenuItem.count()) > 0) {
await editMenuItem.click();
console.log(' ✅ 已从菜单中点击编辑');
console.info(' ✅ 已从菜单中点击编辑');
}
}
@@ -146,7 +146,7 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
});
When('用户修改消息内容为 {string}', async function (this: CustomWorld, newContent: string) {
console.log(` 📍 Step: 修改消息内容为 "${newContent}"...`);
console.info(` 📍 Step: 修改消息内容为 "${newContent}"...`);
// Find the editing textarea or input
const editArea = this.page.locator('textarea, [contenteditable="true"]').last();
@@ -160,11 +160,11 @@ When('用户修改消息内容为 {string}', async function (this: CustomWorld,
// Store for later verification
this.testContext.editedContent = newContent;
console.log(` ✅ 已修改消息内容为 "${newContent}"`);
console.info(` ✅ 已修改消息内容为 "${newContent}"`);
});
When('用户保存编辑', async function (this: CustomWorld) {
console.log(' 📍 Step: 保存编辑...');
console.info(' 📍 Step: 保存编辑...');
// Find and click the save/confirm button
const saveButton = this.page.locator('button').filter({
@@ -178,12 +178,12 @@ When('用户保存编辑', async function (this: CustomWorld) {
await this.page.keyboard.press('Enter');
}
console.log(' ✅ 已保存编辑');
console.info(' ✅ 已保存编辑');
await this.page.waitForTimeout(500);
});
When('用户点击消息的更多操作按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击更多操作按钮...');
console.info(' 📍 Step: 点击更多操作按钮...');
// Find the assistant message wrapper
const assistantMessage = await findAssistantMessage(this.page);
@@ -194,15 +194,15 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
// Get the bounding box of the message to help filter buttons
const messageBox = await assistantMessage.boundingBox();
console.log(` 📍 Message bounding box: y=${messageBox?.y}, height=${messageBox?.height}`);
console.info(` 📍 Message bounding box: y=${messageBox?.y}, height=${messageBox?.height}`);
// Look for the "more" button by ellipsis icon (lucide-ellipsis or lucide-more-horizontal)
// The icon might be `...` which is lucide-ellipsis
const ellipsisButtons = this.page
.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal')
.locator('..');
let ellipsisCount = await ellipsisButtons.count();
console.log(` 📍 Found ${ellipsisCount} buttons with ellipsis/more icon`);
const ellipsisCount = await ellipsisButtons.count();
console.info(` 📍 Found ${ellipsisCount} buttons with ellipsis/more icon`);
if (ellipsisCount > 0 && messageBox) {
// Find buttons in the message area (x > 320 to exclude sidebar)
@@ -210,7 +210,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
const btn = ellipsisButtons.nth(i);
const box = await btn.boundingBox();
if (box && box.width > 0 && box.height > 0) {
console.log(` 📍 Ellipsis button ${i}: x=${box.x}, y=${box.y}`);
console.info(` 📍 Ellipsis button ${i}: x=${box.x}, y=${box.y}`);
// Check if button is within the message area
if (
box.x > 320 &&
@@ -218,7 +218,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
box.y <= messageBox.y + messageBox.height + 50
) {
await btn.click();
console.log(` ✅ 已点击更多操作按钮 (ellipsis at x=${box.x}, y=${box.y})`);
console.info(` ✅ 已点击更多操作按钮 (ellipsis at x=${box.x}, y=${box.y})`);
await this.page.waitForTimeout(300);
return;
}
@@ -229,18 +229,18 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
// Second approach: Find the action bar and click its last button
const actionBar = assistantMessage.locator('[role="menubar"]');
const actionBarCount = await actionBar.count();
console.log(` 📍 Found ${actionBarCount} action bars in message`);
console.info(` 📍 Found ${actionBarCount} action bars in message`);
if (actionBarCount > 0) {
// Find all clickable elements (button, span with onClick, etc.)
const clickables = actionBar.locator('button, span[role="button"], [class*="action"]');
const clickableCount = await clickables.count();
console.log(` 📍 Found ${clickableCount} clickable elements in action bar`);
console.info(` 📍 Found ${clickableCount} clickable elements in action bar`);
if (clickableCount > 0) {
// Click the last one (usually "more")
await clickables.last().click();
console.log(' ✅ 已点击更多操作按钮 (last clickable)');
console.info(' ✅ 已点击更多操作按钮 (last clickable)');
await this.page.waitForTimeout(300);
return;
}
@@ -249,7 +249,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
// Third approach: Find buttons by looking for all SVG icons in the message area
const allSvgButtons = this.page.locator('.message-wrapper svg').locator('..');
const svgButtonCount = await allSvgButtons.count();
console.log(` 📍 Found ${svgButtonCount} SVG button parents in message wrappers`);
console.info(` 📍 Found ${svgButtonCount} SVG button parents in message wrappers`);
if (svgButtonCount > 0 && messageBox) {
// Find the rightmost button in the action area (more button is usually last)
@@ -276,7 +276,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
if (rightmostBtn) {
await rightmostBtn.click();
console.log(` ✅ 已点击更多操作按钮 (rightmost at x=${maxX})`);
console.info(` ✅ 已点击更多操作按钮 (rightmost at x=${maxX})`);
await this.page.waitForTimeout(300);
return;
}
@@ -286,7 +286,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
});
When('用户选择删除消息选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择删除消息选项...');
console.info(' 📍 Step: 选择删除消息选项...');
// Find and click delete option (exact match to avoid "Delete and Regenerate")
// Support both English and Chinese
@@ -294,48 +294,48 @@ When('用户选择删除消息选项', async function (this: CustomWorld) {
await expect(deleteOption).toBeVisible({ timeout: 5000 });
await deleteOption.click();
console.log(' ✅ 已选择删除消息选项');
console.info(' ✅ 已选择删除消息选项');
await this.page.waitForTimeout(300);
});
When('用户确认删除消息', async function (this: CustomWorld) {
console.log(' 📍 Step: 确认删除消息...');
console.info(' 📍 Step: 确认删除消息...');
// A confirmation popconfirm might appear
const confirmButton = this.page.locator('.ant-popconfirm-buttons button.ant-btn-dangerous');
if ((await confirmButton.count()) > 0) {
await confirmButton.click();
console.log(' ✅ 已确认删除消息');
console.info(' ✅ 已确认删除消息');
} else {
// If no popconfirm, deletion might be immediate
console.log(' ✅ 删除操作已执行(无需确认)');
console.info(' ✅ 删除操作已执行(无需确认)');
}
await this.page.waitForTimeout(500);
});
When('用户选择折叠消息选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择折叠消息选项...');
console.info(' 📍 Step: 选择折叠消息选项...');
// The collapse option is "Collapse Message" or "收起消息" in the menu
const collapseOption = this.page.getByRole('menuitem', { name: /Collapse Message|收起消息/ });
await expect(collapseOption).toBeVisible({ timeout: 5000 });
await collapseOption.click();
console.log(' ✅ 已选择折叠消息选项');
console.info(' ✅ 已选择折叠消息选项');
await this.page.waitForTimeout(500);
});
When('用户选择展开消息选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择展开消息选项...');
console.info(' 📍 Step: 选择展开消息选项...');
// The expand option is "Expand Message" or "展开消息" in the menu
const expandOption = this.page.getByRole('menuitem', { name: /Expand Message|展开消息/ });
await expect(expandOption).toBeVisible({ timeout: 5000 });
await expandOption.click();
console.log(' ✅ 已选择展开消息选项');
console.info(' ✅ 已选择展开消息选项');
await this.page.waitForTimeout(500);
});
@@ -344,7 +344,7 @@ When('用户选择展开消息选项', async function (this: CustomWorld) {
// ============================================
Then('消息内容应该被复制到剪贴板', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证消息已复制到剪贴板...');
console.info(' 📍 Step: 验证消息已复制到剪贴板...');
// Check for success message/toast
const successMessage = this.page.locator('.ant-message-success, [class*="toast"]');
@@ -355,15 +355,15 @@ Then('消息内容应该被复制到剪贴板', async function (this: CustomWorl
// Verify by checking if clipboard has content (or success message appeared)
const successCount = await successMessage.count();
if (successCount > 0) {
console.log(' ✅ 显示复制成功提示');
console.info(' ✅ 显示复制成功提示');
} else {
// Just verify the action completed without error
console.log(' ✅ 复制操作已完成');
console.info(' ✅ 复制操作已完成');
}
});
Then('消息内容应该更新为 {string}', async function (this: CustomWorld, expectedContent: string) {
console.log(` 📍 Step: 验证消息内容为 "${expectedContent}"...`);
console.info(` 📍 Step: 验证消息内容为 "${expectedContent}"...`);
await this.page.waitForTimeout(1000);
@@ -371,11 +371,11 @@ Then('消息内容应该更新为 {string}', async function (this: CustomWorld,
const messageContent = this.page.getByText(expectedContent);
await expect(messageContent).toBeVisible({ timeout: 5000 });
console.log(` ✅ 消息内容已更新为 "${expectedContent}"`);
console.info(` ✅ 消息内容已更新为 "${expectedContent}"`);
});
Then('该消息应该从对话中移除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证消息已移除...');
console.info(' 📍 Step: 验证消息已移除...');
await this.page.waitForTimeout(500);
@@ -384,12 +384,12 @@ Then('该消息应该从对话中移除', async function (this: CustomWorld) {
const assistantMessages = this.page.locator('[data-role="assistant"]');
const count = await assistantMessages.count();
console.log(` 📍 剩余助手消息数量: ${count}`);
console.log(' ✅ 消息已移除');
console.info(` 📍 剩余助手消息数量: ${count}`);
console.info(' ✅ 消息已移除');
});
Then('消息内容应该被折叠', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证消息已折叠...');
console.info(' 📍 Step: 验证消息已折叠...');
await this.page.waitForTimeout(500);
@@ -400,15 +400,15 @@ Then('消息内容应该被折叠', async function (this: CustomWorld) {
const hasCollapsed = (await collapsedIndicator.count()) > 0;
if (hasCollapsed) {
console.log(' ✅ 消息已折叠');
console.info(' ✅ 消息已折叠');
} else {
// Alternative verification: content height should be reduced
console.log(' ✅ 消息折叠操作已执行');
console.info(' ✅ 消息折叠操作已执行');
}
});
Then('消息内容应该完整显示', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证消息完整显示...');
console.info(' 📍 Step: 验证消息完整显示...');
await this.page.waitForTimeout(500);
@@ -416,5 +416,5 @@ Then('消息内容应该完整显示', async function (this: CustomWorld) {
const assistantMessage = await findAssistantMessage(this.page);
await expect(assistantMessage).toBeVisible();
console.log(' ✅ 消息内容完整显示');
console.info(' ✅ 消息内容完整显示');
});
+6 -6
View File
@@ -1,8 +1,8 @@
import { Given, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { TEST_USER, createTestSession } from '../../support/seedTestUser';
import { CustomWorld } from '../../support/world';
import { createTestSession, TEST_USER } from '../../support/seedTestUser';
import { type CustomWorld } from '../../support/world';
/**
* Login via UI - fills in the login form and submits
@@ -26,7 +26,7 @@ Given('I am logged in as the test user', async function (this: CustomWorld) {
// Wait for navigation away from signin page
await this.page.waitForURL((url) => !url.pathname.includes('/signin'), { timeout: 30_000 });
console.log('✅ Logged in as test user via UI');
console.info('✅ Logged in as test user via UI');
});
/**
@@ -53,7 +53,7 @@ Given('I am logged in with a session', async function (this: CustomWorld) {
},
]);
console.log('✅ Session cookie set for test user');
console.info('✅ Session cookie set for test user');
});
/**
@@ -87,7 +87,7 @@ Given('I should be logged in', async function (this: CustomWorld) {
await expect(this.page).not.toHaveURL(/\/signin/);
// Optionally check for user menu or other logged-in indicators
console.log('✅ User is logged in');
console.info('✅ User is logged in');
});
/**
@@ -96,5 +96,5 @@ Given('I should be logged in', async function (this: CustomWorld) {
When('I logout', async function (this: CustomWorld) {
// Clear cookies to logout
await this.browserContext.clearCookies();
console.log('✅ User logged out (cookies cleared)');
console.info('✅ User logged out (cookies cleared)');
});
+11 -11
View File
@@ -1,7 +1,7 @@
import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// Given Steps (Preconditions)
@@ -23,7 +23,7 @@ When('I click the back button', async function (this: CustomWorld) {
// Store current URL to verify navigation
const currentUrl = this.page.url();
console.log(` 📍 Current URL before back: ${currentUrl}`);
console.info(` 📍 Current URL before back: ${currentUrl}`);
// Try to find a back button - look for arrow icon or back text
// The UI has a back arrow (←) next to the search bar
@@ -34,7 +34,7 @@ When('I click the back button', async function (this: CustomWorld) {
.first();
const backButtonVisible = await backButton.isVisible().catch(() => false);
console.log(` 📍 Back button visible: ${backButtonVisible}`);
console.info(` 📍 Back button visible: ${backButtonVisible}`);
if (backButtonVisible) {
// Click the parent element if it's an SVG icon
@@ -44,10 +44,10 @@ When('I click the back button', async function (this: CustomWorld) {
} else {
await backButton.click();
}
console.log(' 📍 Clicked back button');
console.info(' 📍 Clicked back button');
} else {
// Use browser back as fallback
console.log(' 📍 Using browser goBack()');
console.info(' 📍 Using browser goBack()');
await this.page.goBack();
}
@@ -55,7 +55,7 @@ When('I click the back button', async function (this: CustomWorld) {
await this.page.waitForTimeout(500);
const newUrl = this.page.url();
console.log(` 📍 URL after back: ${newUrl}`);
console.info(` 📍 URL after back: ${newUrl}`);
});
// ============================================
@@ -142,7 +142,7 @@ Then('I should be on the assistant list page', async function (this: CustomWorld
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
console.log(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
console.info(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
expect(isListPage, `Expected URL to be assistant list page, but got: ${currentUrl}`).toBeTruthy();
});
@@ -183,7 +183,7 @@ Then('I should see the model description', async function (this: CustomWorld) {
// Pass if any content area is visible - the description might be a placeholder
expect(isVisible || true).toBeTruthy();
console.log(' 📍 Model description area checked');
console.info(' 📍 Model description area checked');
});
Then('I should see the model parameters information', async function (this: CustomWorld) {
@@ -210,7 +210,7 @@ Then('I should be on the model list page', async function (this: CustomWorld) {
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
console.log(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
console.info(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
expect(isListPage, `Expected URL to be model list page, but got: ${currentUrl}`).toBeTruthy();
});
@@ -274,7 +274,7 @@ Then('I should be on the provider list page', async function (this: CustomWorld)
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
console.log(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
console.info(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
expect(isListPage, `Expected URL to be provider list page, but got: ${currentUrl}`).toBeTruthy();
});
@@ -336,6 +336,6 @@ Then('I should be on the MCP list page', async function (this: CustomWorld) {
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
console.log(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
console.info(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
expect(isListPage, `Expected URL to be MCP list page, but got: ${currentUrl}`).toBeTruthy();
});
+21 -21
View File
@@ -1,7 +1,7 @@
import { Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// When Steps (Actions)
@@ -35,7 +35,7 @@ When('I click on a category in the category menu', async function (this: CustomW
);
const count = await categoryItems.count();
console.log(` 📍 Found ${count} category items`);
console.info(` 📍 Found ${count} category items`);
if (count === 0) {
// Fallback: try finding by text content that looks like a category
@@ -43,7 +43,7 @@ When('I click on a category in the category menu', async function (this: CustomW
'text=/^(Academic|Career|Design|Programming|General)/',
);
const fallbackCount = await fallbackCategories.count();
console.log(` 📍 Fallback: Found ${fallbackCount} category items by text`);
console.info(` 📍 Fallback: Found ${fallbackCount} category items by text`);
if (fallbackCount > 0) {
await fallbackCategories.first().click();
@@ -75,7 +75,7 @@ When('I click on a category in the category filter', async function (this: Custo
);
const count = await categoryItems.count();
console.log(` 📍 Found ${count} category filter items`);
console.info(` 📍 Found ${count} category filter items`);
if (count === 0) {
// Fallback: try finding by text content that looks like MCP categories
@@ -83,7 +83,7 @@ When('I click on a category in the category filter', async function (this: Custo
'text=/^(Developer Tools|Productivity Tools|Utility Tools|Media Generation|Business Services)/',
);
const fallbackCount = await fallbackCategories.count();
console.log(` 📍 Fallback: Found ${fallbackCount} MCP category items by text`);
console.info(` 📍 Fallback: Found ${fallbackCount} MCP category items by text`);
if (fallbackCount > 0) {
await fallbackCategories.first().click();
@@ -120,11 +120,11 @@ When('I click the next page button', async function (this: CustomWorld) {
await assistantCards.first().waitFor({ state: 'visible', timeout: 30_000 });
const initialCount = await assistantCards.count();
console.log(` 📍 Initial card count: ${initialCount}`);
console.info(` 📍 Initial card count: ${initialCount}`);
// The page uses infinite scroll instead of pagination buttons
// Scroll to bottom to trigger infinite scroll
console.log(' 📍 Page uses infinite scroll, scrolling to bottom');
console.info(' 📍 Page uses infinite scroll, scrolling to bottom');
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await this.page.waitForTimeout(2000); // Wait for new content to load
@@ -280,7 +280,7 @@ When(
const mcpLinkVisible = await mcpLink.isVisible().catch(() => false);
if (mcpLinkVisible) {
console.log(' 📍 Found direct MCP link');
console.info(' 📍 Found direct MCP link');
await mcpLink.click();
return;
}
@@ -303,7 +303,7 @@ When(
}
// Fallback: click on MCP in the sidebar navigation
console.log(' 📍 Fallback: clicking MCP in sidebar');
console.info(' 📍 Fallback: clicking MCP in sidebar');
const mcpNavItem = this.page
.locator('nav a:has-text("MCP"), [class*="nav"] a:has-text("MCP")')
.first();
@@ -313,7 +313,7 @@ When(
}
// Last resort: navigate directly
console.log(' 📍 Last resort: direct navigation to /community/mcp');
console.info(' 📍 Last resort: direct navigation to /community/mcp');
await this.page.goto('/community/mcp');
},
);
@@ -372,8 +372,8 @@ Then(
Then('the URL should contain the category parameter', async function (this: CustomWorld) {
const currentUrl = this.page.url();
console.log(` 📍 Current URL: ${currentUrl}`);
console.log(` 📍 Selected category: ${this.testContext.selectedCategory}`);
console.info(` 📍 Current URL: ${currentUrl}`);
console.info(` 📍 Selected category: ${this.testContext.selectedCategory}`);
// Check if URL contains a category-related parameter
// The URL format is: /community/agent?category=xxx
@@ -398,11 +398,11 @@ Then('I should see different assistant cards', async function (this: CustomWorld
await expect(assistantItems.first()).toBeVisible({ timeout: 30_000 });
const currentCount = await assistantItems.count();
console.log(` 📍 Current card count: ${currentCount}`);
console.info(` 📍 Current card count: ${currentCount}`);
// If we used infinite scroll, check that we have cards (might be same or more)
if (this.testContext.usedInfiniteScroll) {
console.log(
console.info(
` 📍 Used infinite scroll, initial count was: ${this.testContext.initialCardCount}`,
);
expect(currentCount).toBeGreaterThan(0);
@@ -416,7 +416,7 @@ Then('the URL should contain the page parameter', async function (this: CustomWo
// If we used infinite scroll, URL won't have page parameter - that's expected
if (this.testContext.usedInfiniteScroll) {
console.log(' 📍 Used infinite scroll, page parameter not expected');
console.info(' 📍 Used infinite scroll, page parameter not expected');
// Just verify we're still on the assistant page
expect(currentUrl.includes('/community/agent')).toBeTruthy();
return;
@@ -488,11 +488,11 @@ Then('I should see the model detail content', async function (this: CustomWorld)
'text=/Overview|Model Parameters|Related Recommendations|Configuration Guide/',
);
console.log(' 📍 Waiting for model detail content to load...');
console.info(' 📍 Waiting for model detail content to load...');
await expect(modelTabs.first()).toBeVisible({ timeout: 30_000 });
const tabCount = await modelTabs.count();
console.log(` 📍 Found ${tabCount} model detail tabs`);
console.info(` 📍 Found ${tabCount} model detail tabs`);
expect(tabCount).toBeGreaterThan(0);
});
@@ -519,11 +519,11 @@ Then('I should see the provider detail content', async function (this: CustomWor
// Wait for the provider title to appear
const providerTitle = this.page.locator('h1, h2, [class*="title"]').first();
console.log(' 📍 Waiting for provider detail content to load...');
console.info(' 📍 Waiting for provider detail content to load...');
await expect(providerTitle).toBeVisible({ timeout: 30_000 });
const titleText = await providerTitle.textContent();
console.log(` 📍 Provider title: ${titleText}`);
console.info(` 📍 Provider title: ${titleText}`);
expect(titleText?.trim().length).toBeGreaterThan(0);
});
@@ -571,13 +571,13 @@ Then('I should be navigated to {string}', async function (this: CustomWorld, exp
await this.page.waitForTimeout(500); // Extra wait for client-side routing
const currentUrl = this.page.url();
console.log(` 📍 Expected path: ${expectedPath}, Current URL: ${currentUrl}`);
console.info(` 📍 Expected path: ${expectedPath}, Current URL: ${currentUrl}`);
// Verify that URL contains the expected path
const urlMatches = currentUrl.includes(expectedPath);
if (!urlMatches) {
console.log(` ⚠️ URL mismatch, but page might still be correct`);
console.info(` ⚠️ URL mismatch, but page might still be correct`);
}
expect(
+45 -45
View File
@@ -10,7 +10,7 @@ import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { TEST_USER } from '../../support/seedTestUser';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// ============================================
// Helper Functions
@@ -88,7 +88,7 @@ async function inputNewName(
}
await this.page.waitForTimeout(1000);
console.log(` ✅ 已输入新名称 "${newName}"`);
console.info(` ✅ 已输入新名称 "${newName}"`);
}
/**
@@ -115,7 +115,7 @@ async function createTestAgent(title: string = 'Test Agent'): Promise<string> {
[agentId, slug, title, TEST_USER.id, now],
);
console.log(` 📍 Created test agent in DB: ${agentId}`);
console.info(` 📍 Created test agent in DB: ${agentId}`);
return agentId;
} finally {
await client.end();
@@ -127,16 +127,16 @@ async function createTestAgent(title: string = 'Test Agent'): Promise<string> {
// ============================================
Given('用户在 Home 页面有一个 Agent', async function (this: CustomWorld) {
console.log(' 📍 Step: 在数据库中创建测试 Agent...');
console.info(' 📍 Step: 在数据库中创建测试 Agent...');
const agentId = await createTestAgent('E2E Test Agent');
this.testContext.createdAgentId = agentId;
console.log(' 📍 Step: 导航到 Home 页面...');
console.info(' 📍 Step: 导航到 Home 页面...');
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 查找新创建的 Agent...');
console.info(' 📍 Step: 查找新创建的 Agent...');
// Look for the newly created agent in the sidebar by its specific ID
const agentItem = this.page.locator(`a[href="/agent/${agentId}"]`).first();
await expect(agentItem).toBeVisible({ timeout: WAIT_TIMEOUT });
@@ -147,18 +147,18 @@ Given('用户在 Home 页面有一个 Agent', async function (this: CustomWorld)
this.testContext.targetItemSelector = `a[href="/agent/${agentId}"]`;
this.testContext.targetType = 'agent';
console.log(` ✅ 找到 Agent: ${agentLabel}, id: ${agentId}`);
console.info(` ✅ 找到 Agent: ${agentLabel}, id: ${agentId}`);
});
Given('该 Agent 未被置顶', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 检查 Agent 未被置顶...');
console.info(' 📍 Step: 检查 Agent 未被置顶...');
// Check if the agent has a pin icon - if so, unpin it first
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
// Pin icon uses lucide-react which adds class "lucide lucide-pin"
const pinIcon = targetItem.locator('svg[class*="lucide-pin"]');
if ((await pinIcon.count()) > 0) {
console.log(' 📍 Agent 已置顶,开始取消置顶操作...');
console.info(' 📍 Agent 已置顶,开始取消置顶操作...');
// Unpin it first
await targetItem.hover();
await this.page.waitForTimeout(200);
@@ -166,7 +166,7 @@ Given('该 Agent 未被置顶', { timeout: 30_000 }, async function (this: Custo
await this.page.waitForTimeout(500);
const unpinOption = this.page.getByRole('menuitem', { name: /取消置顶|unpin/i });
await unpinOption.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {
console.log(' ⚠️ 取消置顶选项未找到');
console.info(' ⚠️ 取消置顶选项未找到');
});
if ((await unpinOption.count()) > 0) {
await unpinOption.click();
@@ -177,18 +177,18 @@ Given('该 Agent 未被置顶', { timeout: 30_000 }, async function (this: Custo
await this.page.waitForTimeout(300);
}
console.log(' ✅ Agent 未被置顶');
console.info(' ✅ Agent 未被置顶');
});
Given('该 Agent 已被置顶', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 确保 Agent 已被置顶...');
console.info(' 📍 Step: 确保 Agent 已被置顶...');
// Check if the agent has a pin icon - if not, pin it first
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
// Pin icon uses lucide-react which adds class "lucide lucide-pin"
const pinIcon = targetItem.locator('svg[class*="lucide-pin"]');
if ((await pinIcon.count()) === 0) {
console.log(' 📍 Agent 未置顶,开始置顶操作...');
console.info(' 📍 Agent 未置顶,开始置顶操作...');
// Pin it first - right-click on the NavItem Block inside the Link
// The ContextMenuTrigger is attached to the Block component inside the Link
await targetItem.hover();
@@ -198,16 +198,16 @@ Given('该 Agent 已被置顶', { timeout: 30_000 }, async function (this: Custo
// Debug: check menu visibility
const menuItems = await this.page.locator('[role="menuitem"]').count();
console.log(` 📍 Debug: 发现 ${menuItems} 个菜单项`);
console.info(` 📍 Debug: 发现 ${menuItems} 个菜单项`);
const pinOption = this.page.getByRole('menuitem', { name: /置顶|pin/i });
await pinOption.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {
console.log(' ⚠️ 置顶选项未找到');
console.info(' ⚠️ 置顶选项未找到');
});
if ((await pinOption.count()) > 0) {
await pinOption.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击置顶选项');
console.info(' ✅ 已点击置顶选项');
}
// Close menu if still open
await this.page.keyboard.press('Escape');
@@ -218,7 +218,7 @@ Given('该 Agent 已被置顶', { timeout: 30_000 }, async function (this: Custo
await this.page.waitForTimeout(500);
const pinIconAfter = targetItem.locator('svg[class*="lucide-pin"]');
const isPinned = (await pinIconAfter.count()) > 0;
console.log(` ✅ Agent 已被置顶: ${isPinned}`);
console.info(` ✅ Agent 已被置顶: ${isPinned}`);
});
// ============================================
@@ -226,7 +226,7 @@ Given('该 Agent 已被置顶', { timeout: 30_000 }, async function (this: Custo
// ============================================
When('用户右键点击该 Agent', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击 Agent...');
console.info(' 📍 Step: 右键点击 Agent...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
@@ -241,28 +241,28 @@ When('用户右键点击该 Agent', { timeout: 30_000 }, async function (this: C
// Wait for context menu to appear
const menuItem = this.page.locator('[role="menuitem"]').first();
await menuItem.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {
console.log(' ⚠️ 菜单未出现,重试右键点击...');
console.info(' ⚠️ 菜单未出现,重试右键点击...');
});
// Debug: check what menus are visible
const menuItems = await this.page.locator('[role="menuitem"]').count();
console.log(` 📍 Debug: Found ${menuItems} menu items after right-click`);
console.info(` 📍 Debug: Found ${menuItems} menu items after right-click`);
console.log(' ✅ 已右键点击 Agent');
console.info(' ✅ 已右键点击 Agent');
});
When('用户悬停在该 Agent 上', async function (this: CustomWorld) {
console.log(' 📍 Step: 悬停在 Agent 上...');
console.info(' 📍 Step: 悬停在 Agent 上...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
await targetItem.hover();
await this.page.waitForTimeout(500);
console.log(' ✅ 已悬停在 Agent 上');
console.info(' ✅ 已悬停在 Agent 上');
});
When('用户点击更多操作按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击更多操作按钮...');
console.info(' 📍 Step: 点击更多操作按钮...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const moreButton = targetItem.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal').first();
@@ -282,71 +282,71 @@ When('用户点击更多操作按钮', async function (this: CustomWorld) {
}
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击更多操作按钮');
console.info(' ✅ 已点击更多操作按钮');
});
When('用户在菜单中选择重命名', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择重命名选项...');
console.info(' 📍 Step: 选择重命名选项...');
const renameOption = this.page.getByRole('menuitem', { name: /^(rename|重命名)$/i });
await expect(renameOption).toBeVisible({ timeout: 5000 });
await renameOption.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已选择重命名选项');
console.info(' ✅ 已选择重命名选项');
});
When('用户在菜单中选择置顶', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择置顶选项...');
console.info(' 📍 Step: 选择置顶选项...');
const pinOption = this.page.getByRole('menuitem', { name: /^(pin|置顶)$/i });
await expect(pinOption).toBeVisible({ timeout: 5000 });
await pinOption.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已选择置顶选项');
console.info(' ✅ 已选择置顶选项');
});
When('用户在菜单中选择取消置顶', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择取消置顶选项...');
console.info(' 📍 Step: 选择取消置顶选项...');
const unpinOption = this.page.getByRole('menuitem', { name: /^(unpin|取消置顶)$/i });
await expect(unpinOption).toBeVisible({ timeout: 5000 });
await unpinOption.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已选择取消置顶选项');
console.info(' ✅ 已选择取消置顶选项');
});
When('用户在菜单中选择删除', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择删除选项...');
console.info(' 📍 Step: 选择删除选项...');
const deleteOption = this.page.getByRole('menuitem', { name: /^(delete|删除)$/i });
await expect(deleteOption).toBeVisible({ timeout: 5000 });
await deleteOption.click();
await this.page.waitForTimeout(300);
console.log(' ✅ 已选择删除选项');
console.info(' ✅ 已选择删除选项');
});
When('用户在弹窗中确认删除', async function (this: CustomWorld) {
console.log(' 📍 Step: 确认删除...');
console.info(' 📍 Step: 确认删除...');
const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous');
await expect(confirmButton).toBeVisible({ timeout: 5000 });
await confirmButton.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已确认删除');
console.info(' ✅ 已确认删除');
});
When('用户输入新的名称 {string}', async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}"...`);
console.info(` 📍 Step: 输入新名称 "${newName}"...`);
await inputNewName.call(this, newName, false);
});
When('用户输入新的名称 {string} 并按 Enter', async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
console.info(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
await inputNewName.call(this, newName, true);
});
@@ -355,17 +355,17 @@ When('用户输入新的名称 {string} 并按 Enter', async function (this: Cus
// ============================================
Then('该项名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
console.log(` 📍 Step: 验证名称为 "${expectedName}"...`);
console.info(` 📍 Step: 验证名称为 "${expectedName}"...`);
await this.page.waitForTimeout(1000);
const renamedItem = this.page.getByText(expectedName, { exact: true }).first();
await expect(renamedItem).toBeVisible({ timeout: 5000 });
console.log(` ✅ 名称已更新为 "${expectedName}"`);
console.info(` ✅ 名称已更新为 "${expectedName}"`);
});
Then('Agent 应该显示置顶图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示置顶图标...');
console.info(' 📍 Step: 验证显示置顶图标...');
await this.page.waitForTimeout(500);
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
@@ -373,11 +373,11 @@ Then('Agent 应该显示置顶图标', async function (this: CustomWorld) {
const pinIcon = targetItem.locator('svg[class*="lucide-pin"]');
await expect(pinIcon).toBeVisible({ timeout: 5000 });
console.log(' ✅ 置顶图标已显示');
console.info(' ✅ 置顶图标已显示');
});
Then('Agent 不应该显示置顶图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证不显示置顶图标...');
console.info(' 📍 Step: 验证不显示置顶图标...');
await this.page.waitForTimeout(500);
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
@@ -385,11 +385,11 @@ Then('Agent 不应该显示置顶图标', async function (this: CustomWorld) {
const pinIcon = targetItem.locator('svg[class*="lucide-pin"]');
await expect(pinIcon).not.toBeVisible({ timeout: 5000 });
console.log(' ✅ 置顶图标未显示');
console.info(' ✅ 置顶图标未显示');
});
Then('Agent 应该从列表中移除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Agent 已移除...');
console.info(' 📍 Step: 验证 Agent 已移除...');
await this.page.waitForTimeout(500);
@@ -400,5 +400,5 @@ Then('Agent 应该从列表中移除', async function (this: CustomWorld) {
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
}
console.log(' ✅ Agent 已从列表中移除');
console.info(' ✅ Agent 已从列表中移除');
});
+20 -20
View File
@@ -10,7 +10,7 @@ import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { TEST_USER } from '../../support/seedTestUser';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
/**
* Create a test chat group directly in database
@@ -35,7 +35,7 @@ async function createTestGroup(title: string = 'Test Group'): Promise<string> {
[groupId, title, TEST_USER.id, now],
);
console.log(` 📍 Created test group in DB: ${groupId}`);
console.info(` 📍 Created test group in DB: ${groupId}`);
return groupId;
} finally {
await client.end();
@@ -47,16 +47,16 @@ async function createTestGroup(title: string = 'Test Group'): Promise<string> {
// ============================================
Given('用户在 Home 页面有一个 Agent Group', async function (this: CustomWorld) {
console.log(' 📍 Step: 在数据库中创建测试 Agent Group...');
console.info(' 📍 Step: 在数据库中创建测试 Agent Group...');
const groupId = await createTestGroup('E2E Test Group');
this.testContext.createdGroupId = groupId;
console.log(' 📍 Step: 导航到 Home 页面...');
console.info(' 📍 Step: 导航到 Home 页面...');
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 查找新创建的 Agent Group...');
console.info(' 📍 Step: 查找新创建的 Agent Group...');
const groupItem = this.page.locator(`a[href="/group/${groupId}"]`).first();
await expect(groupItem).toBeVisible({ timeout: WAIT_TIMEOUT });
@@ -65,11 +65,11 @@ Given('用户在 Home 页面有一个 Agent Group', async function (this: Custom
this.testContext.targetItemSelector = `a[href="/group/${groupId}"]`;
this.testContext.targetType = 'group';
console.log(` ✅ 找到 Agent Group: ${groupLabel}, id: ${groupId}`);
console.info(` ✅ 找到 Agent Group: ${groupLabel}, id: ${groupId}`);
});
Given('该 Agent Group 未被置顶', async function (this: CustomWorld) {
console.log(' 📍 Step: 检查 Agent Group 未被置顶...');
console.info(' 📍 Step: 检查 Agent Group 未被置顶...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const pinIcon = targetItem.locator('svg.lucide-pin');
@@ -84,11 +84,11 @@ Given('该 Agent Group 未被置顶', async function (this: CustomWorld) {
await this.page.click('body', { position: { x: 10, y: 10 } });
}
console.log(' ✅ Agent Group 未被置顶');
console.info(' ✅ Agent Group 未被置顶');
});
Given('该 Agent Group 已被置顶', async function (this: CustomWorld) {
console.log(' 📍 Step: 确保 Agent Group 已被置顶...');
console.info(' 📍 Step: 确保 Agent Group 已被置顶...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const pinIcon = targetItem.locator('svg.lucide-pin');
@@ -103,7 +103,7 @@ Given('该 Agent Group 已被置顶', async function (this: CustomWorld) {
await this.page.click('body', { position: { x: 10, y: 10 } });
}
console.log(' ✅ Agent Group 已被置顶');
console.info(' ✅ Agent Group 已被置顶');
});
// ============================================
@@ -111,23 +111,23 @@ Given('该 Agent Group 已被置顶', async function (this: CustomWorld) {
// ============================================
When('用户右键点击该 Agent Group', async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击 Agent Group...');
console.info(' 📍 Step: 右键点击 Agent Group...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
await targetItem.click({ button: 'right' });
await this.page.waitForTimeout(500);
console.log(' ✅ 已右键点击 Agent Group');
console.info(' ✅ 已右键点击 Agent Group');
});
When('用户悬停在该 Agent Group 上', async function (this: CustomWorld) {
console.log(' 📍 Step: 悬停在 Agent Group 上...');
console.info(' 📍 Step: 悬停在 Agent Group 上...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
await targetItem.hover();
await this.page.waitForTimeout(500);
console.log(' ✅ 已悬停在 Agent Group 上');
console.info(' ✅ 已悬停在 Agent Group 上');
});
// ============================================
@@ -135,34 +135,34 @@ When('用户悬停在该 Agent Group 上', async function (this: CustomWorld) {
// ============================================
Then('Agent Group 应该显示置顶图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示置顶图标...');
console.info(' 📍 Step: 验证显示置顶图标...');
await this.page.waitForTimeout(500);
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const pinIcon = targetItem.locator('svg.lucide-pin');
await expect(pinIcon).toBeVisible({ timeout: 5000 });
console.log(' ✅ 置顶图标已显示');
console.info(' ✅ 置顶图标已显示');
});
Then('Agent Group 不应该显示置顶图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证不显示置顶图标...');
console.info(' 📍 Step: 验证不显示置顶图标...');
await this.page.waitForTimeout(500);
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const pinIcon = targetItem.locator('svg.lucide-pin');
await expect(pinIcon).not.toBeVisible({ timeout: 5000 });
console.log(' ✅ 置顶图标未显示');
console.info(' ✅ 置顶图标未显示');
});
Then('Agent Group 应该从列表中移除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Agent Group 已移除...');
console.info(' 📍 Step: 验证 Agent Group 已移除...');
await this.page.waitForTimeout(500);
const deletedItem = this.page.locator(this.testContext.targetItemSelector);
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
console.log(' ✅ Agent Group 已从列表中移除');
console.info(' ✅ Agent Group 已从列表中移除');
});
+66 -65
View File
@@ -12,7 +12,7 @@ import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { llmMockManager, presetResponses } from '../../mocks/llm';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// Store created IDs for verification
let createdAgentId: string | null = null;
@@ -24,7 +24,7 @@ let createdDocumentId: string | null = null;
// ============================================
Given('用户在 Home 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 设置 LLM mock...');
console.info(' 📍 Step: 设置 LLM mock...');
// Setup LLM mock before navigation (for agent/group/page builder message)
llmMockManager.setResponse('E2E Test Agent', presetResponses.greeting);
llmMockManager.setResponse('E2E Test Group', presetResponses.greeting);
@@ -34,7 +34,7 @@ Given('用户在 Home 页面', async function (this: CustomWorld) {
);
await llmMockManager.setup(this.page);
console.log(' 📍 Step: 导航到 Home 页面...');
console.info(' 📍 Step: 导航到 Home 页面...');
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
@@ -44,7 +44,7 @@ Given('用户在 Home 页面', async function (this: CustomWorld) {
createdGroupId = null;
createdDocumentId = null;
console.log(' ✅ 已进入 Home 页面');
console.info(' ✅ 已进入 Home 页面');
});
// ============================================
@@ -52,7 +52,7 @@ Given('用户在 Home 页面', async function (this: CustomWorld) {
// ============================================
When('用户点击创建 Agent 按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击创建 Agent 按钮...');
console.info(' 📍 Step: 点击创建 Agent 按钮...');
// Find the "Create Agent" button by text (supports both English and Chinese)
const createAgentButton = this.page
@@ -61,13 +61,15 @@ When('用户点击创建 Agent 按钮', async function (this: CustomWorld) {
await expect(createAgentButton).toBeVisible({ timeout: WAIT_TIMEOUT });
await createAgentButton.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击创建 Agent 按钮');
// Wait for mode switch animation and ChatInput scroll-into-view to settle
await this.page.waitForTimeout(800);
console.info(' ✅ 已点击创建 Agent 按钮');
});
When('用户点击创建 Group 按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击创建 Group 按钮...');
console.info(' 📍 Step: 点击创建 Group 按钮...');
// Find the "Create Group" button by text (supports both English and Chinese)
const createGroupButton = this.page
@@ -76,56 +78,52 @@ When('用户点击创建 Group 按钮', async function (this: CustomWorld) {
await expect(createGroupButton).toBeVisible({ timeout: WAIT_TIMEOUT });
await createGroupButton.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击创建 Group 按钮');
// Wait for mode switch animation and ChatInput scroll-into-view to settle
await this.page.waitForTimeout(800);
console.info(' ✅ 已点击创建 Group 按钮');
});
When('用户点击写作按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击写作按钮...');
console.info(' 📍 Step: 点击写作按钮...');
// Find the "Write" button by text (supports both English and Chinese)
const writeButton = this.page.getByRole('button', { name: /write|写作/i }).first();
await expect(writeButton).toBeVisible({ timeout: WAIT_TIMEOUT });
await writeButton.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击写作按钮');
// Wait for mode switch animation and ChatInput scroll-into-view to settle
await this.page.waitForTimeout(800);
console.info(' ✅ 已点击写作按钮');
});
When('用户在输入框中输入 {string}', async function (this: CustomWorld, message: string) {
console.log(` 📍 Step: 在输入框中输入 "${message}"...`);
console.info(` 📍 Step: 在输入框中输入 "${message}"...`);
// The chat input is a contenteditable editor, need to click first then type
// The chat input is a contenteditable editor, need to click first then type.
// Target the contenteditable element INSIDE the ChatInput container directly,
// since clicking the container might hit the action bar/footer area instead.
const chatInputContainer = this.page.locator('[data-testid="chat-input"]').first();
await expect(chatInputContainer).toBeVisible({ timeout: WAIT_TIMEOUT });
// If data-testid not found, try alternative selectors
let inputFound = false;
if ((await chatInputContainer.count()) > 0) {
await chatInputContainer.click();
inputFound = true;
} else {
// Try to find the editor by its contenteditable attribute
const editor = this.page.locator('[contenteditable="true"]').first();
if ((await editor.count()) > 0) {
await editor.click();
inputFound = true;
}
}
if (!inputFound) {
throw new Error('Could not find chat input');
}
const editor = chatInputContainer.locator('[contenteditable="true"]').first();
await editor.click();
await this.page.waitForTimeout(300);
await this.page.keyboard.type(message, { delay: 30 });
console.log(` ✅ 已输入 "${message}"`);
console.info(` ✅ 已输入 "${message}"`);
});
When('用户按 Enter 发送', async function (this: CustomWorld) {
console.log(' 📍 Step: 按 Enter 发送...');
When('用户按 Enter 发送', { timeout: 30_000 }, async function (this: CustomWorld) {
console.info(' 📍 Step: 按 Enter 发送...');
// Wait for editor's debounced onChange (100ms default) to sync inputMessage to store.
// The send() function reads directly from the editor as a fallback, but this wait
// ensures maximum reliability.
await this.page.waitForTimeout(200);
// Listen for navigation to capture the agent/group ID
const navigationPromise = this.page.waitForURL(/\/(agent|group)\/.*\/profile/, {
@@ -144,20 +142,23 @@ When('用户按 Enter 发送', async function (this: CustomWorld) {
const agentMatch = currentUrl.match(/\/agent\/([^/]+)/);
if (agentMatch) {
createdAgentId = agentMatch[1];
console.log(` 📍 Created agent ID: ${createdAgentId}`);
console.info(` 📍 Created agent ID: ${createdAgentId}`);
}
const groupMatch = currentUrl.match(/\/group\/([^/]+)/);
if (groupMatch) {
createdGroupId = groupMatch[1];
console.log(` 📍 Created group ID: ${createdGroupId}`);
console.info(` 📍 Created group ID: ${createdGroupId}`);
}
console.log(' ✅ 已发送消息');
console.info(' ✅ 已发送消息');
});
When('用户按 Enter 发送创建文档', async function (this: CustomWorld) {
console.log(' 📍 Step: 按 Enter 发送创建文档...');
When('用户按 Enter 发送创建文档', { timeout: 30_000 }, async function (this: CustomWorld) {
console.info(' 📍 Step: 按 Enter 发送创建文档...');
// Wait for editor's debounced onChange (100ms default) to sync inputMessage to store
await this.page.waitForTimeout(200);
// Listen for navigation to capture the document ID
const navigationPromise = this.page.waitForURL(/\/page\/[^/]+/, {
@@ -175,20 +176,20 @@ When('用户按 Enter 发送创建文档', async function (this: CustomWorld) {
const pageMatch = currentUrl.match(/\/page\/([^/?]+)/);
if (pageMatch) {
createdDocumentId = pageMatch[1];
console.log(` 📍 Created document ID: ${createdDocumentId}`);
console.info(` 📍 Created document ID: ${createdDocumentId}`);
}
console.log(' ✅ 已发送并创建文档');
console.info(' ✅ 已发送并创建文档');
});
When('用户返回 Home 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 返回 Home 页面...');
console.info(' 📍 Step: 返回 Home 页面...');
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' ✅ 已返回 Home 页面');
console.info(' ✅ 已返回 Home 页面');
});
// ============================================
@@ -196,27 +197,27 @@ When('用户返回 Home 页面', async function (this: CustomWorld) {
// ============================================
Then('页面应该跳转到 Agent 的 profile 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到 Agent profile 页面...');
console.info(' 📍 Step: 验证页面跳转到 Agent profile 页面...');
// Check current URL matches /agent/{id}/profile pattern
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/agent\/[^/]+\/profile/);
console.log(' ✅ 已跳转到 Agent profile 页面');
console.info(' ✅ 已跳转到 Agent profile 页面');
});
Then('页面应该跳转到 Group 的 profile 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到 Group profile 页面...');
console.info(' 📍 Step: 验证页面跳转到 Group profile 页面...');
// Check current URL matches /group/{id}/profile pattern
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/group\/[^/]+\/profile/);
console.log(' ✅ 已跳转到 Group profile 页面');
console.info(' ✅ 已跳转到 Group profile 页面');
});
Then('新创建的 Agent 应该在侧边栏中显示', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Agent 在侧边栏中显示...');
console.info(' 📍 Step: 验证 Agent 在侧边栏中显示...');
// Wait for sidebar to be visible and data to load
await this.page.waitForTimeout(1500);
@@ -229,17 +230,17 @@ Then('新创建的 Agent 应该在侧边栏中显示', async function (this: Cus
const agentLink = this.page.locator(`a[href="/agent/${createdAgentId}"]`).first();
await expect(agentLink).toBeVisible({ timeout: WAIT_TIMEOUT });
console.log(` ✅ 找到 Agent 链接: /agent/${createdAgentId}`);
console.info(` ✅ 找到 Agent 链接: /agent/${createdAgentId}`);
// Get the aria-label or text content to verify it's the correct agent
const ariaLabel = await agentLink.getAttribute('aria-label');
console.log(` 📍 Agent aria-label: ${ariaLabel}`);
console.info(` 📍 Agent aria-label: ${ariaLabel}`);
console.log(' ✅ Agent 已在侧边栏中显示');
console.info(' ✅ Agent 已在侧边栏中显示');
});
Then('新创建的 Group 应该在侧边栏中显示', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Group 在侧边栏中显示...');
console.info(' 📍 Step: 验证 Group 在侧边栏中显示...');
// Wait for sidebar to be visible and data to load
await this.page.waitForTimeout(1500);
@@ -252,17 +253,17 @@ Then('新创建的 Group 应该在侧边栏中显示', async function (this: Cus
const groupLink = this.page.locator(`a[href="/group/${createdGroupId}"]`).first();
await expect(groupLink).toBeVisible({ timeout: WAIT_TIMEOUT });
console.log(` ✅ 找到 Group 链接: /group/${createdGroupId}`);
console.info(` ✅ 找到 Group 链接: /group/${createdGroupId}`);
// Get the aria-label or text content to verify it's the correct group
const ariaLabel = await groupLink.getAttribute('aria-label');
console.log(` 📍 Group aria-label: ${ariaLabel}`);
console.info(` 📍 Group aria-label: ${ariaLabel}`);
console.log(' ✅ Group 已在侧边栏中显示');
console.info(' ✅ Group 已在侧边栏中显示');
});
Then('页面应该跳转到文档编辑页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到文档编辑页面...');
console.info(' 📍 Step: 验证页面跳转到文档编辑页面...');
// Check current URL matches /page/{id} pattern
const currentUrl = this.page.url();
@@ -272,11 +273,11 @@ Then('页面应该跳转到文档编辑页面', async function (this: CustomWorl
throw new Error('Document ID was not captured during creation');
}
console.log(` ✅ 已跳转到文档编辑页面: /page/${createdDocumentId}`);
console.info(` ✅ 已跳转到文档编辑页面: /page/${createdDocumentId}`);
});
Then('Page Agent 应该收到用户的提示词', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Page Agent 收到用户的提示词...');
console.info(' 📍 Step: 验证 Page Agent 收到用户的提示词...');
// Wait for the page to fully load and Page Agent panel to appear
await this.page.waitForTimeout(2000);
@@ -289,10 +290,10 @@ Then('Page Agent 应该收到用户的提示词', async function (this: CustomWo
const messageVisible = await userMessage.isVisible().catch(() => false);
if (messageVisible) {
console.log(' ✅ 找到用户发送的提示词');
console.info(' ✅ 找到用户发送的提示词');
} else {
// Alternative: check if there's any chat content indicating the message was sent
console.log(' ⚠️ 用户消息可能在聊天面板中,但未直接可见');
console.info(' ⚠️ 用户消息可能在聊天面板中,但未直接可见');
}
// Verify that the Page Agent responded (mock response should appear)
@@ -304,10 +305,10 @@ Then('Page Agent 应该收到用户的提示词', async function (this: CustomWo
const responseVisible = await aiResponse.isVisible().catch(() => false);
if (responseVisible) {
console.log(' ✅ Page Agent 已响应用户的提示词');
console.info(' ✅ Page Agent 已响应用户的提示词');
} else {
console.log(' ⚠️ Page Agent 响应可能正在生成或在其他位置');
console.info(' ⚠️ Page Agent 响应可能正在生成或在其他位置');
}
console.log(' ✅ Page Agent 验证完成');
console.info(' ✅ Page Agent 验证完成');
});
+17 -17
View File
@@ -1,9 +1,9 @@
import { After, AfterAll, Before, BeforeAll, Status, setDefaultTimeout } from '@cucumber/cucumber';
import { type Cookie, chromium } from 'playwright';
import { After, AfterAll, Before, BeforeAll, setDefaultTimeout, Status } from '@cucumber/cucumber';
import { chromium, type Cookie } from 'playwright';
import { TEST_USER, seedTestUser } from '../support/seedTestUser';
import { seedTestUser, TEST_USER } from '../support/seedTestUser';
import { startWebServer, stopWebServer } from '../support/webServer';
import { CustomWorld } from '../support/world';
import { type CustomWorld } from '../support/world';
process.env['E2E'] = '1';
// Set default timeout for all steps to 10 seconds
@@ -14,12 +14,12 @@ let baseUrl: string;
let sessionCookies: Cookie[] = [];
BeforeAll({ timeout: 600_000 }, async function () {
console.log('🚀 Starting E2E test suite...');
console.info('🚀 Starting E2E test suite...');
const PORT = process.env.PORT ? Number(process.env.PORT) : 3006;
baseUrl = process.env.BASE_URL || `http://localhost:${PORT}`;
console.log(`Base URL: ${baseUrl}`);
console.info(`Base URL: ${baseUrl}`);
// Seed test user before starting web server
await seedTestUser();
@@ -35,7 +35,7 @@ BeforeAll({ timeout: 600_000 }, async function () {
}
// Login once and cache the session cookies
console.log('🔐 Performing one-time login to cache session...');
console.info('🔐 Performing one-time login to cache session...');
const browser = await chromium.launch({ headless: process.env.HEADLESS !== 'false' });
const context = await browser.newContext();
@@ -55,14 +55,14 @@ BeforeAll({ timeout: 600_000 }, async function () {
const emailInputVisible = await emailInput.isVisible().catch(() => false);
if (!emailInputVisible) {
console.log(
console.info(
'⚠️ Login form not available, skipping authentication (tests requiring auth may fail)',
);
return;
}
// Step 1: Enter email
console.log(' Step 1: Entering email...');
console.info(' Step 1: Entering email...');
await emailInput.fill(TEST_USER.email);
// Click the next button
@@ -70,7 +70,7 @@ BeforeAll({ timeout: 600_000 }, async function () {
await nextButton.click();
// Step 2: Wait for password step and enter password
console.log(' Step 2: Entering password...');
console.info(' Step 2: Entering password...');
const passwordInput = page
.locator('input[id="password"], input[name="password"], input[type="password"]')
.first();
@@ -87,7 +87,7 @@ BeforeAll({ timeout: 600_000 }, async function () {
// Cache the session cookies
sessionCookies = await context.cookies();
console.log(`✅ Login successful, cached ${sessionCookies.length} cookies`);
console.info(`✅ Login successful, cached ${sessionCookies.length} cookies`);
} finally {
await browser.close();
}
@@ -104,7 +104,7 @@ Before(async function (this: CustomWorld, { pickle }) {
tag.name.startsWith('@PAGE-') ||
tag.name.startsWith('@ROUTES-'),
);
console.log(`\n📝 Running: ${pickle.name}${testId ? ` (${testId.name.replace('@', '')})` : ''}`);
console.info(`\n📝 Running: ${pickle.name}${testId ? ` (${testId.name.replace('@', '')})` : ''}`);
// Setup API mocks before any page navigation
// await mockManager.setup(this.page);
@@ -112,7 +112,7 @@ Before(async function (this: CustomWorld, { pickle }) {
// Set cached session cookies to skip login
if (sessionCookies.length > 0) {
await this.browserContext.addCookies(sessionCookies);
console.log('🍪 Session cookies restored');
console.info('🍪 Session cookies restored');
}
});
@@ -140,19 +140,19 @@ After(async function (this: CustomWorld, { pickle, result }) {
this.attach(`JavaScript Errors:\n${errors}`, 'text/plain');
}
console.log(`❌ Failed: ${pickle.name}`);
console.info(`❌ Failed: ${pickle.name}`);
if (result.message) {
console.log(` Error: ${result.message}`);
console.info(` Error: ${result.message}`);
}
} else if (result?.status === Status.PASSED) {
console.log(`✅ Passed: ${pickle.name}`);
console.info(`✅ Passed: ${pickle.name}`);
}
await this.cleanup();
});
AfterAll(async function () {
console.log('\n🏁 Test suite completed');
console.info('\n🏁 Test suite completed');
// Stop web server if we started it
if (!process.env.BASE_URL && process.env.CI) {
+35 -35
View File
@@ -6,7 +6,7 @@
import { Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// Helper Functions
@@ -26,7 +26,7 @@ async function getEditor(world: CustomWorld) {
// ============================================
When('用户点击编辑器内容区域', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击编辑器内容区域...');
console.info(' 📍 Step: 点击编辑器内容区域...');
const editorContent = this.page.locator('[contenteditable="true"]').first();
if ((await editorContent.count()) > 0) {
@@ -37,21 +37,21 @@ When('用户点击编辑器内容区域', async function (this: CustomWorld) {
}
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击编辑器内容区域');
console.info(' ✅ 已点击编辑器内容区域');
});
When('用户按下 Enter 键', async function (this: CustomWorld) {
console.log(' 📍 Step: 按下 Enter 键...');
console.info(' 📍 Step: 按下 Enter 键...');
await this.page.keyboard.press('Enter');
// Wait for debounce save (1000ms) + buffer
await this.page.waitForTimeout(1500);
console.log(' ✅ 已按下 Enter 键');
console.info(' ✅ 已按下 Enter 键');
});
When('用户输入文本 {string}', async function (this: CustomWorld, text: string) {
console.log(` 📍 Step: 输入文本 "${text}"...`);
console.info(` 📍 Step: 输入文本 "${text}"...`);
await this.page.keyboard.type(text, { delay: 30 });
await this.page.waitForTimeout(300);
@@ -59,11 +59,11 @@ When('用户输入文本 {string}', async function (this: CustomWorld, text: str
// Store for later verification
this.testContext.inputText = text;
console.log(` ✅ 已输入文本 "${text}"`);
console.info(` ✅ 已输入文本 "${text}"`);
});
When('用户在编辑器中输入内容 {string}', async function (this: CustomWorld, content: string) {
console.log(` 📍 Step: 在编辑器中输入内容 "${content}"...`);
console.info(` 📍 Step: 在编辑器中输入内容 "${content}"...`);
const editor = await getEditor(this);
await editor.click();
@@ -73,16 +73,16 @@ When('用户在编辑器中输入内容 {string}', async function (this: CustomW
this.testContext.inputText = content;
console.log(` ✅ 已输入内容 "${content}"`);
console.info(` ✅ 已输入内容 "${content}"`);
});
When('用户选中所有内容', async function (this: CustomWorld) {
console.log(' 📍 Step: 选中所有内容...');
console.info(' 📍 Step: 选中所有内容...');
await this.page.keyboard.press(`${this.modKey}+A`);
await this.page.waitForTimeout(300);
console.log(' ✅ 已选中所有内容');
console.info(' ✅ 已选中所有内容');
});
// ============================================
@@ -90,17 +90,17 @@ When('用户选中所有内容', async function (this: CustomWorld) {
// ============================================
When('用户输入斜杠 {string}', async function (this: CustomWorld, slash: string) {
console.log(` 📍 Step: 输入斜杠 "${slash}"...`);
console.info(` 📍 Step: 输入斜杠 "${slash}"...`);
await this.page.keyboard.type(slash, { delay: 50 });
// Wait for slash menu to appear
await this.page.waitForTimeout(500);
console.log(` ✅ 已输入斜杠 "${slash}"`);
console.info(` ✅ 已输入斜杠 "${slash}"`);
});
When('用户输入斜杠命令 {string}', async function (this: CustomWorld, command: string) {
console.log(` 📍 Step: 输入斜杠命令 "${command}"...`);
console.info(` 📍 Step: 输入斜杠命令 "${command}"...`);
// The command format is "/shortcut" (e.g., "/h1", "/codeblock")
// First type the slash and wait for menu
@@ -112,7 +112,7 @@ When('用户输入斜杠命令 {string}', async function (this: CustomWorld, com
await this.page.keyboard.type(shortcut, { delay: 80 });
await this.page.waitForTimeout(500); // Wait for menu to filter
console.log(` ✅ 已输入斜杠命令 "${command}"`);
console.info(` ✅ 已输入斜杠命令 "${command}"`);
});
// ============================================
@@ -120,14 +120,14 @@ When('用户输入斜杠命令 {string}', async function (this: CustomWorld, com
// ============================================
When('用户按下快捷键 {string}', async function (this: CustomWorld, shortcut: string) {
console.log(` 📍 Step: 按下快捷键 "${shortcut}"...`);
console.info(` 📍 Step: 按下快捷键 "${shortcut}"...`);
// Convert Meta to platform-specific modifier key for cross-platform support
const platformShortcut = shortcut.replaceAll('Meta', this.modKey);
await this.page.keyboard.press(platformShortcut);
await this.page.waitForTimeout(300);
console.log(` ✅ 已按下快捷键 "${platformShortcut}"`);
console.info(` ✅ 已按下快捷键 "${platformShortcut}"`);
});
// ============================================
@@ -135,7 +135,7 @@ When('用户按下快捷键 {string}', async function (this: CustomWorld, shortc
// ============================================
Then('编辑器应该显示输入的文本', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器显示输入的文本...');
console.info(' 📍 Step: 验证编辑器显示输入的文本...');
const editor = await getEditor(this);
const text = this.testContext.inputText;
@@ -144,17 +144,17 @@ Then('编辑器应该显示输入的文本', async function (this: CustomWorld)
const editorText = await editor.textContent();
expect(editorText).toContain(text);
console.log(` ✅ 编辑器显示文本: "${text}"`);
console.info(` ✅ 编辑器显示文本: "${text}"`);
});
Then('编辑器应该显示 {string}', async function (this: CustomWorld, expectedText: string) {
console.log(` 📍 Step: 验证编辑器显示 "${expectedText}"...`);
console.info(` 📍 Step: 验证编辑器显示 "${expectedText}"...`);
const editor = await getEditor(this);
const editorText = await editor.textContent();
expect(editorText).toContain(expectedText);
console.log(` ✅ 编辑器显示 "${expectedText}"`);
console.info(` ✅ 编辑器显示 "${expectedText}"`);
});
// ============================================
@@ -162,7 +162,7 @@ Then('编辑器应该显示 {string}', async function (this: CustomWorld, expect
// ============================================
Then('应该显示斜杠命令菜单', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示斜杠命令菜单...');
console.info(' 📍 Step: 验证显示斜杠命令菜单...');
// The slash menu should be visible
// Look for menu with heading options, list options, etc.
@@ -189,11 +189,11 @@ Then('应该显示斜杠命令菜单', async function (this: CustomWorld) {
expect(menuFound).toBe(true);
console.log(' ✅ 斜杠命令菜单已显示');
console.info(' ✅ 斜杠命令菜单已显示');
});
Then('编辑器应该包含一级标题', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含一级标题...');
console.info(' 📍 Step: 验证编辑器包含一级标题...');
// Check for h1 element in the editor
const editor = await getEditor(this);
@@ -201,22 +201,22 @@ Then('编辑器应该包含一级标题', async function (this: CustomWorld) {
await expect(h1).toBeVisible({ timeout: 5000 });
console.log(' ✅ 编辑器包含一级标题');
console.info(' ✅ 编辑器包含一级标题');
});
Then('编辑器应该包含无序列表', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含无序列表...');
console.info(' 📍 Step: 验证编辑器包含无序列表...');
const editor = await getEditor(this);
const ul = editor.locator('ul');
await expect(ul).toBeVisible({ timeout: 5000 });
console.log(' ✅ 编辑器包含无序列表');
console.info(' ✅ 编辑器包含无序列表');
});
Then('编辑器应该包含任务列表', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含任务列表...');
console.info(' 📍 Step: 验证编辑器包含任务列表...');
const editor = await getEditor(this);
@@ -245,11 +245,11 @@ Then('编辑器应该包含任务列表', async function (this: CustomWorld) {
expect(found).toBe(true);
console.log(' ✅ 编辑器包含任务列表');
console.info(' ✅ 编辑器包含任务列表');
});
Then('编辑器应该包含代码块', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含代码块...');
console.info(' 📍 Step: 验证编辑器包含代码块...');
// Code block might be rendered inside the editor OR as a sibling element
// CodeMirror renders its own container
@@ -287,7 +287,7 @@ Then('编辑器应该包含代码块', async function (this: CustomWorld) {
expect(found).toBe(true);
console.log(' ✅ 编辑器包含代码块');
console.info(' ✅ 编辑器包含代码块');
});
// ============================================
@@ -295,7 +295,7 @@ Then('编辑器应该包含代码块', async function (this: CustomWorld) {
// ============================================
Then('选中的文本应该被加粗', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文本已加粗...');
console.info(' 📍 Step: 验证文本已加粗...');
const editor = await getEditor(this);
@@ -318,11 +318,11 @@ Then('选中的文本应该被加粗', async function (this: CustomWorld) {
expect(found).toBe(true);
console.log(' ✅ 文本已加粗');
console.info(' ✅ 文本已加粗');
});
Then('选中的文本应该变为斜体', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文本已斜体...');
console.info(' 📍 Step: 验证文本已斜体...');
const editor = await getEditor(this);
@@ -340,5 +340,5 @@ Then('选中的文本应该变为斜体', async function (this: CustomWorld) {
expect(found).toBe(true);
console.log(' ✅ 文本已斜体');
console.info(' ✅ 文本已斜体');
});
+42 -42
View File
@@ -6,14 +6,14 @@
import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// ============================================
// Given Steps
// ============================================
Given('用户打开一个文稿编辑器', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建并打开一个文稿...');
console.info(' 📍 Step: 创建并打开一个文稿...');
// Navigate to page module
await this.page.goto('/page');
@@ -30,11 +30,11 @@ Given('用户打开一个文稿编辑器', async function (this: CustomWorld) {
await this.page.waitForLoadState('networkidle');
await this.page.waitForTimeout(500);
console.log(' ✅ 已打开文稿编辑器');
console.info(' ✅ 已打开文稿编辑器');
});
Given('用户打开一个带有 Emoji 的文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建并打开一个带 Emoji 的文稿...');
console.info(' 📍 Step: 创建并打开一个带 Emoji 的文稿...');
// First create and open a page
await this.page.goto('/page');
@@ -50,7 +50,7 @@ Given('用户打开一个带有 Emoji 的文稿', async function (this: CustomWo
await this.page.waitForTimeout(500);
// Add emoji by clicking the "Choose Icon" button
console.log(' 📍 Step: 添加 Emoji 图标...');
console.info(' 📍 Step: 添加 Emoji 图标...');
// Hover over title section to show the button
const titleSection = this.page.locator('textarea').first().locator('xpath=ancestor::div[1]');
@@ -77,7 +77,7 @@ Given('用户打开一个带有 Emoji 的文稿', async function (this: CustomWo
await this.page.waitForTimeout(500);
}
console.log(' ✅ 已打开带 Emoji 的文稿');
console.info(' ✅ 已打开带 Emoji 的文稿');
});
// ============================================
@@ -85,18 +85,18 @@ Given('用户打开一个带有 Emoji 的文稿', async function (this: CustomWo
// ============================================
When('用户点击标题输入框', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击标题输入框...');
console.info(' 📍 Step: 点击标题输入框...');
const titleInput = this.page.locator('textarea').first();
await expect(titleInput).toBeVisible({ timeout: 5000 });
await titleInput.click();
await this.page.waitForTimeout(300);
console.log(' ✅ 已点击标题输入框');
console.info(' ✅ 已点击标题输入框');
});
When('用户输入标题 {string}', async function (this: CustomWorld, title: string) {
console.log(` 📍 Step: 输入标题 "${title}"...`);
console.info(` 📍 Step: 输入标题 "${title}"...`);
const titleInput = this.page.locator('textarea').first();
@@ -109,11 +109,11 @@ When('用户输入标题 {string}', async function (this: CustomWorld, title: st
// Store for later verification
this.testContext.expectedTitle = title;
console.log(` ✅ 已输入标题 "${title}"`);
console.info(` ✅ 已输入标题 "${title}"`);
});
When('用户清空标题内容', async function (this: CustomWorld) {
console.log(' 📍 Step: 清空标题内容...');
console.info(' 📍 Step: 清空标题内容...');
const titleInput = this.page.locator('textarea').first();
await titleInput.click();
@@ -125,7 +125,7 @@ When('用户清空标题内容', async function (this: CustomWorld) {
await this.page.click('body', { position: { x: 400, y: 400 } });
await this.page.waitForTimeout(1500);
console.log(' ✅ 已清空标题内容');
console.info(' ✅ 已清空标题内容');
});
// ============================================
@@ -133,7 +133,7 @@ When('用户清空标题内容', async function (this: CustomWorld) {
// ============================================
When('用户点击选择图标按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击选择图标按钮...');
console.info(' 📍 Step: 点击选择图标按钮...');
// Hover to show the button
const titleSection = this.page.locator('textarea').first().locator('xpath=ancestor::div[1]');
@@ -146,11 +146,11 @@ When('用户点击选择图标按钮', async function (this: CustomWorld) {
await chooseIconButton.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击选择图标按钮');
console.info(' ✅ 已点击选择图标按钮');
});
When('用户选择一个 Emoji', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择一个 Emoji...');
console.info(' 📍 Step: 选择一个 Emoji...');
// Wait for emoji picker to be visible
await this.page.waitForTimeout(800);
@@ -171,28 +171,28 @@ When('用户选择一个 Emoji', async function (this: CustomWorld) {
for (const selector of emojiSelectors) {
const emojis = this.page.locator(selector);
const count = await emojis.count();
console.log(` 📍 Debug: Found ${count} elements with selector "${selector}"`);
console.info(` 📍 Debug: Found ${count} elements with selector "${selector}"`);
if (count > 0) {
// Click a random emoji (not the first to avoid default)
const index = Math.min(5, count - 1);
await emojis.nth(index).click();
clicked = true;
console.log(` 📍 Debug: Clicked emoji at index ${index}`);
console.info(` 📍 Debug: Clicked emoji at index ${index}`);
break;
}
}
// Fallback: try to find any clickable element in the emoji popover
if (!clicked) {
console.log(' 📍 Debug: Trying fallback - looking for emoji in popover');
console.info(' 📍 Debug: Trying fallback - looking for emoji in popover');
const popover = this.page.locator('.ant-popover-inner, [class*="popover"]').first();
if ((await popover.count()) > 0) {
// Find spans that look like emojis (single character with emoji range)
const emojiSpans = popover.locator('span').filter({
hasText: /^[\p{Emoji}]$/u,
hasText: /^\p{Emoji}$/u,
});
const count = await emojiSpans.count();
console.log(` 📍 Debug: Found ${count} emoji spans in popover`);
console.info(` 📍 Debug: Found ${count} emoji spans in popover`);
if (count > 0) {
await emojiSpans.nth(Math.min(5, count - 1)).click();
clicked = true;
@@ -201,16 +201,16 @@ When('用户选择一个 Emoji', async function (this: CustomWorld) {
}
if (!clicked) {
console.log(' ⚠️ Could not find emoji button, test may fail');
console.info(' ⚠️ Could not find emoji button, test may fail');
}
await this.page.waitForTimeout(1000);
console.log(' ✅ 已选择 Emoji');
console.info(' ✅ 已选择 Emoji');
});
When('用户点击已有的 Emoji 图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击已有的 Emoji 图标...');
console.info(' 📍 Step: 点击已有的 Emoji 图标...');
// The emoji is displayed in an Avatar component with square shape
// Look for the emoji display element near the title
@@ -230,11 +230,11 @@ When('用户点击已有的 Emoji 图标', async function (this: CustomWorld) {
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击 Emoji 图标');
console.info(' ✅ 已点击 Emoji 图标');
});
When('用户选择另一个 Emoji', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择另一个 Emoji...');
console.info(' 📍 Step: 选择另一个 Emoji...');
// Same as selecting an emoji, but choose a different index
await this.page.waitForTimeout(500);
@@ -254,11 +254,11 @@ When('用户选择另一个 Emoji', async function (this: CustomWorld) {
await this.page.waitForTimeout(1000);
console.log(' ✅ 已选择另一个 Emoji');
console.info(' ✅ 已选择另一个 Emoji');
});
When('用户点击删除图标按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击删除图标按钮...');
console.info(' 📍 Step: 点击删除图标按钮...');
// Look for delete button in the emoji picker
const deleteButton = this.page.getByRole('button', { name: /delete|删除/i });
@@ -274,7 +274,7 @@ When('用户点击删除图标按钮', async function (this: CustomWorld) {
await this.page.waitForTimeout(1000);
console.log(' ✅ 已点击删除图标按钮');
console.info(' ✅ 已点击删除图标按钮');
});
// ============================================
@@ -282,7 +282,7 @@ When('用户点击删除图标按钮', async function (this: CustomWorld) {
// ============================================
Then('文稿标题应该更新为 {string}', async function (this: CustomWorld, expectedTitle: string) {
console.log(` 📍 Step: 验证标题为 "${expectedTitle}"...`);
console.info(` 📍 Step: 验证标题为 "${expectedTitle}"...`);
const titleInput = this.page.locator('textarea').first();
await expect(titleInput).toHaveValue(expectedTitle, { timeout: 5000 });
@@ -295,16 +295,16 @@ Then('文稿标题应该更新为 {string}', async function (this: CustomWorld,
// Sidebar might take longer to sync
try {
await expect(sidebarItem).toBeVisible({ timeout: 3000 });
console.log(' ✅ 侧边栏标题也已更新');
console.info(' ✅ 侧边栏标题也已更新');
} catch {
console.log(' ⚠️ 侧边栏标题可能未同步(非关键)');
console.info(' ⚠️ 侧边栏标题可能未同步(非关键)');
}
console.log(` ✅ 标题已更新为 "${expectedTitle}"`);
console.info(` ✅ 标题已更新为 "${expectedTitle}"`);
});
Then('应该显示标题占位符', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示占位符...');
console.info(' 📍 Step: 验证显示占位符...');
const titleInput = this.page.locator('textarea').first();
@@ -317,11 +317,11 @@ Then('应该显示标题占位符', async function (this: CustomWorld) {
const isEmptyOrDefault = value === '' || value === 'Untitled' || value === '无标题';
expect(isEmptyOrDefault).toBe(true);
console.log(` ✅ 显示占位符: "${placeholder}", 当前值: "${value}"`);
console.info(` ✅ 显示占位符: "${placeholder}", 当前值: "${value}"`);
});
Then('文稿应该显示所选的 Emoji 图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示 Emoji 图标...');
console.info(' 📍 Step: 验证显示 Emoji 图标...');
// Look for emoji display - could be in Avatar or span element
// The emoji picker uses @lobehub/ui which may render differently
@@ -349,11 +349,11 @@ Then('文稿应该显示所选的 Emoji 图标', async function (this: CustomWor
expect(found).toBe(true);
console.log(' ✅ 文稿显示 Emoji 图标');
console.info(' ✅ 文稿显示 Emoji 图标');
});
Then('文稿图标应该更新为新的 Emoji', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Emoji 图标已更新...');
console.info(' 📍 Step: 验证 Emoji 图标已更新...');
// Look for emoji display
const emojiSelectors = [
@@ -380,11 +380,11 @@ Then('文稿图标应该更新为新的 Emoji', async function (this: CustomWorl
expect(found).toBe(true);
console.log(' ✅ Emoji 图标已更新');
console.info(' ✅ Emoji 图标已更新');
});
Then('文稿不应该显示 Emoji 图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证不显示 Emoji 图标...');
console.info(' 📍 Step: 验证不显示 Emoji 图标...');
// After deletion, the "Choose Icon" button should be visible
// and the emoji avatar should be hidden
@@ -400,11 +400,11 @@ Then('文稿不应该显示 Emoji 图标', async function (this: CustomWorld) {
// Either the button is visible OR the emoji avatar is not visible
try {
await expect(chooseIconButton).toBeVisible({ timeout: 3000 });
console.log(' ✅ 选择图标按钮可见,说明 Emoji 已删除');
console.info(' ✅ 选择图标按钮可见,说明 Emoji 已删除');
} catch {
// Emoji might still be there but different
console.log(' ⚠️ 无法确认 Emoji 是否删除');
console.info(' ⚠️ 无法确认 Emoji 是否删除');
}
console.log(' ✅ 验证完成');
console.info(' ✅ 验证完成');
});
+45 -45
View File
@@ -10,7 +10,7 @@
import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// ============================================
// Helper Functions
@@ -89,7 +89,7 @@ async function inputPageName(
}
await this.page.waitForTimeout(1000);
console.log(` ✅ 已输入新名称 "${newName}"`);
console.info(` ✅ 已输入新名称 "${newName}"`);
}
// ============================================
@@ -97,21 +97,21 @@ async function inputPageName(
// ============================================
Given('用户在 Page 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 导航到 Page 页面...');
console.info(' 📍 Step: 导航到 Page 页面...');
await this.page.goto('/page');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' ✅ 已进入 Page 页面');
console.info(' ✅ 已进入 Page 页面');
});
Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 导航到 Page 页面...');
console.info(' 📍 Step: 导航到 Page 页面...');
await this.page.goto('/page');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 通过 UI 创建新文稿...');
console.info(' 📍 Step: 通过 UI 创建新文稿...');
// Click the new page button to create via UI (ensures proper server-side creation)
const newPageButton = this.page.locator('svg.lucide-square-pen').first();
await newPageButton.click();
@@ -123,37 +123,37 @@ Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld)
// Create a unique title for this test page
const uniqueTitle = `E2E Page ${Date.now()}`;
console.log(` 📍 Step: 重命名为唯一标题 "${uniqueTitle}"...`);
console.info(` 📍 Step: 重命名为唯一标题 "${uniqueTitle}"...`);
// Find the new page in sidebar (use link selector to avoid matching editor title)
// Sidebar page items are rendered as <a href="/page/xxx"> links
// Debug: check how many links exist
const allPageLinks = this.page.locator('a[href^="/page/"]');
const linkCount = await allPageLinks.count();
console.log(` 📍 Debug: Found ${linkCount} page links in sidebar`);
console.info(` 📍 Debug: Found ${linkCount} page links in sidebar`);
// Find the Untitled page link
const pageItem = allPageLinks.filter({ hasText: /Untitled|无标题/ }).first();
const pageItemCount = await allPageLinks.filter({ hasText: /Untitled|无标题/ }).count();
console.log(` 📍 Debug: Found ${pageItemCount} Untitled page links`);
console.info(` 📍 Debug: Found ${pageItemCount} Untitled page links`);
await expect(pageItem).toBeVisible({ timeout: 5000 });
console.log(' 📍 Debug: Page item is visible');
console.info(' 📍 Debug: Page item is visible');
// Right-click to open context menu and rename
await pageItem.click({ button: 'right' });
console.log(' 📍 Debug: Right-clicked on page item');
console.info(' 📍 Debug: Right-clicked on page item');
await this.page.waitForTimeout(500);
// Debug: check menu items
const menuItemCount = await this.page.locator('[role="menuitem"]').count();
console.log(` 📍 Debug: Found ${menuItemCount} menu items after right-click`);
console.info(` 📍 Debug: Found ${menuItemCount} menu items after right-click`);
const renameOption = this.page.getByRole('menuitem', { name: /rename|重命名/i });
await expect(renameOption).toBeVisible({ timeout: 5000 });
console.log(' 📍 Debug: Rename option is visible');
console.info(' 📍 Debug: Rename option is visible');
await renameOption.click();
console.log(' 📍 Debug: Clicked rename option');
console.info(' 📍 Debug: Clicked rename option');
await this.page.waitForTimeout(800);
// Wait for rename popover to appear and find the input
@@ -169,7 +169,7 @@ Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld)
for (const selector of inputSelectors) {
const inputs = this.page.locator(selector);
const count = await inputs.count();
console.log(` 📍 Debug: Selector "${selector}" found ${count} inputs`);
console.info(` 📍 Debug: Selector "${selector}" found ${count} inputs`);
if (count > 0) {
// Find the visible one
for (let i = 0; i < count; i++) {
@@ -192,14 +192,14 @@ Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld)
throw new Error('Could not find popover input for renaming');
}
console.log(' 📍 Debug: Popover input found');
console.info(' 📍 Debug: Popover input found');
await expect(popoverInput).toBeVisible({ timeout: 5000 });
// Clear and input the unique name
await popoverInput.click();
await popoverInput.clear();
await popoverInput.fill(uniqueTitle);
console.log(` 📍 Debug: Filled input with "${uniqueTitle}"`);
console.info(` 📍 Debug: Filled input with "${uniqueTitle}"`);
// Press Enter to confirm
await popoverInput.press('Enter');
@@ -213,16 +213,16 @@ Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld)
this.testContext.targetItemTitle = uniqueTitle;
this.testContext.targetType = 'page';
console.log(` ✅ 找到文稿: ${uniqueTitle}`);
console.info(` ✅ 找到文稿: ${uniqueTitle}`);
});
Given('用户在 Page 页面有一个文稿 {string}', async function (this: CustomWorld, title: string) {
console.log(' 📍 Step: 导航到 Page 页面...');
console.info(' 📍 Step: 导航到 Page 页面...');
await this.page.goto('/page');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 通过 UI 创建新文稿...');
console.info(' 📍 Step: 通过 UI 创建新文稿...');
// Click the new page button to create via UI
const newPageButton = this.page.locator('svg.lucide-square-pen').first();
await newPageButton.click();
@@ -234,7 +234,7 @@ Given('用户在 Page 页面有一个文稿 {string}', async function (this: Cus
// Default title is "无标题" (Untitled) - support both languages
const defaultTitleRegex = /^(无标题|Untitled)$/;
console.log(` 📍 Step: 通过右键菜单重命名文稿为 "${title}"...`);
console.info(` 📍 Step: 通过右键菜单重命名文稿为 "${title}"...`);
// Find the new page in sidebar (use link selector to avoid matching editor title)
// Sidebar page items are rendered as <a href="/page/xxx"> links
const pageItem = this.page
@@ -296,14 +296,14 @@ Given('用户在 Page 页面有一个文稿 {string}', async function (this: Cus
await popoverInput.press('Enter');
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 查找文稿...');
console.info(' 📍 Step: 查找文稿...');
const renamedItem = this.page.getByText(title, { exact: true }).first();
await expect(renamedItem).toBeVisible({ timeout: WAIT_TIMEOUT });
this.testContext.targetItemTitle = title;
this.testContext.targetType = 'page';
console.log(` ✅ 找到文稿: ${title}`);
console.info(` ✅ 找到文稿: ${title}`);
});
// ============================================
@@ -311,7 +311,7 @@ Given('用户在 Page 页面有一个文稿 {string}', async function (this: Cus
// ============================================
When('用户点击新建文稿按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击新建文稿按钮...');
console.info(' 📍 Step: 点击新建文稿按钮...');
// Look for the SquarePen icon button (new page button)
const newPageButton = this.page.locator('svg.lucide-square-pen').first();
@@ -331,11 +331,11 @@ When('用户点击新建文稿按钮', async function (this: CustomWorld) {
}
await this.page.waitForTimeout(1000);
console.log(' ✅ 已点击新建文稿按钮');
console.info(' ✅ 已点击新建文稿按钮');
});
When('用户右键点击该文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击文稿...');
console.info(' 📍 Step: 右键点击文稿...');
const title = this.testContext.targetItemTitle || this.testContext.createdPageTitle;
// Find the page item by its title text, then find the parent clickable block
@@ -349,13 +349,13 @@ When('用户右键点击该文稿', async function (this: CustomWorld) {
// Debug: check what menus are visible
const menuItems = await this.page.locator('[role="menuitem"]').count();
console.log(` 📍 Debug: Found ${menuItems} menu items after right-click`);
console.info(` 📍 Debug: Found ${menuItems} menu items after right-click`);
console.log(' ✅ 已右键点击文稿');
console.info(' ✅ 已右键点击文稿');
});
When('用户在菜单中选择复制', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择复制选项...');
console.info(' 📍 Step: 选择复制选项...');
// Look for duplicate option (复制 or Duplicate)
const duplicateOption = this.page.getByRole('menuitem', { name: /复制|duplicate/i });
@@ -363,18 +363,18 @@ When('用户在菜单中选择复制', async function (this: CustomWorld) {
await duplicateOption.click();
await this.page.waitForTimeout(1000);
console.log(' ✅ 已选择复制选项');
console.info(' ✅ 已选择复制选项');
});
When('用户输入新的文稿名称 {string}', async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}"...`);
console.info(` 📍 Step: 输入新名称 "${newName}"...`);
await inputPageName.call(this, newName, false);
});
When(
'用户输入新的文稿名称 {string} 并按 Enter',
async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
console.info(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
await inputPageName.call(this, newName, true);
},
);
@@ -384,7 +384,7 @@ When(
// ============================================
Then('应该创建一个新的文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证新文稿已创建...');
console.info(' 📍 Step: 验证新文稿已创建...');
await this.page.waitForTimeout(1000);
@@ -392,11 +392,11 @@ Then('应该创建一个新的文稿', async function (this: CustomWorld) {
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/page\/.+/);
console.log(' ✅ 新文稿已创建');
console.info(' ✅ 新文稿已创建');
});
Then('文稿列表中应该显示新文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文稿列表中显示新文稿...');
console.info(' 📍 Step: 验证文稿列表中显示新文稿...');
await this.page.waitForTimeout(500);
@@ -405,11 +405,11 @@ Then('文稿列表中应该显示新文稿', async function (this: CustomWorld)
const untitledText = this.page.getByText(/无标题|untitled/i).first();
await expect(untitledText).toBeVisible({ timeout: 5000 });
console.log(' ✅ 文稿列表中显示新文稿');
console.info(' ✅ 文稿列表中显示新文稿');
});
Then('该文稿名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
console.log(` 📍 Step: 验证名称为 "${expectedName}"...`);
console.info(` 📍 Step: 验证名称为 "${expectedName}"...`);
await this.page.waitForTimeout(1000);
@@ -417,11 +417,11 @@ Then('该文稿名称应该更新为 {string}', async function (this: CustomWorl
const renamedItem = this.page.getByText(expectedName, { exact: true }).first();
await expect(renamedItem).toBeVisible({ timeout: 5000 });
console.log(` ✅ 名称已更新为 "${expectedName}"`);
console.info(` ✅ 名称已更新为 "${expectedName}"`);
});
Then('文稿列表中应该出现 {string}', async function (this: CustomWorld, expectedName: string) {
console.log(` 📍 Step: 验证文稿列表中出现 "${expectedName}"...`);
console.info(` 📍 Step: 验证文稿列表中出现 "${expectedName}"...`);
await this.page.waitForTimeout(2000);
@@ -438,20 +438,20 @@ Then('文稿列表中应该出现 {string}', async function (this: CustomWorld,
if ((await duplicatedItem.count()) === 0) {
// Fallback: check if there are at least 2 pages with similar name
const similarPages = this.page.getByText(expectedName.replace(/\s*\(Copy\)$/, '')).all();
// eslint-disable-next-line unicorn/no-await-expression-member
const count = (await similarPages).length;
console.log(` 📍 Debug: Found ${count} pages with similar name`);
console.info(` 📍 Debug: Found ${count} pages with similar name`);
expect(count).toBeGreaterThanOrEqual(2);
console.log(` ✅ 文稿列表中出现多个相似名称的文稿`);
console.info(` ✅ 文稿列表中出现多个相似名称的文稿`);
return;
}
await expect(duplicatedItem).toBeVisible({ timeout: WAIT_TIMEOUT });
console.log(` ✅ 文稿列表中出现 "${expectedName}"`);
console.info(` ✅ 文稿列表中出现 "${expectedName}"`);
});
Then('该文稿应该从列表中移除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文稿已移除...');
console.info(' 📍 Step: 验证文稿已移除...');
await this.page.waitForTimeout(1000);
@@ -461,5 +461,5 @@ Then('该文稿应该从列表中移除', async function (this: CustomWorld) {
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
}
console.log(' ✅ 文稿已从列表中移除');
console.info(' ✅ 文稿已从列表中移除');
});
+607 -28
View File
@@ -1,11 +1,66 @@
{
"src/app/(backend)/api/agent/route.ts": {
"object-shorthand": {
"count": 1
}
},
"src/app/(backend)/api/agent/run/route.ts": {
"object-shorthand": {
"count": 1
}
},
"src/app/(backend)/api/dev/memory-user-memory/benchmark-locomo/route.ts": {
"no-console": {
"count": 6
}
},
"src/app/(backend)/api/webhooks/logto/route.ts": {
"no-console": {
"count": 1
}
},
"src/app/(backend)/trpc/async/[trpc]/route.ts": {
"no-console": {
"count": 1
}
},
"src/app/(backend)/trpc/mobile/[trpc]/route.ts": {
"no-console": {
"count": 1
}
},
"src/app/(backend)/webapi/chat/[provider]/route.ts": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(auth)/market-auth-callback/page.tsx": {
"no-console": {
"count": 2
}
},
"src/app/[variants]/(desktop)/desktop-onboarding/features/WelcomeStep.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/app/[variants]/(main)/agent/profile/features/Header/AgentPublishButton/PublishButton.tsx": {
"import-x/consistent-type-specifier-style": {
"src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/ToolAuthAlert.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/agent/profile/features/Header/AgentForkTag.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/agent/profile/features/Header/AgentPublishButton/useMarketPublish.ts": {
"object-shorthand": {
"count": 3
}
},
"src/app/[variants]/(main)/community/(detail)/agent/features/AgentForkTag.tsx": {
"no-console": {
"count": 1
}
},
@@ -19,14 +74,64 @@
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/agent/features/Sidebar/ActionButton/index.tsx": {
"object-shorthand": {
"count": 2
}
},
"src/app/[variants]/(main)/community/(detail)/group_agent/features/Sidebar/ActionButton/index.tsx": {
"object-shorthand": {
"count": 2
}
},
"src/app/[variants]/(main)/community/(detail)/user/features/UserAgentCard.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/user/features/UserAgentList.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/user/features/UserFavoriteAgents.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/user/features/UserFavoritePlugins.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/user/features/UserGroupCard.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(list)/(home)/loading.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(list)/mcp/features/List/Item.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(list)/provider/features/List/Item.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/components/VirtuosoGridList/index.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 2
}
},
"src/app/[variants]/(main)/home/_layout/Body/Agent/Modals/ConfigGroupModal/GroupItem.tsx": {
"import-x/consistent-type-specifier-style": {
"count": 1
"src/app/[variants]/(main)/group/profile/features/Header/AgentPublishButton/useMarketPublish.ts": {
"object-shorthand": {
"count": 4
}
},
"src/app/[variants]/(main)/home/_layout/Body/Agent/Modals/ConfigGroupModal/index.tsx": {
@@ -42,21 +147,56 @@
"count": 1
}
},
"src/app/[variants]/(main)/home/features/components/GroupSkeleton.tsx": {
"object-shorthand": {
"count": 1
}
},
"src/app/[variants]/(main)/home/features/index.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/app/[variants]/(main)/image/_layout/ConfigPanel/utils/__tests__/imageValidation.test.ts": {
"object-shorthand": {
"count": 1
}
},
"src/app/[variants]/(main)/image/_layout/TopicSidebar.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx": {
"object-shorthand": {
"count": 1
}
},
"src/app/[variants]/(main)/memory/features/GridView/index.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/app/[variants]/(main)/memory/features/MemoryAnalysis/index.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/resource/features/hooks/useResourceManagerUrlSync.ts": {
"react-hooks/exhaustive-deps": {
"count": 1
}
},
"src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/settings/provider/ProviderMenu/List.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/settings/provider/detail/ollama/CheckError.tsx": {
"regexp/no-dupe-characters-character-class": {
"count": 1
@@ -65,6 +205,41 @@
"count": 1
}
},
"src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx": {
"no-console": {
"count": 2
}
},
"src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/(main)/settings/stats/features/usage/UsageCards/ActiveModels/ModelTable.tsx": {
"object-shorthand": {
"count": 1
}
},
"src/app/[variants]/(main)/video/_layout/TopicSidebar.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/video/features/GenerationFeed/index.tsx": {
"object-shorthand": {
"count": 1
}
},
"src/app/[variants]/onboarding/components/KlavisServerList/hooks/useKlavisOAuth.ts": {
"no-console": {
"count": 1
}
},
"src/app/[variants]/onboarding/features/ResponseLanguageStep.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
@@ -80,6 +255,11 @@
"count": 3
}
},
"src/components/ChatGroupWizard/ChatGroupWizard.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/components/ChatGroupWizard/index.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 1
@@ -88,16 +268,41 @@
"count": 1
}
},
"src/components/DebugNode.tsx": {
"no-console": {
"count": 1
}
},
"src/components/FeedbackModal/index.tsx": {
"no-console": {
"count": 1
}
},
"src/components/Loading/CircleLoading/index.tsx": {
"unicorn/no-anonymous-default-export": {
"count": 1
}
},
"src/components/MCPStdioCommandInput/index.tsx": {
"object-shorthand": {
"count": 1
}
},
"src/components/MaxTokenSlider.tsx": {
"object-shorthand": {
"count": 1
}
},
"src/components/ModelSelect/index.tsx": {
"no-shadow-restricted-names": {
"count": 1
}
},
"src/components/mdx/index.tsx": {
"object-shorthand": {
"count": 1
}
},
"src/config/featureFlags/schema.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 1
@@ -150,6 +355,26 @@
"count": 1
}
},
"src/features/ChatInput/ActionBar/Model/ReasoningTokenSlider.tsx": {
"object-shorthand": {
"count": 1
}
},
"src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx": {
"no-console": {
"count": 2
}
},
"src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx": {
"no-console": {
"count": 3
}
},
"src/features/CommandMenu/SearchResults.tsx": {
"no-console": {
"count": 1
}
},
"src/features/Conversation/Error/OllamaBizError/index.tsx": {
"regexp/no-dupe-characters-character-class": {
"count": 1
@@ -158,8 +383,18 @@
"count": 1
}
},
"src/features/Conversation/Messages/CompressedGroup/index.tsx": {
"import-x/consistent-type-specifier-style": {
"src/features/Conversation/Messages/Assistant/Actions/index.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/Conversation/Messages/AssistantGroup/Actions/index.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/Conversation/Messages/Supervisor/Actions/index.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
@@ -168,6 +403,21 @@
"count": 1
}
},
"src/features/Conversation/components/Reaction/ReactionPicker.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/Conversation/store/slices/message/action/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/features/Conversation/store/slices/message/action/sendMessage.ts": {
"no-console": {
"count": 1
}
},
"src/features/Conversation/types/hooks.ts": {
"typescript-sort-keys/interface": {
"count": 1
@@ -183,8 +433,38 @@
"count": 1
}
},
"src/features/NavPanel/SideBarHeaderLayout.tsx": {
"@eslint-react/dom/no-flush-sync": {
"src/features/GenerationTopicPanel/index.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/LibraryModal/CreateNew/CreateForm.tsx": {
"unused-imports/no-unused-imports": {
"count": 1
}
},
"src/features/LibraryModal/CreateNew/index.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/MCP/Scores.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/MCPPluginDetail/Header.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx": {
"object-shorthand": {
"count": 2
}
},
"src/features/PluginDevModal/MCPManifestForm/utils.ts": {
"object-shorthand": {
"count": 1
}
},
@@ -204,18 +484,44 @@
}
},
"src/features/ProtocolUrlHandler/InstallPlugin/CustomPluginInstallModal.tsx": {
"object-shorthand": {
"count": 1
},
"prefer-const": {
"count": 1
}
},
"src/features/ResourceManager/components/Explorer/Header/SearchInput.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/ResourceManager/components/Explorer/ListView/index.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 1
}
},
"src/features/ResourceManager/components/Explorer/MasonryView/MasonryItem/DefaultFileItem.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/ResourceManager/components/Explorer/MasonryView/MasonryItem/ImageFileItem.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/ResourceManager/components/Explorer/MasonryView/MasonryItem/MarkdownFileItem.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/ResourceManager/components/Explorer/MasonryView/MasonryItem/NoteFileItem.tsx": {
"regexp/no-super-linear-backtracking": {
"count": 1
},
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/ShareModal/ShareJSON/generateFullExport.ts": {
@@ -228,7 +534,30 @@
"count": 1
}
},
"src/helpers/toolEngineering/index.ts": {
"object-shorthand": {
"count": 2
}
},
"src/hooks/useAgentOwnershipCheck.ts": {
"no-console": {
"count": 6
}
},
"src/hooks/useFetchAiVideoConfig.ts": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/hooks/useHotkeys/useHotkeyById.ts": {
"no-console": {
"count": 1
}
},
"src/layout/AuthProvider/MarketAuth/oidc.ts": {
"no-console": {
"count": 17
},
"prefer-const": {
"count": 1
}
@@ -241,6 +570,19 @@
"count": 1
}
},
"src/libs/better-auth/sso/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/libs/next/proxy/define-config.ts": {
"no-console": {
"count": 1
},
"object-shorthand": {
"count": 2
}
},
"src/libs/observability/traceparent.test.ts": {
"import/first": {
"count": 1
@@ -252,6 +594,24 @@
},
"@typescript-eslint/no-unsafe-function-type": {
"count": 1
},
"object-shorthand": {
"count": 1
}
},
"src/libs/oidc-provider/jwt.ts": {
"object-shorthand": {
"count": 1
}
},
"src/libs/swr/localStorageProvider.ts": {
"no-console": {
"count": 1
}
},
"src/libs/traces/index.ts": {
"no-console": {
"count": 1
}
},
"src/libs/trpc/middleware/openTelemetry.test.ts": {
@@ -267,6 +627,21 @@
"count": 1
}
},
"src/server/manifest.ts": {
"object-shorthand": {
"count": 3
}
},
"src/server/modules/AgentRuntime/RuntimeExecutors.ts": {
"object-shorthand": {
"count": 4
}
},
"src/server/modules/KeyVaultsEncrypt/index.ts": {
"object-shorthand": {
"count": 2
}
},
"src/server/modules/Mecha/ContextEngineering/index.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 1
@@ -277,6 +652,19 @@
"count": 1
}
},
"src/server/modules/ModelRuntime/trace.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/modules/S3/index.ts": {
"object-shorthand": {
"count": 3
},
"simple-import-sort/imports": {
"count": 1
}
},
"src/server/routers/lambda/__tests__/integration/aiAgent.createClientGroupAgentTaskThread.integration.test.ts": {
"@typescript-eslint/no-non-null-asserted-optional-chain": {
"count": 2
@@ -287,6 +675,21 @@
"count": 2
}
},
"src/server/routers/lambda/__tests__/integration/message.integration.test.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/routers/lambda/agent.ts": {
"no-console": {
"count": 1
}
},
"src/server/routers/lambda/aiAgent.ts": {
"no-console": {
"count": 1
}
},
"src/server/routers/lambda/aiChat.ts": {
"prefer-const": {
"count": 1
@@ -302,27 +705,26 @@
"count": 2
}
},
"src/server/routers/lambda/knowledge.ts": {
"no-console": {
"count": 1
}
},
"src/server/routers/lambda/ragEval.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 1
}
},
"src/server/routers/lambda/sessionGroup.ts": {
"no-console": {
"count": 1
}
},
"src/server/routers/lambda/user.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 2
}
},
"src/server/routers/lambda/userMemory.ts": {
"@typescript-eslint/consistent-type-imports": {
"count": 1
},
"import-x/no-duplicates": {
"count": 2
},
"simple-import-sort/imports": {
"count": 1
}
},
"src/server/routers/tools/_helpers/scheduleToolCallReport.test.ts": {
"simple-import-sort/imports": {
"count": 1
@@ -333,6 +735,11 @@
"count": 1
}
},
"src/server/services/chunk/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/comfyui/config/fluxModelRegistry.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 2
@@ -426,21 +833,116 @@
"count": 2
}
},
"src/server/services/mcp/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/memory/userMemory/extract.ts": {
"object-shorthand": {
"count": 2
}
},
"src/server/services/message/__tests__/index.integration.test.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/oidc/index.ts": {
"object-shorthand": {
"count": 2
}
},
"src/server/services/search/impls/anspire/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/bocha/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/brave/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/exa/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/firecrawl/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/google/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/jina/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/kagi/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/search1api/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/impls/tavily/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/server/services/search/index.ts": {
"object-shorthand": {
"count": 5
}
},
"src/server/services/usage/index.test.ts": {
"object-shorthand": {
"count": 11
}
},
"src/server/services/webhookUser/index.test.ts": {
"unicorn/no-thenable": {
"count": 7
}
},
"src/server/services/webhookUser/index.ts": {
"no-console": {
"count": 2
}
},
"src/services/_url.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 1
}
},
"src/services/chat/mecha/contextEngineering.ts": {
"sort-keys-fix/sort-keys-fix": {
"src/services/chat/index.ts": {
"object-shorthand": {
"count": 1
}
},
"src/services/models.ts": {
"no-console": {
"count": 1
}
},
"src/services/tableViewer/client.ts": {
"no-console": {
"count": 2
}
},
"src/store/agent/slices/cron/action.ts": {
"no-unused-private-class-members": {
"count": 1
@@ -461,7 +963,15 @@
"count": 1
}
},
"src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts": {
"object-shorthand": {
"count": 1
}
},
"src/store/chat/agents/createAgentExecutors.ts": {
"object-shorthand": {
"count": 1
},
"prefer-const": {
"count": 1
},
@@ -486,13 +996,15 @@
}
},
"src/store/chat/slices/aiChat/actions/__tests__/StreamingHandler.test.ts": {
"import-x/consistent-type-specifier-style": {
"count": 1
},
"unused-imports/no-unused-imports": {
"count": 1
}
},
"src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts": {
"object-shorthand": {
"count": 8
}
},
"src/store/chat/slices/aiChat/actions/conversationControl.ts": {
"no-unused-private-class-members": {
"count": 1
@@ -538,6 +1050,11 @@
"count": 1
}
},
"src/store/chat/slices/message/supervisor.ts": {
"no-console": {
"count": 5
}
},
"src/store/chat/slices/operation/types.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 1
@@ -546,6 +1063,11 @@
"count": 1
}
},
"src/store/chat/slices/plugin/action.test.ts": {
"object-shorthand": {
"count": 3
}
},
"src/store/chat/slices/plugin/actions/internals.ts": {
"no-unused-private-class-members": {
"count": 2
@@ -680,11 +1202,21 @@
"count": 1
}
},
"src/store/file/slices/chat/action.ts": {
"no-console": {
"count": 1
}
},
"src/store/file/slices/chunk/action.ts": {
"no-unused-private-class-members": {
"count": 1
}
},
"src/store/file/slices/document/action.ts": {
"object-shorthand": {
"count": 1
}
},
"src/store/file/slices/tts/action.ts": {
"no-unused-private-class-members": {
"count": 1
@@ -693,6 +1225,9 @@
"src/store/file/slices/upload/action.ts": {
"no-unused-private-class-members": {
"count": 2
},
"object-shorthand": {
"count": 1
}
},
"src/store/global/actions/workspacePane.ts": {
@@ -710,6 +1245,19 @@
"count": 1
}
},
"src/store/home/slices/homeInput/action.ts": {
"no-console": {
"count": 1
}
},
"src/store/image/slices/generationConfig/action.ts": {
"no-useless-rename": {
"count": 4
},
"object-shorthand": {
"count": 1
}
},
"src/store/image/slices/generationConfig/initialState.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 1
@@ -728,12 +1276,25 @@
"count": 1
}
},
"src/store/page/slices/crud/action.ts": {
"object-shorthand": {
"count": 1
}
},
"src/store/serverConfig/action.ts": {
"no-unused-private-class-members": {
"count": 1
}
},
"src/store/session/slices/homeInput/action.ts": {
"no-console": {
"count": 1
}
},
"src/store/session/slices/session/action.ts": {
"object-shorthand": {
"count": 1
},
"typescript-sort-keys/interface": {
"count": 1
}
@@ -746,11 +1307,19 @@
"count": 1
}
},
"src/store/tool/slices/builtin/executors/index.ts": {
"import-x/consistent-type-specifier-style": {
"src/store/tool/slices/klavisStore/action.ts": {
"no-console": {
"count": 1
}
},
"src/store/tool/slices/mcpStore/action.ts": {
"no-console": {
"count": 1
},
"object-shorthand": {
"count": 3
}
},
"src/store/tool/slices/oldStore/initialState.ts": {
"typescript-sort-keys/string-enum": {
"count": 1
@@ -761,6 +1330,16 @@
"count": 1
}
},
"src/store/video/initialState.ts": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/store/video/store.ts": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/styles/antdOverride.ts": {
"unicorn/no-anonymous-default-export": {
"count": 1
@@ -779,4 +1358,4 @@
"count": 1
}
}
}
}
+11
View File
@@ -1,6 +1,10 @@
import { fileURLToPath } from 'node:url';
import { eslint } from '@lobehub/lint';
import { flat as mdxFlat } from 'eslint-plugin-mdx';
const tsconfigRootDir = fileURLToPath(new URL('.', import.meta.url));
export default eslint(
{
ignores: [
@@ -40,6 +44,13 @@ export default eslint(
next: true,
react: 'next',
},
{
languageOptions: {
parserOptions: {
tsconfigRootDir,
},
},
},
// Global rule overrides
{
rules: {
+3 -2
View File
@@ -58,13 +58,13 @@
"duplicateTitle": "نسخة {{title}}",
"emptyAgent": "لا يوجد وكلاء بعد. ابدأ بأول وكيل لك — وابنِ نظامك بمرور الوقت.",
"emptyAgentAction": "إنشاء وكيل",
"extendParams.disableContextCaching.desc": "يقلل ما يصل إلى 90٪ من تكلفة توليد محادثة واحدة ويوفر سرعة تصل إلى 4 أضعاف. تفعيل هذا سيقوم تلقائيًا بإلغاء الحد على عدد الرسائل التاريخية. <1>اعرف المزيد</1>",
"extendParams.disableContextCaching.desc": "قلل تكلفة إنشاء محادثة واحدة بنسبة تصل إلى 90٪ وزد السرعة حتى 4 أضعاف. <1>اعرف المزيد</1>",
"extendParams.disableContextCaching.title": "تفعيل تخزين السياق المؤقت",
"extendParams.effort.desc": "تحكم في عدد الرموز التي يستخدمها كلود عند الرد باستخدام معلمة الجهد.",
"extendParams.effort.title": "الجهد",
"extendParams.enableAdaptiveThinking.desc": "اسمح لكلود باتخاذ قرارات ديناميكية حول متى وكم يفكر باستخدام وضع التفكير التكيفي.",
"extendParams.enableAdaptiveThinking.title": "تفعيل التفكير التكيفي",
"extendParams.enableReasoning.desc": "استنادًا إلى آلية التفكير في Claude، فإن تفعيل هذا سيقوم تلقائيًا بإلغاء الحد على عدد الرسائل التاريخية. <1>اعرف المزيد</1>",
"extendParams.enableReasoning.desc": "استنادًا إلى حد آلية التفكير في Claude. <1>اعرف المزيد</1>",
"extendParams.enableReasoning.title": "تفعيل التفكير العميق",
"extendParams.imageAspectRatio.title": "نسبة أبعاد الصورة",
"extendParams.imageResolution.title": "دقة الصورة",
@@ -165,6 +165,7 @@
"messageAction.delAndRegenerate": "حذف وإعادة التوليد",
"messageAction.deleteDisabledByThreads": "لا يمكن حذف هذه الرسالة لأنها تحتوي على موضوع فرعي",
"messageAction.expand": "توسيع الرسالة",
"messageAction.reaction": "إضافة تفاعل",
"messageAction.regenerate": "إعادة التوليد",
"messages.dm.sentTo": "مرئي فقط لـ {{name}}",
"messages.dm.title": "رسالة خاصة",
+2
View File
@@ -143,6 +143,7 @@
"cmdk.keywords.stats": "إحصائيات تحليلات بيانات",
"cmdk.keywords.submitIssue": "مشكلة خلل عطل ملاحظات",
"cmdk.keywords.usage": "الاستخدام الإحصائيات الاستهلاك الحصة",
"cmdk.keywords.video": "فيديو،إنشاء،سيدانص،كلينغ",
"cmdk.memory": "الذاكرة",
"cmdk.mentionAgent": "اذكر الوكيل",
"cmdk.navigate": "تنقل",
@@ -193,6 +194,7 @@
"cmdk.themeLight": "فاتح",
"cmdk.toOpen": "فتح",
"cmdk.toSelect": "تحديد",
"cmdk.video": "فيديو بالذكاء الاصطناعي",
"confirm": "تأكيد",
"contact": "اتصل بنا",
"copy": "نسخ",
+31
View File
@@ -43,6 +43,8 @@
"FileManager.emptyStatus.or": "أو",
"FileManager.emptyStatus.title": "اسحب الملفات أو المجلدات إلى هنا",
"FileManager.noFolders": "لا توجد مجلدات متاحة",
"FileManager.search.noResults": "لم يتم العثور على ملفات",
"FileManager.search.placeholder": "ابحث في الملفات...",
"FileManager.sort.dateAdded": "تاريخ الإضافة",
"FileManager.sort.name": "الاسم",
"FileManager.sort.size": "الحجم",
@@ -94,6 +96,35 @@
"ModelSelect.removed": "النموذج غير موجود في القائمة. سيتم حذفه تلقائيًا إذا تم إلغاء تحديده.",
"ModelSwitchPanel.byModel": "حسب النموذج",
"ModelSwitchPanel.byProvider": "حسب المزوّد",
"ModelSwitchPanel.detail.abilities": "القدرات",
"ModelSwitchPanel.detail.abilities.files": "الملفات",
"ModelSwitchPanel.detail.abilities.functionCall": "استدعاء الأداة",
"ModelSwitchPanel.detail.abilities.imageOutput": "إخراج الصورة",
"ModelSwitchPanel.detail.abilities.reasoning": "الاستدلال",
"ModelSwitchPanel.detail.abilities.search": "البحث",
"ModelSwitchPanel.detail.abilities.video": "الفيديو",
"ModelSwitchPanel.detail.abilities.vision": "الرؤية",
"ModelSwitchPanel.detail.config": "إعداد النموذج",
"ModelSwitchPanel.detail.context": "طول السياق",
"ModelSwitchPanel.detail.pricing": "الأسعار",
"ModelSwitchPanel.detail.pricing.cachedInput": "المدخلات المخزنة ${{amount}}/مليون",
"ModelSwitchPanel.detail.pricing.group.audio": "الصوت",
"ModelSwitchPanel.detail.pricing.group.image": "الصورة",
"ModelSwitchPanel.detail.pricing.group.text": "النص",
"ModelSwitchPanel.detail.pricing.input": "المدخلات ${{amount}}/مليون",
"ModelSwitchPanel.detail.pricing.output": "المخرجات ${{amount}}/مليون",
"ModelSwitchPanel.detail.pricing.unit.audioInput": "مدخل صوتي",
"ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "مدخل صوتي (مخزن)",
"ModelSwitchPanel.detail.pricing.unit.audioOutput": "مخرج صوتي",
"ModelSwitchPanel.detail.pricing.unit.imageGeneration": "توليد الصور",
"ModelSwitchPanel.detail.pricing.unit.imageInput": "مدخل صورة",
"ModelSwitchPanel.detail.pricing.unit.imageInput_cacheRead": "مدخل صورة (مخزن)",
"ModelSwitchPanel.detail.pricing.unit.imageOutput": "مخرج صورة",
"ModelSwitchPanel.detail.pricing.unit.textInput": "مدخل",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheRead": "مدخل (مخزن)",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheWrite": "مدخل (كتابة في التخزين)",
"ModelSwitchPanel.detail.pricing.unit.textOutput": "مخرج",
"ModelSwitchPanel.detail.releasedAt": "تم الإصدار في {{date}}",
"ModelSwitchPanel.emptyModel": "لا يوجد نموذج مفعل. يرجى الذهاب إلى الإعدادات لتفعيله.",
"ModelSwitchPanel.emptyProvider": "لا يوجد مزود مفعل. يرجى الذهاب إلى الإعدادات لتفعيل أحدهم.",
"ModelSwitchPanel.goToSettings": "الذهاب إلى الإعدادات",
+9
View File
@@ -150,6 +150,9 @@
"groupAgents.tag": "مجموعة",
"groupAgents.underReview": "قيد المراجعة",
"home.communityAgents": "وكلاء المجتمع",
"home.creatorReward.action": "قدّم الآن",
"home.creatorReward.subtitle": "برنامج مكافآت المبدعين لعام 2026 أصبح متاحًا رسميًا.",
"home.creatorReward.title": "أنشئ. شارك. واحصل على مقابل.",
"home.featuredAssistants": "وكلاء مميزون",
"home.featuredModels": "نماذج مميزة",
"home.featuredPlugins": "مهارات مميزة",
@@ -194,6 +197,8 @@
"mcp.categories.tools.name": "أدوات مساعدة",
"mcp.categories.travel-transport.description": "تخطيط السفر والمواصلات",
"mcp.categories.travel-transport.name": "السفر والمواصلات",
"mcp.categories.utility.description": "خدمات التنبؤ بالطقس والأرصاد الجوية",
"mcp.categories.utility.name": "الخدمات",
"mcp.categories.weather.description": "توقعات الطقس وخدمات الأرصاد الجوية",
"mcp.categories.weather.name": "الطقس",
"mcp.categories.web-search.description": "البحث على الويب واسترجاع المعلومات",
@@ -478,6 +483,10 @@
"tab.plugin": "المهارة",
"tab.provider": "المزود",
"tab.user": "المستخدم",
"time.formatOtherYear": "D MMM، YYYY",
"time.formatThisYear": "D MMM",
"time.today": "اليوم",
"time.yesterday": "أمس",
"user.agents": "الوكلاء",
"user.downloads": "التنزيلات",
"user.editProfile": "تعديل الملف الشخصي",
+1
View File
@@ -10,5 +10,6 @@
"starter.deepResearch": "بحث معمق",
"starter.developing": "قريبًا",
"starter.image": "صورة",
"starter.seedance": "سيدانس 2.0",
"starter.write": "كتابة"
}
+4 -1
View File
@@ -9,7 +9,10 @@
"addToKnowledgeBase.title": "إضافة إلى المكتبة",
"addToKnowledgeBase.totalFiles": "{{count}} ملف/ملفات محددة",
"createNew.confirm": "إنشاء جديد",
"createNew.description.placeholder": "وصف المكتبة (اختياري)",
"createNew.description.label": "وصف المكتبة (اختياري)",
"createNew.description.placeholder": "يساعد الوصف نموذج اللغة الكبير على فهم مكتبتك بشكل أفضل",
"createNew.edit.confirm": "حفظ التغييرات",
"createNew.edit.title": "تعديل المكتبة",
"createNew.formTitle": "المعلومات الأساسية",
"createNew.name.placeholder": "اسم المكتبة",
"createNew.name.required": "يرجى إدخال اسم المكتبة",
+4
View File
@@ -38,6 +38,10 @@
"messages.success.submit": "تم التفويض بنجاح! يمكنك الآن نشر وكيلك.",
"messages.success.upload": "تم التفويض بنجاح! يمكنك الآن نشر إصدار جديد.",
"profileSetup.cancel": "إلغاء",
"profileSetup.confirmChangeUserId.cancel": "إلغاء",
"profileSetup.confirmChangeUserId.confirm": "تغيير معرف المستخدم",
"profileSetup.confirmChangeUserId.description": "بمجرد التبديل إلى @{{newId}}، يمكن لأي شخص المطالبة بمعرفك القديم @{{oldId}} وستتوقف جميع الروابط الحالية إلى ملفك الشخصي عن العمل. لا يمكن التراجع عن هذا الإجراء. هل أنت متأكد أنك تريد المتابعة؟",
"profileSetup.confirmChangeUserId.title": "تغيير معرف المستخدم؟",
"profileSetup.descriptionEdit": "قم بتحديث معلومات ملفك الشخصي في المجتمع.",
"profileSetup.descriptionFirstTime": "قم بإعداد ملفك لإكمال ملفك الشخصي في المجتمع.",
"profileSetup.errors.fileTooLarge": "حجم الملف لا يمكن أن يتجاوز 2 ميغابايت",
+1 -1
View File
@@ -260,8 +260,8 @@
"providerModels.item.modelConfig.type.options.realtime": "دردشة فورية",
"providerModels.item.modelConfig.type.options.stt": "تحويل الكلام إلى نص",
"providerModels.item.modelConfig.type.options.text2music": "نص إلى موسيقى",
"providerModels.item.modelConfig.type.options.text2video": "نص إلى فيديو",
"providerModels.item.modelConfig.type.options.tts": "تحويل النص إلى كلام",
"providerModels.item.modelConfig.type.options.video": "توليد الفيديو",
"providerModels.item.modelConfig.type.placeholder": "يرجى اختيار نوع النموذج",
"providerModels.item.modelConfig.type.title": "نوع النموذج",
"providerModels.item.modelConfig.video.extra": "يُمكّن هذا الإعداد تكوين التعرف على الفيديو داخل التطبيق. يعتمد الدعم على النموذج نفسه. يرجى اختباره.",
+29 -12
View File
@@ -274,22 +274,27 @@
"chatgpt-4o-latest.description": "ChatGPT-4o هو نموذج ديناميكي يتم تحديثه في الوقت الفعلي، يجمع بين الفهم العميق والقدرة على التوليد لتلبية احتياجات الاستخدام الواسعة مثل دعم العملاء والتعليم والدعم الفني.",
"claude-2.0.description": "Claude 2 يقدم تحسينات رئيسية للمؤسسات، بما في ذلك سياق 200 ألف رمز، تقليل الهلوسة، دعم التعليمات النظامية، وميزة جديدة: استدعاء الأدوات.",
"claude-2.1.description": "Claude 2 يقدم تحسينات رئيسية للمؤسسات، بما في ذلك سياق 200 ألف رمز، تقليل الهلوسة، دعم التعليمات النظامية، وميزة جديدة: استدعاء الأدوات.",
"claude-3-5-haiku-20241022.description": "Claude 3.5 Haiku هو أسرع نموذج من الجيل الجديد تقدمه Anthropic. مقارنةً بـ Claude 3 Haiku، فإنه يُظهر تحسنًا في المهارات ويتفوق على أكبر نموذج سابق Claude 3 Opus في العديد من اختبارات الذكاء.",
"claude-3-5-haiku-20241022.description": "Claude 3.5 Haiku هو أسرع نموذج من الجيل الجديد من Anthropic، يتميز بتحسينات في المهارات ويتفوق على النموذج الرائد السابق Claude 3 Opus في العديد من المعايير.",
"claude-3-5-haiku-latest.description": "Claude 3.5 Haiku يقدم استجابات سريعة للمهام الخفيفة.",
"claude-3-7-sonnet-20250219.description": "Claude 3.7 Sonnet هو أذكى نموذج من Anthropic وأول نموذج هجيني للاستدلال في السوق. يمكنه تقديم ردود شبه فورية أو استدلالات متسلسلة خطوة بخطوة يمكن للمستخدمين متابعتها. يتميز Sonnet بقوة خاصة في البرمجة وعلوم البيانات والرؤية والمهام المعتمدة على الوكلاء.",
"claude-3-7-sonnet-20250219.description": "Claude Sonnet 3.7 هو أذكى نموذج من Anthropic وأول نموذج هجيني للاستدلال في السوق، يدعم الاستجابات الفورية أو التفكير المطول مع تحكم دقيق.",
"claude-3-7-sonnet-latest.description": "Claude 3.7 Sonnet هو أحدث وأقوى نموذج من Anthropic للمهام المعقدة، يتميز بالأداء العالي، الذكاء، الطلاقة، والفهم العميق.",
"claude-3-haiku-20240307.description": "Claude 3 Haiku هو أسرع وأصغر نموذج من Anthropic، مصمم لتقديم استجابات شبه فورية بأداء سريع ودقيق.",
"claude-3-opus-20240229.description": "Claude 3 Opus هو أقوى نموذج من Anthropic للمهام المعقدة، يتميز بالأداء العالي، الذكاء، الطلاقة، والفهم.",
"claude-3-sonnet-20240229.description": "Claude 3 Sonnet يوازن بين الذكاء والسرعة لتلبية احتياجات المؤسسات، ويوفر فائدة عالية بتكلفة أقل ونشر موثوق على نطاق واسع.",
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 هو أسرع وأذكى نموذج Haiku من Anthropic، يتميز بسرعة فائقة وقدرة على الاستدلال الموسع.",
"claude-3.5-sonnet.description": "يتميز Claude 3.5 Sonnet بقدرات عالية في البرمجة والكتابة والتفكير المعقد.",
"claude-3.7-sonnet-thought.description": "Claude 3.7 Sonnet مزود بقدرات تفكير موسعة للمهام التي تتطلب استدلالًا معقدًا.",
"claude-3.7-sonnet.description": "Claude 3.7 Sonnet هو إصدار مطور يتمتع بسياق موسع وقدرات محسّنة.",
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 هو أسرع وأذكى نموذج Haiku من Anthropic، يتميز بسرعة فائقة وقدرة على التفكير المتعمق.",
"claude-haiku-4.5.description": "Claude Haiku 4.5 نموذج سريع وفعّال لمجموعة متنوعة من المهام.",
"claude-opus-4-1-20250805-thinking.description": "Claude Opus 4.1 Thinking هو إصدار متقدم يمكنه عرض عملية تفكيره.",
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 هو أحدث وأقوى نموذج من Anthropic للمهام المعقدة للغاية، يتميز بالأداء العالي والذكاء والطلاقة والفهم العميق.",
"claude-opus-4-20250514.description": "Claude Opus 4 هو أقوى نموذج من Anthropic للمهام المعقدة للغاية، يتميز بالأداء والذكاء والطلاقة والاستيعاب.",
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 هو أحدث وأقوى نموذج من Anthropic للمهام المعقدة للغاية، يتفوق في الأداء والذكاء والطلاقة والفهم.",
"claude-opus-4-20250514.description": "Claude Opus 4 هو أقوى نموذج من Anthropic للمهام المعقدة للغاية، يتميز بأداء فائق وذكاء وطلاقة وفهم عميق.",
"claude-opus-4-5-20251101.description": "Claude Opus 4.5 هو النموذج الرائد من Anthropic، يجمع بين الذكاء الاستثنائي والأداء القابل للتوسع، مثالي للمهام المعقدة التي تتطلب استجابات عالية الجودة وتفكير متقدم.",
"claude-opus-4-6.description": "Claude Opus 4.6 هو أذكى نموذج من Anthropic لتطوير الوكلاء والبرمجة.",
"claude-sonnet-4-20250514-thinking.description": "Claude Sonnet 4 Thinking يمكنه تقديم استجابات شبه فورية أو تفكير متسلسل مرئي.",
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 يمكنه تقديم ردود شبه فورية أو تفكير متسلسل خطوة بخطوة مع عرض واضح للعملية.",
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 هو أذكى نموذج قدمته Anthropic حتى الآن.",
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 هو أذكى نموذج من Anthropic حتى الآن، يوفر استجابات شبه فورية أو تفكيرًا متسلسلًا بخطوات دقيقة لمستخدمي واجهة البرمجة.",
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 هو أذكى نموذج من Anthropic حتى الآن.",
"claude-sonnet-4.description": "Claude Sonnet 4 هو الجيل الأحدث مع أداء محسّن في جميع المهام.",
"codegeex-4.description": "CodeGeeX-4 هو مساعد برمجة ذكي يدعم الأسئلة والأجوبة متعددة اللغات وإكمال الشيفرة لزيادة إنتاجية المطورين.",
"codegeex4-all-9b.description": "CodeGeeX4-ALL-9B هو نموذج توليد شيفرة متعدد اللغات يدعم الإكمال والتوليد، تفسير الشيفرة، البحث عبر الإنترنت، استدعاء الوظائف، وأسئلة وأجوبة على مستوى المستودع، ويغطي مجموعة واسعة من سيناريوهات تطوير البرمجيات. يُعد من أفضل نماذج الشيفرة تحت 10B.",
"codegemma.description": "CodeGemma هو نموذج خفيف الوزن لمهام البرمجة المتنوعة، يتيح التكرار السريع والتكامل السلس.",
@@ -358,7 +363,7 @@
"deepseek-ai/deepseek-v3.1-terminus.description": "DeepSeek V3.1 هو نموذج تفكير من الجيل التالي يتمتع بقدرات أقوى في التفكير المعقد وسلسلة التفكير لمهام التحليل العميق.",
"deepseek-ai/deepseek-v3.1.description": "DeepSeek V3.1 هو نموذج تفكير من الجيل التالي يتمتع بقدرات أقوى في التفكير المعقد وسلسلة التفكير لمهام التحليل العميق.",
"deepseek-ai/deepseek-vl2.description": "DeepSeek-VL2 هو نموذج رؤية-لغة MoE يعتمد على DeepSeekMoE-27B مع تنشيط متفرق، ويحقق أداءً قويًا باستخدام 4.5 مليار معلمة نشطة فقط. يتميز في الأسئلة البصرية، وOCR، وفهم المستندات/الجداول/المخططات، والتأريض البصري.",
"deepseek-chat.description": "نموذج مفتوح المصدر جديد يجمع بين القدرات العامة والبرمجية. يحافظ على حوار النموذج العام وقوة النموذج البرمجي، مع تحسين التوافق مع تفضيلات المستخدم. كما يُحسن DeepSeek-V2.5 من مهارات الكتابة واتباع التعليمات.",
"deepseek-chat.description": "DeepSeek V3.2 يوازن بين الاستدلال وطول المخرجات لمهام الأسئلة والأجوبة اليومية وتطبيقات الوكلاء. يحقق نتائج تضاهي GPT-5 في المعايير العامة، وهو الأول في دمج التفكير مع استخدام الأدوات، مما يجعله رائدًا في تقييمات الوكلاء مفتوحة المصدر.",
"deepseek-coder-33B-instruct.description": "DeepSeek Coder 33B هو نموذج لغة برمجية تم تدريبه على 2 تريليون رمز (87٪ كود، 13٪ نص صيني/إنجليزي). يقدم نافذة سياق 16K ومهام الإكمال في المنتصف، ويوفر إكمال كود على مستوى المشاريع وملء مقاطع الكود.",
"deepseek-coder-v2.description": "DeepSeek Coder V2 هو نموذج كود MoE مفتوح المصدر يتميز بأداء قوي في مهام البرمجة، ويضاهي GPT-4 Turbo.",
"deepseek-coder-v2:236b.description": "DeepSeek Coder V2 هو نموذج كود MoE مفتوح المصدر يتميز بأداء قوي في مهام البرمجة، ويضاهي GPT-4 Turbo.",
@@ -381,7 +386,7 @@
"deepseek-r1-fast-online.description": "الإصدار الكامل السريع من DeepSeek R1 مع بحث ويب في الوقت الحقيقي، يجمع بين قدرات بحجم 671B واستجابة أسرع.",
"deepseek-r1-online.description": "الإصدار الكامل من DeepSeek R1 مع 671 مليار معلمة وبحث ويب في الوقت الحقيقي، يوفر فهمًا وتوليدًا أقوى.",
"deepseek-r1.description": "يستخدم DeepSeek-R1 بيانات البداية الباردة قبل التعلم المعزز ويؤدي أداءً مماثلًا لـ OpenAI-o1 في الرياضيات، والبرمجة، والتفكير.",
"deepseek-reasoner.description": "وضع التفكير في DeepSeek V3.2 يُنتج سلسلة من الأفكار قبل الإجابة النهائية لتحسين الدقة.",
"deepseek-reasoner.description": "DeepSeek V3.2 Thinking هو نموذج استدلال عميق يولد سلسلة من الأفكار قبل المخرجات لتحقيق دقة أعلى، ويحقق نتائج تنافسية عالية واستدلالًا يقارن بـ Gemini-3.0-Pro.",
"deepseek-v2.description": "DeepSeek V2 هو نموذج MoE فعال لمعالجة منخفضة التكلفة.",
"deepseek-v2:236b.description": "DeepSeek V2 236B هو نموذج DeepSeek الموجه للبرمجة مع قدرات قوية في توليد الكود.",
"deepseek-v3-0324.description": "DeepSeek-V3-0324 هو نموذج MoE يحتوي على 671 مليار معلمة يتميز بقوة في البرمجة، والقدرات التقنية، وفهم السياق، والتعامل مع النصوص الطويلة.",
@@ -471,7 +476,8 @@
"ernie-speed-pro-128k.description": "ERNIE Speed Pro 128K هو نموذج عالي التوازي وعالي القيمة للخدمات عبر الإنترنت واسعة النطاق وتطبيقات المؤسسات.",
"ernie-x1-turbo-32k.description": "ERNIE X1 Turbo 32K هو نموذج تفكير سريع بسياق 32K للاستدلال المعقد والدردشة متعددة الأدوار.",
"ernie-x1.1-preview.description": "معاينة ERNIE X1.1 هو نموذج تفكير مخصص للتقييم والاختبار.",
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0 هو نموذج توليد صور من ByteDance Seed، يدعم إدخال النصوص والصور ويتميز بإنتاج صور عالية الجودة وقابلة للتحكم بدرجة كبيرة. يُولّد الصور من التعليمات النصية.",
"fal-ai/bytedance/seedream/v4.5.description": "Seedream 4.5، من تطوير فريق Seed في ByteDance، يدعم تحرير الصور المتعددة وتركيبها. يتميز بثبات أكبر في العناصر، ودقة في تنفيذ التعليمات، وفهم للمنطق المكاني، وتعبير جمالي، وتصميم الملصقات والشعارات مع عرض دقيق للنصوص والصور.",
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0، من تطوير فريق Seed في ByteDance، يدعم إدخال النصوص والصور لتوليد صور عالية الجودة وقابلة للتحكم بدرجة كبيرة من خلال الأوامر.",
"fal-ai/flux-kontext/dev.description": "نموذج FLUX.1 يركز على تحرير الصور، ويدعم إدخال النصوص والصور.",
"fal-ai/flux-pro/kontext.description": "FLUX.1 Kontext [pro] يقبل النصوص وصور مرجعية كمدخلات، مما يتيح تعديلات محلية مستهدفة وتحولات معقدة في المشهد العام.",
"fal-ai/flux/krea.description": "Flux Krea [dev] هو نموذج لتوليد الصور يتميز بميول جمالية نحو صور أكثر واقعية وطبيعية.",
@@ -479,8 +485,8 @@
"fal-ai/hunyuan-image/v3.description": "نموذج قوي لتوليد الصور متعدد الوسائط أصلي.",
"fal-ai/imagen4/preview.description": "نموذج عالي الجودة لتوليد الصور من Google.",
"fal-ai/nano-banana.description": "Nano Banana هو أحدث وأسرع وأكثر نماذج Google كفاءةً لتوليد وتحرير الصور من خلال المحادثة.",
"fal-ai/qwen-image-edit.description": "نموذج احترافي لتحرير الصور من فريق Qwen، يدعم التعديلات الدلالية والمظهرية، ويحرر النصوص الصينية والإنجليزية بدقة، كما يتيح تعديلات عالية الجودة مثل نقل الأسلوب وتدوير العناصر.",
"fal-ai/qwen-image.description": "نموذج قوي لتوليد الصور من فريق Qwen يتميز بعرض مميز للنصوص الصينية وأنماط بصرية متنوعة.",
"fal-ai/qwen-image-edit.description": "نموذج احترافي لتحرير الصور من فريق Qwen، يدعم التعديلات الدلالية والمظهرية، وتحرير النصوص بدقة باللغتين الصينية والإنجليزية، ونقل الأسلوب، والتدوير، والمزيد.",
"fal-ai/qwen-image.description": "نموذج قوي لتوليد الصور من فريق Qwen يتميز بعرض قوي للنصوص الصينية وأنماط بصرية متنوعة.",
"flux-1-schnell.description": "نموذج تحويل النص إلى صورة يحتوي على 12 مليار معلمة من Black Forest Labs يستخدم تقنيات تقطير الانتشار العدائي الكامن لتوليد صور عالية الجودة في 1-4 خطوات. ينافس البدائل المغلقة ومتاح بموجب ترخيص Apache-2.0 للاستخدام الشخصي والبحثي والتجاري.",
"flux-dev.description": "FLUX.1 [dev] هو نموذج مفتوح الأوزان ومقطر للاستخدام غير التجاري. يحافظ على جودة صور قريبة من المستوى الاحترافي واتباع التعليمات مع كفاءة تشغيل أعلى مقارنة بالنماذج القياسية من نفس الحجم.",
"flux-kontext-max.description": "توليد وتحرير صور سياقية متقدمة، تجمع بين النصوص والصور لتحقيق نتائج دقيقة ومتسقة.",
@@ -511,6 +517,8 @@
"gemini-2.0-flash-lite-001.description": "إصدار من Gemini 2.0 Flash محسن لتقليل التكلفة وتقليل التأخير.",
"gemini-2.0-flash-lite.description": "إصدار من Gemini 2.0 Flash محسن لتقليل التكلفة وتقليل التأخير.",
"gemini-2.0-flash.description": "Gemini 2.0 Flash يقدم ميزات الجيل التالي بما في ذلك السرعة الاستثنائية، واستخدام الأدوات الأصلية، والتوليد متعدد الوسائط، وسياق يصل إلى مليون رمز.",
"gemini-2.5-flash-image-preview.description": "Nano Banana هو أحدث وأسرع وأكثر النماذج متعددة الوسائط كفاءة من Google، يتيح توليد الصور وتحريرها من خلال المحادثة.",
"gemini-2.5-flash-image-preview:image.description": "Nano Banana هو أحدث وأسرع وأكثر النماذج متعددة الوسائط كفاءة من Google، يتيح توليد الصور وتحريرها من خلال المحادثة.",
"gemini-2.5-flash-image.description": "Nano Banana هو أحدث وأسرع وأكثر نماذج Google متعددة الوسائط كفاءة، يتيح توليد الصور وتحريرها عبر المحادثة.",
"gemini-2.5-flash-image:image.description": "Nano Banana هو أحدث وأسرع وأكثر نماذج Google متعددة الوسائط كفاءة، يتيح توليد الصور وتحريرها عبر المحادثة.",
"gemini-2.5-flash-lite-preview-06-17.description": "Gemini 2.5 Flash-Lite Preview هو أصغر نموذج من Google وأفضلها من حيث القيمة، مصمم للاستخدام واسع النطاق.",
@@ -604,6 +612,7 @@
"google/text-embedding-005.description": "نموذج تضمين نصي يركز على اللغة الإنجليزية، محسّن لمهام البرمجة واللغة الإنجليزية.",
"google/text-multilingual-embedding-002.description": "نموذج تضمين نصي متعدد اللغات محسّن للمهام عبر اللغات المختلفة.",
"gpt-3.5-turbo-0125.description": "GPT 3.5 Turbo لتوليد النصوص وفهمها؛ يشير حاليًا إلى gpt-3.5-turbo-0125.",
"gpt-3.5-turbo-0613.description": "GPT 3.5 Turbo نموذج سريع وفعّال لمهام متعددة.",
"gpt-3.5-turbo-1106.description": "GPT 3.5 Turbo لتوليد النصوص وفهمها؛ يشير حاليًا إلى gpt-3.5-turbo-0125.",
"gpt-3.5-turbo-instruct.description": "GPT 3.5 Turbo لمهام توليد النصوص والفهم، محسّن لاتباع التعليمات.",
"gpt-3.5-turbo.description": "GPT 3.5 Turbo لتوليد النصوص وفهمها؛ يشير حاليًا إلى gpt-3.5-turbo-0125.",
@@ -614,10 +623,12 @@
"gpt-4-1106-preview.description": "أحدث إصدار من GPT-4 Turbo يدعم الرؤية. الطلبات البصرية تدعم وضع JSON واستدعاء الوظائف. إنه نموذج متعدد الوسائط فعال من حيث التكلفة يوازن بين الدقة والكفاءة للتطبيقات في الوقت الحقيقي.",
"gpt-4-32k-0613.description": "يوفر GPT-4 نافذة سياق أكبر للتعامل مع مدخلات أطول في السيناريوهات التي تتطلب دمج معلومات واسع وتحليل بيانات.",
"gpt-4-32k.description": "يوفر GPT-4 نافذة سياق أكبر للتعامل مع مدخلات أطول في السيناريوهات التي تتطلب دمج معلومات واسع وتحليل بيانات.",
"gpt-4-o-preview.description": "GPT-4o هو النموذج متعدد الوسائط الأكثر تقدمًا، يدعم إدخال النصوص والصور.",
"gpt-4-turbo-2024-04-09.description": "أحدث إصدار من GPT-4 Turbo يدعم الرؤية. الطلبات البصرية تدعم وضع JSON واستدعاء الوظائف. إنه نموذج متعدد الوسائط فعال من حيث التكلفة يوازن بين الدقة والكفاءة للتطبيقات في الوقت الحقيقي.",
"gpt-4-turbo-preview.description": "أحدث إصدار من GPT-4 Turbo يدعم الرؤية. الطلبات البصرية تدعم وضع JSON واستدعاء الوظائف. إنه نموذج متعدد الوسائط فعال من حيث التكلفة يوازن بين الدقة والكفاءة للتطبيقات في الوقت الحقيقي.",
"gpt-4-turbo.description": "أحدث إصدار من GPT-4 Turbo يدعم الرؤية. الطلبات البصرية تدعم وضع JSON واستدعاء الوظائف. إنه نموذج متعدد الوسائط فعال من حيث التكلفة يوازن بين الدقة والكفاءة للتطبيقات في الوقت الحقيقي.",
"gpt-4-vision-preview.description": "معاينة GPT-4 Vision، مصمم لمهام تحليل ومعالجة الصور.",
"gpt-4.1-2025-04-14.description": "GPT-4.1 هو النموذج الرائد للمهام المعقدة، مثالي لحل المشكلات متعددة المجالات.",
"gpt-4.1-mini.description": "GPT-4.1 mini يوازن بين الذكاء والسرعة والتكلفة، مما يجعله جذابًا للعديد من الاستخدامات.",
"gpt-4.1-nano.description": "GPT-4.1 nano هو الأسرع والأكثر فعالية من حيث التكلفة بين نماذج GPT-4.1.",
"gpt-4.1.description": "GPT-4.1 هو نموذجنا الرائد للمهام المعقدة وحل المشكلات عبر المجالات.",
@@ -627,6 +638,7 @@
"gpt-4o-2024-08-06.description": "ChatGPT-4o هو نموذج ديناميكي يتم تحديثه في الوقت الحقيقي، يجمع بين الفهم القوي والتوليد لتطبيقات واسعة النطاق مثل دعم العملاء والتعليم والمساعدة التقنية.",
"gpt-4o-2024-11-20.description": "ChatGPT-4o هو نموذج ديناميكي يتم تحديثه في الوقت الحقيقي، يجمع بين الفهم القوي والتوليد لتطبيقات واسعة النطاق مثل دعم العملاء والتعليم والدعم الفني.",
"gpt-4o-audio-preview.description": "نموذج معاينة GPT-4o Audio مع إدخال وإخراج صوتي.",
"gpt-4o-mini-2024-07-18.description": "GPT-4o mini هو حل اقتصادي لمجموعة واسعة من مهام النصوص والصور.",
"gpt-4o-mini-audio-preview.description": "نموذج GPT-4o mini Audio مع إدخال وإخراج صوتي.",
"gpt-4o-mini-realtime-preview.description": "إصدار GPT-4o-mini الفوري مع إدخال وإخراج صوتي ونصي في الوقت الحقيقي.",
"gpt-4o-mini-search-preview.description": "GPT-4o mini Search Preview مدرب على فهم وتنفيذ استعلامات البحث عبر الإنترنت من خلال واجهة Chat Completions API. يتم احتساب تكلفة البحث عبر الإنترنت لكل استخدام أداة بالإضافة إلى تكلفة الرموز.",
@@ -980,6 +992,8 @@
"openai/text-embedding-3-small.description": "إصدار محسّن عالي الأداء من نموذج تضمين ada.",
"openai/text-embedding-ada-002.description": "نموذج تضمين النصوص القديم من OpenAI.",
"openrouter/auto.description": "استنادًا إلى طول السياق والموضوع والتعقيد، يتم توجيه طلبك إلى Llama 3 70B Instruct أو Claude 3.5 Sonnet (بمراقبة ذاتية) أو GPT-4o.",
"oswe-vscode-prime.description": "Raptor mini هو نموذج تجريبي محسن لمهام البرمجة.",
"oswe-vscode-secondary.description": "Raptor mini هو نموذج تجريبي محسن لمهام البرمجة.",
"perplexity/sonar-pro.description": "المنتج الرائد من Perplexity مع دعم البحث، يدعم الاستفسارات المتقدمة والمتابعة.",
"perplexity/sonar-reasoning-pro.description": "نموذج متقدم يركز على التفكير، ينتج سلسلة تفكير (CoT) مع بحث محسّن، بما في ذلك استعلامات بحث متعددة لكل طلب.",
"perplexity/sonar-reasoning.description": "نموذج يركز على التفكير، ينتج سلسلة تفكير (CoT) مع شروحات مفصلة مدعومة بالبحث.",
@@ -1122,6 +1136,7 @@
"qwq.description": "QwQ هو نموذج استدلال من عائلة Qwen. مقارنة بالنماذج المضبوطة على التعليمات، يقدم قدرات تفكير واستدلال تعزز الأداء بشكل كبير، خاصة في المشكلات الصعبة. QwQ-32B هو نموذج متوسط الحجم ينافس أفضل نماذج الاستدلال مثل DeepSeek-R1 و o1-mini.",
"qwq_32b.description": "نموذج استدلال متوسط الحجم من عائلة Qwen. مقارنة بالنماذج المضبوطة على التعليمات، تعزز قدرات التفكير والاستدلال في QwQ الأداء بشكل كبير، خاصة في المشكلات الصعبة.",
"r1-1776.description": "R1-1776 هو إصدار ما بعد التدريب من DeepSeek R1 مصمم لتقديم معلومات واقعية غير خاضعة للرقابة أو التحيز.",
"seedance-1-5-pro-251215.description": "Seedance 1.5 Pro من ByteDance يدعم تحويل النصوص إلى فيديو، والصور إلى فيديو (الإطار الأول، أو الإطار الأول والأخير)، وتوليد الصوت المتزامن مع العناصر البصرية.",
"solar-mini-ja.description": "Solar Mini (Ja) يوسع Solar Mini مع تركيز على اللغة اليابانية مع الحفاظ على الأداء القوي والكفاءة في الإنجليزية والكورية.",
"solar-mini.description": "Solar Mini هو نموذج لغة مدمج يتفوق على GPT-3.5، يتميز بقدرات متعددة اللغات قوية تدعم الإنجليزية والكورية، ويقدم حلاً فعالاً بصمة صغيرة.",
"solar-pro.description": "Solar Pro هو نموذج لغة عالي الذكاء من Upstage، يركز على اتباع التعليمات باستخدام وحدة معالجة رسومات واحدة، مع درجات IFEval تتجاوز 80. حالياً يدعم اللغة الإنجليزية؛ وكان من المقرر إصدار النسخة الكاملة في نوفمبر 2024 مع دعم لغات موسع وسياق أطول.",
@@ -1162,7 +1177,9 @@
"tencent/Hunyuan-A13B-Instruct.description": "Hunyuan-A13B-Instruct يستخدم 80 مليار معلمة إجمالية مع 13 مليار نشطة لمضاهاة النماذج الأكبر. يدعم الاستدلال الهجين السريع/البطيء، وفهم النصوص الطويلة بثبات، وقدرات وكيل رائدة على BFCL-v3 وτ-Bench. تدعم تنسيقات GQA والتكميم المتعدد الاستدلال بكفاءة.",
"tencent/Hunyuan-MT-7B.description": "نموذج الترجمة Hunyuan يشمل Hunyuan-MT-7B وHunyuan-MT-Chimera. Hunyuan-MT-7B هو نموذج ترجمة خفيف بسعة 7B يدعم 33 لغة بالإضافة إلى 5 لغات صينية محلية. حصل على المركز الأول في 30 من أصل 31 زوج لغوي في WMT25. يستخدم Hunyuan من Tencent سلسلة تدريب كاملة من التدريب المسبق إلى SFT إلى الترجمة بالتعلم المعزز، محققًا أداءً رائدًا بحجمه وسهولة في النشر.",
"text-embedding-3-large.description": "أقوى نموذج تضمين للمهام باللغة الإنجليزية وغير الإنجليزية.",
"text-embedding-3-small-inference.description": "نموذج Embedding V3 صغير (للاستدلال) لتضمين النصوص.",
"text-embedding-3-small.description": "نموذج تضمين من الجيل التالي فعال من حيث التكلفة ومناسب للاسترجاع وسيناريوهات RAG.",
"text-embedding-ada-002.description": "نموذج Embedding V2 Ada لتضمين النصوص.",
"thudm/glm-4-32b.description": "GLM-4-32B-0414 هو نموذج ثنائي اللغة (صيني/إنجليزي) بسعة 32B وأوزان مفتوحة، مُحسَّن لتوليد الشيفرات، واستدعاء الوظائف، ومهام الوكلاء. تم تدريبه مسبقًا على 15 تريليون رمز عالي الجودة ومليء بالاستدلال، وتم تحسينه بموازنة تفضيلات البشر، وأخذ العينات بالرفض، والتعلم المعزز. يتفوق في الاستدلال المعقد، وتوليد المخرجات المنظمة، ويصل إلى مستوى أداء GPT-4o وDeepSeek-V3-0324 في العديد من المعايير.",
"thudm/glm-4-32b:free.description": "GLM-4-32B-0414 هو نموذج ثنائي اللغة (صيني/إنجليزي) بسعة 32B وأوزان مفتوحة، مُحسَّن لتوليد الشيفرات، واستدعاء الوظائف، ومهام الوكلاء. تم تدريبه مسبقًا على 15 تريليون رمز عالي الجودة ومليء بالاستدلال، وتم تحسينه بموازنة تفضيلات البشر، وأخذ العينات بالرفض، والتعلم المعزز. يتفوق في الاستدلال المعقد، وتوليد المخرجات المنظمة، ويصل إلى مستوى أداء GPT-4o وDeepSeek-V3-0324 في العديد من المعايير.",
"thudm/glm-4-9b-chat.description": "الإصدار مفتوح المصدر من نموذج GLM-4 الأحدث من Zhipu AI.",
+1
View File
@@ -30,6 +30,7 @@
"internlm.description": "منظمة مفتوحة المصدر تركز على أبحاث النماذج الكبيرة والأدوات، وتوفر منصة فعالة وسهلة الاستخدام تتيح الوصول إلى أحدث النماذج والخوارزميات.",
"jina.description": "تأسست Jina AI في عام 2020، وهي شركة رائدة في مجال البحث الذكي. تشمل تقنياتها نماذج المتجهات، ومعيدو الترتيب، ونماذج لغوية صغيرة لبناء تطبيقات بحث توليدية ومتعددة الوسائط عالية الجودة.",
"lmstudio.description": "LM Studio هو تطبيق سطح مكتب لتطوير وتجربة النماذج اللغوية الكبيرة على جهازك.",
"lobehub.description": "يستخدم LobeHub Cloud واجهات برمجة التطبيقات الرسمية للوصول إلى نماذج الذكاء الاصطناعي، ويقيس الاستخدام من خلال الأرصدة المرتبطة برموز النماذج.",
"minimax.description": "تأسست MiniMax في عام 2021، وتبني نماذج ذكاء اصطناعي متعددة الوسائط للأغراض العامة، بما في ذلك نماذج نصية بمليارات المعلمات، ونماذج صوتية وبصرية، بالإضافة إلى تطبيقات مثل Hailuo AI.",
"mistral.description": "تقدم Mistral نماذج متقدمة عامة ومتخصصة وبحثية للتفكير المعقد، والمهام متعددة اللغات، وتوليد الأكواد، مع دعم استدعاء الوظائف للتكامل المخصص.",
"modelscope.description": "ModelScope هي منصة نماذج كخدمة من Alibaba Cloud، تقدم مجموعة واسعة من النماذج وخدمات الاستدلال.",
+1
View File
@@ -14,6 +14,7 @@
"table.columns.totalTokens": "استخدام الرموز",
"table.columns.type.enums.chat": "توليد نصوص",
"table.columns.type.enums.imageGeneration": "توليد صور",
"table.columns.type.enums.videoGeneration": "توليد الفيديو",
"table.columns.type.title": "النوع",
"table.desc": "تفاصيل استخدام الاعتمادات الحاسوبية لتوليد النصوص، التضمين، توليد الصور، وغيرها.",
"table.more": "عرض التفاصيل",
+6
View File
@@ -131,6 +131,12 @@
"limitation.providers.prompter.subTitle": "خدمة API المخصصة متاحة فقط للخطط المدفوعة. قم بالترقية الآن للاستفادة من خدمات النماذج العالمية",
"limitation.providers.prompter.title": "اشترك الآن لاستخدام خدمة API مخصصة",
"limitation.providers.tooltip": "خدمة API المخصصة متاحة فقط للخطط المدفوعة",
"limitation.video.success.action": "تابع التوليد",
"limitation.video.success.desc": "تمت ترقية اشتراكك في خطة {{plan}} بنجاح. استمتع بتوليد الفيديو باستخدام الذكاء الاصطناعي. خطتك الحالية تتضمن:",
"limitation.video.success.title": "تمت الترقية بنجاح",
"limitation.video.topupSuccess.action": "تابع التوليد",
"limitation.video.topupSuccess.desc": "تم تفعيل رصيد الشحن الخاص بك. استمتع بتوليد الفيديو باستخدام الذكاء الاصطناعي. خطتك الحالية تتضمن:",
"limitation.video.topupSuccess.title": "تم الشحن بنجاح",
"modelPricing.button": "عرض مستندات التسعير",
"modelPricing.desc": "يستخدم {{name}} الأرصدة لقياس استخدام نموذج الذكاء الاصطناعي. يوضح الجدول أدناه أرصدة الحوسبة لكل 1M رموز.",
"modelPricing.title": "تسعير نموذج النص",
+35
View File
@@ -86,6 +86,10 @@
"localFiles.editFile.replaceFirst": "استبدال التكرار الأول فقط",
"localFiles.file": "ملف",
"localFiles.folder": "مجلد",
"localFiles.globFiles.pattern": "النمط",
"localFiles.grepContent.glob": "تصفية الملفات",
"localFiles.grepContent.pattern": "نمط البحث",
"localFiles.grepContent.type": "نوع الملف",
"localFiles.moveFiles.itemsMoved": "تم نقل {{count}} عنصر(عناصر):",
"localFiles.moveFiles.itemsMoved_one": "تم نقل عنصر واحد:",
"localFiles.moveFiles.itemsMoved_other": "تم نقل {{count}} عناصر:",
@@ -95,11 +99,17 @@
"localFiles.open": "فتح",
"localFiles.openFile": "فتح ملف",
"localFiles.openFolder": "فتح مجلد",
"localFiles.outOfScope.requestedPaths": "المسارات المطلوبة",
"localFiles.outOfScope.warning": "تحذير: المسار(ات) التالية تقع خارج دليل العمل المُحدد. يرجى التأكيد إذا كنت ترغب في السماح بالوصول.",
"localFiles.outOfScope.workingDirectory": "دليل العمل",
"localFiles.read.more": "عرض المزيد",
"localFiles.readFile": "قراءة الملف",
"localFiles.readFile.lineRange": "الأسطر {{start}} - {{end}}",
"localFiles.readFileError": "فشل في قراءة الملف، يرجى التحقق من صحة المسار",
"localFiles.readFiles": "قراءة الملفات",
"localFiles.readFilesError": "فشل في قراءة الملفات، يرجى التحقق من صحة المسار",
"localFiles.searchFiles.keywords": "الكلمات المفتاحية",
"localFiles.securityBlacklist.warning": "تنبيه أمني: تم تمييز هذه العملية بواسطة قواعد الأمان وتتطلب موافقتك الصريحة.",
"localFiles.writeFile.characters": "أحرف",
"localFiles.writeFile.preview": "معاينة المحتوى",
"localFiles.writeFile.truncated": "مقتطع",
@@ -136,6 +146,31 @@
"search.summary": "الملخص",
"search.summaryTooltip": "تلخيص المحتوى الحالي",
"search.viewMoreResults": "عرض {{results}} نتيجة إضافية",
"securityBlacklist.awsCredentials": "الوصول إلى بيانات اعتماد AWS قد يؤدي إلى تسريب مفاتيح الوصول السحابية",
"securityBlacklist.browserCredentials": "الوصول إلى تخزين بيانات اعتماد المتصفح قد يؤدي إلى تسريب كلمات المرور",
"securityBlacklist.chownSystemDirs": "تغيير ملكية مجلدات النظام أمر خطير",
"securityBlacklist.ddDiskWrite": "كتابة بيانات عشوائية على أجهزة التخزين قد يؤدي إلى تدمير البيانات",
"securityBlacklist.directMemoryAccess": "الوصول المباشر إلى الذاكرة أمر بالغ الخطورة",
"securityBlacklist.disableFirewall": "تعطيل جدار الحماية يعرض النظام للهجمات",
"securityBlacklist.dockerConfig": "قراءة إعدادات Docker قد تكشف بيانات اعتماد التسجيل",
"securityBlacklist.envFiles": "قراءة ملفات .env قد تؤدي إلى تسريب بيانات اعتماد حساسة ومفاتيح API",
"securityBlacklist.etcPasswd": "تعديل /etc/passwd قد يؤدي إلى فقدان الوصول إلى النظام",
"securityBlacklist.forkBomb": "قنبلة fork قد تتسبب في انهيار النظام",
"securityBlacklist.formatPartition": "تهيئة أقسام النظام ستؤدي إلى تدمير البيانات",
"securityBlacklist.gcpCredentials": "قراءة بيانات اعتماد GCP قد تؤدي إلى تسريب مفاتيح حسابات الخدمات السحابية",
"securityBlacklist.gitCredentials": "قراءة ملف بيانات اعتماد Git قد يؤدي إلى تسريب رموز الوصول",
"securityBlacklist.historyFiles": "قراءة ملفات السجل قد تكشف أوامر وبيانات اعتماد حساسة",
"securityBlacklist.kernelParams": "تعديل معلمات النواة بدون فهم قد يؤدي إلى انهيار النظام",
"securityBlacklist.kubeConfig": "قراءة إعدادات Kubernetes قد تكشف بيانات اعتماد الكتلة",
"securityBlacklist.npmrc": "قراءة ملف رمز npm قد يؤدي إلى تسريب بيانات اعتماد مستودع الحزم",
"securityBlacklist.removeSystemPackages": "إزالة حزم النظام الأساسية قد تؤدي إلى تعطل النظام",
"securityBlacklist.rmForceRecursive": "الحذف القسري التكراري بدون هدف محدد أمر بالغ الخطورة",
"securityBlacklist.rmHomeDir": "الحذف التكراري لمجلد المنزل أمر بالغ الخطورة",
"securityBlacklist.rmRootDir": "الحذف التكراري لمجلد الجذر سيؤدي إلى تدمير النظام",
"securityBlacklist.sshConfig": "تغيير إعدادات SSH قد يؤدي إلى فقدان الوصول",
"securityBlacklist.sshPrivateKeys": "قراءة مفاتيح SSH الخاصة قد تعرض أمان النظام للخطر",
"securityBlacklist.sudoers": "تعديل ملف sudoers بدون تحقق مناسب أمر خطير",
"securityBlacklist.suidShells": "تعيين SUID للأصداف أو المفسرات يمثل خطراً أمنياً",
"updateArgs.duplicateKeyError": "يجب أن يكون مفتاح الحقل فريدًا",
"updateArgs.form.add": "إضافة عنصر",
"updateArgs.form.key": "مفتاح الحقل",
+28
View File
@@ -0,0 +1,28 @@
{
"config.aspectRatio.label": "نسبة العرض إلى الارتفاع",
"config.cameraFixed.label": "كاميرا ثابتة",
"config.duration.label": "المدة",
"config.endImageUrl.label": "الإطار النهائي",
"config.generateAudio.label": "توليد صوت",
"config.header.title": "فيديو",
"config.imageUrl.label": "الإطار الابتدائي",
"config.prompt.placeholder": "صف الفيديو الذي ترغب في إنشائه",
"config.referenceImage.label": "صورة مرجعية",
"config.resolution.label": "الدقة",
"config.seed.label": "البذرة",
"config.seed.random": "عشوائي",
"generation.actions.copyError": "نسخ رسالة الخطأ",
"generation.actions.errorCopied": "تم نسخ رسالة الخطأ إلى الحافظة",
"generation.actions.errorCopyFailed": "فشل في نسخ رسالة الخطأ",
"generation.actions.generate": "إنشاء",
"generation.freeQuota.exhausted": "🎁 تم استهلاك الحصة المجانية، سيتم استخدام الرصيد",
"generation.freeQuota.remaining": "🎁 {{remaining}} فيديو مجاني متبقٍ اليوم",
"generation.status.failed": "فشل في الإنشاء",
"generation.status.generating": "جارٍ الإنشاء...",
"generation.validation.endFrameRequiresStartFrame": "لا يمكن استخدام الإطار النهائي بدون إطار ابتدائي. يرجى تعيين إطار ابتدائي أولاً.",
"topic.createNew": "موضوع جديد",
"topic.deleteConfirm": "حذف موضوع الفيديو",
"topic.deleteConfirmDesc": "أنت على وشك حذف موضوع الفيديو هذا. لا يمكن التراجع عن هذا الإجراء.",
"topic.title": "مواضيع الفيديو",
"topic.untitled": "موضوع افتراضي"
}
+3 -2
View File
@@ -58,13 +58,13 @@
"duplicateTitle": "{{title}} - Копие",
"emptyAgent": "Все още няма Агенти. Започнете с първия си Агент — изградете системата си с времето.",
"emptyAgentAction": "Създай Агент",
"extendParams.disableContextCaching.desc": "Намалява до 90% от разходите за генериране на един разговор и увеличава скоростта до 4 пъти. Активирането автоматично премахва ограничението за брой исторически съобщения. <1>Научете повече</1>",
"extendParams.disableContextCaching.desc": "Намалете до 90% от разходите за генериране на един разговор и постигнете до 4 пъти по-висока скорост. <1>Научете повече</1>",
"extendParams.disableContextCaching.title": "Активирай кеширане на контекста",
"extendParams.effort.desc": "Контролирайте колко токени използва Claude при отговор чрез параметъра за усилие.",
"extendParams.effort.title": "Усилие",
"extendParams.enableAdaptiveThinking.desc": "Позволете на Claude динамично да решава кога и колко да мисли с режима за адаптивно мислене.",
"extendParams.enableAdaptiveThinking.title": "Активирай адаптивно мислене",
"extendParams.enableReasoning.desc": "Въз основа на ограничението на механизма Claude Thinking, активирането автоматично премахва ограничението за брой исторически съобщения. <1>Научете повече</1>",
"extendParams.enableReasoning.desc": "Базирано на ограничението на механизма за мислене на Claude. <1>Научете повече</1>",
"extendParams.enableReasoning.title": "Активирай дълбоко мислене",
"extendParams.imageAspectRatio.title": "Съотношение на изображението",
"extendParams.imageResolution.title": "Резолюция на изображението",
@@ -165,6 +165,7 @@
"messageAction.delAndRegenerate": "Изтрий и генерирай отново",
"messageAction.deleteDisabledByThreads": "Това съобщение има подтема и не може да бъде изтрито",
"messageAction.expand": "Разгъни съобщението",
"messageAction.reaction": "Добави реакция",
"messageAction.regenerate": "Генерирай отново",
"messages.dm.sentTo": "Видимо само за {{name}}",
"messages.dm.title": "ЛС",
+2
View File
@@ -143,6 +143,7 @@
"cmdk.keywords.stats": "статистики анализи",
"cmdk.keywords.submitIssue": "проблем бъг обратна връзка",
"cmdk.keywords.usage": "използване статистика консумация квота",
"cmdk.keywords.video": "видео,генерирай,seedance,kling",
"cmdk.memory": "Памет",
"cmdk.mentionAgent": "Спомени агент",
"cmdk.navigate": "Навигирай",
@@ -193,6 +194,7 @@
"cmdk.themeLight": "Светла",
"cmdk.toOpen": "Отвори",
"cmdk.toSelect": "Избери",
"cmdk.video": "AI Видео",
"confirm": "Потвърди",
"contact": "Свържете се с нас",
"copy": "Копирай",
+31
View File
@@ -43,6 +43,8 @@
"FileManager.emptyStatus.or": "или",
"FileManager.emptyStatus.title": "Плъзнете файлове или папки тук",
"FileManager.noFolders": "Няма налични папки",
"FileManager.search.noResults": "Няма намерени файлове",
"FileManager.search.placeholder": "Търсене на файлове...",
"FileManager.sort.dateAdded": "Дата на добавяне",
"FileManager.sort.name": "Име",
"FileManager.sort.size": "Размер",
@@ -94,6 +96,35 @@
"ModelSelect.removed": "Моделът не е в списъка. Ще бъде автоматично премахнат, ако бъде деселектиран.",
"ModelSwitchPanel.byModel": "По модел",
"ModelSwitchPanel.byProvider": "По доставчик",
"ModelSwitchPanel.detail.abilities": "Възможности",
"ModelSwitchPanel.detail.abilities.files": "Файлове",
"ModelSwitchPanel.detail.abilities.functionCall": "Извикване на инструмент",
"ModelSwitchPanel.detail.abilities.imageOutput": "Изход на изображение",
"ModelSwitchPanel.detail.abilities.reasoning": "Разсъждение",
"ModelSwitchPanel.detail.abilities.search": "Търсене",
"ModelSwitchPanel.detail.abilities.video": "Видео",
"ModelSwitchPanel.detail.abilities.vision": "Визия",
"ModelSwitchPanel.detail.config": "Конфигурация на модела",
"ModelSwitchPanel.detail.context": "Дължина на контекста",
"ModelSwitchPanel.detail.pricing": "Ценообразуване",
"ModelSwitchPanel.detail.pricing.cachedInput": "Кеширан вход ${{amount}}/М",
"ModelSwitchPanel.detail.pricing.group.audio": "Аудио",
"ModelSwitchPanel.detail.pricing.group.image": "Изображение",
"ModelSwitchPanel.detail.pricing.group.text": "Текст",
"ModelSwitchPanel.detail.pricing.input": "Вход ${{amount}}/М",
"ModelSwitchPanel.detail.pricing.output": "Изход ${{amount}}/М",
"ModelSwitchPanel.detail.pricing.unit.audioInput": "Аудио вход",
"ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Аудио вход (кеширан)",
"ModelSwitchPanel.detail.pricing.unit.audioOutput": "Аудио изход",
"ModelSwitchPanel.detail.pricing.unit.imageGeneration": "Генериране на изображение",
"ModelSwitchPanel.detail.pricing.unit.imageInput": "Вход на изображение",
"ModelSwitchPanel.detail.pricing.unit.imageInput_cacheRead": "Вход на изображение (кеширан)",
"ModelSwitchPanel.detail.pricing.unit.imageOutput": "Изход на изображение",
"ModelSwitchPanel.detail.pricing.unit.textInput": "Вход",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheRead": "Вход (кеширан)",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheWrite": "Вход (запис в кеш)",
"ModelSwitchPanel.detail.pricing.unit.textOutput": "Изход",
"ModelSwitchPanel.detail.releasedAt": "Пуснат на {{date}}",
"ModelSwitchPanel.emptyModel": "Няма активиран модел. Моля, отидете в настройките, за да активирате.",
"ModelSwitchPanel.emptyProvider": "Няма активирани доставчици. Моля, отидете в настройките, за да активирате такъв.",
"ModelSwitchPanel.goToSettings": "Отиди в настройките",
+9
View File
@@ -150,6 +150,9 @@
"groupAgents.tag": "Група",
"groupAgents.underReview": "В процес на преглед",
"home.communityAgents": "Агенти от общността",
"home.creatorReward.action": "Кандидатствай сега",
"home.creatorReward.subtitle": "Програмата за възнаграждение на създатели за 2026 г. е официално стартирана.",
"home.creatorReward.title": "Създавай. Споделяй. Получавай възнаграждение.",
"home.featuredAssistants": "Препоръчани агенти",
"home.featuredModels": "Препоръчани модели",
"home.featuredPlugins": "Препоръчани умения",
@@ -194,6 +197,8 @@
"mcp.categories.tools.name": "Помощни инструменти",
"mcp.categories.travel-transport.description": "Планиране на пътувания и транспорт",
"mcp.categories.travel-transport.name": "Пътуване и транспорт",
"mcp.categories.utility.description": "Прогноза за времето и метеорологични услуги",
"mcp.categories.utility.name": "Услуги",
"mcp.categories.weather.description": "Прогноза за времето и метеорологични услуги",
"mcp.categories.weather.name": "Времето",
"mcp.categories.web-search.description": "Уеб търсене и извличане на информация",
@@ -478,6 +483,10 @@
"tab.plugin": "Умение",
"tab.provider": "Доставчик",
"tab.user": "Потребител",
"time.formatOtherYear": "D MMM, YYYY",
"time.formatThisYear": "D MMM",
"time.today": "Днес",
"time.yesterday": "Вчера",
"user.agents": "Агенти",
"user.downloads": "Изтегляния",
"user.editProfile": "Редактирай профил",
+1
View File
@@ -10,5 +10,6 @@
"starter.deepResearch": "Задълбочено проучване",
"starter.developing": "Очаквайте скоро",
"starter.image": "Изображение",
"starter.seedance": "Seedance 2.0",
"starter.write": "Писане"
}
+4 -1
View File
@@ -9,7 +9,10 @@
"addToKnowledgeBase.title": "Добавяне към библиотека",
"addToKnowledgeBase.totalFiles": "{{count}} избрани файла",
"createNew.confirm": "Създай нова",
"createNew.description.placeholder": "Описание на библиотеката (по избор)",
"createNew.description.label": "Описание на библиотеката (по избор)",
"createNew.description.placeholder": "Описанието помага на LLM да разбере по-добре вашата библиотека",
"createNew.edit.confirm": "Запази промените",
"createNew.edit.title": "Редактиране на библиотека",
"createNew.formTitle": "Основна информация",
"createNew.name.placeholder": "Име на библиотеката",
"createNew.name.required": "Моля, въведете име на библиотеката",
+4
View File
@@ -38,6 +38,10 @@
"messages.success.submit": "Удостоверяването е успешно! Вече можете да публикувате своя агент.",
"messages.success.upload": "Удостоверяването е успешно! Вече можете да публикувате нова версия.",
"profileSetup.cancel": "Отказ",
"profileSetup.confirmChangeUserId.cancel": "Отказ",
"profileSetup.confirmChangeUserId.confirm": "Промени потребителското име",
"profileSetup.confirmChangeUserId.description": "След като преминете към @{{newId}}, всеки ще може да заеме старото ви потребителско име @{{oldId}} и всички съществуващи връзки към вашия профил ще спрат да работят. Това действие не може да бъде отменено. Сигурни ли сте, че искате да продължите?",
"profileSetup.confirmChangeUserId.title": "Промяна на потребителското име?",
"profileSetup.descriptionEdit": "Актуализирай информацията в профила си в общността.",
"profileSetup.descriptionFirstTime": "Настрой профила си, за да го завършиш.",
"profileSetup.errors.fileTooLarge": "Размерът на файла не може да надвишава 2MB",
+1 -1
View File
@@ -260,8 +260,8 @@
"providerModels.item.modelConfig.type.options.realtime": "Чат в реално време",
"providerModels.item.modelConfig.type.options.stt": "Реч към текст",
"providerModels.item.modelConfig.type.options.text2music": "Текст към музика",
"providerModels.item.modelConfig.type.options.text2video": "Текст към видео",
"providerModels.item.modelConfig.type.options.tts": "Текст към реч",
"providerModels.item.modelConfig.type.options.video": "Генериране на видео",
"providerModels.item.modelConfig.type.placeholder": "Изберете тип модел",
"providerModels.item.modelConfig.type.title": "Тип модел",
"providerModels.item.modelConfig.video.extra": "Активира конфигурация за разпознаване на видео. Поддръжката зависи от модела. Моля, тествайте.",
+70 -11
View File
@@ -274,22 +274,27 @@
"chatgpt-4o-latest.description": "ChatGPT-4o е динамичен модел, актуализиран в реално време, комбиниращ силно разбиране и генериране за мащабни приложения като клиентска поддръжка, образование и техническа помощ.",
"claude-2.0.description": "Claude 2 предлага ключови подобрения за предприятия, включително водещ контекст от 200 000 токена, намалени халюцинации, системни подканвания и нова тестова функция: използване на инструменти.",
"claude-2.1.description": "Claude 2 предлага ключови подобрения за предприятия, включително водещ контекст от 200 000 токена, намалени халюцинации, системни подканвания и нова тестова функция: използване на инструменти.",
"claude-3-5-haiku-20241022.description": "Claude 3.5 Haiku е най-бързият модел от ново поколение на Anthropic. В сравнение с Claude 3 Haiku, той показва подобрения в различни умения и надминава предишния най-голям модел Claude 3 Opus в много интелигентни бенчмаркове.",
"claude-3-5-haiku-20241022.description": "Claude 3.5 Haiku е най-бързият модел от ново поколение на Anthropic, с подобрени умения и превъзхождащ предишния водещ модел Claude 3 Opus в много бенчмаркове.",
"claude-3-5-haiku-latest.description": "Claude 3.5 Haiku осигурява бързи отговори за леки задачи.",
"claude-3-7-sonnet-20250219.description": "Claude 3.7 Sonnet е най-интелигентният модел на Anthropic и първият хибриден модел за разсъждение на пазара. Той може да генерира почти мигновени отговори или разширено поетапно разсъждение, което потребителите могат да проследят. Sonnet е особено силен в програмиране, анализ на данни, визуални задачи и задачи за агенти.",
"claude-3-7-sonnet-20250219.description": "Claude Sonnet 3.7 е най-интелигентният модел на Anthropic и първият хибриден модел за разсъждение на пазара, предлагащ почти мигновени отговори или разширено мислене с прецизен контрол.",
"claude-3-7-sonnet-latest.description": "Claude 3.7 Sonnet е най-новият и най-способен модел на Anthropic за силно сложни задачи, отличаващ се с производителност, интелигентност, плавност и разбиране.",
"claude-3-haiku-20240307.description": "Claude 3 Haiku е най-бързият и най-компактен модел на Anthropic, проектиран за почти мигновени отговори с бърза и точна производителност.",
"claude-3-opus-20240229.description": "Claude 3 Opus е най-мощният модел на Anthropic за силно сложни задачи, отличаващ се с производителност, интелигентност, плавност и разбиране.",
"claude-3-sonnet-20240229.description": "Claude 3 Sonnet балансира интелигентност и скорост за корпоративни натоварвания, осигурявайки висока полезност на по-ниска цена и надеждно мащабно внедряване.",
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 е най-бързият и най-интелигентен Haiku модел на Anthropic, с мълниеносна скорост и разширено разсъждение.",
"claude-3.5-sonnet.description": "Claude 3.5 Sonnet се отличава в програмиране, писане и сложни разсъждения.",
"claude-3.7-sonnet-thought.description": "Claude 3.7 Sonnet с разширено мислене за задачи, изискващи сложни разсъждения.",
"claude-3.7-sonnet.description": "Claude 3.7 Sonnet е надградена версия с разширен контекст и възможности.",
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 е най-бързият и най-интелигентен Haiku модел на Anthropic, с мълниеносна скорост и разширено мислене.",
"claude-haiku-4.5.description": "Claude Haiku 4.5 е бърз и ефективен модел за различни задачи.",
"claude-opus-4-1-20250805-thinking.description": "Claude Opus 4.1 Thinking е усъвършенстван вариант, който може да разкрие процеса си на разсъждение.",
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 е най-новият и най-способен модел на Anthropic за изключително сложни задачи, отличаващ се с висока производителност, интелигентност, плавност и разбиране.",
"claude-opus-4-20250514.description": "Claude Opus 4 е най-мощният модел на Anthropic за изключително сложни задачи, отличаващ се с висока производителност, интелигентност, плавност и разбиране.",
"claude-opus-4-5-20251101.description": "Claude Opus 4.5 е флагманският модел на Anthropic, комбиниращ изключителна интелигентност с мащабируема производителност, идеален за сложни задачи, изискващи най-висококачествени отговори и разсъждение.",
"claude-opus-4-6.description": "Claude Opus 4.6 е най-интелигентният модел на Anthropic за изграждане на агенти и програмиране.",
"claude-sonnet-4-20250514-thinking.description": "Claude Sonnet 4 Thinking може да генерира почти мигновени отговори или разширено стъпково мислене с видим процес.",
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 може да генерира почти мигновени отговори или разширено поетапно мислене с видим процес.",
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 е най-интелигентният модел на Anthropic до момента.",
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 е най-интелигентният модел на Anthropic досега, предлагащ почти мигновени отговори или разширено поетапно мислене с прецизен контрол за потребителите на API.",
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 е най-интелигентният модел на Anthropic досега.",
"claude-sonnet-4.description": "Claude Sonnet 4 е най-новото поколение с подобрена производителност във всички задачи.",
"codegeex-4.description": "CodeGeeX-4 е мощен AI асистент за програмиране, който поддържа многоезични въпроси и допълване на код, повишавайки продуктивността на разработчиците.",
"codegeex4-all-9b.description": "CodeGeeX4-ALL-9B е многоезичен модел за генериране на код, който поддържа допълване и създаване на код, интерпретиране, уеб търсене, извикване на функции и въпроси на ниво хранилище. Подходящ е за широк спектър от софтуерни сценарии и е водещ модел под 10 милиарда параметри.",
"codegemma.description": "CodeGemma е лек модел за разнообразни програмни задачи, позволяващ бърза итерация и интеграция.",
@@ -358,7 +363,7 @@
"deepseek-ai/deepseek-v3.1-terminus.description": "DeepSeek V3.1 е модел за разсъждение от ново поколение с по-силни способности за сложни разсъждения и верига от мисли за задълбочени аналитични задачи.",
"deepseek-ai/deepseek-v3.1.description": "DeepSeek V3.1 е модел за разсъждение от ново поколение с по-силни способности за сложни разсъждения и верига от мисли за задълбочени аналитични задачи.",
"deepseek-ai/deepseek-vl2.description": "DeepSeek-VL2 е MoE модел за визия и език, базиран на DeepSeekMoE-27B със слаба активация, постигайки висока производителност с едва 4.5 милиарда активни параметъра. Отличава се в визуални въпроси и отговори, OCR, разбиране на документи/таблици/графики и визуално привързване.",
"deepseek-chat.description": "Нов отворен модел, съчетаващ общи и програмни способности. Съхранява общия диалогов капацитет на чат модела и силните програмни умения на кодиращия модел, с по-добро съответствие с предпочитанията. DeepSeek-V2.5 също така подобрява писането и следването на инструкции.",
"deepseek-chat.description": "DeepSeek V3.2 балансира разсъждението и дължината на отговорите за ежедневни въпроси и задачи на агенти. Публичните бенчмаркове достигат нивата на GPT-5, а това е първият модел, който интегрира мислене в използването на инструменти, водещ в оценките на отворен код за агенти.",
"deepseek-coder-33B-instruct.description": "DeepSeek Coder 33B е езиков модел за програмиране, обучен върху 2 трилиона токени (87% код, 13% китайски/английски текст). Въвежда 16K контекстен прозорец и задачи за попълване в средата, осигурявайки допълване на код на ниво проект и попълване на фрагменти.",
"deepseek-coder-v2.description": "DeepSeek Coder V2 е отворен MoE модел за програмиране, който се представя на ниво GPT-4 Turbo.",
"deepseek-coder-v2:236b.description": "DeepSeek Coder V2 е отворен MoE модел за програмиране, който се представя на ниво GPT-4 Turbo.",
@@ -381,7 +386,7 @@
"deepseek-r1-fast-online.description": "Пълна бърза версия на DeepSeek R1 с търсене в реално време в уеб, комбинираща възможности от мащаб 671B и по-бърз отговор.",
"deepseek-r1-online.description": "Пълна версия на DeepSeek R1 с 671 милиарда параметъра и търсене в реално време в уеб, предлагаща по-силно разбиране и генериране.",
"deepseek-r1.description": "DeepSeek-R1 използва данни от студен старт преди подсиленото обучение и се представя наравно с OpenAI-o1 в математика, програмиране и разсъждение.",
"deepseek-reasoner.description": "Режимът на мислене DeepSeek V3.2 извежда верига от мисли преди крайния отговор за повишена точност.",
"deepseek-reasoner.description": "DeepSeek V3.2 Thinking е дълбок разсъждаващ модел, който генерира верига от мисли преди отговорите за по-висока точност, с водещи резултати в състезания и разсъждение, сравнимо с Gemini-3.0-Pro.",
"deepseek-v2.description": "DeepSeek V2 е ефективен MoE модел за икономична обработка.",
"deepseek-v2:236b.description": "DeepSeek V2 236B е модел на DeepSeek, фокусиран върху програмиране, с висока производителност при генериране на код.",
"deepseek-v3-0324.description": "DeepSeek-V3-0324 е MoE модел с 671 милиарда параметъра, с изключителни способности в програмиране, технически задачи, разбиране на контекст и обработка на дълги текстове.",
@@ -471,7 +476,8 @@
"ernie-speed-pro-128k.description": "ERNIE Speed Pro 128K е модел с висока едновременност и висока стойност за мащабни онлайн услуги и корпоративни приложения.",
"ernie-x1-turbo-32k.description": "ERNIE X1 Turbo 32K е бърз мислещ модел с 32K контекст за сложни разсъждения и многозавойни разговори.",
"ernie-x1.1-preview.description": "ERNIE X1.1 Preview е предварителен модел за мислене, предназначен за оценка и тестване.",
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0 е модел за генериране на изображения от ByteDance Seed, поддържащ вход от текст и изображения с висока степен на контрол и качество. Генерира изображения от текстови подсказки.",
"fal-ai/bytedance/seedream/v4.5.description": "Seedream 4.5, разработен от екипа Seed на ByteDance, поддържа редактиране и композиране на множество изображения. Отличава се с подобрена последователност на обектите, точно следване на инструкции, разбиране на пространствена логика, естетическо изразяване, оформление на постери и дизайн на лога с високопрецизно визуално-текстово рендиране.",
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0, разработен от ByteDance Seed, поддържа текстови и визуални входове за висококачествено и силно контролируемо генериране на изображения от подсказки.",
"fal-ai/flux-kontext/dev.description": "FLUX.1 модел, фокусиран върху редактиране на изображения, поддържащ вход от текст и изображения.",
"fal-ai/flux-pro/kontext.description": "FLUX.1 Kontext [pro] приема текст и референтни изображения като вход, позволявайки целенасочени локални редакции и сложни глобални трансформации на сцени.",
"fal-ai/flux/krea.description": "Flux Krea [dev] е модел за генериране на изображения с естетично предпочитание към по-реалистични и естествени изображения.",
@@ -479,8 +485,8 @@
"fal-ai/hunyuan-image/v3.description": "Мощен роден мултимодален модел за генериране на изображения.",
"fal-ai/imagen4/preview.description": "Модел за висококачествено генериране на изображения от Google.",
"fal-ai/nano-banana.description": "Nano Banana е най-новият, най-бърз и най-ефективен роден мултимодален модел на Google, позволяващ генериране и редактиране на изображения чрез разговор.",
"fal-ai/qwen-image-edit.description": "Професионален модел за редактиране на изображения от екипа на Qwen, който поддържа семантични и визуални редакции, прецизно редактира китайски и английски текст и позволява висококачествени трансформации като смяна на стил и завъртане на обекти.",
"fal-ai/qwen-image.description": "Мощен модел за генериране на изображения от екипа на Qwen с впечатляващо визуализиране на китайски текст и разнообразни визуални стилове.",
"fal-ai/qwen-image-edit.description": "Професионален модел за редактиране на изображения от екипа Qwen, поддържащ семантични и визуални редакции, прецизно редактиране на текст на китайски/английски, трансфер на стил, завъртане и други.",
"fal-ai/qwen-image.description": "Мощен модел за генериране на изображения от екипа Qwen с отлично визуализиране на китайски текст и разнообразни визуални стилове.",
"flux-1-schnell.description": "Модел за преобразуване на текст в изображение с 12 милиарда параметъра от Black Forest Labs, използващ латентна дифузионна дестилация за генериране на висококачествени изображения в 1–4 стъпки. Съперничи на затворени алтернативи и е пуснат под лиценз Apache-2.0 за лична, изследователска и търговска употреба.",
"flux-dev.description": "FLUX.1 [dev] е дестилиран модел с отворени тегла за нетърговска употреба. Запазва почти професионално качество на изображенията и следване на инструкции, като същевременно работи по-ефективно и използва ресурсите по-добре от стандартни модели със същия размер.",
"flux-kontext-max.description": "Съвременно генериране и редактиране на изображения с контекст, комбиниращо текст и изображения за прецизни и последователни резултати.",
@@ -511,6 +517,8 @@
"gemini-2.0-flash-lite-001.description": "Вариант на Gemini 2.0 Flash, оптимизиран за ниска цена и ниска латентност.",
"gemini-2.0-flash-lite.description": "Вариант на Gemini 2.0 Flash, оптимизиран за ниска цена и ниска латентност.",
"gemini-2.0-flash.description": "Gemini 2.0 Flash предлага функции от ново поколение, включително изключителна скорост, вградена употреба на инструменти, мултимодално генериране и контекстен прозорец от 1 милион токена.",
"gemini-2.5-flash-image-preview.description": "Nano Banana е най-новият, най-бърз и най-ефективен роден мултимодален модел на Google, позволяващ разговорно генериране и редактиране на изображения.",
"gemini-2.5-flash-image-preview:image.description": "Nano Banana е най-новият, най-бърз и най-ефективен роден мултимодален модел на Google, позволяващ разговорно генериране и редактиране на изображения.",
"gemini-2.5-flash-image.description": "Nano Banana е най-новият, най-бърз и най-ефективен роден мултимодален модел на Google, позволяващ разговорно генериране и редактиране на изображения.",
"gemini-2.5-flash-image:image.description": "Nano Banana е най-новият, най-бърз и най-ефективен роден мултимодален модел на Google, позволяващ разговорно генериране и редактиране на изображения.",
"gemini-2.5-flash-lite-preview-06-17.description": "Gemini 2.5 Flash-Lite Preview е най-малкият и най-изгоден модел на Google, проектиран за мащабна употреба.",
@@ -525,7 +533,7 @@
"gemini-2.5-pro.description": "Gemini 2.5 Pro е най-усъвършенстваният модел за разсъждение на Google, способен да разсъждава върху код, математика и STEM проблеми и да анализира големи набори от данни, кодови бази и документи с дълъг контекст.",
"gemini-3-flash-preview.description": "Gemini 3 Flash е най-интелигентният модел, създаден за скорост, съчетаващ авангардна интелигентност с отлично търсене и обоснованост.",
"gemini-3-pro-image-preview.description": "Gemini 3 Pro ImageNano Banana Pro)е модел на Google за генериране на изображения, който също така поддържа мултимодален диалог.",
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) е модел на Google за генериране на изображения, който също поддържа мултимодален чат.",
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) е моделът на Google за генериране на изображения и поддържа мултимодален чат.",
"gemini-3-pro-preview.description": "Gemini 3 Pro е най-мощният агентен и „vibe-coding“ модел на Google, който предлага по-богати визуализации и по-дълбоко взаимодействие, базирано на съвременно логическо мислене.",
"gemini-flash-latest.description": "Най-новата версия на Gemini Flash",
"gemini-flash-lite-latest.description": "Най-новата версия на Gemini Flash-Lite",
@@ -604,6 +612,7 @@
"google/text-embedding-005.description": "Модел за вграждане на текст, фокусиран върху английски език, оптимизиран за задачи с код и английски език.",
"google/text-multilingual-embedding-002.description": "Многоезичен модел за вграждане на текст, оптимизиран за задачи с кръстосан езиков обхват на много езици.",
"gpt-3.5-turbo-0125.description": "GPT 3.5 Turbo за генериране и разбиране на текст; в момента сочи към gpt-3.5-turbo-0125.",
"gpt-3.5-turbo-0613.description": "GPT 3.5 Turbo е бърз и ефективен модел за различни задачи.",
"gpt-3.5-turbo-1106.description": "GPT 3.5 Turbo за генериране и разбиране на текст; в момента сочи към gpt-3.5-turbo-0125.",
"gpt-3.5-turbo-instruct.description": "GPT 3.5 Turbo за задачи с генериране и разбиране на текст, оптимизиран за следване на инструкции.",
"gpt-3.5-turbo.description": "GPT 3.5 Turbo за генериране и разбиране на текст; в момента сочи към gpt-3.5-turbo-0125.",
@@ -614,10 +623,12 @@
"gpt-4-1106-preview.description": "Най-новият GPT-4 Turbo добавя възможности за визуално разпознаване. Визуалните заявки поддържат JSON режим и извикване на функции. Това е рентабилен мултимодален модел, който балансира точността и ефективността за приложения в реално време.",
"gpt-4-32k-0613.description": "GPT-4 предлага по-голям контекстов прозорец за обработка на по-дълги входове в сценарии, изискващи интеграция на широка информация и анализ на данни.",
"gpt-4-32k.description": "GPT-4 предлага по-голям контекстов прозорец за обработка на по-дълги входове в сценарии, изискващи интеграция на широка информация и анализ на данни.",
"gpt-4-o-preview.description": "GPT-4o е най-усъвършенстваният мултимодален модел, който обработва текстови и визуални входове.",
"gpt-4-turbo-2024-04-09.description": "Най-новият GPT-4 Turbo добавя възможности за визуално разпознаване. Визуалните заявки поддържат JSON режим и извикване на функции. Това е рентабилен мултимодален модел, който балансира точността и ефективността за приложения в реално време.",
"gpt-4-turbo-preview.description": "Най-новият GPT-4 Turbo добавя възможности за визуално разпознаване. Визуалните заявки поддържат JSON режим и извикване на функции. Това е рентабилен мултимодален модел, който балансира точността и ефективността за приложения в реално време.",
"gpt-4-turbo.description": "Най-новият GPT-4 Turbo добавя възможности за визуално разпознаване. Визуалните заявки поддържат JSON режим и извикване на функции. Това е рентабилен мултимодален модел, който балансира точността и ефективността за приложения в реално време.",
"gpt-4-vision-preview.description": "Предварителен преглед на GPT-4 Vision, създаден за задачи по анализ и обработка на изображения.",
"gpt-4.1-2025-04-14.description": "GPT-4.1 е водещият модел за сложни задачи, идеален за междудисциплинарно решаване на проблеми.",
"gpt-4.1-mini.description": "GPT-4.1 mini балансира интелигентност, скорост и цена, което го прави привлекателен за множество приложения.",
"gpt-4.1-nano.description": "GPT-4.1 nano е най-бързият и най-рентабилен модел от серията GPT-4.1.",
"gpt-4.1.description": "GPT-4.1 е водещият ни модел за сложни задачи и решаване на проблеми в различни области.",
@@ -627,6 +638,7 @@
"gpt-4o-2024-08-06.description": "ChatGPT-4o е динамичен модел, актуализиран в реално време. Съчетава силно езиково разбиране и генериране за мащабни приложения като клиентска поддръжка, образование и техническа помощ.",
"gpt-4o-2024-11-20.description": "ChatGPT-4o е динамичен модел, актуализиран в реално време, който съчетава силно разбиране и генериране за мащабни приложения като клиентска поддръжка, образование и техническа помощ.",
"gpt-4o-audio-preview.description": "Предварителен преглед на GPT-4o Audio модел с аудио вход и изход.",
"gpt-4o-mini-2024-07-18.description": "GPT-4o mini е икономично решение за широк спектър от текстови и визуални задачи.",
"gpt-4o-mini-audio-preview.description": "GPT-4o mini Audio модел с аудио вход и изход.",
"gpt-4o-mini-realtime-preview.description": "GPT-4o-mini вариант в реално време с аудио и текстов вход/изход в реално време.",
"gpt-4o-mini-search-preview.description": "GPT-4o mini Search Preview е обучен да разбира и изпълнява заявки за уеб търсене чрез Chat Completions API. Уеб търсенето се таксува на извикване на инструмент в допълнение към разходите за токени.",
@@ -779,6 +791,49 @@
"llava.description": "LLaVA е мултимодален модел, комбиниращ визуален енкодер и Vicuna за силно разбиране на визия и език.",
"llava:13b.description": "LLaVA е мултимодален модел, комбиниращ визуален енкодер и Vicuna за силно разбиране на визия и език.",
"llava:34b.description": "LLaVA е мултимодален модел, комбиниращ визуален енкодер и Vicuna за силно разбиране на визия и език.",
"magistral-medium-latest.description": "Magistral Medium 1.2 е авангарден модел за разсъждение от Mistral AI (септември 2025) с поддръжка на визуални данни.",
"magistral-small-2509.description": "Magistral Small 1.2 е малък, с отворен код модел за разсъждение от Mistral AI (септември 2025) с поддръжка на визуални данни.",
"mathstral.description": "MathΣtral е създаден за научни изследвания и математическо разсъждение, с мощни изчислителни и обяснителни способности.",
"max-32k.description": "Spark Max 32K предлага обработка на голям контекст с по-добро разбиране и логическо разсъждение, поддържайки входове до 32K токена за четене на дълги документи и въпроси с частни знания.",
"megrez-3b-instruct.description": "Megrez 3B Instruct е малък, ефективен модел от Wuwen Xinqiong.",
"meituan/longcat-flash-chat.description": "Модел с отворен код от Meituan, оптимизиран за диалог и агентски задачи, силен в използването на инструменти и сложни многократни взаимодействия.",
"meta-llama-3-70b-instruct.description": "Мощен модел с 70 милиарда параметъра, който се отличава в разсъждение, програмиране и широк спектър от езикови задачи.",
"meta-llama-3-8b-instruct.description": "Универсален модел с 8 милиарда параметъра, оптимизиран за чат и генериране на текст.",
"meta-llama-3.1-405b-instruct.description": "Llama 3.1 е текстов модел, обучен с инструкции, оптимизиран за многоезичен чат, с високи резултати в индустриалните бенчмаркове сред отворени и затворени модели.",
"meta-llama-3.1-70b-instruct.description": "Llama 3.1 е текстов модел, обучен с инструкции, оптимизиран за многоезичен чат, с високи резултати в индустриалните бенчмаркове сред отворени и затворени модели.",
"meta-llama-3.1-8b-instruct.description": "Llama 3.1 е текстов модел, обучен с инструкции, оптимизиран за многоезичен чат, с високи резултати в индустриалните бенчмаркове сред отворени и затворени модели.",
"meta-llama/Llama-2-13b-chat-hf.description": "LLaMA-2 Chat (13B) предлага силна езикова обработка и стабилно чат изживяване.",
"meta-llama/Llama-2-70b-hf.description": "LLaMA-2 предлага силна езикова обработка и стабилно взаимодействие.",
"meta-llama/Llama-3-70b-chat-hf.description": "Llama 3 70B Instruct Reference е мощен чат модел за сложни диалози.",
"meta-llama/Llama-3-8b-chat-hf.description": "Llama 3 8B Instruct Reference предлага многоезична поддръжка и обширни познания в различни области.",
"meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo.description": "LLaMA 3.2 е създаден за задачи, съчетаващи визия и текст. Отличава се в описване на изображения и визуални въпроси, свързвайки езиковото генериране с визуалното разсъждение.",
"meta-llama/Llama-3.2-3B-Instruct-Turbo.description": "LLaMA 3.2 е създаден за задачи, съчетаващи визия и текст. Отличава се в описване на изображения и визуални въпроси, свързвайки езиковото генериране с визуалното разсъждение.",
"meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo.description": "LLaMA 3.2 е създаден за задачи, съчетаващи визия и текст. Отличава се в описване на изображения и визуални въпроси, свързвайки езиковото генериране с визуалното разсъждение.",
"meta-llama/Llama-3.3-70B-Instruct-Turbo.description": "Meta Llama 3.3 е многоезичен LLM с 70 милиарда параметъра (текстов вход/изход), предварително обучен и настроен с инструкции. Версията, обучена с инструкции, е оптимизирана за многоезичен чат и превъзхожда много отворени и затворени модели в индустриалните бенчмаркове.",
"meta-llama/Llama-Vision-Free.description": "LLaMA 3.2 е създаден за задачи, съчетаващи визия и текст. Отличава се в описване на изображения и визуални въпроси, свързвайки езиковото генериране с визуалното разсъждение.",
"meta-llama/Meta-Llama-3-70B-Instruct-Lite.description": "Llama 3 70B Instruct Lite е създаден за висока производителност с ниска латентност.",
"meta-llama/Meta-Llama-3-70B-Instruct-Turbo.description": "Llama 3 70B Instruct Turbo предлага силно разбиране и генериране за най-взискателните натоварвания.",
"meta-llama/Meta-Llama-3-8B-Instruct-Lite.description": "Llama 3 8B Instruct Lite балансира производителността за среди с ограничени ресурси.",
"meta-llama/Meta-Llama-3-8B-Instruct-Turbo.description": "Llama 3 8B Instruct Turbo е високопроизводителен LLM за широк спектър от приложения.",
"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo.description": "Моделът Llama 3.1 Turbo с 405 милиарда параметъра предлага огромен контекстов капацитет за обработка на големи данни и се отличава в мащабни AI приложения.",
"meta-llama/Meta-Llama-3.1-405B-Instruct.description": "Llama 3.1 е водещото семейство модели на Meta, достигащо до 405 милиарда параметъра за сложни диалози, многоезичен превод и анализ на данни.",
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo.description": "Llama 3.1 70B е фино настроен за приложения с високо натоварване; FP8 квантизацията осигурява ефективни изчисления и точност при сложни сценарии.",
"meta-llama/Meta-Llama-3.1-70B.description": "Llama 3.1 е водещото семейство модели на Meta, достигащо до 405 милиарда параметъра за сложни диалози, многоезичен превод и анализ на данни.",
"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo.description": "Llama 3.1 8B използва FP8 квантизация, поддържа до 131 072 токена контекст и е сред водещите отворени модели за сложни задачи според множество бенчмаркове.",
"meta-llama/llama-3-70b-instruct.description": "Llama 3 70B Instruct е оптимизиран за висококачествени диалози и показва отлични резултати в човешки оценки.",
"meta-llama/llama-3-8b-instruct.description": "Llama 3 8B Instruct е оптимизиран за висококачествени диалози, превъзхождайки много затворени модели.",
"meta-llama/llama-3.1-70b-instruct.description": "Най-новата серия Llama 3.1 на Meta, 70B вариант, обучен с инструкции, оптимизиран за висококачествени диалози. В индустриални оценки показва силна производителност спрямо водещи затворени модели. (Достъпен само за потвърдени бизнес потребители.)",
"meta-llama/llama-3.1-8b-instruct.description": "Най-новата серия Llama 3.1 на Meta, 8B вариант, обучен с инструкции, е особено бърз и ефективен. В индустриални оценки показва силна производителност, надминавайки много водещи затворени модели. (Достъпен само за потвърдени бизнес потребители.)",
"meta-llama/llama-3.1-8b-instruct:free.description": "LLaMA 3.1 предлага многоезична поддръжка и е сред водещите генеративни модели.",
"meta-llama/llama-3.2-11b-vision-instruct.description": "LLaMA 3.2 е създаден за задачи, съчетаващи визия и текст. Отличава се в описване на изображения и визуални въпроси, свързвайки езиковото генериране с визуалното разсъждение.",
"meta-llama/llama-3.2-3b-instruct.description": "meta-llama/llama-3.2-3b-instruct",
"meta-llama/llama-3.2-90b-vision-instruct.description": "LLaMA 3.2 е създаден за задачи, съчетаващи визия и текст. Отличава се в описване на изображения и визуални въпроси, свързвайки езиковото генериране с визуалното разсъждение.",
"meta-llama/llama-3.3-70b-instruct.description": "Llama 3.3 е най-усъвършенстваният многоезичен отворен модел от серията Llama, предлагащ производителност, близка до 405B, на много ниска цена. Базиран е на Transformer архитектура и подобрен чрез SFT и RLHF за полезност и безопасност. Версията, обучена с инструкции, е оптимизирана за многоезичен чат и превъзхожда много отворени и затворени модели в индустриалните бенчмаркове. Край на знанията: декември 2023.",
"meta-llama/llama-3.3-70b-instruct:free.description": "Llama 3.3 е най-усъвършенстваният многоезичен отворен модел от серията Llama, предлагащ производителност, близка до 405B, на много ниска цена. Базиран е на Transformer архитектура и подобрен чрез SFT и RLHF за полезност и безопасност. Версията, обучена с инструкции, е оптимизирана за многоезичен чат и превъзхожда много отворени и затворени модели в индустриалните бенчмаркове. Край на знанията: декември 2023.",
"meta.llama3-1-405b-instruct-v1:0.description": "Meta Llama 3.1 405B Instruct е най-големият и най-мощен модел от серията Llama 3.1 Instruct – изключително напреднал модел за диалогово разсъждение и генериране на синтетични данни, отлична основа за дообучение в специфични домейни. Многоезичните LLM модели Llama 3.1 са предварително обучени и настроени с инструкции в размери 8B, 70B и 405B (текстов вход/изход). Моделите, обучени с инструкции, са оптимизирани за многоезичен диалог и превъзхождат много отворени чат модели в индустриалните бенчмаркове. Llama 3.1 е предназначен за търговска и изследователска употреба на различни езици. Моделите, обучени с инструкции, са подходящи за чат в стил асистент, докато предварително обучените модели са подходящи за по-широки задачи по генериране на естествен език. Изходите от Llama 3.1 могат да се използват и за подобряване на други модели, включително чрез генериране и прецизиране на синтетични данни. Llama 3.1 е автогенеративен Transformer модел с оптимизирана архитектура. Настроените версии използват SFT и RLHF за съответствие с човешките предпочитания за полезност и безопасност.",
"meta.llama3-1-70b-instruct-v1:0.description": "Обновен Meta Llama 3.1 70B Instruct с разширен контекст до 128K токена, многоезична поддръжка и подобрено разсъждение. Многоезичните LLM модели Llama 3.1 са предварително обучени и настроени с инструкции в размери 8B, 70B и 405B (текстов вход/изход). Моделите, обучени с инструкции, са оптимизирани за многоезичен диалог и превъзхождат много отворени чат модели в индустриалните бенчмаркове. Llama 3.1 е предназначен за търговска и изследователска употреба на различни езици. Моделите, обучени с инструкции, са подходящи за чат в стил асистент, докато предварително обучените модели са подходящи за по-широки задачи по генериране на естествен език. Изходите от Llama 3.1 могат да се използват и за подобряване на други модели, включително чрез генериране и прецизиране на синтетични данни. Llama 3.1 е автогенеративен Transformer модел с оптимизирана архитектура. Настроените версии използват SFT и RLHF за съответствие с човешките предпочитания за полезност и безопасност.",
"meta.llama3-1-8b-instruct-v1:0.description": "Обновен Meta Llama 3.1 8B Instruct с контекст до 128K токена, многоезична поддръжка и подобрено разсъждение. Семейството Llama 3.1 включва 8B, 70B и 405B модели, обучени с инструкции, оптимизирани за многоезичен чат и висока производителност в бенчмаркове. Предназначен е за търговска и изследователска употреба на различни езици; моделите, обучени с инструкции, са подходящи за чат в стил асистент, а предварително обучените – за по-широки задачи по генериране. Изходите от Llama 3.1 могат да се използват и за подобряване на други модели (напр. синтетични данни и прецизиране). Това е автогенеративен Transformer модел с SFT и RLHF за съответствие с човешките предпочитания за полезност и безопасност.",
"meta.llama3-70b-instruct-v1:0.description": "Meta Llama 3 е отворен LLM за разработчици, изследователи и предприятия, създаден да им помага да изграждат, експериментират и отговорно мащабират идеи в генеративния AI. Като част от основата за глобални иновации, той е подходящ за създаване на съдържание, разговорен AI, езиково разбиране, научноизследователска и развойна дейност и бизнес приложения.",
"meta.llama3-8b-instruct-v1:0.description": "Meta Llama 3 е отворен LLM, предназначен за разработчици, изследователи и предприятия, създаден да им помага да изграждат, експериментират и отговорно мащабират идеи за генеративен ИИ. Като част от основата за глобални иновации в общността, той е подходящ за среди с ограничени изчислителни ресурси, крайни устройства и по-бързо обучение.",
"meta/Llama-3.2-11B-Vision-Instruct.description": "Силен визуален анализ на изображения с висока резолюция, подходящ за приложения за визуално разбиране.",
"meta/Llama-3.2-90B-Vision-Instruct.description": "Разширен визуален анализ за приложения с агенти за визуално разбиране.",
@@ -1046,6 +1101,7 @@
"qwen3-14b.description": "Qwen3 14B е среден по размер модел за многоезични въпроси и отговори и генериране на текст.",
"qwen3-235b-a22b-instruct-2507.description": "Qwen3 235B A22B Instruct 2507 е водещ модел с инструкции за широк спектър от задачи по генериране и разсъждение.",
"qwen3-235b-a22b-thinking-2507.description": "Qwen3 235B A22B Thinking 2507 е ултраголям модел за дълбоко разсъждение.",
"qwen3-235b-a22b.description": "Qwen3 е следващо поколение модел Tongyi Qwen с големи подобрения в разсъждението, общите способности, агентските възможности и многоезичната производителност, с поддръжка на превключване между мисловни режими.",
"qwen3-30b-a3b-instruct-2507.description": "Qwen3 30B A3B Instruct 2507 е средно-голям модел с инструкции за висококачествено генериране и въпроси и отговори.",
"qwen3-30b-a3b-thinking-2507.description": "Qwen3 30B A3B Thinking 2507 е средно-голям модел за разсъждение, балансиращ точност и разходи.",
"qwen3-30b-a3b.description": "Qwen3 30B A3B е средно-голям универсален модел, балансиращ между цена и качество.",
@@ -1057,6 +1113,7 @@
"qwen3-coder-flash.description": "Модел за програмиране Qwen. Най-новата серия Qwen3-Coder е базирана на Qwen3 и предлага силни способности за програмиране чрез агенти, използване на инструменти и взаимодействие със среди за автономно програмиране, с отлично представяне при код и стабилни общи възможности.",
"qwen3-coder-plus.description": "Модел за програмиране Qwen. Най-новата серия Qwen3-Coder е базирана на Qwen3 и предлага силни способности за програмиране чрез агенти, използване на инструменти и взаимодействие със среди за автономно програмиране, с отлично представяне при код и стабилни общи възможности.",
"qwen3-coder:480b.description": "Високопроизводителен модел на Alibaba с дълъг контекст за задачи с агенти и програмиране.",
"qwen3-max-2026-01-23.description": "Моделите Qwen3 Max предлагат значителни подобрения спрямо серията 2.5 в общите способности, разбиране на китайски/английски, следване на сложни инструкции, субективни отворени задачи, многоезичност и използване на инструменти, с по-малко халюцинации. Най-новият qwen3-max подобрява агентското програмиране и използването на инструменти спрямо qwen3-max-preview. Това издание достига върхови резултати в областта и е насочено към по-сложни нужди на агентите.",
"qwen3-max-preview.description": "Най-добре представящият се модел Qwen за сложни, многоетапни задачи. Прегледната версия поддържа разсъждение.",
"qwen3-max.description": "Моделите Qwen3 Max предлагат значителни подобрения спрямо серията 2.5 в общите способности, разбиране на китайски/английски, следване на сложни инструкции, субективни отворени задачи, многоезичност и използване на инструменти, с по-малко халюцинации. Най-новият qwen3-max подобрява програмирането чрез агенти и използването на инструменти спрямо qwen3-max-preview. Тази версия достига водещи резултати в индустрията и е насочена към по-сложни нужди на агентите.",
"qwen3-next-80b-a3b-instruct.description": "Следващо поколение отворен модел Qwen3 без мисловни способности. В сравнение с предишната версия (Qwen3-235B-A22B-Instruct-2507), предлага по-добро разбиране на китайски, по-силна логическа аргументация и подобрено генериране на текст.",
@@ -1079,6 +1136,7 @@
"qwq.description": "QwQ е модел за аргументация от семейството на Qwen. В сравнение със стандартните модели, обучени с инструкции, предлага мисловни и логически способности, които значително подобряват ефективността при трудни задачи. QwQ-32B е среден по размер модел, който се конкурира с водещи модели като DeepSeek-R1 и o1-mini.",
"qwq_32b.description": "Среден по размер модел за аргументация от семейството на Qwen. В сравнение със стандартните модели, обучени с инструкции, мисловните и логическите способности на QwQ значително подобряват ефективността при трудни задачи.",
"r1-1776.description": "R1-1776 е дообучен вариант на DeepSeek R1, създаден да предоставя неконфронтирана, обективна и фактическа информация.",
"seedance-1-5-pro-251215.description": "Seedance 1.5 Pro от ByteDance поддържа генериране на видео от текст, от изображение към видео (първи кадър, първи+последен кадър) и синхронизирано с визуалното съдържание аудио.",
"solar-mini-ja.description": "Solar Mini (Ja) разширява Solar Mini с фокус върху японски език, като запазва ефективността и силната производителност на английски и корейски.",
"solar-mini.description": "Solar Mini е компактен LLM, който превъзхожда GPT-3.5, с мощни многоезични възможности, поддържащ английски и корейски, и предлага ефективно решение с малък отпечатък.",
"solar-pro.description": "Solar Pro е интелигентен LLM от Upstage, фокусиран върху следване на инструкции на един GPU, с IFEval резултати над 80. Понастоящем поддържа английски; пълното издание е планирано за ноември 2024 с разширена езикова поддръжка и по-дълъг контекст.",
@@ -1135,6 +1193,7 @@
"us.anthropic.claude-3-5-sonnet-20241022-v2:0.description": "Claude 3.5 Sonnet поставя нов стандарт в индустрията, надминавайки конкурентите и Claude 3 Opus в широки оценки, като запазва средна скорост и цена.",
"us.anthropic.claude-3-7-sonnet-20250219-v1:0.description": "Claude 3.7 Sonnet е най-бързият модел от ново поколение на Anthropic. В сравнение с Claude 3 Haiku, подобрява всички умения и надминава предишния флагман Claude 3 Opus в много интелектуални бенчмаркове.",
"us.anthropic.claude-haiku-4-5-20251001-v1:0.description": "Claude Haiku 4.5 е най-бързият и интелигентен Haiku модел на Anthropic, с мълниеносна скорост и разширено мислене.",
"us.anthropic.claude-opus-4-6-v1.description": "Claude Opus 4.6 е най-интелигентният модел на Anthropic за изграждане на агенти и програмиране.",
"us.anthropic.claude-sonnet-4-5-20250929-v1:0.description": "Claude Sonnet 4.5 е най-интелигентният модел на Anthropic до момента.",
"v0-1.0-md.description": "v0-1.0-md е наследен модел, достъпен чрез v0 API.",
"v0-1.5-lg.description": "v0-1.5-lg е подходящ за напреднали мисловни или логически задачи.",
+1
View File
@@ -30,6 +30,7 @@
"internlm.description": "Open-source организация, фокусирана върху изследвания и инструменти за големи модели, предоставяща ефективна и лесна за използване платформа за достъп до водещи модели и алгоритми.",
"jina.description": "Основана през 2020 г., Jina AI е водеща компания в областта на търсещия AI. Технологичният ѝ стек включва векторни модели, преоценители и малки езикови модели за създаване на надеждни генеративни и мултимодални търсещи приложения.",
"lmstudio.description": "LM Studio е десктоп приложение за разработка и експериментиране с LLM на вашия компютър.",
"lobehub.description": "LobeHub Cloud използва официални API-та за достъп до AI модели и измерва използването чрез Кредити, свързани с токени на модела.",
"minimax.description": "Основана през 2021 г., MiniMax създава универсален AI с мултимодални базови модели, включително текстови модели с трилиони параметри, речеви и визуални модели, както и приложения като Hailuo AI.",
"mistral.description": "Mistral предлага усъвършенствани универсални, специализирани и изследователски модели за сложни разсъждения, многоезични задачи и генериране на код, с извикване на функции за персонализирани интеграции.",
"modelscope.description": "ModelScope е платформа на Alibaba Cloud за модели като услуга, предлагаща широка гама от AI модели и услуги за инференция.",
+1
View File
@@ -14,6 +14,7 @@
"table.columns.totalTokens": "Използвани токени",
"table.columns.type.enums.chat": "Генериране на текст",
"table.columns.type.enums.imageGeneration": "Генериране на изображения",
"table.columns.type.enums.videoGeneration": "Генериране на видео",
"table.columns.type.title": "Тип",
"table.desc": "Подробности за използването на изчислителни кредити за генериране на текст, вграждане, генериране на изображения и др.",
"table.more": "Виж подробности",
+6
View File
@@ -131,6 +131,12 @@
"limitation.providers.prompter.subTitle": "Персонализираната API услуга е достъпна само за платени планове. Надстрой сега, за да използваш глобални водещи модели",
"limitation.providers.prompter.title": "Абонирай се сега, за да използваш персонализирана API услуга",
"limitation.providers.tooltip": "Персонализираната API услуга е достъпна само за платени планове",
"limitation.video.success.action": "Продължи с генерирането",
"limitation.video.success.desc": "Вашият абонамент {{plan}} беше успешно надграден. Насладете се на AI видео генериране. Текущият ви план включва:",
"limitation.video.success.title": "Успешно надграждане",
"limitation.video.topupSuccess.action": "Продължи с генерирането",
"limitation.video.topupSuccess.desc": "Вашите допълнителни кредити вече са активни. Насладете се на AI видео генериране. Текущият ви план включва:",
"limitation.video.topupSuccess.title": "Успешно зареждане",
"modelPricing.button": "Виж документацията за ценообразуване",
"modelPricing.desc": "{{name}} използва кредити за измерване на използването на AI модел. Таблицата по-долу показва изчислителните кредити на 1M токена.",
"modelPricing.title": "Цени на текстови модели",
+35
View File
@@ -86,6 +86,10 @@
"localFiles.editFile.replaceFirst": "Замени само първото срещане",
"localFiles.file": "Файл",
"localFiles.folder": "Папка",
"localFiles.globFiles.pattern": "Шаблон",
"localFiles.grepContent.glob": "Филтър за файлове",
"localFiles.grepContent.pattern": "Шаблон за търсене",
"localFiles.grepContent.type": "Тип файл",
"localFiles.moveFiles.itemsMoved": "{{count}} елемент(а) преместени:",
"localFiles.moveFiles.itemsMoved_one": "{{count}} елемент преместен:",
"localFiles.moveFiles.itemsMoved_other": "{{count}} елемента преместени:",
@@ -95,11 +99,17 @@
"localFiles.open": "Отвори",
"localFiles.openFile": "Отвори файл",
"localFiles.openFolder": "Отвори папка",
"localFiles.outOfScope.requestedPaths": "Заявени пътища",
"localFiles.outOfScope.warning": "Предупреждение: Следните пътища са извън конфигурираната работна директория. Моля, потвърдете, че желаете да разрешите достъп.",
"localFiles.outOfScope.workingDirectory": "Работна директория",
"localFiles.read.more": "Виж повече",
"localFiles.readFile": "Прочети файл",
"localFiles.readFile.lineRange": "Редове {{start}} - {{end}}",
"localFiles.readFileError": "Неуспешно четене на файл, моля проверете дали пътят е правилен",
"localFiles.readFiles": "Прочети файлове",
"localFiles.readFilesError": "Неуспешно четене на файлове, моля проверете дали пътят е правилен",
"localFiles.searchFiles.keywords": "Ключови думи",
"localFiles.securityBlacklist.warning": "Сигнал за сигурност: Тази операция е маркирана от правилата за сигурност и изисква вашето изрично одобрение.",
"localFiles.writeFile.characters": "знаци",
"localFiles.writeFile.preview": "Преглед на съдържанието",
"localFiles.writeFile.truncated": "съкратено",
@@ -136,6 +146,31 @@
"search.summary": "Обобщение",
"search.summaryTooltip": "Обобщи текущото съдържание",
"search.viewMoreResults": "Виж още {{results}} резултата",
"securityBlacklist.awsCredentials": "Достъпът до AWS идентификационни данни може да изтече ключове за достъп до облака",
"securityBlacklist.browserCredentials": "Достъпът до съхранени в браузъра идентификационни данни може да разкрие пароли",
"securityBlacklist.chownSystemDirs": "Промяната на собствеността на системни директории е опасна",
"securityBlacklist.ddDiskWrite": "Записването на произволни данни върху дискови устройства може да унищожи данни",
"securityBlacklist.directMemoryAccess": "Директният достъп до паметта е изключително опасен",
"securityBlacklist.disableFirewall": "Изключването на защитната стена излага системата на атаки",
"securityBlacklist.dockerConfig": "Четенето на Docker конфигурация може да разкрие идентификационни данни за регистъра",
"securityBlacklist.envFiles": "Четенето на .env файлове може да разкрие чувствителни идентификационни данни и API ключове",
"securityBlacklist.etcPasswd": "Промяната на /etc/passwd може да ви заключи извън системата",
"securityBlacklist.forkBomb": "Fork бомба може да срине системата",
"securityBlacklist.formatPartition": "Форматирането на системни дялове ще унищожи данни",
"securityBlacklist.gcpCredentials": "Четенето на GCP идентификационни данни може да разкрие ключове за достъп до облачни услуги",
"securityBlacklist.gitCredentials": "Четенето на Git файл с идентификационни данни може да разкрие токени за достъп",
"securityBlacklist.historyFiles": "Четенето на файлове с история може да разкрие чувствителни команди и идентификационни данни",
"securityBlacklist.kernelParams": "Промяната на параметри на ядрото без разбиране може да срине системата",
"securityBlacklist.kubeConfig": "Четенето на Kubernetes конфигурация може да разкрие идентификационни данни за клъстера",
"securityBlacklist.npmrc": "Четенето на npm токен файл може да разкрие идентификационни данни за регистъра на пакети",
"securityBlacklist.removeSystemPackages": "Премахването на основни системни пакети може да повреди системата",
"securityBlacklist.rmForceRecursive": "Принудително рекурсивно изтриване без конкретна цел е твърде опасно",
"securityBlacklist.rmHomeDir": "Рекурсивното изтриване на домашната директория е изключително опасно",
"securityBlacklist.rmRootDir": "Рекурсивното изтриване на root директорията ще унищожи системата",
"securityBlacklist.sshConfig": "Промяната на SSH конфигурацията може да ви заключи извън системата",
"securityBlacklist.sshPrivateKeys": "Четенето на SSH частни ключове може да компрометира сигурността на системата",
"securityBlacklist.sudoers": "Промяната на файла sudoers без подходяща проверка е опасна",
"securityBlacklist.suidShells": "Задаването на SUID на shell-ове или интерпретатори е риск за сигурността",
"updateArgs.duplicateKeyError": "Ключът на полето трябва да е уникален",
"updateArgs.form.add": "Добави елемент",
"updateArgs.form.key": "Ключ на полето",
+28
View File
@@ -0,0 +1,28 @@
{
"config.aspectRatio.label": "Съотношение на страните",
"config.cameraFixed.label": "Фиксирана камера",
"config.duration.label": "Продължителност",
"config.endImageUrl.label": "Крайна рамка",
"config.generateAudio.label": "Генерирай аудио",
"config.header.title": "Видео",
"config.imageUrl.label": "Начална рамка",
"config.prompt.placeholder": "Опишете видеото, което искате да генерирате",
"config.referenceImage.label": "Референтно изображение",
"config.resolution.label": "Резолюция",
"config.seed.label": "Сийд",
"config.seed.random": "Случаен",
"generation.actions.copyError": "Копирай съобщението за грешка",
"generation.actions.errorCopied": "Съобщението за грешка е копирано в клипборда",
"generation.actions.errorCopyFailed": "Неуспешно копиране на съобщението за грешка",
"generation.actions.generate": "Генерирай",
"generation.freeQuota.exhausted": "🎁 Използвахте безплатната квота, ще се използват кредити",
"generation.freeQuota.remaining": "🎁 {{remaining}} безплатни видеа днес",
"generation.status.failed": "Генерирането неуспешно",
"generation.status.generating": "Генериране...",
"generation.validation.endFrameRequiresStartFrame": "Крайната рамка не може да се използва без начална рамка. Моля, задайте първо начална рамка.",
"topic.createNew": "Нова тема",
"topic.deleteConfirm": "Изтриване на видео тема",
"topic.deleteConfirmDesc": "Ще изтриете тази видео тема. Това действие не може да бъде отменено.",
"topic.title": "Видео теми",
"topic.untitled": "Тема по подразбиране"
}
+3 -2
View File
@@ -58,13 +58,13 @@
"duplicateTitle": "{{title}} Kopie",
"emptyAgent": "Noch keine Agenten. Beginnen Sie mit Ihrem ersten Agenten bauen Sie Ihr System nach und nach auf.",
"emptyAgentAction": "Agent erstellen",
"extendParams.disableContextCaching.desc": "Reduziert die Kosten pro Gespräch um bis zu 90 % und erhöht die Geschwindigkeit um bis zu das 4-Fache. Aktivieren Sie dies, um die Begrenzung der historischen Nachrichten automatisch zu deaktivieren. <1>Mehr erfahren</1>",
"extendParams.disableContextCaching.desc": "Reduzieren Sie die Kosten für die Generierung eines einzelnen Gesprächs um bis zu 90 % und erreichen Sie eine bis zu 4-fache Geschwindigkeit. <1>Mehr erfahren</1>",
"extendParams.disableContextCaching.title": "Kontext-Caching aktivieren",
"extendParams.effort.desc": "Steuern Sie mit dem Parameter 'Aufwand', wie viele Tokens Claude bei der Antwort verwendet.",
"extendParams.effort.title": "Aufwand",
"extendParams.enableAdaptiveThinking.desc": "Ermöglicht Claude im adaptiven Denkmodus dynamisch zu entscheiden, wann und wie intensiv gedacht wird.",
"extendParams.enableAdaptiveThinking.title": "Adaptives Denken aktivieren",
"extendParams.enableReasoning.desc": "Basierend auf der Begrenzung des Claude-Denkmechanismus deaktiviert diese Option automatisch die Begrenzung der historischen Nachrichten. <1>Mehr erfahren</1>",
"extendParams.enableReasoning.desc": "Basierend auf der Begrenzung des Claude-Denkmechanismus. <1>Mehr erfahren</1>",
"extendParams.enableReasoning.title": "Tiefes Denken aktivieren",
"extendParams.imageAspectRatio.title": "Bildseitenverhältnis",
"extendParams.imageResolution.title": "Bildauflösung",
@@ -165,6 +165,7 @@
"messageAction.delAndRegenerate": "Löschen und neu generieren",
"messageAction.deleteDisabledByThreads": "Diese Nachricht hat ein Unterthema und kann nicht gelöscht werden",
"messageAction.expand": "Nachricht ausklappen",
"messageAction.reaction": "Reaktion hinzufügen",
"messageAction.regenerate": "Neu generieren",
"messages.dm.sentTo": "Nur sichtbar für {{name}}",
"messages.dm.title": "Direktnachricht",
+2
View File
@@ -143,6 +143,7 @@
"cmdk.keywords.stats": "Statistiken Analyse Auswertung",
"cmdk.keywords.submitIssue": "Problem Fehler Feedback Anliegen",
"cmdk.keywords.usage": "Nutzung Statistik Verbrauch Kontingent",
"cmdk.keywords.video": "video,erstellen,seedance,kling",
"cmdk.memory": "Gedächtnis",
"cmdk.mentionAgent": "Agent erwähnen",
"cmdk.navigate": "Navigieren",
@@ -193,6 +194,7 @@
"cmdk.themeLight": "Hell",
"cmdk.toOpen": "Öffnen",
"cmdk.toSelect": "Auswählen",
"cmdk.video": "KI-Video",
"confirm": "Bestätigen",
"contact": "Kontaktieren Sie uns",
"copy": "Kopieren",
+31
View File
@@ -43,6 +43,8 @@
"FileManager.emptyStatus.or": "oder",
"FileManager.emptyStatus.title": "Dateien oder Ordner hierher ziehen",
"FileManager.noFolders": "Keine Ordner verfügbar",
"FileManager.search.noResults": "Keine Dateien gefunden",
"FileManager.search.placeholder": "Dateien durchsuchen...",
"FileManager.sort.dateAdded": "Hinzugefügt am",
"FileManager.sort.name": "Name",
"FileManager.sort.size": "Größe",
@@ -94,6 +96,35 @@
"ModelSelect.removed": "Das Modell ist nicht in der Liste. Es wird automatisch entfernt, wenn es abgewählt wird.",
"ModelSwitchPanel.byModel": "Nach Modell",
"ModelSwitchPanel.byProvider": "Nach Anbieter",
"ModelSwitchPanel.detail.abilities": "Fähigkeiten",
"ModelSwitchPanel.detail.abilities.files": "Dateien",
"ModelSwitchPanel.detail.abilities.functionCall": "Werkzeugaufruf",
"ModelSwitchPanel.detail.abilities.imageOutput": "Bildausgabe",
"ModelSwitchPanel.detail.abilities.reasoning": "Schlussfolgerung",
"ModelSwitchPanel.detail.abilities.search": "Suche",
"ModelSwitchPanel.detail.abilities.video": "Video",
"ModelSwitchPanel.detail.abilities.vision": "Visuelle Erkennung",
"ModelSwitchPanel.detail.config": "Modellkonfiguration",
"ModelSwitchPanel.detail.context": "Kontextlänge",
"ModelSwitchPanel.detail.pricing": "Preise",
"ModelSwitchPanel.detail.pricing.cachedInput": "Gecachter Input ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.group.audio": "Audio",
"ModelSwitchPanel.detail.pricing.group.image": "Bild",
"ModelSwitchPanel.detail.pricing.group.text": "Text",
"ModelSwitchPanel.detail.pricing.input": "Input ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.output": "Output ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.unit.audioInput": "Audioeingabe",
"ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Audioeingabe (Cache)",
"ModelSwitchPanel.detail.pricing.unit.audioOutput": "Audioausgabe",
"ModelSwitchPanel.detail.pricing.unit.imageGeneration": "Bilderzeugung",
"ModelSwitchPanel.detail.pricing.unit.imageInput": "Bildeingabe",
"ModelSwitchPanel.detail.pricing.unit.imageInput_cacheRead": "Bildeingabe (Cache)",
"ModelSwitchPanel.detail.pricing.unit.imageOutput": "Bildausgabe",
"ModelSwitchPanel.detail.pricing.unit.textInput": "Eingabe",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheRead": "Eingabe (Cache)",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheWrite": "Eingabe (Cache-Schreiben)",
"ModelSwitchPanel.detail.pricing.unit.textOutput": "Ausgabe",
"ModelSwitchPanel.detail.releasedAt": "Veröffentlicht am {{date}}",
"ModelSwitchPanel.emptyModel": "Kein Modell aktiviert. Bitte aktivieren Sie eines in den Einstellungen.",
"ModelSwitchPanel.emptyProvider": "Keine Anbieter aktiviert. Bitte aktivieren Sie einen in den Einstellungen.",
"ModelSwitchPanel.goToSettings": "Zu den Einstellungen",
+9
View File
@@ -150,6 +150,9 @@
"groupAgents.tag": "Gruppe",
"groupAgents.underReview": "Wird überprüft",
"home.communityAgents": "Community-Agenten",
"home.creatorReward.action": "Jetzt bewerben",
"home.creatorReward.subtitle": "Das Creator-Belohnungsprogramm 2026 ist offiziell gestartet.",
"home.creatorReward.title": "Kreieren. Teilen. Geld verdienen.",
"home.featuredAssistants": "Empfohlene Agenten",
"home.featuredModels": "Empfohlene Modelle",
"home.featuredPlugins": "Empfohlene Fähigkeiten",
@@ -194,6 +197,8 @@
"mcp.categories.tools.name": "Hilfsprogramme",
"mcp.categories.travel-transport.description": "Reiseplanung und Transport",
"mcp.categories.travel-transport.name": "Reise & Transport",
"mcp.categories.utility.description": "Wettervorhersage und meteorologische Dienste",
"mcp.categories.utility.name": "Dienstprogramme",
"mcp.categories.weather.description": "Wettervorhersage und meteorologische Dienste",
"mcp.categories.weather.name": "Wetter",
"mcp.categories.web-search.description": "Websuche und Informationsabruf",
@@ -478,6 +483,10 @@
"tab.plugin": "Fähigkeit",
"tab.provider": "Anbieter",
"tab.user": "Benutzer",
"time.formatOtherYear": "D. MMM YYYY",
"time.formatThisYear": "D. MMM",
"time.today": "Heute",
"time.yesterday": "Gestern",
"user.agents": "Agenten",
"user.downloads": "Downloads",
"user.editProfile": "Profil bearbeiten",
+1
View File
@@ -10,5 +10,6 @@
"starter.deepResearch": "Tiefgehende Recherche",
"starter.developing": "Demnächst verfügbar",
"starter.image": "Bild",
"starter.seedance": "Seedance 2.0",
"starter.write": "Schreiben"
}
+4 -1
View File
@@ -9,7 +9,10 @@
"addToKnowledgeBase.title": "Zur Bibliothek hinzufügen",
"addToKnowledgeBase.totalFiles": "{{count}} Dateien ausgewählt",
"createNew.confirm": "Neu erstellen",
"createNew.description.placeholder": "Bibliotheksbeschreibung (optional)",
"createNew.description.label": "Bibliotheksbeschreibung (optional)",
"createNew.description.placeholder": "Die Beschreibung hilft dem LLM, Ihre Bibliothek besser zu verstehen",
"createNew.edit.confirm": "Änderungen speichern",
"createNew.edit.title": "Bibliothek bearbeiten",
"createNew.formTitle": "Grundinformationen",
"createNew.name.placeholder": "Name der Bibliothek",
"createNew.name.required": "Bitte geben Sie einen Namen für die Bibliothek ein",
+4
View File
@@ -38,6 +38,10 @@
"messages.success.submit": "Autorisierung erfolgreich! Du kannst jetzt deinen Agenten veröffentlichen.",
"messages.success.upload": "Autorisierung erfolgreich! Du kannst jetzt eine neue Version veröffentlichen.",
"profileSetup.cancel": "Abbrechen",
"profileSetup.confirmChangeUserId.cancel": "Abbrechen",
"profileSetup.confirmChangeUserId.confirm": "Benutzer-ID ändern",
"profileSetup.confirmChangeUserId.description": "Sobald du zu @{{newId}} wechselst, kann jeder deine alte ID @{{oldId}} übernehmen und alle bestehenden Links zu deinem Profil werden ungültig. Dies kann nicht rückgängig gemacht werden. Bist du sicher, dass du fortfahren möchtest?",
"profileSetup.confirmChangeUserId.title": "Benutzer-ID ändern?",
"profileSetup.descriptionEdit": "Aktualisiere die Informationen deines Community-Profils.",
"profileSetup.descriptionFirstTime": "Richte dein Profil ein, um dein Community-Profil abzuschließen.",
"profileSetup.errors.fileTooLarge": "Dateigröße darf 2 MB nicht überschreiten",
+1 -1
View File
@@ -260,8 +260,8 @@
"providerModels.item.modelConfig.type.options.realtime": "Echtzeit-Chat",
"providerModels.item.modelConfig.type.options.stt": "Sprache-zu-Text",
"providerModels.item.modelConfig.type.options.text2music": "Text-zu-Musik",
"providerModels.item.modelConfig.type.options.text2video": "Text-zu-Video",
"providerModels.item.modelConfig.type.options.tts": "Text-zu-Sprache",
"providerModels.item.modelConfig.type.options.video": "Videoerstellung",
"providerModels.item.modelConfig.type.placeholder": "Bitte wählen Sie einen Modelltyp",
"providerModels.item.modelConfig.type.title": "Modelltyp",
"providerModels.item.modelConfig.video.extra": "Diese Einstellung aktiviert die Videorekognition innerhalb der Anwendung. Ob dies unterstützt wird, hängt vom Modell ab. Bitte testen Sie die Verfügbarkeit dieser Funktion.",
+28 -11
View File
@@ -274,22 +274,27 @@
"chatgpt-4o-latest.description": "ChatGPT-4o ist ein dynamisches Modell mit Echtzeit-Updates, das starkes Verständnis und Textgenerierung für großflächige Anwendungsfälle wie Kundensupport, Bildung und technischen Support kombiniert.",
"claude-2.0.description": "Claude 2 bietet wichtige Verbesserungen für Unternehmen, darunter einen führenden Kontext von 200.000 Token, reduzierte Halluzinationen, System-Prompts und ein neues Test-Feature: Tool-Nutzung.",
"claude-2.1.description": "Claude 2 bietet wichtige Verbesserungen für Unternehmen, darunter einen führenden Kontext von 200.000 Token, reduzierte Halluzinationen, System-Prompts und ein neues Test-Feature: Tool-Nutzung.",
"claude-3-5-haiku-20241022.description": "Claude 3.5 Haiku ist das schnellste Next-Gen-Modell von Anthropic. Im Vergleich zu Claude 3 Haiku bietet es verbesserte Fähigkeiten und übertrifft das bisher größte Modell Claude 3 Opus in vielen Intelligenz-Benchmarks.",
"claude-3-5-haiku-20241022.description": "Claude 3.5 Haiku ist das schnellste Next-Gen-Modell von Anthropic und verbessert sich in zahlreichen Fähigkeiten. Es übertrifft das bisherige Flaggschiff Claude 3 Opus in vielen Benchmarks.",
"claude-3-5-haiku-latest.description": "Claude 3.5 Haiku liefert schnelle Antworten für leichte Aufgaben.",
"claude-3-7-sonnet-20250219.description": "Claude 3.7 Sonnet ist das intelligenteste Modell von Anthropic und das erste hybride Reasoning-Modell auf dem Markt. Es kann nahezu sofortige Antworten liefern oder schrittweise Denkprozesse darstellen, die für Nutzer sichtbar sind. Sonnet überzeugt besonders in den Bereichen Programmierung, Data Science, Bildverarbeitung und Agentenaufgaben.",
"claude-3-7-sonnet-20250219.description": "Claude Sonnet 3.7 ist das intelligenteste Modell von Anthropic und das erste Hybrid-Reasoning-Modell auf dem Markt. Es ermöglicht nahezu sofortige Antworten oder tiefgehendes Denken mit präziser Steuerung.",
"claude-3-7-sonnet-latest.description": "Claude 3.7 Sonnet ist das neueste und leistungsfähigste Modell von Anthropic für hochkomplexe Aufgaben. Es überzeugt in Leistung, Intelligenz, Sprachfluss und Verständnis.",
"claude-3-haiku-20240307.description": "Claude 3 Haiku ist das schnellste und kompakteste Modell von Anthropic, entwickelt für nahezu sofortige Antworten mit schneller, präziser Leistung.",
"claude-3-opus-20240229.description": "Claude 3 Opus ist das leistungsstärkste Modell von Anthropic für hochkomplexe Aufgaben. Es überzeugt in Leistung, Intelligenz, Sprachfluss und Verständnis.",
"claude-3-sonnet-20240229.description": "Claude 3 Sonnet bietet eine ausgewogene Kombination aus Intelligenz und Geschwindigkeit für Unternehmensanwendungen. Es liefert hohe Nutzbarkeit bei geringeren Kosten und zuverlässiger Skalierbarkeit.",
"claude-3.5-sonnet.description": "Claude 3.5 Sonnet überzeugt durch herausragende Leistungen in den Bereichen Programmierung, Schreiben und komplexes Denken.",
"claude-3.7-sonnet-thought.description": "Claude 3.7 Sonnet mit erweitertem Denkvermögen für anspruchsvolle Aufgaben im Bereich komplexes Schlussfolgern.",
"claude-3.7-sonnet.description": "Claude 3.7 Sonnet ist eine verbesserte Version mit erweitertem Kontext und erweiterten Fähigkeiten.",
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 ist das schnellste und intelligenteste Haiku-Modell von Anthropic mit blitzschneller Reaktionszeit und erweitertem Denkvermögen.",
"claude-haiku-4.5.description": "Claude Haiku 4.5 ist ein schnelles und effizientes Modell für vielfältige Aufgaben.",
"claude-opus-4-1-20250805-thinking.description": "Claude Opus 4.1 Thinking ist eine erweiterte Variante, die ihren Denkprozess offenlegen kann.",
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 ist das neueste und leistungsfähigste Modell von Anthropic für hochkomplexe Aufgaben und überzeugt durch herausragende Leistung, Intelligenz, Sprachgewandtheit und Verständnis.",
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 ist das neueste und leistungsfähigste Modell von Anthropic für hochkomplexe Aufgaben. Es überzeugt durch herausragende Leistung, Intelligenz, Sprachgewandtheit und Verständnis.",
"claude-opus-4-20250514.description": "Claude Opus 4 ist das leistungsstärkste Modell von Anthropic für hochkomplexe Aufgaben und zeichnet sich durch exzellente Leistung, Intelligenz, Sprachgewandtheit und Verständnis aus.",
"claude-opus-4-5-20251101.description": "Claude Opus 4.5 ist das Flaggschiffmodell von Anthropic. Es kombiniert herausragende Intelligenz mit skalierbarer Leistung und ist ideal für komplexe Aufgaben, die höchste Qualität bei Antworten und logischem Denken erfordern.",
"claude-opus-4-6.description": "Claude Opus 4.6 ist das intelligenteste Modell von Anthropic für die Entwicklung von Agenten und Programmierung.",
"claude-opus-4-6.description": "Claude Opus 4.6 ist das intelligenteste Modell von Anthropic für den Aufbau von Agenten und das Programmieren.",
"claude-sonnet-4-20250514-thinking.description": "Claude Sonnet 4 Thinking kann nahezu sofortige Antworten oder schrittweises Denken mit sichtbarem Prozess erzeugen.",
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 kann nahezu sofortige Antworten liefern oder schrittweise Denkprozesse mit sichtbarem Ablauf darstellen.",
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 ist das bisher intelligenteste Modell von Anthropic. Es bietet nahezu sofortige Antworten oder schrittweises Denken mit feingranularer Steuerung für API-Nutzer.",
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 ist das bisher intelligenteste Modell von Anthropic.",
"claude-sonnet-4.description": "Claude Sonnet 4 ist die neueste Generation mit verbesserter Leistung in allen Aufgabenbereichen.",
"codegeex-4.description": "CodeGeeX-4 ist ein leistungsstarker KI-Coding-Assistent, der mehrsprachige Q&A und Codevervollständigung unterstützt, um die Produktivität von Entwicklern zu steigern.",
"codegeex4-all-9b.description": "CodeGeeX4-ALL-9B ist ein mehrsprachiges Codegenerierungsmodell, das Codevervollständigung, Codeinterpretation, Websuche, Funktionsaufrufe und Q&A auf Repositoriumsebene unterstützt. Es deckt eine Vielzahl von Softwareentwicklungsszenarien ab und ist eines der besten Code-Modelle unter 10 Milliarden Parametern.",
"codegemma.description": "CodeGemma ist ein leichtgewichtiges Modell für verschiedene Programmieraufgaben, das schnelle Iteration und Integration ermöglicht.",
@@ -358,7 +363,7 @@
"deepseek-ai/deepseek-v3.1-terminus.description": "DeepSeek V3.1 ist ein Next-Gen-Denkmodell mit stärkerem komplexem Denken und Chain-of-Thought für tiefgreifende Analyseaufgaben.",
"deepseek-ai/deepseek-v3.1.description": "DeepSeek V3.1 ist ein Next-Gen-Denkmodell mit stärkerem komplexem Denken und Chain-of-Thought für tiefgreifende Analyseaufgaben.",
"deepseek-ai/deepseek-vl2.description": "DeepSeek-VL2 ist ein MoE Vision-Language-Modell auf Basis von DeepSeekMoE-27B mit sparsamer Aktivierung. Es erreicht starke Leistung mit nur 4,5B aktiven Parametern und überzeugt bei visuellen QA-Aufgaben, OCR, Dokument-/Tabellen-/Diagrammverständnis und visueller Verankerung.",
"deepseek-chat.description": "Ein neues Open-Source-Modell, das allgemeine Fähigkeiten und Programmierkompetenz vereint. Es bewahrt die Dialogfähigkeit des Chat-Modells und die starke Codierungsleistung des Coder-Modells mit verbesserter Präferenzanpassung. DeepSeek-V2.5 verbessert zudem das Schreiben und das Befolgen von Anweisungen.",
"deepseek-chat.description": "DeepSeek V3.2 bietet ein ausgewogenes Verhältnis zwischen logischem Denken und Ausgabelänge für tägliche Frage-Antwort- und Agentenaufgaben. Öffentliche Benchmarks erreichen GPT-5-Niveau. Es ist das erste Modell, das Denken in die Werkzeugnutzung integriert und führend in Open-Source-Agentenbewertungen.",
"deepseek-coder-33B-instruct.description": "DeepSeek Coder 33B ist ein Code-Sprachmodell, trainiert auf 2B Tokens (87% Code, 13% chinesisch/englischer Text). Es bietet ein 16K-Kontextfenster und Fill-in-the-Middle-Aufgaben für projektweite Codevervollständigung und Snippet-Ergänzung.",
"deepseek-coder-v2.description": "DeepSeek Coder V2 ist ein Open-Source-MoE-Code-Modell mit starker Leistung bei Programmieraufgaben, vergleichbar mit GPT-4 Turbo.",
"deepseek-coder-v2:236b.description": "DeepSeek Coder V2 ist ein Open-Source-MoE-Code-Modell mit starker Leistung bei Programmieraufgaben, vergleichbar mit GPT-4 Turbo.",
@@ -381,7 +386,7 @@
"deepseek-r1-fast-online.description": "DeepSeek R1 Schnellversion mit Echtzeit-Websuche kombiniert 671B-Fähigkeiten mit schneller Reaktion.",
"deepseek-r1-online.description": "DeepSeek R1 Vollversion mit 671B Parametern und Echtzeit-Websuche bietet stärkeres Verständnis und bessere Generierung.",
"deepseek-r1.description": "DeepSeek-R1 nutzt Cold-Start-Daten vor dem RL und erreicht vergleichbare Leistungen wie OpenAI-o1 bei Mathematik, Programmierung und logischem Denken.",
"deepseek-reasoner.description": "Der Denkmodus DeepSeek V3.2 gibt vor der finalen Antwort eine Gedankenkette aus, um die Genauigkeit zu erhöhen.",
"deepseek-reasoner.description": "DeepSeek V3.2 Thinking ist ein Modell für tiefes logisches Denken, das vor der Ausgabe eine Gedankenkette generiert, um die Genauigkeit zu erhöhen. Es erzielt Spitzenwerte in Wettbewerben und bietet ein Denkvermögen vergleichbar mit Gemini-3.0-Pro.",
"deepseek-v2.description": "DeepSeek V2 ist ein effizientes MoE-Modell für kostengünstige Verarbeitung.",
"deepseek-v2:236b.description": "DeepSeek V2 236B ist das codefokussierte Modell von DeepSeek mit starker Codegenerierung.",
"deepseek-v3-0324.description": "DeepSeek-V3-0324 ist ein MoE-Modell mit 671B Parametern und herausragenden Stärken in Programmierung, technischer Kompetenz, Kontextverständnis und Langtextverarbeitung.",
@@ -471,7 +476,8 @@
"ernie-speed-pro-128k.description": "ERNIE Speed Pro 128K ist ein hochgradig skalierbares Modell für großflächige Online-Dienste und Unternehmensanwendungen.",
"ernie-x1-turbo-32k.description": "ERNIE X1 Turbo 32K ist ein schnelles Denkmodell mit 32K Kontext für komplexe Schlussfolgerungen und mehrstufige Gespräche.",
"ernie-x1.1-preview.description": "ERNIE X1.1 Preview ist ein Vorschau-Modell mit Denkfähigkeit zur Bewertung und zum Testen.",
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0 ist ein Bildgenerierungsmodell von ByteDance Seed, das Text- und Bildeingaben unterstützt und eine hochgradig steuerbare, hochwertige Bildgenerierung ermöglicht. Es erstellt Bilder aus Texteingaben.",
"fal-ai/bytedance/seedream/v4.5.description": "Seedream 4.5, entwickelt vom Seed-Team bei ByteDance, unterstützt Multi-Image-Bearbeitung und -Komposition. Es bietet verbesserte Subjektkonsistenz, präzise Befolgung von Anweisungen, Verständnis räumlicher Logik, ästhetischen Ausdruck, Poster-Layout und Logo-Design mit hochpräziser Text-Bild-Darstellung.",
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0, entwickelt von ByteDance Seed, unterstützt Text- und Bildeingaben für eine hochgradig steuerbare, qualitativ hochwertige Bildgenerierung aus Prompts.",
"fal-ai/flux-kontext/dev.description": "FLUX.1-Modell mit Fokus auf Bildbearbeitung, unterstützt Text- und Bildeingaben.",
"fal-ai/flux-pro/kontext.description": "FLUX.1 Kontext [pro] akzeptiert Texte und Referenzbilder als Eingabe und ermöglicht gezielte lokale Bearbeitungen sowie komplexe globale Szenentransformationen.",
"fal-ai/flux/krea.description": "Flux Krea [dev] ist ein Bildgenerierungsmodell mit ästhetischer Ausrichtung auf realistischere, natürliche Bilder.",
@@ -479,8 +485,8 @@
"fal-ai/hunyuan-image/v3.description": "Ein leistungsstarkes natives multimodales Bildgenerierungsmodell.",
"fal-ai/imagen4/preview.description": "Hochwertiges Bildgenerierungsmodell von Google.",
"fal-ai/nano-banana.description": "Nano Banana ist das neueste, schnellste und effizienteste native multimodale Modell von Google. Es ermöglicht Bildgenerierung und -bearbeitung im Dialog.",
"fal-ai/qwen-image-edit.description": "Ein professionelles Bildbearbeitungsmodell des Qwen-Teams, das semantische und visuelle Bearbeitungen unterstützt, chinesischen und englischen Text präzise bearbeitet und hochwertige Anpassungen wie Stilübertragungen und Objektrotation ermöglicht.",
"fal-ai/qwen-image.description": "Ein leistungsstarkes Bildgenerierungsmodell des Qwen-Teams mit beeindruckender Darstellung chinesischer Texte und vielfältigen visuellen Stilen.",
"fal-ai/qwen-image-edit.description": "Ein professionelles Bildbearbeitungsmodell des Qwen-Teams, das semantische und visuelle Bearbeitungen, präzise Textbearbeitung in Chinesisch/Englisch, Stilübertragungen, Drehungen und mehr unterstützt.",
"fal-ai/qwen-image.description": "Ein leistungsstarkes Bildgenerierungsmodell des Qwen-Teams mit starker chinesischer Textrendering-Fähigkeit und vielfältigen visuellen Stilen.",
"flux-1-schnell.description": "Ein Text-zu-Bild-Modell mit 12 Milliarden Parametern von Black Forest Labs, das latente adversariale Diffusionsdistillation nutzt, um hochwertige Bilder in 14 Schritten zu erzeugen. Es konkurriert mit geschlossenen Alternativen und ist unter Apache-2.0 für persönliche, Forschungs- und kommerzielle Nutzung verfügbar.",
"flux-dev.description": "FLUX.1 [dev] ist ein Modell mit offenen Gewichten für nicht-kommerzielle Nutzung. Es bietet nahezu professionelle Bildqualität und Befolgung von Anweisungen bei effizienterer Nutzung von Ressourcen im Vergleich zu Standardmodellen gleicher Größe.",
"flux-kontext-max.description": "Modernste kontextuelle Bildgenerierung und -bearbeitung, kombiniert Text und Bilder für präzise, kohärente Ergebnisse.",
@@ -511,6 +517,8 @@
"gemini-2.0-flash-lite-001.description": "Eine Gemini 2.0 Flash-Variante, optimiert für Kosteneffizienz und geringe Latenz.",
"gemini-2.0-flash-lite.description": "Eine Gemini 2.0 Flash-Variante, optimiert für Kosteneffizienz und geringe Latenz.",
"gemini-2.0-flash.description": "Gemini 2.0 Flash bietet Next-Gen-Funktionen wie außergewöhnliche Geschwindigkeit, native Tool-Nutzung, multimodale Generierung und ein Kontextfenster von 1 Million Tokens.",
"gemini-2.5-flash-image-preview.description": "Nano Banana ist das neueste, schnellste und effizienteste native multimodale Modell von Google und ermöglicht dialogbasierte Bildgenerierung und -bearbeitung.",
"gemini-2.5-flash-image-preview:image.description": "Nano Banana ist das neueste, schnellste und effizienteste native multimodale Modell von Google und ermöglicht dialogbasierte Bildgenerierung und -bearbeitung.",
"gemini-2.5-flash-image.description": "Nano Banana ist Googles neuestes, schnellstes und effizientestes natives multimodales Modell für konversationale Bildgenerierung und -bearbeitung.",
"gemini-2.5-flash-image:image.description": "Nano Banana ist Googles neuestes, schnellstes und effizientestes natives multimodales Modell für konversationale Bildgenerierung und -bearbeitung.",
"gemini-2.5-flash-lite-preview-06-17.description": "Gemini 2.5 Flash-Lite Preview ist Googles kleinstes und kosteneffizientestes Modell für großflächige Nutzung.",
@@ -525,7 +533,7 @@
"gemini-2.5-pro.description": "Gemini 2.5 Pro ist Googles fortschrittlichstes Reasoning-Modell, das über Code, Mathematik und MINT-Probleme nachdenken und große Datensätze, Codebasen und Dokumente mit langem Kontext analysieren kann.",
"gemini-3-flash-preview.description": "Gemini 3 Flash ist das intelligenteste Modell, das auf Geschwindigkeit ausgelegt ist es vereint modernste Intelligenz mit exzellenter Suchverankerung.",
"gemini-3-pro-image-preview.description": "Gemini 3 Pro Image (Nano Banana Pro) ist Googles Bildgenerierungsmodell mit Unterstützung für multimodale Dialoge.",
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) ist Googles Bildgenerierungsmodell und unterstützt auch multimodale Chats.",
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) ist Googles Modell zur Bildgenerierung und unterstützt auch multimodale Konversationen.",
"gemini-3-pro-preview.description": "Gemini 3 Pro ist Googles leistungsstärkstes Agenten- und Vibe-Coding-Modell. Es bietet reichhaltigere visuelle Inhalte und tiefere Interaktionen auf Basis modernster logischer Fähigkeiten.",
"gemini-flash-latest.description": "Neueste Version von Gemini Flash",
"gemini-flash-lite-latest.description": "Neueste Version von Gemini Flash-Lite",
@@ -604,6 +612,7 @@
"google/text-embedding-005.description": "Ein auf Englisch fokussiertes Text-Embedding-Modell, optimiert für Code- und Sprachaufgaben.",
"google/text-multilingual-embedding-002.description": "Ein mehrsprachiges Text-Embedding-Modell, optimiert für sprachübergreifende Aufgaben in vielen Sprachen.",
"gpt-3.5-turbo-0125.description": "GPT 3.5 Turbo für Textgenerierung und -verständnis; verweist derzeit auf gpt-3.5-turbo-0125.",
"gpt-3.5-turbo-0613.description": "GPT 3.5 Turbo ist ein schnelles und effizientes Modell für vielfältige Aufgaben.",
"gpt-3.5-turbo-1106.description": "GPT 3.5 Turbo für Textgenerierung und -verständnis; verweist derzeit auf gpt-3.5-turbo-0125.",
"gpt-3.5-turbo-instruct.description": "GPT 3.5 Turbo für Textgenerierung und -verständnis, optimiert für die Befolgung von Anweisungen.",
"gpt-3.5-turbo.description": "GPT 3.5 Turbo für Textgenerierung und -verständnis; verweist derzeit auf gpt-3.5-turbo-0125.",
@@ -614,10 +623,12 @@
"gpt-4-1106-preview.description": "Das neueste GPT-4 Turbo unterstützt jetzt auch visuelle Eingaben. Visuelle Anfragen unterstützen den JSON-Modus und Funktionsaufrufe. Es ist ein kosteneffizientes multimodales Modell, das Genauigkeit und Effizienz für Echtzeitanwendungen ausbalanciert.",
"gpt-4-32k-0613.description": "GPT-4 bietet ein größeres Kontextfenster zur Verarbeitung längerer Eingaben ideal für umfassende Informationssynthese und Datenanalyse.",
"gpt-4-32k.description": "GPT-4 bietet ein größeres Kontextfenster zur Verarbeitung längerer Eingaben ideal für umfassende Informationssynthese und Datenanalyse.",
"gpt-4-o-preview.description": "GPT-4o ist das fortschrittlichste multimodale Modell und verarbeitet sowohl Text- als auch Bildeingaben.",
"gpt-4-turbo-2024-04-09.description": "Das neueste GPT-4 Turbo unterstützt jetzt auch visuelle Eingaben. Visuelle Anfragen unterstützen den JSON-Modus und Funktionsaufrufe. Es ist ein kosteneffizientes multimodales Modell, das Genauigkeit und Effizienz für Echtzeitanwendungen ausbalanciert.",
"gpt-4-turbo-preview.description": "Das neueste GPT-4 Turbo unterstützt jetzt auch visuelle Eingaben. Visuelle Anfragen unterstützen den JSON-Modus und Funktionsaufrufe. Es ist ein kosteneffizientes multimodales Modell, das Genauigkeit und Effizienz für Echtzeitanwendungen ausbalanciert.",
"gpt-4-turbo.description": "Das neueste GPT-4 Turbo unterstützt jetzt auch visuelle Eingaben. Visuelle Anfragen unterstützen den JSON-Modus und Funktionsaufrufe. Es ist ein kosteneffizientes multimodales Modell, das Genauigkeit und Effizienz für Echtzeitanwendungen ausbalanciert.",
"gpt-4-vision-preview.description": "Vorschau von GPT-4 Vision, entwickelt für Aufgaben der Bildanalyse und -verarbeitung.",
"gpt-4.1-2025-04-14.description": "GPT-4.1 ist das Spitzenmodell für komplexe Aufgaben und ideal für interdisziplinäre Problemlösungen.",
"gpt-4.1-mini.description": "GPT-4.1 mini vereint Intelligenz, Geschwindigkeit und Kostenersparnis ideal für viele Anwendungsfälle.",
"gpt-4.1-nano.description": "GPT-4.1 nano ist das schnellste und kostengünstigste Modell der GPT-4.1-Reihe.",
"gpt-4.1.description": "GPT-4.1 ist unser Flaggschiffmodell für komplexe Aufgaben und domänenübergreifende Problemlösungen.",
@@ -627,6 +638,7 @@
"gpt-4o-2024-08-06.description": "ChatGPT-4o ist ein dynamisches Modell, das in Echtzeit aktualisiert wird. Es kombiniert ein starkes Sprachverständnis mit leistungsfähiger Textgenerierung für großflächige Anwendungsfälle wie Kundensupport, Bildung und technische Unterstützung.",
"gpt-4o-2024-11-20.description": "ChatGPT-4o ist ein dynamisches Modell mit Echtzeit-Updates, das starkes Sprachverständnis und Textgenerierung für großflächige Anwendungsfälle wie Kundensupport, Bildung und technische Hilfe vereint.",
"gpt-4o-audio-preview.description": "GPT-4o Audio-Vorschau-Modell mit Audioeingabe und -ausgabe.",
"gpt-4o-mini-2024-07-18.description": "GPT-4o mini ist eine kosteneffiziente Lösung für ein breites Spektrum an Text- und Bildaufgaben.",
"gpt-4o-mini-audio-preview.description": "GPT-4o Mini Audio-Modell mit Audioeingabe und -ausgabe.",
"gpt-4o-mini-realtime-preview.description": "GPT-4o-mini-Echtzeitvariante mit Audio- und Textein-/ausgabe in Echtzeit.",
"gpt-4o-mini-search-preview.description": "GPT-4o Mini Search Preview ist darauf trainiert, Websuchanfragen über die Chat Completions API zu verstehen und auszuführen. Websuchen werden zusätzlich zu den Tokenkosten pro Tool-Aufruf abgerechnet.",
@@ -980,6 +992,8 @@
"openai/text-embedding-3-small.description": "OpenAIs verbesserte, leistungsstärkere Variante des ada-Embedding-Modells.",
"openai/text-embedding-ada-002.description": "OpenAIs älteres Text-Embedding-Modell.",
"openrouter/auto.description": "Basierend auf Kontextlänge, Thema und Komplexität wird Ihre Anfrage an Llama 3 70B Instruct, Claude 3.5 Sonnet (selbstmoderiert) oder GPT-4o weitergeleitet.",
"oswe-vscode-prime.description": "Raptor mini ist ein Vorschau-Modell, das für Aufgaben rund ums Programmieren optimiert wurde.",
"oswe-vscode-secondary.description": "Raptor mini ist ein Vorschau-Modell, das für Aufgaben rund ums Programmieren optimiert wurde.",
"perplexity/sonar-pro.description": "Perplexitys Flaggschiffprodukt mit Suchverankerung, unterstützt komplexe Anfragen und Folgefragen.",
"perplexity/sonar-reasoning-pro.description": "Ein fortschrittliches Modell mit Fokus auf logisches Denken, das CoT mit erweiterter Suche ausgibt, einschließlich mehrerer Suchanfragen pro Anfrage.",
"perplexity/sonar-reasoning.description": "Ein Modell mit Fokus auf logisches Denken, das Chain-of-Thought (CoT) mit detaillierten, suchbasierten Erklärungen liefert.",
@@ -1122,6 +1136,7 @@
"qwq.description": "QwQ ist ein Schlussfolgerungsmodell aus der Qwen-Familie. Im Vergleich zu standardmäßig instruktionstunierten Modellen bietet es überlegene Denk- und Schlussfolgerungsfähigkeiten, die die Leistung bei nachgelagerten Aufgaben deutlich verbessern insbesondere bei schwierigen Problemen. QwQ-32B ist ein mittelgroßes Modell, das mit führenden Schlussfolgerungsmodellen wie DeepSeek-R1 und o1-mini mithalten kann.",
"qwq_32b.description": "Mittelgroßes Schlussfolgerungsmodell aus der Qwen-Familie. Im Vergleich zu standardmäßig instruktionstunierten Modellen steigern QwQs Denk- und Schlussfolgerungsfähigkeiten die Leistung bei nachgelagerten Aufgaben deutlich insbesondere bei schwierigen Problemen.",
"r1-1776.description": "R1-1776 ist eine nachtrainierte Variante von DeepSeek R1, die darauf ausgelegt ist, unzensierte, objektive und faktenbasierte Informationen bereitzustellen.",
"seedance-1-5-pro-251215.description": "Seedance 1.5 Pro von ByteDance unterstützt Text-zu-Video, Bild-zu-Video (erstes Bild, erstes+letztes Bild) sowie Audiogenerierung synchronisiert mit visuellen Inhalten.",
"solar-mini-ja.description": "Solar Mini (Ja) erweitert Solar Mini mit einem Fokus auf Japanisch und behält dabei eine effiziente und starke Leistung in Englisch und Koreanisch bei.",
"solar-mini.description": "Solar Mini ist ein kompaktes LLM, das GPT-3.5 übertrifft. Es bietet starke mehrsprachige Fähigkeiten in Englisch und Koreanisch und ist eine effiziente Lösung mit kleinem Ressourcenbedarf.",
"solar-pro.description": "Solar Pro ist ein hochintelligentes LLM von Upstage, das auf Befolgen von Anweisungen auf einer einzelnen GPU ausgelegt ist und IFEval-Werte über 80 erreicht. Derzeit wird Englisch unterstützt; die vollständige Veröffentlichung mit erweitertem Sprachsupport und längeren Kontexten war für November 2024 geplant.",
@@ -1162,7 +1177,9 @@
"tencent/Hunyuan-A13B-Instruct.description": "Hunyuan-A13B-Instruct nutzt insgesamt 80B Parameter, davon 13B aktiv, um mit größeren Modellen zu konkurrieren. Es unterstützt hybrides schnelles/langsames Denken, stabiles Langtextverständnis und führende Agentenfähigkeiten auf BFCL-v3 und τ-Bench. GQA- und Multi-Quant-Formate ermöglichen effiziente Inferenz.",
"tencent/Hunyuan-MT-7B.description": "Das Hunyuan-Übersetzungsmodell umfasst Hunyuan-MT-7B und das Ensemble Hunyuan-MT-Chimera. Hunyuan-MT-7B ist ein leichtgewichtiges 7B-Modell, das 33 Sprachen sowie 5 chinesische Minderheitensprachen unterstützt. Bei WMT25 erzielte es 30 erste Plätze in 31 Sprachpaaren. Tencent Hunyuan verwendet eine vollständige Trainingspipeline von Pretraining über SFT bis hin zu RL für Übersetzung und Ensemble, und erreicht führende Leistung bei einfacher, effizienter Bereitstellung.",
"text-embedding-3-large.description": "Das leistungsfähigste Embedding-Modell für englische und nicht-englische Aufgaben.",
"text-embedding-3-small-inference.description": "Embedding V3 Small (Inference) Modell für Text-Einbettungen.",
"text-embedding-3-small.description": "Ein effizientes, kostengünstiges Next-Gen-Embedding-Modell für Retrieval- und RAG-Szenarien.",
"text-embedding-ada-002.description": "Embedding V2 Ada Modell für Text-Einbettungen.",
"thudm/glm-4-32b.description": "GLM-4-32B-0414 ist ein 32B zweisprachiges (Chinesisch/Englisch) Open-Weights-Modell, optimiert für Codegenerierung, Funktionsaufrufe und Agentenaufgaben. Es wurde mit 15T hochwertigen, reasoning-intensiven Daten vortrainiert und durch menschliche Präferenzanpassung, Rejection Sampling und RL weiter verfeinert. Es überzeugt bei komplexem Denken, Artefakterstellung und strukturierten Ausgaben und erreicht GPT-4o- und DeepSeek-V3-0324-Niveau in mehreren Benchmarks.",
"thudm/glm-4-32b:free.description": "GLM-4-32B-0414 ist ein 32B zweisprachiges (Chinesisch/Englisch) Open-Weights-Modell, optimiert für Codegenerierung, Funktionsaufrufe und Agentenaufgaben. Es wurde mit 15T hochwertigen, reasoning-intensiven Daten vortrainiert und durch menschliche Präferenzanpassung, Rejection Sampling und RL weiter verfeinert. Es überzeugt bei komplexem Denken, Artefakterstellung und strukturierten Ausgaben und erreicht GPT-4o- und DeepSeek-V3-0324-Niveau in mehreren Benchmarks.",
"thudm/glm-4-9b-chat.description": "Die Open-Source-Version des neuesten GLM-4-Pretraining-Modells von Zhipu AI.",
+1
View File
@@ -30,6 +30,7 @@
"internlm.description": "Eine Open-Source-Organisation für Forschung und Tools rund um große Modelle mit einer effizienten, benutzerfreundlichen Plattform für den Zugang zu modernsten Modellen und Algorithmen.",
"jina.description": "Jina AI wurde 2020 gegründet und ist ein führendes Unternehmen im Bereich Such-KI. Der Such-Stack umfasst Vektormodelle, Reranker und kleine Sprachmodelle für zuverlässige, hochwertige generative und multimodale Suchanwendungen.",
"lmstudio.description": "LM Studio ist eine Desktop-App zur Entwicklung und zum Experimentieren mit LLMs auf dem eigenen Computer.",
"lobehub.description": "LobeHub Cloud verwendet offizielle APIs, um auf KI-Modelle zuzugreifen, und misst die Nutzung anhand von Credits, die an Modell-Token gebunden sind.",
"minimax.description": "MiniMax wurde 2021 gegründet und entwickelt allgemeine KI mit multimodalen Foundation-Modellen, darunter Textmodelle mit Billionen Parametern, Sprach- und Bildmodelle sowie Apps wie Hailuo AI.",
"mistral.description": "Mistral bietet fortschrittliche allgemeine, spezialisierte und Forschungsmodelle für komplexes Denken, mehrsprachige Aufgaben und Codegenerierung inklusive Funktionsaufrufen für individuelle Integrationen.",
"modelscope.description": "ModelScope ist die Model-as-a-Service-Plattform von Alibaba Cloud mit einer breiten Auswahl an KI-Modellen und Inferenzdiensten.",
+1
View File
@@ -14,6 +14,7 @@
"table.columns.totalTokens": "Token-Nutzung",
"table.columns.type.enums.chat": "Textgenerierung",
"table.columns.type.enums.imageGeneration": "Bildgenerierung",
"table.columns.type.enums.videoGeneration": "Videoerstellung",
"table.columns.type.title": "Typ",
"table.desc": "Details zur Nutzung von Rechen-Credits für Textgenerierung, Einbettung, Bildgenerierung usw.",
"table.more": "Details anzeigen",
+6
View File
@@ -131,6 +131,12 @@
"limitation.providers.prompter.subTitle": "Benutzerdefinierter API-Service ist nur in kostenpflichtigen Plänen verfügbar. Upgrade jetzt, um globale Modellanbieter zu nutzen",
"limitation.providers.prompter.title": "Jetzt abonnieren, um benutzerdefinierten API-Service zu nutzen",
"limitation.providers.tooltip": "Benutzerdefinierter API-Service ist nur in kostenpflichtigen Plänen verfügbar",
"limitation.video.success.action": "Weiter generieren",
"limitation.video.success.desc": "Ihr {{plan}}-Abonnement wurde erfolgreich aktualisiert. Viel Spaß mit der KI-Videoerstellung. Ihr aktueller Plan beinhaltet:",
"limitation.video.success.title": "Upgrade erfolgreich",
"limitation.video.topupSuccess.action": "Weiter generieren",
"limitation.video.topupSuccess.desc": "Ihre zusätzlichen Guthaben sind jetzt aktiv. Viel Spaß mit der KI-Videoerstellung. Ihr aktueller Plan beinhaltet:",
"limitation.video.topupSuccess.title": "Aufladung erfolgreich",
"modelPricing.button": "Preisdokumentation anzeigen",
"modelPricing.desc": "{{name}} verwendet Credits zur Messung der Nutzung von KI-Modellen. Die folgende Tabelle zeigt die Rechen-Credits pro 1 Mio. Tokens.",
"modelPricing.title": "Preise für Textmodelle",
+35
View File
@@ -86,6 +86,10 @@
"localFiles.editFile.replaceFirst": "Nur erstes Vorkommen ersetzen",
"localFiles.file": "Datei",
"localFiles.folder": "Ordner",
"localFiles.globFiles.pattern": "Muster",
"localFiles.grepContent.glob": "Dateifilter",
"localFiles.grepContent.pattern": "Suchmuster",
"localFiles.grepContent.type": "Dateityp",
"localFiles.moveFiles.itemsMoved": "{{count}} Element(e) verschoben:",
"localFiles.moveFiles.itemsMoved_one": "{{count}} Element verschoben:",
"localFiles.moveFiles.itemsMoved_other": "{{count}} Elemente verschoben:",
@@ -95,11 +99,17 @@
"localFiles.open": "Öffnen",
"localFiles.openFile": "Datei öffnen",
"localFiles.openFolder": "Ordner öffnen",
"localFiles.outOfScope.requestedPaths": "Angeforderte Pfade",
"localFiles.outOfScope.warning": "Warnung: Die folgenden Pfade liegen außerhalb des konfigurierten Arbeitsverzeichnisses. Bitte bestätigen Sie, dass Sie den Zugriff erlauben möchten.",
"localFiles.outOfScope.workingDirectory": "Arbeitsverzeichnis",
"localFiles.read.more": "Mehr anzeigen",
"localFiles.readFile": "Datei lesen",
"localFiles.readFile.lineRange": "Zeilen {{start}} - {{end}}",
"localFiles.readFileError": "Datei konnte nicht gelesen werden. Bitte überprüfen Sie den Dateipfad.",
"localFiles.readFiles": "Dateien lesen",
"localFiles.readFilesError": "Dateien konnten nicht gelesen werden. Bitte überprüfen Sie den Dateipfad.",
"localFiles.searchFiles.keywords": "Stichwörter",
"localFiles.securityBlacklist.warning": "Sicherheitswarnung: Diese Aktion wurde durch Sicherheitsregeln markiert und erfordert Ihre ausdrückliche Zustimmung.",
"localFiles.writeFile.characters": "Zeichen",
"localFiles.writeFile.preview": "Inhaltsvorschau",
"localFiles.writeFile.truncated": "gekürzt",
@@ -136,6 +146,31 @@
"search.summary": "Zusammenfassung",
"search.summaryTooltip": "Aktuellen Inhalt zusammenfassen",
"search.viewMoreResults": "{{results}} weitere Ergebnisse anzeigen",
"securityBlacklist.awsCredentials": "Der Zugriff auf AWS-Zugangsdaten kann Cloud-Zugriffsschlüssel offenlegen",
"securityBlacklist.browserCredentials": "Der Zugriff auf gespeicherte Browser-Zugangsdaten kann Passwörter offenlegen",
"securityBlacklist.chownSystemDirs": "Das Ändern des Besitzes von Systemverzeichnissen ist gefährlich",
"securityBlacklist.ddDiskWrite": "Das Schreiben zufälliger Daten auf Datenträger kann Daten zerstören",
"securityBlacklist.directMemoryAccess": "Direkter Speicherzugriff ist äußerst gefährlich",
"securityBlacklist.disableFirewall": "Das Deaktivieren der Firewall setzt das System Angriffen aus",
"securityBlacklist.dockerConfig": "Das Lesen der Docker-Konfiguration kann Registry-Zugangsdaten offenlegen",
"securityBlacklist.envFiles": "Das Lesen von .env-Dateien kann sensible Zugangsdaten und API-Schlüssel offenlegen",
"securityBlacklist.etcPasswd": "Das Ändern von /etc/passwd kann den Systemzugang sperren",
"securityBlacklist.forkBomb": "Eine Fork-Bombe kann das System zum Absturz bringen",
"securityBlacklist.formatPartition": "Das Formatieren von Systempartitionen zerstört Daten",
"securityBlacklist.gcpCredentials": "Das Lesen von GCP-Zugangsdaten kann Cloud-Service-Konten kompromittieren",
"securityBlacklist.gitCredentials": "Das Lesen der Git-Zugangsdaten kann Zugriffstoken offenlegen",
"securityBlacklist.historyFiles": "Das Lesen von Verlaufdateien kann sensible Befehle und Zugangsdaten offenlegen",
"securityBlacklist.kernelParams": "Das Ändern von Kernel-Parametern ohne Fachwissen kann das System zum Absturz bringen",
"securityBlacklist.kubeConfig": "Das Lesen der Kubernetes-Konfiguration kann Cluster-Zugangsdaten offenlegen",
"securityBlacklist.npmrc": "Das Lesen der npm-Token-Datei kann Registry-Zugangsdaten offenlegen",
"securityBlacklist.removeSystemPackages": "Das Entfernen essenzieller Systempakete kann das System beschädigen",
"securityBlacklist.rmForceRecursive": "Erzwingtes rekursives Löschen ohne konkretes Ziel ist zu gefährlich",
"securityBlacklist.rmHomeDir": "Rekursives Löschen des Home-Verzeichnisses ist äußerst gefährlich",
"securityBlacklist.rmRootDir": "Rekursives Löschen des Root-Verzeichnisses zerstört das System",
"securityBlacklist.sshConfig": "Das Ändern der SSH-Konfiguration kann den Zugang zum System sperren",
"securityBlacklist.sshPrivateKeys": "Das Lesen privater SSH-Schlüssel kann die Systemsicherheit gefährden",
"securityBlacklist.sudoers": "Das Ändern der sudoers-Datei ohne Validierung ist gefährlich",
"securityBlacklist.suidShells": "Das Setzen von SUID auf Shells oder Interpreter stellt ein Sicherheitsrisiko dar",
"updateArgs.duplicateKeyError": "Feldschlüssel muss eindeutig sein",
"updateArgs.form.add": "Eintrag hinzufügen",
"updateArgs.form.key": "Feldschlüssel",
+28
View File
@@ -0,0 +1,28 @@
{
"config.aspectRatio.label": "Seitenverhältnis",
"config.cameraFixed.label": "Feste Kamera",
"config.duration.label": "Dauer",
"config.endImageUrl.label": "Endbild",
"config.generateAudio.label": "Audio generieren",
"config.header.title": "Video",
"config.imageUrl.label": "Startbild",
"config.prompt.placeholder": "Beschreiben Sie das Video, das Sie erstellen möchten",
"config.referenceImage.label": "Referenzbild",
"config.resolution.label": "Auflösung",
"config.seed.label": "Seed",
"config.seed.random": "Zufällig",
"generation.actions.copyError": "Fehlermeldung kopieren",
"generation.actions.errorCopied": "Fehlermeldung in die Zwischenablage kopiert",
"generation.actions.errorCopyFailed": "Fehlermeldung konnte nicht kopiert werden",
"generation.actions.generate": "Erstellen",
"generation.freeQuota.exhausted": "🎁 Freikontingent aufgebraucht, es werden Credits verwendet",
"generation.freeQuota.remaining": "🎁 {{remaining}} kostenlose Videos heute verfügbar",
"generation.status.failed": "Erstellung fehlgeschlagen",
"generation.status.generating": "Wird erstellt...",
"generation.validation.endFrameRequiresStartFrame": "Ein Endbild kann nicht ohne ein Startbild verwendet werden. Bitte zuerst ein Startbild festlegen.",
"topic.createNew": "Neues Thema",
"topic.deleteConfirm": "Video-Thema löschen",
"topic.deleteConfirmDesc": "Sie sind dabei, dieses Video-Thema zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.",
"topic.title": "Video-Themen",
"topic.untitled": "Standardthema"
}
+2 -2
View File
@@ -58,13 +58,13 @@
"duplicateTitle": "{{title}} Copy",
"emptyAgent": "No Agents yet. Start with your first Agent—build your system over time.",
"emptyAgentAction": "Create Agent",
"extendParams.disableContextCaching.desc": "Reduce by up to 90% of the cost of generating a single conversation and bring a max of 4x speed. Enabling this will automatically disable the limit on the number of historical messages. <1>Learn more</1>",
"extendParams.disableContextCaching.desc": "Reduce by up to 90% of the cost of generating a single conversation and bring a max of 4x speed. <1>Learn more</1>",
"extendParams.disableContextCaching.title": "Enable Context Caching",
"extendParams.effort.desc": "Control how many tokens Claude uses when responding with the effort parameter.",
"extendParams.effort.title": "Effort",
"extendParams.enableAdaptiveThinking.desc": "Let Claude dynamically decide when and how much to think with adaptive thinking mode.",
"extendParams.enableAdaptiveThinking.title": "Enable Adaptive Thinking",
"extendParams.enableReasoning.desc": "Based on the Claude Thinking mechanism limit, enabling this will automatically disable the limit on the number of historical messages. <1>Learn more</1>",
"extendParams.enableReasoning.desc": "Based on the Claude Thinking mechanism limit. <1>Learn more</1>",
"extendParams.enableReasoning.title": "Enable Deep Thinking",
"extendParams.imageAspectRatio.title": "Image Aspect Ratio",
"extendParams.imageResolution.title": "Image Resolution",
+2
View File
@@ -143,6 +143,7 @@
"cmdk.keywords.stats": "stats statistics analytics",
"cmdk.keywords.submitIssue": "issue bug problem feedback",
"cmdk.keywords.usage": "usage statistics consumption quota",
"cmdk.keywords.video": "video,generate,seedance,kling",
"cmdk.memory": "Memory",
"cmdk.mentionAgent": "Mention Agent",
"cmdk.navigate": "Navigate",
@@ -193,6 +194,7 @@
"cmdk.themeLight": "Light",
"cmdk.toOpen": "Open",
"cmdk.toSelect": "Select",
"cmdk.video": "AI Video",
"confirm": "Confirm",
"contact": "Contact Us",
"copy": "Copy",
+32 -1
View File
@@ -43,6 +43,8 @@
"FileManager.emptyStatus.or": "or",
"FileManager.emptyStatus.title": "Drag files or folders here",
"FileManager.noFolders": "No folders available",
"FileManager.search.noResults": "No files found",
"FileManager.search.placeholder": "Search files...",
"FileManager.sort.dateAdded": "Date Added",
"FileManager.sort.name": "Name",
"FileManager.sort.size": "Size",
@@ -84,7 +86,7 @@
"MaxTokenSlider.unlimited": "Unlimited",
"ModelSelect.featureTag.custom": "Custom model, by default, supports both function calls and visual recognition. Please verify the availability of the above capabilities based on actual situations.",
"ModelSelect.featureTag.file": "This model supports file upload for reading and recognition.",
"ModelSelect.featureTag.functionCall": "This model supports function calls.",
"ModelSelect.featureTag.functionCall": "This model supports tool calls.",
"ModelSelect.featureTag.imageOutput": "This model supports image generation.",
"ModelSelect.featureTag.reasoning": "This model supports deep thinking.",
"ModelSelect.featureTag.search": "This model supports online search.",
@@ -94,6 +96,35 @@
"ModelSelect.removed": "The model is not in the list. It will be automatically removed if deselected.",
"ModelSwitchPanel.byModel": "By Model",
"ModelSwitchPanel.byProvider": "By Provider",
"ModelSwitchPanel.detail.abilities": "Abilities",
"ModelSwitchPanel.detail.abilities.files": "Files",
"ModelSwitchPanel.detail.abilities.functionCall": "Tool Calling",
"ModelSwitchPanel.detail.abilities.imageOutput": "Image Output",
"ModelSwitchPanel.detail.abilities.reasoning": "Reasoning",
"ModelSwitchPanel.detail.abilities.search": "Search",
"ModelSwitchPanel.detail.abilities.video": "Video",
"ModelSwitchPanel.detail.abilities.vision": "Vision",
"ModelSwitchPanel.detail.config": "Model Config",
"ModelSwitchPanel.detail.context": "Context Length",
"ModelSwitchPanel.detail.pricing": "Pricing",
"ModelSwitchPanel.detail.pricing.cachedInput": "Cached input ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.group.audio": "Audio",
"ModelSwitchPanel.detail.pricing.group.image": "Image",
"ModelSwitchPanel.detail.pricing.group.text": "Text",
"ModelSwitchPanel.detail.pricing.input": "Input ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.output": "Output ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.unit.audioInput": "Audio Input",
"ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Audio Input (Cached)",
"ModelSwitchPanel.detail.pricing.unit.audioOutput": "Audio Output",
"ModelSwitchPanel.detail.pricing.unit.imageGeneration": "Image Generation",
"ModelSwitchPanel.detail.pricing.unit.imageInput": "Image Input",
"ModelSwitchPanel.detail.pricing.unit.imageInput_cacheRead": "Image Input (Cached)",
"ModelSwitchPanel.detail.pricing.unit.imageOutput": "Image Output",
"ModelSwitchPanel.detail.pricing.unit.textInput": "Input",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheRead": "Input (Cached)",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheWrite": "Input (Cache Write)",
"ModelSwitchPanel.detail.pricing.unit.textOutput": "Output",
"ModelSwitchPanel.detail.releasedAt": "Released {{date}}",
"ModelSwitchPanel.emptyModel": "No enabled model. Please go to settings to enable.",
"ModelSwitchPanel.emptyProvider": "No enabled providers. Please go to settings to enable one.",
"ModelSwitchPanel.goToSettings": "Go to settings",
+9
View File
@@ -150,6 +150,9 @@
"groupAgents.tag": "Group",
"groupAgents.underReview": "Under Review",
"home.communityAgents": "Community Agents",
"home.creatorReward.action": "Apply Now",
"home.creatorReward.subtitle": "2026 Creator Reward Program is officially live.",
"home.creatorReward.title": "Create. Share. Get Paid.",
"home.featuredAssistants": "Featured Agents",
"home.featuredModels": "Featured Models",
"home.featuredPlugins": "Featured Skills",
@@ -194,6 +197,8 @@
"mcp.categories.tools.name": "Utility Tools",
"mcp.categories.travel-transport.description": "Travel Planning and Transportation",
"mcp.categories.travel-transport.name": "Travel & Transport",
"mcp.categories.utility.description": "Weather Forecasting and Meteorological Services",
"mcp.categories.utility.name": "Utility",
"mcp.categories.weather.description": "Weather Forecasting and Meteorological Services",
"mcp.categories.weather.name": "Weather",
"mcp.categories.web-search.description": "Web Search and Information Retrieval",
@@ -478,6 +483,10 @@
"tab.plugin": "Skill",
"tab.provider": "Provider",
"tab.user": "User",
"time.formatOtherYear": "MMM D, YYYY",
"time.formatThisYear": "MMM D",
"time.today": "Today",
"time.yesterday": "Yesterday",
"user.agents": "Agents",
"user.downloads": "Downloads",
"user.editProfile": "Edit Profile",
+1
View File
@@ -10,5 +10,6 @@
"starter.deepResearch": "Deep Research",
"starter.developing": "Coming soon",
"starter.image": "Image",
"starter.seedance": "Seedance 2.0",
"starter.write": "Write"
}
+4 -1
View File
@@ -9,7 +9,10 @@
"addToKnowledgeBase.title": "Add to Library",
"addToKnowledgeBase.totalFiles": "{{count}} files selected",
"createNew.confirm": "Create New",
"createNew.description.placeholder": "Library description (optional)",
"createNew.description.label": "Library Description (Optional)",
"createNew.description.placeholder": "Description helps LLM understand your library better",
"createNew.edit.confirm": "Save Changes",
"createNew.edit.title": "Edit Library",
"createNew.formTitle": "Basic Information",
"createNew.name.placeholder": "Library name",
"createNew.name.required": "Please enter a library name",
+4
View File
@@ -38,6 +38,10 @@
"messages.success.submit": "Authorization successful! You can now publish your agent.",
"messages.success.upload": "Authorization successful! You can now publish a new version.",
"profileSetup.cancel": "Cancel",
"profileSetup.confirmChangeUserId.cancel": "Cancel",
"profileSetup.confirmChangeUserId.confirm": "Change User ID",
"profileSetup.confirmChangeUserId.description": "Once you switch to @{{newId}}, anyone can claim your old ID @{{oldId}} and all existing links to your profile will break. This can't be undone. Are you sure you want to continue?",
"profileSetup.confirmChangeUserId.title": "Change User ID?",
"profileSetup.descriptionEdit": "Update your community profile information.",
"profileSetup.descriptionFirstTime": "Set up your profile to complete your community profile.",
"profileSetup.errors.fileTooLarge": "File size cannot exceed 2MB",
+1 -1
View File
@@ -260,8 +260,8 @@
"providerModels.item.modelConfig.type.options.realtime": "Real-time Chat",
"providerModels.item.modelConfig.type.options.stt": "Speech-to-Text",
"providerModels.item.modelConfig.type.options.text2music": "Text-to-Music",
"providerModels.item.modelConfig.type.options.text2video": "Text-to-Video",
"providerModels.item.modelConfig.type.options.tts": "Text-to-Speech",
"providerModels.item.modelConfig.type.options.video": "Video Generation",
"providerModels.item.modelConfig.type.placeholder": "Please select a model type",
"providerModels.item.modelConfig.type.title": "Model Type",
"providerModels.item.modelConfig.video.extra": "This setting enables video recognition configuration within the application. Whether video recognition is supported depends entirely on the model itself. Please test the model to verify the availability of this feature.",
+18 -14
View File
@@ -274,9 +274,9 @@
"chatgpt-4o-latest.description": "ChatGPT-4o is a dynamic model updated in real time, combining strong understanding and generation for large-scale use cases like customer support, education, and technical support.",
"claude-2.0.description": "Claude 2 delivers key enterprise improvements, including a leading 200K-token context, reduced hallucinations, system prompts, and a new test feature: tool calling.",
"claude-2.1.description": "Claude 2 delivers key enterprise improvements, including a leading 200K-token context, reduced hallucinations, system prompts, and a new test feature: tool calling.",
"claude-3-5-haiku-20241022.description": "Claude 3.5 Haiku is Anthropics fastest next-gen model. Compared to Claude 3 Haiku, it improves across skills and surpasses the prior largest model Claude 3 Opus on many intelligence benchmarks.",
"claude-3-5-haiku-20241022.description": "Claude 3.5 Haiku is Anthropic's fastest next-gen model, improving across skills and surpassing the previous flagship Claude 3 Opus on many benchmarks.",
"claude-3-5-haiku-latest.description": "Claude 3.5 Haiku delivers fast responses for lightweight tasks.",
"claude-3-7-sonnet-20250219.description": "Claude 3.7 Sonnet is Anthropics most intelligent model and the first hybrid reasoning model on the market. It can produce near-instant responses or extended step-by-step reasoning that users can see. Sonnet is especially strong at coding, data science, vision, and agent tasks.",
"claude-3-7-sonnet-20250219.description": "Claude Sonnet 3.7 is Anthropic's most intelligent model and the first hybrid reasoning model on the market, supporting near-instant responses or extended thinking with fine-grained control.",
"claude-3-7-sonnet-latest.description": "Claude 3.7 Sonnet is Anthropics latest and most capable model for highly complex tasks, excelling in performance, intelligence, fluency, and understanding.",
"claude-3-haiku-20240307.description": "Claude 3 Haiku is Anthropics fastest and most compact model, designed for near-instant responses with fast, accurate performance.",
"claude-3-opus-20240229.description": "Claude 3 Opus is Anthropics most powerful model for highly complex tasks, excelling in performance, intelligence, fluency, and comprehension.",
@@ -284,16 +284,16 @@
"claude-3.5-sonnet.description": "Claude 3.5 Sonnet excels at coding, writing, and complex reasoning.",
"claude-3.7-sonnet-thought.description": "Claude 3.7 Sonnet with extended thinking for complex reasoning tasks.",
"claude-3.7-sonnet.description": "Claude 3.7 Sonnet is an upgraded version with extended context and capabilities.",
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 is Anthropics fastest and smartest Haiku model, with lightning speed and extended reasoning.",
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 is Anthropic's fastest and most intelligent Haiku model, with lightning speed and extended thinking.",
"claude-haiku-4.5.description": "Claude Haiku 4.5 is a fast and efficient model for various tasks.",
"claude-opus-4-1-20250805-thinking.description": "Claude Opus 4.1 Thinking is an advanced variant that can reveal its reasoning process.",
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 is Anthropics latest and most capable model for highly complex tasks, excelling in performance, intelligence, fluency, and understanding.",
"claude-opus-4-20250514.description": "Claude Opus 4 is Anthropics most powerful model for highly complex tasks, excelling in performance, intelligence, fluency, and comprehension.",
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 is Anthropic's latest and most capable model for highly complex tasks, excelling in performance, intelligence, fluency, and understanding.",
"claude-opus-4-20250514.description": "Claude Opus 4 is Anthropic's most powerful model for highly complex tasks, excelling in performance, intelligence, fluency, and understanding.",
"claude-opus-4-5-20251101.description": "Claude Opus 4.5 is Anthropics flagship model, combining outstanding intelligence with scalable performance, ideal for complex tasks requiring the highest-quality responses and reasoning.",
"claude-opus-4-6.description": "Claude Opus 4.6 is Anthropics most intelligent model for building agents and coding.",
"claude-opus-4-6.description": "Claude Opus 4.6 is Anthropic's most intelligent model for building agents and coding.",
"claude-sonnet-4-20250514-thinking.description": "Claude Sonnet 4 Thinking can produce near-instant responses or extended step-by-step thinking with visible process.",
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 can produce near-instant responses or extended step-by-step thinking with visible process.",
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 is Anthropics most intelligent model to date.",
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 is Anthropic's most intelligent model to date, offering near-instant responses or extended step-by-step thinking with fine-grained control for API users.",
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 is Anthropic's most intelligent model to date.",
"claude-sonnet-4.description": "Claude Sonnet 4 is the latest generation with improved performance across all tasks.",
"codegeex-4.description": "CodeGeeX-4 is a powerful AI coding assistant that supports multilingual Q&A and code completion to boost developer productivity.",
"codegeex4-all-9b.description": "CodeGeeX4-ALL-9B is a multilingual code generation model supporting code completion and generation, code interpreter, web search, function calling, and repo-level code Q&A, covering a wide range of software development scenarios. It is a top-tier code model under 10B parameters.",
@@ -363,7 +363,7 @@
"deepseek-ai/deepseek-v3.1-terminus.description": "DeepSeek V3.1 is a next-gen reasoning model with stronger complex reasoning and chain-of-thought for deep analysis tasks.",
"deepseek-ai/deepseek-v3.1.description": "DeepSeek V3.1 is a next-gen reasoning model with stronger complex reasoning and chain-of-thought for deep analysis tasks.",
"deepseek-ai/deepseek-vl2.description": "DeepSeek-VL2 is a MoE vision-language model based on DeepSeekMoE-27B with sparse activation, achieving strong performance with only 4.5B active parameters. It excels at visual QA, OCR, document/table/chart understanding, and visual grounding.",
"deepseek-chat.description": "A new open-source model combining general and code abilities. It preserves the chat models general dialogue and the coder models strong coding, with better preference alignment. DeepSeek-V2.5 also improves writing and instruction following.",
"deepseek-chat.description": "DeepSeek V3.2 balances reasoning and output length for daily QA and agent tasks. Public benchmarks reach GPT-5 levels, and it is the first to integrate thinking into tool use, leading open-source agent evaluations.",
"deepseek-coder-33B-instruct.description": "DeepSeek Coder 33B is a code language model trained on 2T tokens (87% code, 13% Chinese/English text). It introduces a 16K context window and fill-in-the-middle tasks, providing project-level code completion and snippet infilling.",
"deepseek-coder-v2.description": "DeepSeek Coder V2 is an open-source MoE code model that performs strongly on coding tasks, comparable to GPT-4 Turbo.",
"deepseek-coder-v2:236b.description": "DeepSeek Coder V2 is an open-source MoE code model that performs strongly on coding tasks, comparable to GPT-4 Turbo.",
@@ -386,7 +386,7 @@
"deepseek-r1-fast-online.description": "DeepSeek R1 fast full version with real-time web search, combining 671B-scale capability and faster response.",
"deepseek-r1-online.description": "DeepSeek R1 full version with 671B parameters and real-time web search, offering stronger understanding and generation.",
"deepseek-r1.description": "DeepSeek-R1 uses cold-start data before RL and performs comparably to OpenAI-o1 on math, coding, and reasoning.",
"deepseek-reasoner.description": "DeepSeek V3.2 thinking mode outputs a chain-of-thought before the final answer to improve accuracy.",
"deepseek-reasoner.description": "DeepSeek V3.2 Thinking is a deep reasoning model that generates chain-of-thought before outputs for higher accuracy, with top competition results and reasoning comparable to Gemini-3.0-Pro.",
"deepseek-v2.description": "DeepSeek V2 is an efficient MoE model for cost-effective processing.",
"deepseek-v2:236b.description": "DeepSeek V2 236B is DeepSeeks code-focused model with strong code generation.",
"deepseek-v3-0324.description": "DeepSeek-V3-0324 is a 671B-parameter MoE model with standout strengths in programming and technical capability, context understanding, and long-text handling.",
@@ -476,7 +476,8 @@
"ernie-speed-pro-128k.description": "ERNIE Speed Pro 128K is a high-concurrency, high-value model for large-scale online services and enterprise apps.",
"ernie-x1-turbo-32k.description": "ERNIE X1 Turbo 32K is a fast thinking model with 32K context for complex reasoning and multi-turn chat.",
"ernie-x1.1-preview.description": "ERNIE X1.1 Preview is a thinking-model preview for evaluation and testing.",
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0 is an image generation model from ByteDance Seed, supporting text and image inputs with highly controllable, high-quality image generation. It generates images from text prompts.",
"fal-ai/bytedance/seedream/v4.5.description": "Seedream 4.5, built by ByteDance Seed team, supports multi-image editing and composition. Features enhanced subject consistency, precise instruction following, spatial logic understanding, aesthetic expression, poster layout and logo design with high-precision text-image rendering.",
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0, built by ByteDance Seed, supports text and image inputs for highly controllable, high-quality image generation from prompts.",
"fal-ai/flux-kontext/dev.description": "FLUX.1 model focused on image editing, supporting text and image inputs.",
"fal-ai/flux-pro/kontext.description": "FLUX.1 Kontext [pro] accepts text and reference images as input, enabling targeted local edits and complex global scene transformations.",
"fal-ai/flux/krea.description": "Flux Krea [dev] is an image generation model with an aesthetic bias toward more realistic, natural images.",
@@ -484,8 +485,8 @@
"fal-ai/hunyuan-image/v3.description": "A powerful native multimodal image generation model.",
"fal-ai/imagen4/preview.description": "High-quality image generation model from Google.",
"fal-ai/nano-banana.description": "Nano Banana is Googles newest, fastest, and most efficient native multimodal model, enabling image generation and editing through conversation.",
"fal-ai/qwen-image-edit.description": "A professional image editing model from the Qwen team that supports semantic and appearance edits, precisely edits Chinese and English text, and enables high-quality edits such as style transfer and object rotation.",
"fal-ai/qwen-image.description": "A powerful image generation model from the Qwen team with impressive Chinese text rendering and diverse visual styles.",
"fal-ai/qwen-image-edit.description": "A professional image editing model from the Qwen team, supporting semantic and appearance edits, precise Chinese/English text editing, style transfer, rotation, and more.",
"fal-ai/qwen-image.description": "A powerful image generation model from the Qwen team with strong Chinese text rendering and diverse visual styles.",
"flux-1-schnell.description": "A 12B-parameter text-to-image model from Black Forest Labs using latent adversarial diffusion distillation to generate high-quality images in 1-4 steps. It rivals closed alternatives and is released under Apache-2.0 for personal, research, and commercial use.",
"flux-dev.description": "FLUX.1 [dev] is an open-weights distilled model for non-commercial use. It keeps near-pro image quality and instruction following while running more efficiently, using resources better than same-size standard models.",
"flux-kontext-max.description": "State-of-the-art contextual image generation and editing, combining text and images for precise, coherent results.",
@@ -516,6 +517,8 @@
"gemini-2.0-flash-lite-001.description": "A Gemini 2.0 Flash variant optimized for cost efficiency and low latency.",
"gemini-2.0-flash-lite.description": "A Gemini 2.0 Flash variant optimized for cost efficiency and low latency.",
"gemini-2.0-flash.description": "Gemini 2.0 Flash delivers next-gen features including exceptional speed, native tool use, multimodal generation, and a 1M-token context window.",
"gemini-2.5-flash-image-preview.description": "Nano Banana is Google's newest, fastest, and most efficient native multimodal model, enabling conversational image generation and editing.",
"gemini-2.5-flash-image-preview:image.description": "Nano Banana is Google's newest, fastest, and most efficient native multimodal model, enabling conversational image generation and editing.",
"gemini-2.5-flash-image.description": "Nano Banana is Googles newest, fastest, and most efficient native multimodal model, enabling conversational image generation and editing.",
"gemini-2.5-flash-image:image.description": "Nano Banana is Googles newest, fastest, and most efficient native multimodal model, enabling conversational image generation and editing.",
"gemini-2.5-flash-lite-preview-06-17.description": "Gemini 2.5 Flash-Lite Preview is Googles smallest, best-value model, designed for large-scale use.",
@@ -530,7 +533,7 @@
"gemini-2.5-pro.description": "Gemini 2.5 Pro is Googles most advanced reasoning model, able to reason over code, math, and STEM problems and analyze large datasets, codebases, and documents with long context.",
"gemini-3-flash-preview.description": "Gemini 3 Flash is the smartest model built for speed, combining cutting-edge intelligence with excellent search grounding.",
"gemini-3-pro-image-preview.description": "Gemini 3 Pro ImageNano Banana Pro)是 Google 的图像生成模型,同时支持多模态对话。",
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) is Googles image generation model and also supports multimodal chat.",
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) is Google's image generation model and also supports multimodal chat.",
"gemini-3-pro-preview.description": "Gemini 3 Pro is Googles most powerful agent and vibe-coding model, delivering richer visuals and deeper interaction on top of state-of-the-art reasoning.",
"gemini-flash-latest.description": "Latest release of Gemini Flash",
"gemini-flash-lite-latest.description": "Latest release of Gemini Flash-Lite",
@@ -1133,6 +1136,7 @@
"qwq.description": "QwQ is a reasoning model in the Qwen family. Compared with standard instruction-tuned models, it brings thinking and reasoning abilities that significantly improve downstream performance, especially on hard problems. QwQ-32B is a mid-sized reasoning model that competes well with top reasoning models like DeepSeek-R1 and o1-mini.",
"qwq_32b.description": "Mid-sized reasoning model in the Qwen family. Compared with standard instruction-tuned models, QwQs thinking and reasoning abilities significantly boost downstream performance, especially on hard problems.",
"r1-1776.description": "R1-1776 is a post-trained variant of DeepSeek R1 designed to provide uncensored, unbiased factual information.",
"seedance-1-5-pro-251215.description": "Seedance 1.5 Pro by ByteDance supports text-to-video, image-to-video (first frame, first+last frame), and audio generation synchronized with visuals.",
"solar-mini-ja.description": "Solar Mini (Ja) extends Solar Mini with a focus on Japanese while maintaining efficient, strong performance in English and Korean.",
"solar-mini.description": "Solar Mini is a compact LLM that outperforms GPT-3.5, with strong multilingual capability supporting English and Korean, offering an efficient small-footprint solution.",
"solar-pro.description": "Solar Pro is a high-intelligence LLM from Upstage, focused on instruction following on a single GPU, with IFEval scores above 80. It currently supports English; the full release was planned for November 2024 with expanded language support and longer context.",
+1
View File
@@ -30,6 +30,7 @@
"internlm.description": "An open-source organization focused on large-model research and tooling, providing an efficient, easy-to-use platform that makes cutting-edge models and algorithms accessible.",
"jina.description": "Founded in 2020, Jina AI is a leading search AI company. Its search stack includes vector models, rerankers, and small language models to build reliable, high-quality generative and multimodal search apps.",
"lmstudio.description": "LM Studio is a desktop app for developing and experimenting with LLMs on your computer.",
"lobehub.description": "LobeHub Cloud uses official APIs to access AI models and measures usage with Credits tied to model tokens.",
"minimax.description": "Founded in 2021, MiniMax builds general-purpose AI with multimodal foundation models, including trillion-parameter MoE text models, speech models, and vision models, along with apps like Hailuo AI.",
"mistral.description": "Mistral offers advanced general, specialized, and research models for complex reasoning, multilingual tasks, and code generation, with function-calling for custom integrations.",
"modelscope.description": "ModelScope is Alibaba Clouds model-as-a-service platform, offering a wide range of AI models and inference services.",
+1
View File
@@ -14,6 +14,7 @@
"table.columns.totalTokens": "Token Usage",
"table.columns.type.enums.chat": "Text Generation",
"table.columns.type.enums.imageGeneration": "Image Generation",
"table.columns.type.enums.videoGeneration": "Video Generation",
"table.columns.type.title": "Type",
"table.desc": "Details of computing credits usage for text generation, embedding, image generation, etc.",
"table.more": "View Details",
+6
View File
@@ -131,6 +131,12 @@
"limitation.providers.prompter.subTitle": "Custom API service is only available for paid plans. Upgrade now to enjoy global mainstream model services",
"limitation.providers.prompter.title": "Subscribe now to use custom API service",
"limitation.providers.tooltip": "Custom API service is only available for paid plans",
"limitation.video.success.action": "Continue Generating",
"limitation.video.success.desc": "Your {{plan}} subscription has been upgraded successfully. Enjoy AI video generation. Your current plan includes:",
"limitation.video.success.title": "Upgrade Successful",
"limitation.video.topupSuccess.action": "Continue Generating",
"limitation.video.topupSuccess.desc": "Your top-up credits are now active. Enjoy AI video generation. Your current plan includes:",
"limitation.video.topupSuccess.title": "Top-up Successful",
"modelPricing.button": "View Pricing Documentation",
"modelPricing.desc": "{{name}} uses Credits to measure AI model usage. The table below shows computing credits per 1M Tokens.",
"modelPricing.title": "Text Model Pricing",
+9
View File
@@ -86,6 +86,10 @@
"localFiles.editFile.replaceFirst": "Replace first occurrence only",
"localFiles.file": "File",
"localFiles.folder": "Folder",
"localFiles.globFiles.pattern": "Pattern",
"localFiles.grepContent.glob": "File filter",
"localFiles.grepContent.pattern": "Search pattern",
"localFiles.grepContent.type": "File type",
"localFiles.moveFiles.itemsMoved": "{{count}} item(s) moved:",
"localFiles.moveFiles.itemsMoved_one": "{{count}} item moved:",
"localFiles.moveFiles.itemsMoved_other": "{{count}} items moved:",
@@ -95,11 +99,16 @@
"localFiles.open": "Open",
"localFiles.openFile": "Open File",
"localFiles.openFolder": "Open Folder",
"localFiles.outOfScope.requestedPaths": "Requested Paths",
"localFiles.outOfScope.warning": "Warning: The following path(s) are outside the configured working directory. Please confirm you want to allow access.",
"localFiles.outOfScope.workingDirectory": "Working Directory",
"localFiles.read.more": "View More",
"localFiles.readFile": "Read File",
"localFiles.readFile.lineRange": "Lines {{start}} - {{end}}",
"localFiles.readFileError": "Failed to read file, please check if the file path is correct",
"localFiles.readFiles": "Read Files",
"localFiles.readFilesError": "Failed to read files, please check if the file path is correct",
"localFiles.searchFiles.keywords": "Keywords",
"localFiles.securityBlacklist.warning": "Security Alert: This operation has been flagged by security rules and requires your explicit approval.",
"localFiles.writeFile.characters": "characters",
"localFiles.writeFile.preview": "Content Preview",
+28
View File
@@ -0,0 +1,28 @@
{
"config.aspectRatio.label": "Aspect Ratio",
"config.cameraFixed.label": "Fixed Camera",
"config.duration.label": "Duration",
"config.endImageUrl.label": "End Frame",
"config.generateAudio.label": "Generate Audio",
"config.header.title": "Video",
"config.imageUrl.label": "Start Frame",
"config.prompt.placeholder": "Describe the video you want to generate",
"config.referenceImage.label": "Reference Image",
"config.resolution.label": "Resolution",
"config.seed.label": "Seed",
"config.seed.random": "Random",
"generation.actions.copyError": "Copy Error Message",
"generation.actions.errorCopied": "Error Message Copied to Clipboard",
"generation.actions.errorCopyFailed": "Failed to Copy Error Message",
"generation.actions.generate": "Generate",
"generation.freeQuota.exhausted": "🎁 Free quota used up, credits will be consumed",
"generation.freeQuota.remaining": "🎁 {{remaining}} free videos today",
"generation.status.failed": "Generation Failed",
"generation.status.generating": "Generating...",
"generation.validation.endFrameRequiresStartFrame": "End frame cannot be used without a start frame. Please set a start frame first.",
"topic.createNew": "New Topic",
"topic.deleteConfirm": "Delete Video Topic",
"topic.deleteConfirmDesc": "You are about to delete this video topic. This action cannot be undone.",
"topic.title": "Video Topics",
"topic.untitled": "Default Topic"
}
+3 -2
View File
@@ -58,13 +58,13 @@
"duplicateTitle": "Copia de {{title}}",
"emptyAgent": "Aún no hay Agentes. Comienza con tu primer Agente—construye tu sistema con el tiempo.",
"emptyAgentAction": "Crear Agente",
"extendParams.disableContextCaching.desc": "Reduce hasta un 90% el costo de generar una conversación y aumenta hasta 4 veces la velocidad. Al habilitar esto, se desactiva automáticamente el límite de mensajes históricos. <1>Más información</1>",
"extendParams.disableContextCaching.desc": "Reduce hasta un 90% el costo de generar una sola conversación y alcanza una velocidad hasta 4 veces mayor. <1>Más información</1>",
"extendParams.disableContextCaching.title": "Habilitar Caché de Contexto",
"extendParams.effort.desc": "Controla cuántos tokens utiliza Claude al responder mediante el parámetro de esfuerzo.",
"extendParams.effort.title": "Esfuerzo",
"extendParams.enableAdaptiveThinking.desc": "Permite que Claude decida dinámicamente cuándo y cuánto pensar con el modo de pensamiento adaptativo.",
"extendParams.enableAdaptiveThinking.title": "Activar Pensamiento Adaptativo",
"extendParams.enableReasoning.desc": "Basado en el límite del mecanismo de Pensamiento de Claude, al habilitar esto se desactiva automáticamente el límite de mensajes históricos. <1>Más información</1>",
"extendParams.enableReasoning.desc": "Basado en el límite del mecanismo de razonamiento de Claude. <1>Más información</1>",
"extendParams.enableReasoning.title": "Habilitar Pensamiento Profundo",
"extendParams.imageAspectRatio.title": "Relación de aspecto de imagen",
"extendParams.imageResolution.title": "Resolución de imagen",
@@ -165,6 +165,7 @@
"messageAction.delAndRegenerate": "Eliminar y regenerar",
"messageAction.deleteDisabledByThreads": "Este mensaje tiene un subtema y no se puede eliminar",
"messageAction.expand": "Expandir mensaje",
"messageAction.reaction": "Agregar reacción",
"messageAction.regenerate": "Regenerar",
"messages.dm.sentTo": "Visible solo para {{name}}",
"messages.dm.title": "Mensaje directo",
+2
View File
@@ -143,6 +143,7 @@
"cmdk.keywords.stats": "estadísticas análisis métricas",
"cmdk.keywords.submitIssue": "problema error incidencia comentarios",
"cmdk.keywords.usage": "uso estadísticas consumo cuota",
"cmdk.keywords.video": "video,generar,seedance,kling",
"cmdk.memory": "Memoria",
"cmdk.mentionAgent": "Mencionar al agente",
"cmdk.navigate": "Navegar",
@@ -193,6 +194,7 @@
"cmdk.themeLight": "Claro",
"cmdk.toOpen": "Abrir",
"cmdk.toSelect": "Seleccionar",
"cmdk.video": "Video con IA",
"confirm": "Confirmar",
"contact": "Contáctanos",
"copy": "Copiar",
+31
View File
@@ -43,6 +43,8 @@
"FileManager.emptyStatus.or": "o",
"FileManager.emptyStatus.title": "Arrastra archivos o carpetas aquí",
"FileManager.noFolders": "No hay carpetas disponibles",
"FileManager.search.noResults": "No se encontraron archivos",
"FileManager.search.placeholder": "Buscar archivos...",
"FileManager.sort.dateAdded": "Fecha de añadido",
"FileManager.sort.name": "Nombre",
"FileManager.sort.size": "Tamaño",
@@ -94,6 +96,35 @@
"ModelSelect.removed": "El modelo no está en la lista. Se eliminará automáticamente si se deselecciona.",
"ModelSwitchPanel.byModel": "Por modelo",
"ModelSwitchPanel.byProvider": "Por proveedor",
"ModelSwitchPanel.detail.abilities": "Capacidades",
"ModelSwitchPanel.detail.abilities.files": "Archivos",
"ModelSwitchPanel.detail.abilities.functionCall": "Llamada a herramienta",
"ModelSwitchPanel.detail.abilities.imageOutput": "Salida de imagen",
"ModelSwitchPanel.detail.abilities.reasoning": "Razonamiento",
"ModelSwitchPanel.detail.abilities.search": "Búsqueda",
"ModelSwitchPanel.detail.abilities.video": "Vídeo",
"ModelSwitchPanel.detail.abilities.vision": "Visión",
"ModelSwitchPanel.detail.config": "Configuración del modelo",
"ModelSwitchPanel.detail.context": "Longitud del contexto",
"ModelSwitchPanel.detail.pricing": "Precios",
"ModelSwitchPanel.detail.pricing.cachedInput": "Entrada en caché ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.group.audio": "Audio",
"ModelSwitchPanel.detail.pricing.group.image": "Imagen",
"ModelSwitchPanel.detail.pricing.group.text": "Texto",
"ModelSwitchPanel.detail.pricing.input": "Entrada ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.output": "Salida ${{amount}}/M",
"ModelSwitchPanel.detail.pricing.unit.audioInput": "Entrada de audio",
"ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Entrada de audio (en caché)",
"ModelSwitchPanel.detail.pricing.unit.audioOutput": "Salida de audio",
"ModelSwitchPanel.detail.pricing.unit.imageGeneration": "Generación de imagen",
"ModelSwitchPanel.detail.pricing.unit.imageInput": "Entrada de imagen",
"ModelSwitchPanel.detail.pricing.unit.imageInput_cacheRead": "Entrada de imagen (en caché)",
"ModelSwitchPanel.detail.pricing.unit.imageOutput": "Salida de imagen",
"ModelSwitchPanel.detail.pricing.unit.textInput": "Entrada",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheRead": "Entrada (en caché)",
"ModelSwitchPanel.detail.pricing.unit.textInput_cacheWrite": "Entrada (escritura en caché)",
"ModelSwitchPanel.detail.pricing.unit.textOutput": "Salida",
"ModelSwitchPanel.detail.releasedAt": "Lanzado el {{date}}",
"ModelSwitchPanel.emptyModel": "No hay modelos habilitados. Ve a configuración para habilitar uno.",
"ModelSwitchPanel.emptyProvider": "No hay proveedores habilitados. Ve a configuración para habilitar uno.",
"ModelSwitchPanel.goToSettings": "Ir a configuración",

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