From d8a0291382c30abf561f9b36c9b576a56453f7ff Mon Sep 17 00:00:00 2001 From: Xiangxuan Qu Date: Wed, 7 Jan 2026 22:15:43 +0900 Subject: [PATCH 01/12] refactor(web): remove unused type alias VoiceLanguageKey (#30694) Co-authored-by: fghpdf --- .../new-feature-panel/text-to-speech/param-config-content.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx index 41715b3c6b..cab41c66c1 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx @@ -1,7 +1,6 @@ 'use client' import type { OnFeaturesChange } from '@/app/components/base/features/types' import type { Item } from '@/app/components/base/select' -import type { I18nKeysWithPrefix } from '@/types/i18n' import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react' import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid' import { RiCloseLine } from '@remixicon/react' @@ -20,8 +19,6 @@ import { useAppVoices } from '@/service/use-apps' import { TtsAutoPlay } from '@/types/app' import { cn } from '@/utils/classnames' -type VoiceLanguageKey = I18nKeysWithPrefix<'common', 'voice.language.'> - type VoiceParamConfigProps = { onClose: () => void onChange?: OnFeaturesChange From a422908efd39a0764f64402b1c97d8e2f3bccd01 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:17:50 +0800 Subject: [PATCH 02/12] feat(i18n): Migrate translation workflow to Claude Code GitHub Actions (#30692) Co-authored-by: Claude Opus 4.5 Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../translate-i18n-base-on-english.yml | 94 ----- .github/workflows/translate-i18n-claude.yml | 395 ++++++++++++++++++ web/i18n-config/README.md | 30 +- web/package.json | 2 - web/pnpm-lock.yaml | 174 +------- web/scripts/auto-gen-i18n.js | 336 --------------- 6 files changed, 426 insertions(+), 605 deletions(-) delete mode 100644 .github/workflows/translate-i18n-base-on-english.yml create mode 100644 .github/workflows/translate-i18n-claude.yml delete mode 100644 web/scripts/auto-gen-i18n.js diff --git a/.github/workflows/translate-i18n-base-on-english.yml b/.github/workflows/translate-i18n-base-on-english.yml deleted file mode 100644 index 16d36361fd..0000000000 --- a/.github/workflows/translate-i18n-base-on-english.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Translate i18n Files Based on English - -on: - push: - branches: [main] - paths: - - 'web/i18n/en-US/*.json' - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - check-and-update: - if: github.repository == 'langgenius/dify' - runs-on: ubuntu-latest - defaults: - run: - working-directory: web - steps: - # Keep use old checkout action version for https://github.com/peter-evans/create-pull-request/issues/4272 - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Check for file changes in i18n/en-US - id: check_files - run: | - # Skip check for manual trigger, translate all files - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "FILES_CHANGED=true" >> $GITHUB_ENV - echo "FILE_ARGS=" >> $GITHUB_ENV - echo "Manual trigger: translating all files" - else - git fetch origin "${{ github.event.before }}" || true - git fetch origin "${{ github.sha }}" || true - changed_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'i18n/en-US/*.json') - echo "Changed files: $changed_files" - if [ -n "$changed_files" ]; then - echo "FILES_CHANGED=true" >> $GITHUB_ENV - file_args="" - for file in $changed_files; do - filename=$(basename "$file" .json) - file_args="$file_args --file $filename" - done - echo "FILE_ARGS=$file_args" >> $GITHUB_ENV - echo "File arguments: $file_args" - else - echo "FILES_CHANGED=false" >> $GITHUB_ENV - fi - fi - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - package_json_file: web/package.json - run_install: false - - - name: Set up Node.js - if: env.FILES_CHANGED == 'true' - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - cache: pnpm - cache-dependency-path: ./web/pnpm-lock.yaml - - - name: Install dependencies - if: env.FILES_CHANGED == 'true' - working-directory: ./web - run: pnpm install --frozen-lockfile - - - name: Generate i18n translations - if: env.FILES_CHANGED == 'true' - working-directory: ./web - run: pnpm run i18n:gen ${{ env.FILE_ARGS }} - - - name: Create Pull Request - if: env.FILES_CHANGED == 'true' - uses: peter-evans/create-pull-request@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: 'chore(i18n): update translations based on en-US changes' - title: 'chore(i18n): translate i18n files based on en-US changes' - body: | - This PR was automatically created to update i18n translation files based on changes in en-US locale. - - **Triggered by:** ${{ github.sha }} - - **Changes included:** - - Updated translation files for all locales - branch: chore/automated-i18n-updates-${{ github.sha }} - delete-branch: true diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml new file mode 100644 index 0000000000..8dccf8ef93 --- /dev/null +++ b/.github/workflows/translate-i18n-claude.yml @@ -0,0 +1,395 @@ +name: Translate i18n Files with Claude Code + +on: + push: + branches: [main] + paths: + - 'web/i18n/en-US/*.json' + workflow_dispatch: + inputs: + files: + description: 'Specific files to translate (space-separated, e.g., "app common"). Leave empty for all files.' + required: false + type: string + languages: + description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported languages.' + required: false + type: string + mode: + description: 'Sync mode: incremental (only changes) or full (re-check all keys)' + required: false + default: 'incremental' + type: choice + options: + - incremental + - full + +permissions: + contents: write + pull-requests: write + +jobs: + translate: + if: github.repository == 'langgenius/dify' + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: web/package.json + run_install: false + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + cache: pnpm + cache-dependency-path: ./web/pnpm-lock.yaml + + - name: Detect changed files and generate diff + id: detect_changes + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Manual trigger + if [ -n "${{ github.event.inputs.files }}" ]; then + echo "CHANGED_FILES=${{ github.event.inputs.files }}" >> $GITHUB_OUTPUT + else + # Get all JSON files in en-US directory + files=$(ls web/i18n/en-US/*.json 2>/dev/null | xargs -n1 basename | sed 's/.json$//' | tr '\n' ' ') + echo "CHANGED_FILES=$files" >> $GITHUB_OUTPUT + fi + echo "TARGET_LANGS=${{ github.event.inputs.languages }}" >> $GITHUB_OUTPUT + echo "SYNC_MODE=${{ github.event.inputs.mode || 'incremental' }}" >> $GITHUB_OUTPUT + + # For manual trigger with incremental mode, get diff from last commit + # For full mode, we'll do a complete check anyway + if [ "${{ github.event.inputs.mode }}" == "full" ]; then + echo "Full mode: will check all keys" > /tmp/i18n-diff.txt + echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT + else + git diff HEAD~1..HEAD -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt + if [ -s /tmp/i18n-diff.txt ]; then + echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT + else + echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT + fi + fi + else + # Push trigger - detect changed files from the push + BEFORE_SHA="${{ github.event.before }}" + # Handle edge case: first push or force push may have null/zero SHA + if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then + # Fallback to comparing with parent commit + BEFORE_SHA="HEAD~1" + fi + changed=$(git diff --name-only "$BEFORE_SHA" ${{ github.sha }} -- 'web/i18n/en-US/*.json' 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/.json$//' | tr '\n' ' ' || echo "") + echo "CHANGED_FILES=$changed" >> $GITHUB_OUTPUT + echo "TARGET_LANGS=" >> $GITHUB_OUTPUT + echo "SYNC_MODE=incremental" >> $GITHUB_OUTPUT + + # Generate detailed diff for the push + git diff "$BEFORE_SHA"..${{ github.sha }} -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt + if [ -s /tmp/i18n-diff.txt ]; then + echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT + else + echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT + fi + fi + + # Truncate diff if too large (keep first 50KB) + if [ -f /tmp/i18n-diff.txt ]; then + head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt + mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt + fi + + echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')" + + - name: Run Claude Code for Translation Sync + if: steps.detect_changes.outputs.CHANGED_FILES != '' + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + prompt: | + You are a professional i18n synchronization engineer for the Dify project. + Your task is to keep all language translations in sync with the English source (en-US). + + ## CRITICAL TOOL RESTRICTIONS + - Use **Read** tool to read files (NOT cat or bash) + - Use **Edit** tool to modify JSON files (NOT node, jq, or bash scripts) + - Use **Bash** ONLY for: git commands, gh commands, pnpm commands + - Run bash commands ONE BY ONE, never combine with && or || + + ## EFFICIENCY RULES + - **ONE Edit per language file** - batch all key additions into a single Edit + - Insert new keys at the beginning of JSON (after `{`), lint:fix will sort them + - Translate ALL keys for a language mentally first, then do ONE Edit + + ## Context + - Changed/target files: ${{ steps.detect_changes.outputs.CHANGED_FILES }} + - Target languages (empty means all supported): ${{ steps.detect_changes.outputs.TARGET_LANGS }} + - Sync mode: ${{ steps.detect_changes.outputs.SYNC_MODE }} + - Translation files are located in: web/i18n/{locale}/{filename}.json + - Language configuration is in: web/i18n-config/languages.ts + - Git diff is available: ${{ steps.detect_changes.outputs.DIFF_AVAILABLE }} + + ## CRITICAL DESIGN: Verify First, Then Sync + + You MUST follow this three-phase approach: + + ═══════════════════════════════════════════════════════════════ + ║ PHASE 1: VERIFY - Analyze and Generate Change Report ║ + ═══════════════════════════════════════════════════════════════ + + ### Step 1.1: Analyze Git Diff (for incremental mode) + Use the Read tool to read `/tmp/i18n-diff.txt` to see the git diff. + + Parse the diff to categorize changes: + - Lines with `+` (not `+++`): Added or modified values + - Lines with `-` (not `---`): Removed or old values + - Identify specific keys for each category: + * ADD: Keys that appear only in `+` lines (new keys) + * UPDATE: Keys that appear in both `-` and `+` lines (value changed) + * DELETE: Keys that appear only in `-` lines (removed keys) + + ### Step 1.2: Read Language Configuration + Use the Read tool to read `web/i18n-config/languages.ts`. + Extract all languages with `supported: true`. + + ### Step 1.3: Run i18n:check for Each Language + ```bash + pnpm --dir web install --frozen-lockfile + pnpm --dir web run i18n:check + ``` + + This will report: + - Missing keys (need to ADD) + - Extra keys (need to DELETE) + + ### Step 1.4: Generate Change Report + + Create a structured report identifying: + ``` + ╔══════════════════════════════════════════════════════════════╗ + ║ I18N SYNC CHANGE REPORT ║ + ╠══════════════════════════════════════════════════════════════╣ + ║ Files to process: [list] ║ + ║ Languages to sync: [list] ║ + ╠══════════════════════════════════════════════════════════════╣ + ║ ADD (New Keys): ║ + ║ - [filename].[key]: "English value" ║ + ║ ... ║ + ╠══════════════════════════════════════════════════════════════╣ + ║ UPDATE (Modified Keys - MUST re-translate): ║ + ║ - [filename].[key]: "Old value" → "New value" ║ + ║ ... ║ + ╠══════════════════════════════════════════════════════════════╣ + ║ DELETE (Extra Keys): ║ + ║ - [language]/[filename].[key] ║ + ║ ... ║ + ╚══════════════════════════════════════════════════════════════╝ + ``` + + **IMPORTANT**: For UPDATE detection, compare git diff to find keys where + the English value changed. These MUST be re-translated even if target + language already has a translation (it's now stale!). + + ═══════════════════════════════════════════════════════════════ + ║ PHASE 2: SYNC - Execute Changes Based on Report ║ + ═══════════════════════════════════════════════════════════════ + + ### Step 2.1: Process ADD Operations (BATCH per language file) + + **CRITICAL WORKFLOW for efficiency:** + 1. First, translate ALL new keys for ALL languages mentally + 2. Then, for EACH language file, do ONE Edit operation: + - Read the file once + - Insert ALL new keys at the beginning (right after the opening `{`) + - Don't worry about alphabetical order - lint:fix will sort them later + + Example Edit (adding 3 keys to zh-Hans/app.json): + ``` + old_string: '{\n "accessControl"' + new_string: '{\n "newKey1": "translation1",\n "newKey2": "translation2",\n "newKey3": "translation3",\n "accessControl"' + ``` + + **IMPORTANT**: + - ONE Edit per language file (not one Edit per key!) + - Always use the Edit tool. NEVER use bash scripts, node, or jq. + + ### Step 2.2: Process UPDATE Operations + + **IMPORTANT: Special handling for zh-Hans and ja-JP** + If zh-Hans or ja-JP files were ALSO modified in the same push: + - Run: `git diff HEAD~1 --name-only | grep -E "(zh-Hans|ja-JP)"` + - If found, it means someone manually translated them. Apply these rules: + + 1. **Missing keys**: Still ADD them (completeness required) + 2. **Existing translations**: Compare with the NEW English value: + - If translation is **completely wrong** or **unrelated** → Update it + - If translation is **roughly correct** (captures the meaning) → Keep it, respect manual work + - When in doubt, **keep the manual translation** + + Example: + - English changed: "Save" → "Save Changes" + - Manual translation: "保存更改" → Keep it (correct meaning) + - Manual translation: "删除" → Update it (completely wrong) + + For other languages: + Use Edit tool to replace the old value with the new translation. + You can batch multiple updates in one Edit if they are adjacent. + + ### Step 2.3: Process DELETE Operations + For extra keys reported by i18n:check: + - Run: `pnpm --dir web run i18n:check --auto-remove` + - Or manually remove from target language JSON files + + ## Translation Guidelines + + - PRESERVE all placeholders exactly as-is: + - `{{variable}}` - Mustache interpolation + - `${variable}` - Template literal + - `content` - HTML tags + - `_one`, `_other` - Pluralization suffixes (these are KEY suffixes, not values) + - Use appropriate language register (formal/informal) based on existing translations + - Match existing translation style in each language + - Technical terms: check existing conventions per language + - For CJK languages: no spaces between characters unless necessary + - For RTL languages (ar-TN, fa-IR): ensure proper text handling + + ## Output Format Requirements + - Alphabetical key ordering (if original file uses it) + - 2-space indentation + - Trailing newline at end of file + - Valid JSON (use proper escaping for special characters) + + ═══════════════════════════════════════════════════════════════ + ║ PHASE 3: RE-VERIFY - Confirm All Issues Resolved ║ + ═══════════════════════════════════════════════════════════════ + + ### Step 3.1: Run Lint Fix (IMPORTANT!) + ```bash + pnpm --dir web lint:fix --quiet -- 'i18n/**/*.json' + ``` + This ensures: + - JSON keys are sorted alphabetically (jsonc/sort-keys rule) + - Valid i18n keys (dify-i18n/valid-i18n-keys rule) + - No extra keys (dify-i18n/no-extra-keys rule) + + ### Step 3.2: Run Final i18n Check + ```bash + pnpm --dir web run i18n:check + ``` + + ### Step 3.3: Fix Any Remaining Issues + If check reports issues: + - Go back to PHASE 2 for unresolved items + - Repeat until check passes + + ### Step 3.4: Generate Final Summary + ``` + ╔══════════════════════════════════════════════════════════════╗ + ║ SYNC COMPLETED SUMMARY ║ + ╠══════════════════════════════════════════════════════════════╣ + ║ Language │ Added │ Updated │ Deleted │ Status ║ + ╠══════════════════════════════════════════════════════════════╣ + ║ zh-Hans │ 5 │ 2 │ 1 │ ✓ Complete ║ + ║ ja-JP │ 5 │ 2 │ 1 │ ✓ Complete ║ + ║ ... │ ... │ ... │ ... │ ... ║ + ╠══════════════════════════════════════════════════════════════╣ + ║ i18n:check │ PASSED - All keys in sync ║ + ╚══════════════════════════════════════════════════════════════╝ + ``` + + ## Mode-Specific Behavior + + **SYNC_MODE = "incremental"** (default): + - Focus on keys identified from git diff + - Also check i18n:check output for any missing/extra keys + - Efficient for small changes + + **SYNC_MODE = "full"**: + - Compare ALL keys between en-US and each language + - Run i18n:check to identify all discrepancies + - Use for first-time sync or fixing historical issues + + ## Important Notes + + 1. Always run i18n:check BEFORE and AFTER making changes + 2. The check script is the source of truth for missing/extra keys + 3. For UPDATE scenario: git diff is the source of truth for changed values + 4. Create a single commit with all translation changes + 5. If any translation fails, continue with others and report failures + + ═══════════════════════════════════════════════════════════════ + ║ PHASE 4: COMMIT AND CREATE PR ║ + ═══════════════════════════════════════════════════════════════ + + After all translations are complete and verified: + + ### Step 4.1: Check for changes + ```bash + git status --porcelain + ``` + + If there are changes: + + ### Step 4.2: Create a new branch and commit + Run these git commands ONE BY ONE (not combined with &&): + + 1. Create branch: + ```bash + git checkout -b "chore/i18n-sync-$(date +%Y%m%d-%H%M%S)" + ``` + + 2. Stage changes: + ```bash + git add web/i18n/ + ``` + + 3. Commit (use heredoc for multi-line message): + ```bash + git commit -m "chore(i18n): sync translations with en-US - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}" + ``` + + 4. Push: + ```bash + git push origin HEAD + ``` + + ### Step 4.3: Create Pull Request + ```bash + gh pr create \ + --title "chore(i18n): sync translations with en-US" \ + --body "## Summary + + This PR was automatically generated to sync i18n translation files. + + ### Changes + - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }} + - Files processed: ${{ steps.detect_changes.outputs.CHANGED_FILES }} + + ### Verification + - [x] \`i18n:check\` passed + - [x] \`lint:fix\` applied + + 🤖 Generated with Claude Code GitHub Action" \ + --base main + ``` + + claude_args: | + --max-turns 150 + --allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Glob,Grep" diff --git a/web/i18n-config/README.md b/web/i18n-config/README.md index 96c7157114..c90904459c 100644 --- a/web/i18n-config/README.md +++ b/web/i18n-config/README.md @@ -158,10 +158,32 @@ We have a list of languages that we support in the `languages.ts` file. But some ## Utility scripts -- Auto-fill translations: `pnpm run i18n:gen --file app common --lang zh-Hans ja-JP [--dry-run]` - - Use space-separated values; repeat `--file` / `--lang` as needed. Defaults to all en-US files and all supported locales except en-US. - - Protects placeholders (`{{var}}`, `${var}`, ``) before translation and restores them after. - Check missing/extra keys: `pnpm run i18n:check --file app billing --lang zh-Hans [--auto-remove]` - Use space-separated values; repeat `--file` / `--lang` as needed. Returns non-zero on missing/extra keys; `--auto-remove` deletes extra keys automatically. -Workflows: `.github/workflows/translate-i18n-base-on-english.yml` auto-runs the translation generator on `web/i18n/en-US/*.json` changes to main. `i18n:check` is a manual script (not run in CI). +## Automatic Translation + +Translation is handled automatically by Claude Code GitHub Actions. When changes are pushed to `web/i18n/en-US/*.json` on the main branch: + +1. Claude Code analyzes the git diff to detect changes +1. Identifies three types of changes: + - **ADD**: New keys that need translation + - **UPDATE**: Modified keys that need re-translation (even if target language has existing translation) + - **DELETE**: Removed keys that need to be deleted from other languages +1. Runs `i18n:check` to verify the initial sync status. +1. Translates missing/updated keys while preserving placeholders (`{{var}}`, `${var}`, ``) and removes deleted keys. +1. Runs `lint:fix` to sort JSON keys and `i18n:check` again to ensure everything is synchronized. +1. Creates a PR with the translations. + +### Manual Trigger + +To manually trigger translation: + +1. Go to Actions > "Translate i18n Files with Claude Code" +1. Click "Run workflow" +1. Optionally configure: + - **files**: Specific files to translate (space-separated, e.g., "app common") + - **languages**: Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP") + - **mode**: `incremental` (default, only changes) or `full` (check all keys) + +Workflow: `.github/workflows/translate-i18n-claude.yml` diff --git a/web/package.json b/web/package.json index e94f2fd441..dcebe742a7 100644 --- a/web/package.json +++ b/web/package.json @@ -40,7 +40,6 @@ "gen-icons": "node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/", "uglify-embed": "node ./bin/uglify-embed", "i18n:check": "tsx ./scripts/check-i18n.js", - "i18n:gen": "tsx ./scripts/auto-gen-i18n.js", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest --watch", @@ -196,7 +195,6 @@ "@vitest/coverage-v8": "4.0.16", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", - "bing-translate-api": "^4.1.0", "code-inspector-plugin": "1.2.9", "cross-env": "^10.1.0", "eslint": "^9.39.2", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index a2e95dadd7..5ad6d0481b 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -481,9 +481,6 @@ importers: babel-loader: specifier: ^10.0.0 version: 10.0.0(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) - bing-translate-api: - specifier: ^4.1.0 - version: 4.2.0 code-inspector-plugin: specifier: 1.2.9 version: 1.2.9 @@ -3160,10 +3157,6 @@ packages: resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} engines: {node: '>=18'} - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - '@solid-primitives/event-listener@2.4.3': resolution: {integrity: sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==} peerDependencies: @@ -3324,10 +3317,6 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@szmarczak/http-timer@4.0.6': - resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} - engines: {node: '>=10'} - '@tailwindcss/typography@0.5.19': resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} peerDependencies: @@ -3511,9 +3500,6 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/cacheable-request@6.0.3': - resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -3652,9 +3638,6 @@ packages: '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} - '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} @@ -3667,9 +3650,6 @@ packages: '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} - '@types/keyv@3.1.4': - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -3732,9 +3712,6 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - '@types/responselike@1.0.3': - resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -4338,9 +4315,6 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bing-translate-api@4.2.0: - resolution: {integrity: sha512-7a9yo1NbGcHPS8zXTdz8tCOymHZp2pvCuYOChCaXKjOX8EIwdV3SLd4D7RGIqZt1UhffypYBUcAV2gDcTgK0rA==} - bippy@0.3.34: resolution: {integrity: sha512-vmptmU/20UdIWHHhq7qCSHhHzK7Ro3YJ1utU0fBG7ujUc58LEfTtilKxcF0IOgSjT5XLcm7CBzDjbv4lcKApGQ==} peerDependencies: @@ -4427,14 +4401,6 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - cacheable-lookup@5.0.4: - resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} - engines: {node: '>=10.6.0'} - - cacheable-request@7.0.4: - resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} - engines: {node: '>=8'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -4588,9 +4554,6 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} @@ -4996,10 +4959,6 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} @@ -5712,10 +5671,6 @@ packages: get-own-enumerable-property-symbols@3.0.2: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -5772,10 +5727,6 @@ packages: peerDependencies: csstype: ^3.0.10 - got@11.8.6: - resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} - engines: {node: '>=10.19.0'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -5920,17 +5871,10 @@ packages: htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - http2-wrapper@1.0.3: - resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} - engines: {node: '>=10.19.0'} - https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} @@ -6453,10 +6397,6 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} @@ -6729,10 +6669,6 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -6880,10 +6816,6 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} - normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - normalize-wheel@1.0.1: resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} @@ -6964,10 +6896,6 @@ packages: oxc-resolver@11.15.0: resolution: {integrity: sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw==} - p-cancelable@2.1.1: - resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} - engines: {node: '>=8'} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -7359,10 +7287,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -7705,9 +7629,6 @@ packages: resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} - resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -7724,9 +7645,6 @@ packages: engines: {node: '>= 0.4'} hasBin: true - responselike@2.0.1: - resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} - restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -11717,8 +11635,6 @@ snapshots: '@sindresorhus/base62@1.0.0': {} - '@sindresorhus/is@4.6.0': {} - '@solid-primitives/event-listener@2.4.3(solid-js@1.9.10)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) @@ -11971,10 +11887,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@szmarczak/http-timer@4.0.6': - dependencies: - defer-to-connect: 2.0.1 - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))': dependencies: postcss-selector-parser: 6.0.10 @@ -12199,13 +12111,6 @@ snapshots: dependencies: '@babel/types': 7.28.5 - '@types/cacheable-request@6.0.3': - dependencies: - '@types/http-cache-semantics': 4.0.4 - '@types/keyv': 3.1.4 - '@types/node': 18.15.0 - '@types/responselike': 1.0.3 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -12373,8 +12278,6 @@ snapshots: '@types/html-minifier-terser@6.1.0': {} - '@types/http-cache-semantics@4.0.4': {} - '@types/js-cookie@3.0.6': {} '@types/js-yaml@4.0.9': {} @@ -12383,10 +12286,6 @@ snapshots: '@types/katex@0.16.7': {} - '@types/keyv@3.1.4': - dependencies: - '@types/node': 18.15.0 - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -12450,10 +12349,6 @@ snapshots: '@types/resolve@1.20.6': {} - '@types/responselike@1.0.3': - dependencies: - '@types/node': 18.15.0 - '@types/semver@7.7.1': {} '@types/sortablejs@1.15.9': {} @@ -13160,10 +13055,6 @@ snapshots: binary-extensions@2.3.0: {} - bing-translate-api@4.2.0: - dependencies: - got: 11.8.6 - bippy@0.3.34(@types/react@19.2.7)(react@19.2.3): dependencies: '@types/react-reconciler': 0.28.9(@types/react@19.2.7) @@ -13273,18 +13164,6 @@ snapshots: cac@6.7.14: {} - cacheable-lookup@5.0.4: {} - - cacheable-request@7.0.4: - dependencies: - clone-response: 1.0.3 - get-stream: 5.2.0 - http-cache-semantics: 4.2.0 - keyv: 4.5.4 - lowercase-keys: 2.0.0 - normalize-url: 6.1.0 - responselike: 2.0.1 - callsites@3.1.0: {} camel-case@4.1.2: @@ -13425,10 +13304,6 @@ snapshots: client-only@0.0.1: {} - clone-response@1.0.3: - dependencies: - mimic-response: 1.0.1 - clsx@1.2.1: {} clsx@2.1.1: {} @@ -13859,6 +13734,7 @@ snapshots: decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 + optional: true dedent@0.7.0: {} @@ -13871,8 +13747,6 @@ snapshots: deepmerge@4.3.1: {} - defer-to-connect@2.0.1: {} - define-lazy-prop@2.0.0: {} del@4.1.1: @@ -14013,6 +13887,7 @@ snapshots: end-of-stream@1.4.5: dependencies: once: 1.4.0 + optional: true endent@2.1.0: dependencies: @@ -14799,10 +14674,6 @@ snapshots: get-own-enumerable-property-symbols@3.0.2: {} - get-stream@5.2.0: - dependencies: - pump: 3.0.3 - get-stream@8.0.1: {} get-tsconfig@4.13.0: @@ -14862,20 +14733,6 @@ snapshots: dependencies: csstype: 3.2.3 - got@11.8.6: - dependencies: - '@sindresorhus/is': 4.6.0 - '@szmarczak/http-timer': 4.0.6 - '@types/cacheable-request': 6.0.3 - '@types/responselike': 1.0.3 - cacheable-lookup: 5.0.4 - cacheable-request: 7.0.4 - decompress-response: 6.0.0 - http2-wrapper: 1.0.3 - lowercase-keys: 2.0.0 - p-cancelable: 2.1.1 - responselike: 2.0.1 - graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -15120,8 +14977,6 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 - http-cache-semantics@4.2.0: {} - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -15129,11 +14984,6 @@ snapshots: transitivePeerDependencies: - supports-color - http2-wrapper@1.0.3: - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - https-browserify@1.0.0: {} https-proxy-agent@7.0.6: @@ -15604,8 +15454,6 @@ snapshots: dependencies: tslib: 2.8.1 - lowercase-keys@2.0.0: {} - lowlight@1.20.0: dependencies: fault: 1.0.4 @@ -16186,9 +16034,8 @@ snapshots: mimic-function@5.0.1: {} - mimic-response@1.0.1: {} - - mimic-response@3.1.0: {} + mimic-response@3.1.0: + optional: true min-indent@1.0.1: {} @@ -16360,8 +16207,6 @@ snapshots: normalize-range@0.1.2: {} - normalize-url@6.1.0: {} - normalize-wheel@1.0.1: {} npm-run-path@5.3.0: @@ -16445,8 +16290,6 @@ snapshots: '@oxc-resolver/binding-win32-ia32-msvc': 11.15.0 '@oxc-resolver/binding-win32-x64-msvc': 11.15.0 - p-cancelable@2.1.1: {} - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -16809,6 +16652,7 @@ snapshots: dependencies: end-of-stream: 1.4.5 once: 1.4.0 + optional: true punycode@1.4.1: {} @@ -16828,8 +16672,6 @@ snapshots: queue-microtask@1.2.3: {} - quick-lru@5.1.1: {} - randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -17296,8 +17138,6 @@ snapshots: resize-observer-polyfill@1.5.1: {} - resolve-alpn@1.2.1: {} - resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -17316,10 +17156,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - responselike@2.0.1: - dependencies: - lowercase-keys: 2.0.0 - restore-cursor@5.1.0: dependencies: onetime: 7.0.0 diff --git a/web/scripts/auto-gen-i18n.js b/web/scripts/auto-gen-i18n.js deleted file mode 100644 index bd73a18ab8..0000000000 --- a/web/scripts/auto-gen-i18n.js +++ /dev/null @@ -1,336 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { translate } from 'bing-translate-api' -import data from '../i18n-config/languages' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -const targetLanguage = 'en-US' -const i18nFolder = '../i18n' // Path to i18n folder relative to this script -// https://github.com/plainheart/bing-translate-api/blob/master/src/met/lang.json -const languageKeyMap = data.languages.reduce((map, language) => { - if (language.supported) { - if (language.value === 'zh-Hans' || language.value === 'zh-Hant') - map[language.value] = language.value - else - map[language.value] = language.value.split('-')[0] - } - - return map -}, {}) - -const supportedLanguages = Object.keys(languageKeyMap) - -function parseArgs(argv) { - const args = { - files: [], - languages: [], - isDryRun: false, - help: false, - errors: [], - } - - const collectValues = (startIndex) => { - const values = [] - let cursor = startIndex + 1 - while (cursor < argv.length && !argv[cursor].startsWith('--')) { - const value = argv[cursor].trim() - if (value) - values.push(value) - cursor++ - } - return { values, nextIndex: cursor - 1 } - } - - const validateList = (values, flag) => { - if (!values.length) { - args.errors.push(`${flag} requires at least one value. Example: ${flag} app billing`) - return false - } - - const invalid = values.find(value => value.includes(',')) - if (invalid) { - args.errors.push(`${flag} expects space-separated values. Example: ${flag} app billing`) - return false - } - - return true - } - - for (let index = 2; index < argv.length; index++) { - const arg = argv[index] - - if (arg === '--dry-run') { - args.isDryRun = true - continue - } - - if (arg === '--help' || arg === '-h') { - args.help = true - break - } - - if (arg.startsWith('--file=')) { - args.errors.push('--file expects space-separated values. Example: --file app billing') - continue - } - - if (arg === '--file') { - const { values, nextIndex } = collectValues(index) - if (validateList(values, '--file')) - args.files.push(...values) - index = nextIndex - continue - } - - if (arg.startsWith('--lang=')) { - args.errors.push('--lang expects space-separated values. Example: --lang zh-Hans ja-JP') - continue - } - - if (arg === '--lang') { - const { values, nextIndex } = collectValues(index) - if (validateList(values, '--lang')) - args.languages.push(...values) - index = nextIndex - continue - } - } - - return args -} - -function printHelp() { - console.log(`Usage: pnpm run i18n:gen [options] - -Options: - --file Process only specific files; provide space-separated names and repeat --file if needed - --lang Process only specific locales; provide space-separated locales and repeat --lang if needed (default: all supported except en-US) - --dry-run Preview changes without writing files - -h, --help Show help - -Examples: - pnpm run i18n:gen --file app common --lang zh-Hans ja-JP - pnpm run i18n:gen --dry-run -`) -} - -function protectPlaceholders(text) { - const placeholders = [] - let safeText = text - const patterns = [ - /\{\{[^{}]+\}\}/g, // mustache - /\$\{[^{}]+\}/g, // template expressions - /<[^>]+>/g, // html-like tags - ] - - patterns.forEach((pattern) => { - safeText = safeText.replace(pattern, (match) => { - const token = `__PH_${placeholders.length}__` - placeholders.push({ token, value: match }) - return token - }) - }) - - return { - safeText, - restore(translated) { - return placeholders.reduce((result, { token, value }) => result.replace(new RegExp(token, 'g'), value), translated) - }, - } -} - -async function translateText(source, toLanguage) { - if (typeof source !== 'string') - return { value: source, skipped: false } - - const trimmed = source.trim() - if (!trimmed) - return { value: source, skipped: false } - - const { safeText, restore } = protectPlaceholders(source) - - try { - const { translation } = await translate(safeText, null, languageKeyMap[toLanguage]) - return { value: restore(translation), skipped: false } - } - catch (error) { - console.error(`❌ Error translating to ${toLanguage}:`, error.message) - return { value: source, skipped: true, error: error.message } - } -} - -async function translateMissingKeys(sourceObj, targetObject, toLanguage) { - const skippedKeys = [] - const translatedKeys = [] - - for (const key of Object.keys(sourceObj)) { - const sourceValue = sourceObj[key] - const targetValue = targetObject[key] - - // Skip if target already has this key - if (targetValue !== undefined) - continue - - const translationResult = await translateText(sourceValue, toLanguage) - targetObject[key] = translationResult.value ?? '' - if (translationResult.skipped) - skippedKeys.push(`${key}: ${sourceValue}`) - else - translatedKeys.push(key) - } - - return { skipped: skippedKeys, translated: translatedKeys } -} -async function autoGenTrans(fileName, toGenLanguage, isDryRun = false) { - const fullKeyFilePath = path.resolve(__dirname, i18nFolder, targetLanguage, `${fileName}.json`) - const toGenLanguageFilePath = path.resolve(__dirname, i18nFolder, toGenLanguage, `${fileName}.json`) - - try { - const content = fs.readFileSync(fullKeyFilePath, 'utf8') - const fullKeyContent = JSON.parse(content) - - if (!fullKeyContent || typeof fullKeyContent !== 'object') - throw new Error(`Failed to extract translation object from ${fullKeyFilePath}`) - - // if toGenLanguageFilePath does not exist, create it with empty object - let toGenOutPut = {} - if (fs.existsSync(toGenLanguageFilePath)) { - const existingContent = fs.readFileSync(toGenLanguageFilePath, 'utf8') - toGenOutPut = JSON.parse(existingContent) - } - - console.log(`\n🌍 Processing ${fileName} for ${toGenLanguage}...`) - const result = await translateMissingKeys(fullKeyContent, toGenOutPut, toGenLanguage) - - // Generate summary report - console.log(`\n📊 Translation Summary for ${fileName} -> ${toGenLanguage}:`) - console.log(` ✅ Translated: ${result.translated.length} keys`) - console.log(` ⏭️ Skipped: ${result.skipped.length} keys`) - - if (result.skipped.length > 0) { - console.log(`\n⚠️ Skipped keys in ${fileName} (${toGenLanguage}):`) - result.skipped.slice(0, 5).forEach(item => console.log(` - ${item}`)) - if (result.skipped.length > 5) - console.log(` ... and ${result.skipped.length - 5} more`) - } - - const res = `${JSON.stringify(toGenOutPut, null, 2)}\n` - - if (!isDryRun) { - fs.writeFileSync(toGenLanguageFilePath, res) - console.log(`💾 Saved translations to ${toGenLanguageFilePath}`) - } - else { - console.log(`🔍 [DRY RUN] Would save translations to ${toGenLanguageFilePath}`) - } - - return result - } - catch (error) { - console.error(`Error processing file ${fullKeyFilePath}:`, error.message) - throw error - } -} - -// Add command line argument support -const args = parseArgs(process.argv) -const isDryRun = args.isDryRun -const targetFiles = args.files -const targetLangs = args.languages - -// Rate limiting helper -function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -async function main() { - if (args.help) { - printHelp() - return - } - - if (args.errors.length) { - args.errors.forEach(message => console.error(`❌ ${message}`)) - printHelp() - process.exit(1) - return - } - - console.log('🚀 Starting i18n:gen script...') - console.log(`📋 Mode: ${isDryRun ? 'DRY RUN (no files will be modified)' : 'LIVE MODE'}`) - - const filesInEn = fs - .readdirSync(path.resolve(__dirname, i18nFolder, targetLanguage)) - .filter(file => /\.json$/.test(file)) // Only process .json files - .map(file => file.replace(/\.json$/, '')) - - // Filter by target files if specified - const filesToProcess = targetFiles.length > 0 ? filesInEn.filter(f => targetFiles.includes(f)) : filesInEn - const languagesToProcess = Array.from(new Set((targetLangs.length > 0 ? targetLangs : supportedLanguages) - .filter(lang => lang !== targetLanguage))) - - const unknownLangs = languagesToProcess.filter(lang => !languageKeyMap[lang]) - if (unknownLangs.length) { - console.error(`❌ Unsupported languages: ${unknownLangs.join(', ')}`) - process.exit(1) - } - - if (!filesToProcess.length) { - console.log('ℹ️ No files to process based on provided arguments') - return - } - - if (!languagesToProcess.length) { - console.log('ℹ️ No languages to process (did you only specify en-US?)') - return - } - - console.log(`📁 Files to process: ${filesToProcess.join(', ')}`) - console.log(`🌍 Languages to process: ${languagesToProcess.join(', ')}`) - - let totalTranslated = 0 - let totalSkipped = 0 - let totalErrors = 0 - - // Process files sequentially to avoid API rate limits - for (const file of filesToProcess) { - console.log(`\n📄 Processing file: ${file}`) - - // Process languages with rate limiting - for (const language of languagesToProcess) { - try { - const result = await autoGenTrans(file, language, isDryRun) - totalTranslated += result.translated.length - totalSkipped += result.skipped.length - - // Rate limiting: wait 500ms between language processing - await delay(500) - } - catch (e) { - console.error(`❌ Error translating ${file} to ${language}:`, e.message) - totalErrors++ - } - } - } - - // Final summary - console.log('\n🎉 Auto-translation completed!') - console.log('📊 Final Summary:') - console.log(` ✅ Total keys translated: ${totalTranslated}`) - console.log(` ⏭️ Total keys skipped: ${totalSkipped}`) - console.log(` ❌ Total errors: ${totalErrors}`) - - if (isDryRun) - console.log('\n💡 This was a dry run. To actually translate, run without --dry-run flag.') - - if (totalErrors > 0) - process.exitCode = 1 -} - -main().catch((error) => { - console.error('❌ Unexpected error:', error.message) - process.exit(1) -}) From 885f226f77de86dad7cc67f6018f38cb3bdfd092 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Wed, 7 Jan 2026 22:18:02 +0900 Subject: [PATCH 03/12] =?UTF-8?q?refactor:=20split=20changes=20for=20api/c?= =?UTF-8?q?ontrollers/console/workspace/trigger=E2=80=A6=20(#30627)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../console/workspace/trigger_providers.py | 133 +++++++----------- .../trigger/trigger_provider_service.py | 2 +- 2 files changed, 52 insertions(+), 83 deletions(-) diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index c13bfd986e..6b642af613 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -1,14 +1,14 @@ import logging -from collections.abc import Mapping from typing import Any from flask import make_response, redirect, request -from flask_restx import Resource, reqparse -from pydantic import BaseModel, Field, model_validator +from flask_restx import Resource +from pydantic import BaseModel, model_validator from sqlalchemy.orm import Session from werkzeug.exceptions import BadRequest, Forbidden from configs import dify_config +from controllers.common.schema import register_schema_models from controllers.web.error import NotFoundError from core.model_runtime.utils.encoders import jsonable_encoder from core.plugin.entities.plugin_daemon import CredentialType @@ -35,35 +35,38 @@ from ..wraps import ( logger = logging.getLogger(__name__) -class TriggerSubscriptionUpdateRequest(BaseModel): - """Request payload for updating a trigger subscription""" +class TriggerSubscriptionBuilderCreatePayload(BaseModel): + credential_type: str = CredentialType.UNAUTHORIZED - name: str | None = Field(default=None, description="The name for the subscription") - credentials: Mapping[str, Any] | None = Field(default=None, description="The credentials for the subscription") - parameters: Mapping[str, Any] | None = Field(default=None, description="The parameters for the subscription") - properties: Mapping[str, Any] | None = Field(default=None, description="The properties for the subscription") + +class TriggerSubscriptionBuilderVerifyPayload(BaseModel): + credentials: dict[str, Any] + + +class TriggerSubscriptionBuilderUpdatePayload(BaseModel): + name: str | None = None + parameters: dict[str, Any] | None = None + properties: dict[str, Any] | None = None + credentials: dict[str, Any] | None = None @model_validator(mode="after") def check_at_least_one_field(self): - if all(v is None for v in (self.name, self.credentials, self.parameters, self.properties)): + if all(v is None for v in self.model_dump().values()): raise ValueError("At least one of name, credentials, parameters, or properties must be provided") return self -class TriggerSubscriptionVerifyRequest(BaseModel): - """Request payload for verifying subscription credentials.""" - - credentials: Mapping[str, Any] = Field(description="The credentials to verify") +class TriggerOAuthClientPayload(BaseModel): + client_params: dict[str, Any] | None = None + enabled: bool | None = None -console_ns.schema_model( - TriggerSubscriptionUpdateRequest.__name__, - TriggerSubscriptionUpdateRequest.model_json_schema(ref_template="#/definitions/{model}"), -) - -console_ns.schema_model( - TriggerSubscriptionVerifyRequest.__name__, - TriggerSubscriptionVerifyRequest.model_json_schema(ref_template="#/definitions/{model}"), +register_schema_models( + console_ns, + TriggerSubscriptionBuilderCreatePayload, + TriggerSubscriptionBuilderVerifyPayload, + TriggerSubscriptionBuilderUpdatePayload, + TriggerOAuthClientPayload, ) @@ -132,16 +135,11 @@ class TriggerSubscriptionListApi(Resource): raise -parser = reqparse.RequestParser().add_argument( - "credential_type", type=str, required=False, nullable=True, location="json" -) - - @console_ns.route( "/workspaces/current/trigger-provider//subscriptions/builder/create", ) class TriggerSubscriptionBuilderCreateApi(Resource): - @console_ns.expect(parser) + @console_ns.expect(console_ns.models[TriggerSubscriptionBuilderCreatePayload.__name__]) @setup_required @login_required @edit_permission_required @@ -151,10 +149,10 @@ class TriggerSubscriptionBuilderCreateApi(Resource): user = current_user assert user.current_tenant_id is not None - args = parser.parse_args() + payload = TriggerSubscriptionBuilderCreatePayload.model_validate(console_ns.payload or {}) try: - credential_type = CredentialType.of(args.get("credential_type") or CredentialType.UNAUTHORIZED.value) + credential_type = CredentialType.of(payload.credential_type) subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder( tenant_id=user.current_tenant_id, user_id=user.id, @@ -182,18 +180,11 @@ class TriggerSubscriptionBuilderGetApi(Resource): ) -parser_api = ( - reqparse.RequestParser() - # The credentials of the subscription builder - .add_argument("credentials", type=dict, required=False, nullable=True, location="json") -) - - @console_ns.route( "/workspaces/current/trigger-provider//subscriptions/builder/verify-and-update/", ) -class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource): - @console_ns.expect(parser_api) +class TriggerSubscriptionBuilderVerifyApi(Resource): + @console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__]) @setup_required @login_required @edit_permission_required @@ -203,7 +194,7 @@ class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource): user = current_user assert user.current_tenant_id is not None - args = parser_api.parse_args() + payload = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {}) try: # Use atomic update_and_verify to prevent race conditions @@ -213,7 +204,7 @@ class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource): provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, subscription_builder_updater=SubscriptionBuilderUpdater( - credentials=args.get("credentials", None), + credentials=payload.credentials, ), ) except Exception as e: @@ -221,24 +212,11 @@ class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource): raise ValueError(str(e)) from e -parser_update_api = ( - reqparse.RequestParser() - # The name of the subscription builder - .add_argument("name", type=str, required=False, nullable=True, location="json") - # The parameters of the subscription builder - .add_argument("parameters", type=dict, required=False, nullable=True, location="json") - # The properties of the subscription builder - .add_argument("properties", type=dict, required=False, nullable=True, location="json") - # The credentials of the subscription builder - .add_argument("credentials", type=dict, required=False, nullable=True, location="json") -) - - @console_ns.route( "/workspaces/current/trigger-provider//subscriptions/builder/update/", ) class TriggerSubscriptionBuilderUpdateApi(Resource): - @console_ns.expect(parser_update_api) + @console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__]) @setup_required @login_required @edit_permission_required @@ -249,7 +227,7 @@ class TriggerSubscriptionBuilderUpdateApi(Resource): assert isinstance(user, Account) assert user.current_tenant_id is not None - args = parser_update_api.parse_args() + payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) try: return jsonable_encoder( TriggerSubscriptionBuilderService.update_trigger_subscription_builder( @@ -257,10 +235,10 @@ class TriggerSubscriptionBuilderUpdateApi(Resource): provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, subscription_builder_updater=SubscriptionBuilderUpdater( - name=args.get("name", None), - parameters=args.get("parameters", None), - properties=args.get("properties", None), - credentials=args.get("credentials", None), + name=payload.name, + parameters=payload.parameters, + properties=payload.properties, + credentials=payload.credentials, ), ) ) @@ -295,7 +273,7 @@ class TriggerSubscriptionBuilderLogsApi(Resource): "/workspaces/current/trigger-provider//subscriptions/builder/build/", ) class TriggerSubscriptionBuilderBuildApi(Resource): - @console_ns.expect(parser_update_api) + @console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__]) @setup_required @login_required @edit_permission_required @@ -304,7 +282,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource): """Build a subscription instance for a trigger provider""" user = current_user assert user.current_tenant_id is not None - args = parser_update_api.parse_args() + payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) try: # Use atomic update_and_build to prevent race conditions TriggerSubscriptionBuilderService.update_and_build_builder( @@ -313,9 +291,9 @@ class TriggerSubscriptionBuilderBuildApi(Resource): provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, subscription_builder_updater=SubscriptionBuilderUpdater( - name=args.get("name", None), - parameters=args.get("parameters", None), - properties=args.get("properties", None), + name=payload.name, + parameters=payload.parameters, + properties=payload.properties, ), ) return 200 @@ -328,7 +306,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource): "/workspaces/current/trigger-provider//subscriptions/update", ) class TriggerSubscriptionUpdateApi(Resource): - @console_ns.expect(console_ns.models[TriggerSubscriptionUpdateRequest.__name__]) + @console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__]) @setup_required @login_required @edit_permission_required @@ -338,7 +316,7 @@ class TriggerSubscriptionUpdateApi(Resource): user = current_user assert user.current_tenant_id is not None - request = TriggerSubscriptionUpdateRequest.model_validate(console_ns.payload) + request = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) subscription = TriggerProviderService.get_subscription_by_id( tenant_id=user.current_tenant_id, @@ -568,13 +546,6 @@ class TriggerOAuthCallbackApi(Resource): return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback") -parser_oauth_client = ( - reqparse.RequestParser() - .add_argument("client_params", type=dict, required=False, nullable=True, location="json") - .add_argument("enabled", type=bool, required=False, nullable=True, location="json") -) - - @console_ns.route("/workspaces/current/trigger-provider//oauth/client") class TriggerOAuthClientManageApi(Resource): @setup_required @@ -622,7 +593,7 @@ class TriggerOAuthClientManageApi(Resource): logger.exception("Error getting OAuth client", exc_info=e) raise - @console_ns.expect(parser_oauth_client) + @console_ns.expect(console_ns.models[TriggerOAuthClientPayload.__name__]) @setup_required @login_required @is_admin_or_owner_required @@ -632,15 +603,15 @@ class TriggerOAuthClientManageApi(Resource): user = current_user assert user.current_tenant_id is not None - args = parser_oauth_client.parse_args() + payload = TriggerOAuthClientPayload.model_validate(console_ns.payload or {}) try: provider_id = TriggerProviderID(provider) return TriggerProviderService.save_custom_oauth_client_params( tenant_id=user.current_tenant_id, provider_id=provider_id, - client_params=args.get("client_params"), - enabled=args.get("enabled"), + client_params=payload.client_params, + enabled=payload.enabled, ) except ValueError as e: @@ -676,7 +647,7 @@ class TriggerOAuthClientManageApi(Resource): "/workspaces/current/trigger-provider//subscriptions/verify/", ) class TriggerSubscriptionVerifyApi(Resource): - @console_ns.expect(console_ns.models[TriggerSubscriptionVerifyRequest.__name__]) + @console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__]) @setup_required @login_required @edit_permission_required @@ -686,9 +657,7 @@ class TriggerSubscriptionVerifyApi(Resource): user = current_user assert user.current_tenant_id is not None - verify_request: TriggerSubscriptionVerifyRequest = TriggerSubscriptionVerifyRequest.model_validate( - console_ns.payload - ) + verify_request = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {}) try: result = TriggerProviderService.verify_subscription_credentials( diff --git a/api/services/trigger/trigger_provider_service.py b/api/services/trigger/trigger_provider_service.py index 4131d75145..688993c798 100644 --- a/api/services/trigger/trigger_provider_service.py +++ b/api/services/trigger/trigger_provider_service.py @@ -799,7 +799,7 @@ class TriggerProviderService: user_id: str, provider_id: TriggerProviderID, subscription_id: str, - credentials: Mapping[str, Any], + credentials: dict[str, Any], ) -> dict[str, Any]: """ Verify credentials for an existing subscription without updating it. From 7ccf858ce61606ea4518a64ace44e4bbee4591b5 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 7 Jan 2026 21:47:23 +0800 Subject: [PATCH 04/12] fix(workflow): pass correct user_from/invoke_from into graph init (#30637) --- api/core/app/apps/advanced_chat/app_runner.py | 16 +++++----- api/core/app/apps/pipeline/pipeline_runner.py | 31 ++++++++++++------- api/core/app/apps/workflow/app_runner.py | 16 +++++----- api/core/app/apps/workflow_app_runner.py | 14 +++++++-- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/api/core/app/apps/advanced_chat/app_runner.py b/api/core/app/apps/advanced_chat/app_runner.py index a2ae8dec5b..d636548f2b 100644 --- a/api/core/app/apps/advanced_chat/app_runner.py +++ b/api/core/app/apps/advanced_chat/app_runner.py @@ -39,7 +39,6 @@ from extensions.ext_database import db from extensions.ext_redis import redis_client from extensions.otel import WorkflowAppRunnerHandler, trace_span from models import Workflow -from models.enums import UserFrom from models.model import App, Conversation, Message, MessageAnnotation from models.workflow import ConversationVariable from services.conversation_variable_updater import ConversationVariableUpdater @@ -106,6 +105,11 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): if not app_record: raise ValueError("App not found") + invoke_from = self.application_generate_entity.invoke_from + if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run: + invoke_from = InvokeFrom.DEBUGGER + user_from = self._resolve_user_from(invoke_from) + if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run: # Handle single iteration or single loop run graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution( @@ -158,6 +162,8 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): workflow_id=self._workflow.id, tenant_id=self._workflow.tenant_id, user_id=self.application_generate_entity.user_id, + user_from=user_from, + invoke_from=invoke_from, ) db.session.close() @@ -175,12 +181,8 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): graph=graph, graph_config=self._workflow.graph_dict, user_id=self.application_generate_entity.user_id, - user_from=( - UserFrom.ACCOUNT - if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER} - else UserFrom.END_USER - ), - invoke_from=self.application_generate_entity.invoke_from, + user_from=user_from, + invoke_from=invoke_from, call_depth=self.application_generate_entity.call_depth, variable_pool=variable_pool, graph_runtime_state=graph_runtime_state, diff --git a/api/core/app/apps/pipeline/pipeline_runner.py b/api/core/app/apps/pipeline/pipeline_runner.py index 4be9e01fbf..0157521ae9 100644 --- a/api/core/app/apps/pipeline/pipeline_runner.py +++ b/api/core/app/apps/pipeline/pipeline_runner.py @@ -73,9 +73,15 @@ class PipelineRunner(WorkflowBasedAppRunner): """ app_config = self.application_generate_entity.app_config app_config = cast(PipelineConfig, app_config) + invoke_from = self.application_generate_entity.invoke_from + + if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run: + invoke_from = InvokeFrom.DEBUGGER + + user_from = self._resolve_user_from(invoke_from) user_id = None - if self.application_generate_entity.invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}: + if invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}: end_user = db.session.query(EndUser).where(EndUser.id == self.application_generate_entity.user_id).first() if end_user: user_id = end_user.session_id @@ -117,7 +123,7 @@ class PipelineRunner(WorkflowBasedAppRunner): dataset_id=self.application_generate_entity.dataset_id, datasource_type=self.application_generate_entity.datasource_type, datasource_info=self.application_generate_entity.datasource_info, - invoke_from=self.application_generate_entity.invoke_from.value, + invoke_from=invoke_from.value, ) rag_pipeline_variables = [] @@ -149,6 +155,8 @@ class PipelineRunner(WorkflowBasedAppRunner): graph_runtime_state=graph_runtime_state, start_node_id=self.application_generate_entity.start_node_id, workflow=workflow, + user_from=user_from, + invoke_from=invoke_from, ) # RUN WORKFLOW @@ -159,12 +167,8 @@ class PipelineRunner(WorkflowBasedAppRunner): graph=graph, graph_config=workflow.graph_dict, user_id=self.application_generate_entity.user_id, - user_from=( - UserFrom.ACCOUNT - if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER} - else UserFrom.END_USER - ), - invoke_from=self.application_generate_entity.invoke_from, + user_from=user_from, + invoke_from=invoke_from, call_depth=self.application_generate_entity.call_depth, graph_runtime_state=graph_runtime_state, variable_pool=variable_pool, @@ -210,7 +214,12 @@ class PipelineRunner(WorkflowBasedAppRunner): return workflow def _init_rag_pipeline_graph( - self, workflow: Workflow, graph_runtime_state: GraphRuntimeState, start_node_id: str | None = None + self, + workflow: Workflow, + graph_runtime_state: GraphRuntimeState, + start_node_id: str | None = None, + user_from: UserFrom = UserFrom.ACCOUNT, + invoke_from: InvokeFrom = InvokeFrom.SERVICE_API, ) -> Graph: """ Init pipeline graph @@ -253,8 +262,8 @@ class PipelineRunner(WorkflowBasedAppRunner): workflow_id=workflow.id, graph_config=graph_config, user_id=self.application_generate_entity.user_id, - user_from=UserFrom.ACCOUNT, - invoke_from=InvokeFrom.SERVICE_API, + user_from=user_from, + invoke_from=invoke_from, call_depth=0, ) diff --git a/api/core/app/apps/workflow/app_runner.py b/api/core/app/apps/workflow/app_runner.py index 894e6f397a..8dbdc1d58c 100644 --- a/api/core/app/apps/workflow/app_runner.py +++ b/api/core/app/apps/workflow/app_runner.py @@ -20,7 +20,6 @@ from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_redis import redis_client from extensions.otel import WorkflowAppRunnerHandler, trace_span from libs.datetime_utils import naive_utc_now -from models.enums import UserFrom from models.workflow import Workflow logger = logging.getLogger(__name__) @@ -74,7 +73,12 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): workflow_execution_id=self.application_generate_entity.workflow_execution_id, ) + invoke_from = self.application_generate_entity.invoke_from # if only single iteration or single loop run is requested + if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run: + invoke_from = InvokeFrom.DEBUGGER + user_from = self._resolve_user_from(invoke_from) + if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run: graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution( workflow=self._workflow, @@ -102,6 +106,8 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): workflow_id=self._workflow.id, tenant_id=self._workflow.tenant_id, user_id=self.application_generate_entity.user_id, + user_from=user_from, + invoke_from=invoke_from, root_node_id=self._root_node_id, ) @@ -120,12 +126,8 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): graph=graph, graph_config=self._workflow.graph_dict, user_id=self.application_generate_entity.user_id, - user_from=( - UserFrom.ACCOUNT - if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER} - else UserFrom.END_USER - ), - invoke_from=self.application_generate_entity.invoke_from, + user_from=user_from, + invoke_from=invoke_from, call_depth=self.application_generate_entity.call_depth, variable_pool=variable_pool, graph_runtime_state=graph_runtime_state, diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index 0e125b3538..7adf3504ac 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -77,10 +77,18 @@ class WorkflowBasedAppRunner: self._app_id = app_id self._graph_engine_layers = graph_engine_layers + @staticmethod + def _resolve_user_from(invoke_from: InvokeFrom) -> UserFrom: + if invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}: + return UserFrom.ACCOUNT + return UserFrom.END_USER + def _init_graph( self, graph_config: Mapping[str, Any], graph_runtime_state: GraphRuntimeState, + user_from: UserFrom, + invoke_from: InvokeFrom, workflow_id: str = "", tenant_id: str = "", user_id: str = "", @@ -105,8 +113,8 @@ class WorkflowBasedAppRunner: workflow_id=workflow_id, graph_config=graph_config, user_id=user_id, - user_from=UserFrom.ACCOUNT, - invoke_from=InvokeFrom.SERVICE_API, + user_from=user_from, + invoke_from=invoke_from, call_depth=0, ) @@ -250,7 +258,7 @@ class WorkflowBasedAppRunner: graph_config=graph_config, user_id="", user_from=UserFrom.ACCOUNT, - invoke_from=InvokeFrom.SERVICE_API, + invoke_from=InvokeFrom.DEBUGGER, call_depth=0, ) From 25ff4ae5dad773837c9c36e499609efe98137b29 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:53:32 +0800 Subject: [PATCH 05/12] fix(i18n): resolve Claude Code sandbox path issues in workflow (#30710) --- .github/workflows/translate-i18n-claude.yml | 71 +++++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index 8dccf8ef93..0e05913576 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -132,6 +132,14 @@ jobs: - Use **Edit** tool to modify JSON files (NOT node, jq, or bash scripts) - Use **Bash** ONLY for: git commands, gh commands, pnpm commands - Run bash commands ONE BY ONE, never combine with && or || + - NEVER use `$()` command substitution - it's not supported. Split into separate commands instead. + + ## WORKING DIRECTORY & ABSOLUTE PATHS + Claude Code sandbox working directory may vary. Always use absolute paths: + - For pnpm: `pnpm --dir ${{ github.workspace }}/web ` + - For git: `git -C ${{ github.workspace }} ` + - For gh: `gh --repo ${{ github.repository }} ` + - For file paths: `${{ github.workspace }}/web/i18n/` ## EFFICIENCY RULES - **ONE Edit per language file** - batch all key additions into a single Edit @@ -142,8 +150,8 @@ jobs: - Changed/target files: ${{ steps.detect_changes.outputs.CHANGED_FILES }} - Target languages (empty means all supported): ${{ steps.detect_changes.outputs.TARGET_LANGS }} - Sync mode: ${{ steps.detect_changes.outputs.SYNC_MODE }} - - Translation files are located in: web/i18n/{locale}/{filename}.json - - Language configuration is in: web/i18n-config/languages.ts + - Translation files are located in: ${{ github.workspace }}/web/i18n/{locale}/{filename}.json + - Language configuration is in: ${{ github.workspace }}/web/i18n-config/languages.ts - Git diff is available: ${{ steps.detect_changes.outputs.DIFF_AVAILABLE }} ## CRITICAL DESIGN: Verify First, Then Sync @@ -166,13 +174,15 @@ jobs: * DELETE: Keys that appear only in `-` lines (removed keys) ### Step 1.2: Read Language Configuration - Use the Read tool to read `web/i18n-config/languages.ts`. + Use the Read tool to read `${{ github.workspace }}/web/i18n-config/languages.ts`. Extract all languages with `supported: true`. ### Step 1.3: Run i18n:check for Each Language ```bash - pnpm --dir web install --frozen-lockfile - pnpm --dir web run i18n:check + pnpm --dir ${{ github.workspace }}/web install --frozen-lockfile + ``` + ```bash + pnpm --dir ${{ github.workspace }}/web run i18n:check ``` This will report: @@ -234,7 +244,7 @@ jobs: **IMPORTANT: Special handling for zh-Hans and ja-JP** If zh-Hans or ja-JP files were ALSO modified in the same push: - - Run: `git diff HEAD~1 --name-only | grep -E "(zh-Hans|ja-JP)"` + - Run: `git -C ${{ github.workspace }} diff HEAD~1 --name-only` and check for zh-Hans or ja-JP files - If found, it means someone manually translated them. Apply these rules: 1. **Missing keys**: Still ADD them (completeness required) @@ -254,7 +264,7 @@ jobs: ### Step 2.3: Process DELETE Operations For extra keys reported by i18n:check: - - Run: `pnpm --dir web run i18n:check --auto-remove` + - Run: `pnpm --dir ${{ github.workspace }}/web run i18n:check --auto-remove` - Or manually remove from target language JSON files ## Translation Guidelines @@ -282,7 +292,7 @@ jobs: ### Step 3.1: Run Lint Fix (IMPORTANT!) ```bash - pnpm --dir web lint:fix --quiet -- 'i18n/**/*.json' + pnpm --dir ${{ github.workspace }}/web lint:fix --quiet -- 'i18n/**/*.json' ``` This ensures: - JSON keys are sorted alphabetically (jsonc/sort-keys rule) @@ -291,7 +301,7 @@ jobs: ### Step 3.2: Run Final i18n Check ```bash - pnpm --dir web run i18n:check + pnpm --dir ${{ github.workspace }}/web run i18n:check ``` ### Step 3.3: Fix Any Remaining Issues @@ -342,39 +352,45 @@ jobs: ### Step 4.1: Check for changes ```bash - git status --porcelain + git -C ${{ github.workspace }} status --porcelain ``` If there are changes: ### Step 4.2: Create a new branch and commit - Run these git commands ONE BY ONE (not combined with &&): + Run these git commands ONE BY ONE (not combined with &&). + **IMPORTANT**: Do NOT use `$()` command substitution. Use two separate commands: - 1. Create branch: + 1. First, get the timestamp: ```bash - git checkout -b "chore/i18n-sync-$(date +%Y%m%d-%H%M%S)" + date +%Y%m%d-%H%M%S + ``` + (Note the output, e.g., "20260115-143052") + + 2. Then create branch using the timestamp value: + ```bash + git -C ${{ github.workspace }} checkout -b chore/i18n-sync-20260115-143052 + ``` + (Replace "20260115-143052" with the actual timestamp from step 1) + + 3. Stage changes: + ```bash + git -C ${{ github.workspace }} add web/i18n/ ``` - 2. Stage changes: + 4. Commit: ```bash - git add web/i18n/ + git -C ${{ github.workspace }} commit -m "chore(i18n): sync translations with en-US - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}" ``` - 3. Commit (use heredoc for multi-line message): + 5. Push: ```bash - git commit -m "chore(i18n): sync translations with en-US - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}" - ``` - - 4. Push: - ```bash - git push origin HEAD + git -C ${{ github.workspace }} push origin HEAD ``` ### Step 4.3: Create Pull Request ```bash - gh pr create \ - --title "chore(i18n): sync translations with en-US" \ - --body "## Summary + gh pr create --repo ${{ github.repository }} --title "chore(i18n): sync translations with en-US" --body "## Summary This PR was automatically generated to sync i18n translation files. @@ -386,10 +402,9 @@ jobs: - [x] \`i18n:check\` passed - [x] \`lint:fix\` applied - 🤖 Generated with Claude Code GitHub Action" \ - --base main + 🤖 Generated with Claude Code GitHub Action" --base main ``` claude_args: | --max-turns 150 - --allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Glob,Grep" + --allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Bash(date *),Bash(date:*),Glob,Grep" From 27a803a6f01990d5a38fd6808fe7f51aa1703979 Mon Sep 17 00:00:00 2001 From: Rhon Joe Date: Thu, 8 Jan 2026 09:54:27 +0800 Subject: [PATCH 06/12] fix(web): resolve key-value input box height inconsistency on focus/blur (#30715) (#30716) --- .../components/key-value/key-value-edit/input-item.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx index 7e5fe7da7a..7f1e2df2a0 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx @@ -59,12 +59,12 @@ const InputItem: FC = ({ }, [onRemove]) return ( -
+
{(!readOnly) ? ( = ({ ) : (
{!hasValue &&
{placeholder}
} {hasValue && ( Date: Wed, 7 Jan 2026 18:03:39 -0800 Subject: [PATCH 07/12] feat: add decryption decorators for password and code fields in webapp (#30704) --- api/controllers/web/login.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api/controllers/web/login.py b/api/controllers/web/login.py index 538d0c44be..5847f4ae3a 100644 --- a/api/controllers/web/login.py +++ b/api/controllers/web/login.py @@ -10,7 +10,12 @@ from controllers.console.auth.error import ( InvalidEmailError, ) from controllers.console.error import AccountBannedError -from controllers.console.wraps import only_edition_enterprise, setup_required +from controllers.console.wraps import ( + decrypt_code_field, + decrypt_password_field, + only_edition_enterprise, + setup_required, +) from controllers.web import web_ns from controllers.web.wraps import decode_jwt_token from libs.helper import email @@ -42,6 +47,7 @@ class LoginApi(Resource): 404: "Account not found", } ) + @decrypt_password_field def post(self): """Authenticate user and login.""" parser = ( @@ -181,6 +187,7 @@ class EmailCodeLoginApi(Resource): 404: "Account not found", } ) + @decrypt_code_field def post(self): parser = ( reqparse.RequestParser() From c5b99ebd1711b791d66e08aa9b021f123ab75e8e Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 8 Jan 2026 10:04:42 +0800 Subject: [PATCH 08/12] fix: web app login code encrypt (#30705) --- web/app/(shareLayout)/webapp-signin/check-code/page.tsx | 4 ++-- .../webapp-signin/components/mail-and-password-auth.tsx | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx index bda5484197..72e3b7f2ea 100644 --- a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -8,12 +8,12 @@ import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Toast from '@/app/components/base/toast' import Countdown from '@/app/components/signin/countdown' - import { useLocale } from '@/context/i18n' import { useWebAppStore } from '@/context/web-app-context' import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common' import { fetchAccessToken } from '@/service/share' import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth' +import { encryptVerificationCode } from '@/utils/encryption' export default function CheckCode() { const { t } = useTranslation() @@ -64,7 +64,7 @@ export default function CheckCode() { return } setIsLoading(true) - const ret = await webAppEmailLoginWithCode({ email, code, token }) + const ret = await webAppEmailLoginWithCode({ email, code: encryptVerificationCode(code), token }) if (ret.result === 'success') { setWebAppAccessToken(ret.data.access_token) const { access_token } = await fetchAccessToken({ diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx index 23ac83e76c..11aebe4a5b 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -13,6 +13,7 @@ import { useWebAppStore } from '@/context/web-app-context' import { webAppLogin } from '@/service/common' import { fetchAccessToken } from '@/service/share' import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth' +import { encryptPassword } from '@/utils/encryption' type MailAndPasswordAuthProps = { isEmailSetup: boolean @@ -71,7 +72,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut setIsLoading(true) const loginData: Record = { email, - password, + password: encryptPassword(password), language: locale, remember_me: true, } From 44762a38c2ddfd4cf9e551892e6b49185edafaf8 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Thu, 8 Jan 2026 10:40:37 +0800 Subject: [PATCH 09/12] Update api/configs/feature/hosted_service/__init__.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- api/configs/feature/hosted_service/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/configs/feature/hosted_service/__init__.py b/api/configs/feature/hosted_service/__init__.py index 07036be934..a0c09eac35 100644 --- a/api/configs/feature/hosted_service/__init__.py +++ b/api/configs/feature/hosted_service/__init__.py @@ -270,7 +270,7 @@ class HostedDeepseekConfig(BaseSettings): ) HOSTED_DEEPSEEK_PAID_ENABLED: bool = Field( - description="Enable paid access to hosted XAI service", + description="Enable paid access to hosted Deepseek service", default=False, ) From 38f7c77fa4e1d492eaec23857089c51860fa02be Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Thu, 8 Jan 2026 10:40:53 +0800 Subject: [PATCH 10/12] Update api/configs/feature/hosted_service/__init__.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- api/configs/feature/hosted_service/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/configs/feature/hosted_service/__init__.py b/api/configs/feature/hosted_service/__init__.py index a0c09eac35..42ede718c4 100644 --- a/api/configs/feature/hosted_service/__init__.py +++ b/api/configs/feature/hosted_service/__init__.py @@ -367,7 +367,7 @@ class HostedTongyiConfig(BaseSettings): ) HOSTED_TONGYI_TRIAL_ENABLED: bool = Field( - description="Enable trial access to hosted Anthropic service", + description="Enable trial access to hosted Tongyi service", default=False, ) From c301052789aeea967016568b8e6693413d89d953 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 8 Jan 2026 10:45:28 +0800 Subject: [PATCH 11/12] add rowcount check --- api/services/credit_pool_service.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/services/credit_pool_service.py b/api/services/credit_pool_service.py index 8ae409809a..bc2ca6ca1e 100644 --- a/api/services/credit_pool_service.py +++ b/api/services/credit_pool_service.py @@ -61,7 +61,12 @@ class CreditPoolService: TenantCreditPool.quota_used + credits_required <= TenantCreditPool.quota_limit, ] stmt = update(TenantCreditPool).where(*where_conditions).values(**update_values) - session.execute(stmt) + result = session.execute(stmt) session.commit() + if result.rowcount == 0: + raise QuotaExceededError( + f"Insufficient credits. Required: {credits_required}, Available: {pool.remaining_credits}" + ) except Exception: + logger.exception("Failed to deduct credits for tenant %s", tenant_id) raise QuotaExceededError("Failed to deduct credits") From 75ccba6e52c09b7c9c74445bff2c0675bbe231ce Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 8 Jan 2026 11:01:23 +0800 Subject: [PATCH 12/12] add rowcount check --- api/services/credit_pool_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/services/credit_pool_service.py b/api/services/credit_pool_service.py index bc2ca6ca1e..6214900c61 100644 --- a/api/services/credit_pool_service.py +++ b/api/services/credit_pool_service.py @@ -1,6 +1,6 @@ import logging -from sqlalchemy import update +from sqlalchemy import CursorResult, update from sqlalchemy.orm import Session from configs import dify_config @@ -61,7 +61,7 @@ class CreditPoolService: TenantCreditPool.quota_used + credits_required <= TenantCreditPool.quota_limit, ] stmt = update(TenantCreditPool).where(*where_conditions).values(**update_values) - result = session.execute(stmt) + result: CursorResult = session.execute(stmt) session.commit() if result.rowcount == 0: raise QuotaExceededError(