mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-15 20:16:02 +00:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 862f61f379 | |||
| 8127c1b9e7 | |||
| 8bff2097e0 | |||
| d2a042cd95 | |||
| c1916a1996 | |||
| 502d94bd4c | |||
| f4bd332d11 | |||
| 7df81ffaa1 | |||
| ed076b3cf5 | |||
| 0abde1623d | |||
| 481e5c0066 | |||
| 8719354282 | |||
| 5957bd4578 | |||
| 335e246ac5 | |||
| 918d048b3d | |||
| 398d8b7f3c | |||
| 1529f31dff | |||
| b4bd5b288c | |||
| 448cfb2cfd | |||
| d8c3ef3232 | |||
| 2a23fb9a10 | |||
| 82f9cb4486 | |||
| 6419fd32b1 | |||
| 927fe3fd22 | |||
| 03bda41c07 | |||
| e804773a7e | |||
| 67875bd60a | |||
| 916d4841f4 | |||
| d5b1ff20e0 | |||
| 212348eafe | |||
| 43820eeb2e | |||
| 7397d6f8c1 | |||
| 85d5bc9e08 | |||
| 3fbc46b23a | |||
| 6e2ef05270 | |||
| 06d65e9ce5 | |||
| 6002863c17 | |||
| 8f2e72d1b8 | |||
| fcf2444fa8 | |||
| 661f1a80b4 | |||
| 57772d1f3b | |||
| abe4c969a5 | |||
| b767a66d38 | |||
| 53e4228ea7 | |||
| 7efcdd2f7c | |||
| bde1503309 | |||
| 487713361a | |||
| 7bad876259 | |||
| 229200853a | |||
| 5ec89941f3 | |||
| f46916a74d | |||
| 2ee46b8693 | |||
| baf0b56f64 | |||
| 12dc7f90be | |||
| 1b905ede31 | |||
| cfaa911153 | |||
| e17bf4b0cc | |||
| 58cf27dcf8 | |||
| f12d9fbd22 | |||
| d4f72eb752 | |||
| e112cd6f7f | |||
| 9a9147ca7e | |||
| 2d1eec4482 | |||
| c11d6de7db | |||
| bbbe3a8d09 | |||
| e51fbba881 | |||
| 1bfeeea6f4 | |||
| 12d0ec21d0 | |||
| 2bf0a08919 | |||
| 79e146f1a3 | |||
| 0e42ca5ca2 | |||
| 823aa29c67 | |||
| d225da96df | |||
| 0acaf01f9a | |||
| 5a8911b72d | |||
| e6596e94a5 | |||
| 9c09160154 | |||
| d7d186df1a |
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}.`);
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
Vendored
+2
-1
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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 |
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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()`]
|
||||
|
||||
@@ -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(' ✅ 不相关对话已被过滤');
|
||||
});
|
||||
|
||||
@@ -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)}..."`);
|
||||
});
|
||||
|
||||
@@ -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(' ✅ 消息内容完整显示');
|
||||
});
|
||||
|
||||
@@ -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)');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 已从列表中移除');
|
||||
});
|
||||
|
||||
@@ -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 已从列表中移除');
|
||||
});
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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(' ✅ 文本已斜体');
|
||||
});
|
||||
|
||||
@@ -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(' ✅ 验证完成');
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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": "رسالة خاصة",
|
||||
|
||||
@@ -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": "نسخ",
|
||||
|
||||
@@ -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": "الذهاب إلى الإعدادات",
|
||||
|
||||
@@ -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": "تعديل الملف الشخصي",
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
"starter.deepResearch": "بحث معمق",
|
||||
"starter.developing": "قريبًا",
|
||||
"starter.image": "صورة",
|
||||
"starter.seedance": "سيدانس 2.0",
|
||||
"starter.write": "كتابة"
|
||||
}
|
||||
|
||||
@@ -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": "يرجى إدخال اسم المكتبة",
|
||||
|
||||
@@ -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 ميغابايت",
|
||||
|
||||
@@ -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
@@ -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.",
|
||||
|
||||
@@ -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، تقدم مجموعة واسعة من النماذج وخدمات الاستدلال.",
|
||||
|
||||
@@ -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": "عرض التفاصيل",
|
||||
|
||||
@@ -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": "تسعير نموذج النص",
|
||||
|
||||
@@ -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": "مفتاح الحقل",
|
||||
|
||||
@@ -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": "موضوع افتراضي"
|
||||
}
|
||||
@@ -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": "ЛС",
|
||||
|
||||
@@ -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": "Копирай",
|
||||
|
||||
@@ -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": "Отиди в настройките",
|
||||
|
||||
@@ -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": "Редактирай профил",
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
"starter.deepResearch": "Задълбочено проучване",
|
||||
"starter.developing": "Очаквайте скоро",
|
||||
"starter.image": "Изображение",
|
||||
"starter.seedance": "Seedance 2.0",
|
||||
"starter.write": "Писане"
|
||||
}
|
||||
|
||||
@@ -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": "Моля, въведете име на библиотеката",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 Image(Nano 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 е подходящ за напреднали мисловни или логически задачи.",
|
||||
|
||||
@@ -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 модели и услуги за инференция.",
|
||||
|
||||
@@ -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": "Виж подробности",
|
||||
|
||||
@@ -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": "Цени на текстови модели",
|
||||
|
||||
@@ -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": "Ключ на полето",
|
||||
|
||||
@@ -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": "Тема по подразбиране"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 2 B 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 1–4 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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
"starter.deepResearch": "Deep Research",
|
||||
"starter.developing": "Coming soon",
|
||||
"starter.image": "Image",
|
||||
"starter.seedance": "Seedance 2.0",
|
||||
"starter.write": "Write"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 Anthropic’s 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 Anthropic’s 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 Anthropic’s 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 Anthropic’s fastest and most compact model, designed for near-instant responses with fast, accurate performance.",
|
||||
"claude-3-opus-20240229.description": "Claude 3 Opus is Anthropic’s 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 Anthropic’s 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 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 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 Anthropic’s 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 Anthropic’s 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 Anthropic’s 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 model’s general dialogue and the coder model’s 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 DeepSeek’s 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 Google’s 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 Google’s newest, fastest, and most efficient native multimodal model, enabling conversational image generation and editing.",
|
||||
"gemini-2.5-flash-image: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-lite-preview-06-17.description": "Gemini 2.5 Flash-Lite Preview is Google’s smallest, best-value model, designed for large-scale use.",
|
||||
@@ -530,7 +533,7 @@
|
||||
"gemini-2.5-pro.description": "Gemini 2.5 Pro is Google’s 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 Image(Nano Banana Pro)是 Google 的图像生成模型,同时支持多模态对话。",
|
||||
"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-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 Google’s 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, QwQ’s 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.",
|
||||
|
||||
@@ -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 Cloud’s model-as-a-service platform, offering a wide range of AI models and inference services.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user