Merge branch 'main' into feat/structured-output

This commit is contained in:
Novice 2025-04-16 18:12:00 +08:00
commit 5aa541fe37
115 changed files with 7301 additions and 11331 deletions

View File

@ -2,10 +2,10 @@
npm add -g pnpm@10.8.0 npm add -g pnpm@10.8.0
cd web && pnpm install cd web && pnpm install
pipx install poetry pipx install uv
echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc echo 'alias start-api="cd /workspaces/dify/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc echo 'alias start-worker="cd /workspaces/dify/api && uv run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc

View File

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
cd api && poetry install cd api && uv sync

View File

@ -1,36 +0,0 @@
name: Setup Poetry and Python
inputs:
python-version:
description: Python version to use and the Poetry installed with
required: true
default: '3.11'
poetry-version:
description: Poetry version to set up
required: true
default: '2.0.1'
poetry-lockfile:
description: Path to the Poetry lockfile to restore cache from
required: true
default: ''
runs:
using: composite
steps:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: pip
- name: Install Poetry
shell: bash
run: pip install poetry==${{ inputs.poetry-version }}
- name: Restore Poetry cache
if: ${{ inputs.poetry-lockfile != '' }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: poetry
cache-dependency-path: ${{ inputs.poetry-lockfile }}

34
.github/actions/setup-uv/action.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Setup UV and Python
inputs:
python-version:
description: Python version to use and the UV installed with
required: true
default: '3.12'
uv-version:
description: UV version to set up
required: true
default: '0.6.14'
uv-lockfile:
description: Path to the UV lockfile to restore cache from
required: true
default: ''
enable-cache:
required: true
default: true
runs:
using: composite
steps:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: ${{ inputs.uv-version }}
python-version: ${{ inputs.python-version }}
enable-cache: ${{ inputs.enable-cache }}
cache-dependency-glob: ${{ inputs.uv-lockfile }}

View File

@ -17,6 +17,9 @@ jobs:
test: test:
name: API Tests name: API Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
shell: bash
strategy: strategy:
matrix: matrix:
python-version: python-version:
@ -27,40 +30,44 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Poetry and Python ${{ matrix.python-version }} - name: Setup UV and Python
uses: ./.github/actions/setup-poetry uses: ./.github/actions/setup-uv
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
poetry-lockfile: api/poetry.lock uv-lockfile: api/uv.lock
- name: Check Poetry lockfile - name: Check UV lockfile
run: | run: uv lock --project api --check
poetry check -C api --lock
poetry show -C api
- name: Install dependencies - name: Install dependencies
run: poetry install -C api --with dev run: uv sync --project api --dev
- name: Check dependencies in pyproject.toml
run: poetry run -P api bash dev/pytest/pytest_artifacts.sh
- name: Run Unit tests - name: Run Unit tests
run: poetry run -P api bash dev/pytest/pytest_unit_tests.sh run: |
uv run --project api bash dev/pytest/pytest_unit_tests.sh
# Extract coverage percentage and create a summary
TOTAL_COVERAGE=$(python -c 'import json; print(json.load(open("coverage.json"))["totals"]["percent_covered_display"])')
# Create a detailed coverage summary
echo "### Test Coverage Summary :test_tube:" >> $GITHUB_STEP_SUMMARY
echo "Total Coverage: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
uv run --project api coverage report >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
- name: Run dify config tests - name: Run dify config tests
run: poetry run -P api python dev/pytest/pytest_config_tests.py run: uv run --project api dev/pytest/pytest_config_tests.py
- name: Cache MyPy - name: MyPy Cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: api/.mypy_cache path: api/.mypy_cache
key: mypy-${{ matrix.python-version }}-${{ runner.os }}-${{ hashFiles('api/poetry.lock') }} key: mypy-${{ matrix.python-version }}-${{ runner.os }}-${{ hashFiles('api/uv.lock') }}
- name: Run mypy - name: Run MyPy Checks
run: dev/run-mypy run: dev/mypy-check
- name: Set up dotenvs - name: Set up dotenvs
run: | run: |
@ -80,4 +87,4 @@ jobs:
ssrf_proxy ssrf_proxy
- name: Run Workflow - name: Run Workflow
run: poetry run -P api bash dev/pytest/pytest_workflow.sh run: uv run --project api bash dev/pytest/pytest_workflow.sh

View File

@ -24,13 +24,13 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Poetry and Python - name: Setup UV and Python
uses: ./.github/actions/setup-poetry uses: ./.github/actions/setup-uv
with: with:
poetry-lockfile: api/poetry.lock uv-lockfile: api/uv.lock
- name: Install dependencies - name: Install dependencies
run: poetry install -C api run: uv sync --project api
- name: Prepare middleware env - name: Prepare middleware env
run: | run: |
@ -54,6 +54,4 @@ jobs:
- name: Run DB Migration - name: Run DB Migration
env: env:
DEBUG: true DEBUG: true
run: | run: uv run --directory api flask upgrade-db
cd api
poetry run python -m flask upgrade-db

View File

@ -42,6 +42,7 @@ jobs:
with: with:
push: false push: false
context: "{{defaultContext}}:${{ matrix.context }}" context: "{{defaultContext}}:${{ matrix.context }}"
file: "${{ matrix.file }}"
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

View File

@ -18,7 +18,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Check changed files - name: Check changed files
@ -29,24 +28,27 @@ jobs:
api/** api/**
.github/workflows/style.yml .github/workflows/style.yml
- name: Setup Poetry and Python - name: Setup UV and Python
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/actions/setup-poetry uses: ./.github/actions/setup-uv
with:
uv-lockfile: api/uv.lock
enable-cache: false
- name: Install dependencies - name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: poetry install -C api --only lint run: uv sync --project api --dev
- name: Ruff check - name: Ruff check
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: | run: |
poetry run -C api ruff --version uv run --directory api ruff --version
poetry run -C api ruff check ./ uv run --directory api ruff check ./
poetry run -C api ruff format --check ./ uv run --directory api ruff format --check ./
- name: Dotenv check - name: Dotenv check
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: poetry run -P api dotenv-linter ./api/.env.example ./web/.env.example run: uv run --project api dotenv-linter ./api/.env.example ./web/.env.example
- name: Lint hints - name: Lint hints
if: failure() if: failure()
@ -63,7 +65,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Check changed files - name: Check changed files
@ -102,7 +103,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Check changed files - name: Check changed files
@ -133,7 +133,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Check changed files - name: Check changed files

View File

@ -27,7 +27,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}

View File

@ -8,7 +8,7 @@ on:
- api/core/rag/datasource/** - api/core/rag/datasource/**
- docker/** - docker/**
- .github/workflows/vdb-tests.yml - .github/workflows/vdb-tests.yml
- api/poetry.lock - api/uv.lock
- api/pyproject.toml - api/pyproject.toml
concurrency: concurrency:
@ -29,22 +29,19 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Poetry and Python ${{ matrix.python-version }} - name: Setup UV and Python
uses: ./.github/actions/setup-poetry uses: ./.github/actions/setup-uv
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
poetry-lockfile: api/poetry.lock uv-lockfile: api/uv.lock
- name: Check Poetry lockfile - name: Check UV lockfile
run: | run: uv lock --project api --check
poetry check -C api --lock
poetry show -C api
- name: Install dependencies - name: Install dependencies
run: poetry install -C api --with dev run: uv sync --project api --dev
- name: Set up dotenvs - name: Set up dotenvs
run: | run: |
@ -80,7 +77,7 @@ jobs:
elasticsearch elasticsearch
- name: Check TiDB Ready - name: Check TiDB Ready
run: poetry run -P api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
- name: Test Vector Stores - name: Test Vector Stores
run: poetry run -P api bash dev/pytest/pytest_vdb.sh run: uv run --project api bash dev/pytest/pytest_vdb.sh

View File

@ -23,7 +23,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Check changed files - name: Check changed files

1
.gitignore vendored
View File

@ -46,6 +46,7 @@ htmlcov/
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
coverage.json
*.cover *.cover
*.py,cover *.py,cover
.hypothesis/ .hypothesis/

View File

@ -3,20 +3,11 @@ FROM python:3.12-slim-bookworm AS base
WORKDIR /app/api WORKDIR /app/api
# Install Poetry # Install uv
ENV POETRY_VERSION=2.0.1 ENV UV_VERSION=0.6.14
# if you located in China, you can use aliyun mirror to speed up RUN pip install --no-cache-dir uv==${UV_VERSION}
# RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/
RUN pip install --no-cache-dir poetry==${POETRY_VERSION}
# Configure Poetry
ENV POETRY_CACHE_DIR=/tmp/poetry_cache
ENV POETRY_NO_INTERACTION=1
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
ENV POETRY_VIRTUALENVS_CREATE=true
ENV POETRY_REQUESTS_TIMEOUT=15
FROM base AS packages FROM base AS packages
@ -27,8 +18,8 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc g++ libc-dev libffi-dev libgmp-dev libmpfr-dev libmpc-dev && apt-get install -y --no-install-recommends gcc g++ libc-dev libffi-dev libgmp-dev libmpfr-dev libmpc-dev
# Install Python dependencies # Install Python dependencies
COPY pyproject.toml poetry.lock ./ COPY pyproject.toml uv.lock ./
RUN poetry install --sync --no-cache --no-root RUN uv sync --locked
# production stage # production stage
FROM base AS production FROM base AS production

View File

@ -3,7 +3,10 @@
## Usage ## Usage
> [!IMPORTANT] > [!IMPORTANT]
> In the v0.6.12 release, we deprecated `pip` as the package management tool for Dify API Backend service and replaced it with `poetry`. >
> In the v1.3.0 release, `poetry` has been replaced with
> [`uv`](https://docs.astral.sh/uv/) as the package manager
> for Dify API backend service.
1. Start the docker-compose stack 1. Start the docker-compose stack
@ -37,19 +40,19 @@
4. Create environment. 4. Create environment.
Dify API service uses [Poetry](https://python-poetry.org/docs/) to manage dependencies. First, you need to add the poetry shell plugin, if you don't have it already, in order to run in a virtual environment. [Note: Poetry shell is no longer a native command so you need to install the poetry plugin beforehand] Dify API service uses [UV](https://docs.astral.sh/uv/) to manage dependencies.
First, you need to add the uv package manager, if you don't have it already.
```bash ```bash
poetry self add poetry-plugin-shell pip install uv
# Or on macOS
brew install uv
``` ```
Then, You can execute `poetry shell` to activate the environment.
5. Install dependencies 5. Install dependencies
```bash ```bash
poetry env use 3.12 uv sync --dev
poetry install
``` ```
6. Run migrate 6. Run migrate
@ -57,21 +60,21 @@
Before the first launch, migrate the database to the latest version. Before the first launch, migrate the database to the latest version.
```bash ```bash
poetry run python -m flask db upgrade uv run flask db upgrade
``` ```
7. Start backend 7. Start backend
```bash ```bash
poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug uv run flask run --host 0.0.0.0 --port=5001 --debug
``` ```
8. Start Dify [web](../web) service. 8. Start Dify [web](../web) service.
9. Setup your application by visiting `http://localhost:3000`... 9. Setup your application by visiting `http://localhost:3000`.
10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service. 10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service.
```bash ```bash
poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion
``` ```
## Testing ## Testing
@ -79,11 +82,11 @@
1. Install dependencies for both the backend and the test environment 1. Install dependencies for both the backend and the test environment
```bash ```bash
poetry install -C api --with dev uv sync --dev
``` ```
2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml` 2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
```bash ```bash
poetry run -P api bash dev/pytest/pytest_all_tests.sh uv run -P api bash dev/pytest/pytest_all_tests.sh
``` ```

View File

@ -139,7 +139,9 @@ class DatasetListApi(DatasetApiResource):
external_knowledge_id=args["external_knowledge_id"], external_knowledge_id=args["external_knowledge_id"],
embedding_model_provider=args["embedding_model_provider"], embedding_model_provider=args["embedding_model_provider"],
embedding_model_name=args["embedding_model"], embedding_model_name=args["embedding_model"],
retrieval_model=RetrievalModel(**args["retrieval_model"]), retrieval_model=RetrievalModel(**args["retrieval_model"])
if args["retrieval_model"] is not None
else None,
) )
except services.errors.dataset.DatasetNameDuplicateError: except services.errors.dataset.DatasetNameDuplicateError:
raise DatasetNameDuplicateError() raise DatasetNameDuplicateError()

View File

@ -122,6 +122,8 @@ class SegmentApi(DatasetApiResource):
tenant_id=current_user.current_tenant_id, tenant_id=current_user.current_tenant_id,
status_list=args["status"], status_list=args["status"],
keyword=args["keyword"], keyword=args["keyword"],
page=page,
limit=limit,
) )
response = { response = {

View File

@ -191,7 +191,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
# action is final answer, return final answer directly # action is final answer, return final answer directly
try: try:
if isinstance(scratchpad.action.action_input, dict): if isinstance(scratchpad.action.action_input, dict):
final_answer = json.dumps(scratchpad.action.action_input) final_answer = json.dumps(scratchpad.action.action_input, ensure_ascii=False)
elif isinstance(scratchpad.action.action_input, str): elif isinstance(scratchpad.action.action_input, str):
final_answer = scratchpad.action.action_input final_answer = scratchpad.action.action_input
else: else:

View File

@ -52,6 +52,7 @@ class AgentStrategyParameter(PluginParameter):
return cast_parameter_value(self, value) return cast_parameter_value(self, value)
type: AgentStrategyParameterType = Field(..., description="The type of the parameter") type: AgentStrategyParameterType = Field(..., description="The type of the parameter")
help: Optional[I18nObject] = None
def init_frontend_parameter(self, value: Any): def init_frontend_parameter(self, value: Any):
return init_frontend_parameter(self, self.type, value) return init_frontend_parameter(self, self.type, value)

View File

@ -48,25 +48,26 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
write=dify_config.SSRF_DEFAULT_WRITE_TIME_OUT, write=dify_config.SSRF_DEFAULT_WRITE_TIME_OUT,
) )
if "ssl_verify" not in kwargs:
kwargs["ssl_verify"] = HTTP_REQUEST_NODE_SSL_VERIFY
ssl_verify = kwargs.pop("ssl_verify")
retries = 0 retries = 0
while retries <= max_retries: while retries <= max_retries:
try: try:
if dify_config.SSRF_PROXY_ALL_URL: if dify_config.SSRF_PROXY_ALL_URL:
with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client: with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL, verify=ssl_verify) as client:
response = client.request(method=method, url=url, **kwargs) response = client.request(method=method, url=url, **kwargs)
elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL: elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL:
proxy_mounts = { proxy_mounts = {
"http://": httpx.HTTPTransport( "http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL, verify=ssl_verify),
proxy=dify_config.SSRF_PROXY_HTTP_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY "https://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTPS_URL, verify=ssl_verify),
),
"https://": httpx.HTTPTransport(
proxy=dify_config.SSRF_PROXY_HTTPS_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY
),
} }
with httpx.Client(mounts=proxy_mounts, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client: with httpx.Client(mounts=proxy_mounts, verify=ssl_verify) as client:
response = client.request(method=method, url=url, **kwargs) response = client.request(method=method, url=url, **kwargs)
else: else:
with httpx.Client(verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client: with httpx.Client(verify=ssl_verify) as client:
response = client.request(method=method, url=url, **kwargs) response = client.request(method=method, url=url, **kwargs)
if response.status_code not in STATUS_FORCELIST: if response.status_code not in STATUS_FORCELIST:

View File

@ -131,7 +131,7 @@ def cast_parameter_value(typ: enum.StrEnum, value: Any, /):
raise ValueError("The selector must be a dictionary.") raise ValueError("The selector must be a dictionary.")
return value return value
case PluginParameterType.TOOLS_SELECTOR: case PluginParameterType.TOOLS_SELECTOR:
if not isinstance(value, list): if value and not isinstance(value, list):
raise ValueError("The tools selector must be a list.") raise ValueError("The tools selector must be a list.")
return value return value
case _: case _:
@ -147,7 +147,7 @@ def init_frontend_parameter(rule: PluginParameter, type: enum.StrEnum, value: An
init frontend parameter by rule init frontend parameter by rule
""" """
parameter_value = value parameter_value = value
if not parameter_value and parameter_value != 0 and type != PluginParameterType.TOOLS_SELECTOR: if not parameter_value and parameter_value != 0:
# get default value # get default value
parameter_value = rule.default parameter_value = rule.default
if not parameter_value and rule.required: if not parameter_value and rule.required:

View File

@ -82,7 +82,7 @@ class BasePluginManager:
Make a stream request to the plugin daemon inner API Make a stream request to the plugin daemon inner API
""" """
response = self._request(method, path, headers, data, params, files, stream=True) response = self._request(method, path, headers, data, params, files, stream=True)
for line in response.iter_lines(): for line in response.iter_lines(chunk_size=1024 * 8):
line = line.decode("utf-8").strip() line = line.decode("utf-8").strip()
if line.startswith("data:"): if line.startswith("data:"):
line = line[5:].strip() line = line[5:].strip()

View File

@ -110,7 +110,62 @@ class PluginToolManager(BasePluginManager):
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
) )
return response
class FileChunk:
"""
Only used for internal processing.
"""
bytes_written: int
total_length: int
data: bytearray
def __init__(self, total_length: int):
self.bytes_written = 0
self.total_length = total_length
self.data = bytearray(total_length)
files: dict[str, FileChunk] = {}
for resp in response:
if resp.type == ToolInvokeMessage.MessageType.BLOB_CHUNK:
assert isinstance(resp.message, ToolInvokeMessage.BlobChunkMessage)
# Get blob chunk information
chunk_id = resp.message.id
total_length = resp.message.total_length
blob_data = resp.message.blob
is_end = resp.message.end
# Initialize buffer for this file if it doesn't exist
if chunk_id not in files:
files[chunk_id] = FileChunk(total_length)
# If this is the final chunk, yield a complete blob message
if is_end:
yield ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB,
message=ToolInvokeMessage.BlobMessage(blob=files[chunk_id].data),
meta=resp.meta,
)
else:
# Check if file is too large (30MB limit)
if files[chunk_id].bytes_written + len(blob_data) > 30 * 1024 * 1024:
# Delete the file if it's too large
del files[chunk_id]
# Skip yielding this message
raise ValueError("File is too large which reached the limit of 30MB")
# Check if single chunk is too large (8KB limit)
if len(blob_data) > 8192:
# Skip yielding this message
raise ValueError("File chunk is too large which reached the limit of 8KB")
# Append the blob data to the buffer
files[chunk_id].data[
files[chunk_id].bytes_written : files[chunk_id].bytes_written + len(blob_data)
] = blob_data
files[chunk_id].bytes_written += len(blob_data)
else:
yield resp
def validate_provider_credentials( def validate_provider_credentials(
self, tenant_id: str, user_id: str, provider: str, credentials: dict[str, Any] self, tenant_id: str, user_id: str, provider: str, credentials: dict[str, Any]

View File

@ -139,13 +139,17 @@ class AnalyticdbVectorBySql:
) )
if embedding_dimension is not None: if embedding_dimension is not None:
index_name = f"{self._collection_name}_embedding_idx" index_name = f"{self._collection_name}_embedding_idx"
cur.execute(f"ALTER TABLE {self.table_name} ALTER COLUMN vector SET STORAGE PLAIN") try:
cur.execute( cur.execute(f"ALTER TABLE {self.table_name} ALTER COLUMN vector SET STORAGE PLAIN")
f"CREATE INDEX {index_name} ON {self.table_name} USING ann(vector) " cur.execute(
f"WITH(dim='{embedding_dimension}', distancemeasure='{self.config.metrics}', " f"CREATE INDEX {index_name} ON {self.table_name} USING ann(vector) "
f"pq_enable=0, external_storage=0)" f"WITH(dim='{embedding_dimension}', distancemeasure='{self.config.metrics}', "
) f"pq_enable=0, external_storage=0)"
cur.execute(f"CREATE INDEX ON {self.table_name} USING gin(to_tsvector)") )
cur.execute(f"CREATE INDEX ON {self.table_name} USING gin(to_tsvector)")
except Exception as e:
if "already exists" not in str(e):
raise e
redis_client.set(collection_exist_cache_key, 1, ex=3600) redis_client.set(collection_exist_cache_key, 1, ex=3600)
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs): def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
@ -177,9 +181,11 @@ class AnalyticdbVectorBySql:
return cur.fetchone() is not None return cur.fetchone() is not None
def delete_by_ids(self, ids: list[str]) -> None: def delete_by_ids(self, ids: list[str]) -> None:
if not ids:
return
with self._get_cursor() as cur: with self._get_cursor() as cur:
try: try:
cur.execute(f"DELETE FROM {self.table_name} WHERE ref_doc_id IN %s", (tuple(ids),)) cur.execute(f"DELETE FROM {self.table_name} WHERE ref_doc_id = ANY(%s)", (ids,))
except Exception as e: except Exception as e:
if "does not exist" not in str(e): if "does not exist" not in str(e):
raise e raise e
@ -240,7 +246,7 @@ class AnalyticdbVectorBySql:
ts_rank(to_tsvector, to_tsquery_from_text(%s, 'zh_cn'), 32) AS score ts_rank(to_tsvector, to_tsquery_from_text(%s, 'zh_cn'), 32) AS score
FROM {self.table_name} FROM {self.table_name}
WHERE to_tsvector@@to_tsquery_from_text(%s, 'zh_cn') {where_clause} WHERE to_tsvector@@to_tsquery_from_text(%s, 'zh_cn') {where_clause}
ORDER BY score DESC ORDER BY (score,id) DESC
LIMIT {top_k}""", LIMIT {top_k}""",
(f"'{query}'", f"'{query}'"), (f"'{query}'", f"'{query}'"),
) )

View File

@ -39,6 +39,12 @@ class EnhanceRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter):
else: else:
return [GPT2Tokenizer.get_num_tokens(text) for text in texts] return [GPT2Tokenizer.get_num_tokens(text) for text in texts]
def _character_encoder(texts: list[str]) -> list[int]:
if not texts:
return []
return [len(text) for text in texts]
if issubclass(cls, TokenTextSplitter): if issubclass(cls, TokenTextSplitter):
extra_kwargs = { extra_kwargs = {
"model_name": embedding_model_instance.model if embedding_model_instance else "gpt2", "model_name": embedding_model_instance.model if embedding_model_instance else "gpt2",
@ -47,7 +53,7 @@ class EnhanceRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter):
} }
kwargs = {**kwargs, **extra_kwargs} kwargs = {**kwargs, **extra_kwargs}
return cls(length_function=_token_encoder, **kwargs) return cls(length_function=_character_encoder, **kwargs)
class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter): class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter):
@ -103,7 +109,7 @@ class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter)
_good_splits_lengths = [] # cache the lengths of the splits _good_splits_lengths = [] # cache the lengths of the splits
_separator = "" if self._keep_separator else separator _separator = "" if self._keep_separator else separator
s_lens = self._length_function(splits) s_lens = self._length_function(splits)
if _separator != "": if separator != "":
for s, s_len in zip(splits, s_lens): for s, s_len in zip(splits, s_lens):
if s_len < self._chunk_size: if s_len < self._chunk_size:
_good_splits.append(s) _good_splits.append(s)

View File

@ -120,6 +120,13 @@ class ToolInvokeMessage(BaseModel):
class BlobMessage(BaseModel): class BlobMessage(BaseModel):
blob: bytes blob: bytes
class BlobChunkMessage(BaseModel):
id: str = Field(..., description="The id of the blob")
sequence: int = Field(..., description="The sequence of the chunk")
total_length: int = Field(..., description="The total length of the blob")
blob: bytes = Field(..., description="The blob data of the chunk")
end: bool = Field(..., description="Whether the chunk is the last chunk")
class FileMessage(BaseModel): class FileMessage(BaseModel):
pass pass
@ -180,12 +187,15 @@ class ToolInvokeMessage(BaseModel):
VARIABLE = "variable" VARIABLE = "variable"
FILE = "file" FILE = "file"
LOG = "log" LOG = "log"
BLOB_CHUNK = "blob_chunk"
type: MessageType = MessageType.TEXT type: MessageType = MessageType.TEXT
""" """
plain text, image url or link url plain text, image url or link url
""" """
message: JsonMessage | TextMessage | BlobMessage | LogMessage | FileMessage | None | VariableMessage message: (
JsonMessage | TextMessage | BlobChunkMessage | BlobMessage | LogMessage | FileMessage | None | VariableMessage
)
meta: dict[str, Any] | None = None meta: dict[str, Any] | None = None
@field_validator("message", mode="before") @field_validator("message", mode="before")

View File

@ -90,6 +90,7 @@ class HttpRequestNodeData(BaseNodeData):
params: str params: str
body: Optional[HttpRequestNodeBody] = None body: Optional[HttpRequestNodeBody] = None
timeout: Optional[HttpRequestNodeTimeout] = None timeout: Optional[HttpRequestNodeTimeout] = None
ssl_verify: Optional[bool] = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY
class Response: class Response:

View File

@ -1,3 +1,4 @@
import base64
import json import json
from collections.abc import Mapping from collections.abc import Mapping
from copy import deepcopy from copy import deepcopy
@ -87,6 +88,7 @@ class Executor:
self.method = node_data.method self.method = node_data.method
self.auth = node_data.authorization self.auth = node_data.authorization
self.timeout = timeout self.timeout = timeout
self.ssl_verify = node_data.ssl_verify
self.params = [] self.params = []
self.headers = {} self.headers = {}
self.content = None self.content = None
@ -259,7 +261,9 @@ class Executor:
if self.auth.config.type == "bearer": if self.auth.config.type == "bearer":
headers[authorization.config.header] = f"Bearer {authorization.config.api_key}" headers[authorization.config.header] = f"Bearer {authorization.config.api_key}"
elif self.auth.config.type == "basic": elif self.auth.config.type == "basic":
headers[authorization.config.header] = f"Basic {authorization.config.api_key}" credentials = authorization.config.api_key
encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8")
headers[authorization.config.header] = f"Basic {encoded_credentials}"
elif self.auth.config.type == "custom": elif self.auth.config.type == "custom":
headers[authorization.config.header] = authorization.config.api_key or "" headers[authorization.config.header] = authorization.config.api_key or ""
@ -313,6 +317,7 @@ class Executor:
"headers": headers, "headers": headers,
"params": self.params, "params": self.params,
"timeout": (self.timeout.connect, self.timeout.read, self.timeout.write), "timeout": (self.timeout.connect, self.timeout.read, self.timeout.write),
"ssl_verify": self.ssl_verify,
"follow_redirects": True, "follow_redirects": True,
"max_retries": self.max_retries, "max_retries": self.max_retries,
} }

View File

@ -51,6 +51,7 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]):
"max_read_timeout": dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT, "max_read_timeout": dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
"max_write_timeout": dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT, "max_write_timeout": dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
}, },
"ssl_verify": dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
}, },
"retry_config": { "retry_config": {
"max_retries": dify_config.SSRF_DEFAULT_MAX_RETRIES, "max_retries": dify_config.SSRF_DEFAULT_MAX_RETRIES,

View File

@ -149,7 +149,10 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]):
def _extract_slice( def _extract_slice(
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment] self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]: ) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]:
value = int(self.graph_runtime_state.variable_pool.convert_template(self.node_data.extract_by.serial).text) - 1 value = int(self.graph_runtime_state.variable_pool.convert_template(self.node_data.extract_by.serial).text)
if value < 1:
raise ValueError(f"Invalid serial index: must be >= 1, got {value}")
value -= 1
if len(variable.value) > int(value): if len(variable.value) > int(value):
result = variable.value[value] result = variable.value[value]
else: else:

View File

@ -1,16 +1,20 @@
import atexit import atexit
import logging
import os import os
import platform import platform
import socket import socket
import sys
from typing import Union from typing import Union
from celery.signals import worker_init # type: ignore
from flask_login import user_loaded_from_request, user_logged_in # type: ignore from flask_login import user_loaded_from_request, user_logged_in # type: ignore
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.metrics import set_meter_provider from opentelemetry.metrics import get_meter_provider, set_meter_provider
from opentelemetry.propagate import set_global_textmap from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3Format from opentelemetry.propagators.b3 import B3Format
from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.propagators.composite import CompositePropagator
@ -24,7 +28,7 @@ from opentelemetry.sdk.trace.export import (
) )
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import Span, get_current_span, set_tracer_provider from opentelemetry.trace import Span, get_current_span, get_tracer_provider, set_tracer_provider
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.trace.status import StatusCode from opentelemetry.trace.status import StatusCode
@ -96,22 +100,37 @@ def init_app(app: DifyApp):
export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT, export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT,
) )
set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader])) set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader]))
if not is_celery_worker():
def response_hook(span: Span, status: str, response_headers: list): init_flask_instrumentor(app)
if span and span.is_recording(): CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument()
if status.startswith("2"): init_sqlalchemy_instrumentor(app)
span.set_status(StatusCode.OK)
else:
span.set_status(StatusCode.ERROR, status)
instrumentor = FlaskInstrumentor()
instrumentor.instrument_app(app, response_hook=response_hook)
with app.app_context():
engines = list(app.extensions["sqlalchemy"].engines.values())
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
atexit.register(shutdown_tracer) atexit.register(shutdown_tracer)
def is_celery_worker():
return "celery" in sys.argv[0].lower()
def init_flask_instrumentor(app: DifyApp):
def response_hook(span: Span, status: str, response_headers: list):
if span and span.is_recording():
if status.startswith("2"):
span.set_status(StatusCode.OK)
else:
span.set_status(StatusCode.ERROR, status)
instrumentor = FlaskInstrumentor()
if dify_config.DEBUG:
logging.info("Initializing Flask instrumentor")
instrumentor.instrument_app(app, response_hook=response_hook)
def init_sqlalchemy_instrumentor(app: DifyApp):
with app.app_context():
engines = list(app.extensions["sqlalchemy"].engines.values())
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
def setup_context_propagation(): def setup_context_propagation():
# Configure propagators # Configure propagators
set_global_textmap( set_global_textmap(
@ -124,6 +143,15 @@ def setup_context_propagation():
) )
@worker_init.connect(weak=False)
def init_celery_worker(*args, **kwargs):
tracer_provider = get_tracer_provider()
metric_provider = get_meter_provider()
if dify_config.DEBUG:
logging.info("Initializing OpenTelemetry for Celery worker")
CeleryInstrumentor(tracer_provider=tracer_provider, meter_provider=metric_provider).instrument()
def shutdown_tracer(): def shutdown_tracer():
provider = trace.get_tracer_provider() provider = trace.get_tracer_provider()
if hasattr(provider, "force_flush"): if hasattr(provider, "force_flush"):

View File

@ -42,6 +42,7 @@ message_file_fields = {
"size": fields.Integer, "size": fields.Integer,
"transfer_method": fields.String, "transfer_method": fields.String,
"belongs_to": fields.String(default="user"), "belongs_to": fields.String(default="user"),
"upload_file_id": fields.String(default=None),
} }
agent_thought_fields = { agent_thought_fields = {

View File

@ -1155,7 +1155,7 @@ class Message(db.Model): # type: ignore[name-defined]
files.append(file) files.append(file)
result = [ result = [
{"belongs_to": message_file.belongs_to, **file.to_dict()} {"belongs_to": message_file.belongs_to, "upload_file_id": message_file.upload_file_id, **file.to_dict()}
for (file, message_file) in zip(files, message_files) for (file, message_file) in zip(files, message_files)
] ]

10605
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +0,0 @@
[virtualenvs]
in-project = true
create = true
prefer-active-python = true

View File

@ -1,225 +1,194 @@
[project] [project]
name = "dify-api" name = "dify-api"
version = "1.2.0"
requires-python = ">=3.11,<3.13" requires-python = ">=3.11,<3.13"
dynamic = ["dependencies"]
[build-system] dependencies = [
requires = ["poetry-core>=2.0.0"] "authlib==1.3.1",
build-backend = "poetry.core.masonry.api" "azure-identity==1.16.1",
"beautifulsoup4==4.12.2",
"boto3==1.35.99",
"bs4~=0.0.1",
"cachetools~=5.3.0",
"celery~=5.4.0",
"chardet~=5.1.0",
"flask~=3.1.0",
"flask-compress~=1.17",
"flask-cors~=4.0.0",
"flask-login~=0.6.3",
"flask-migrate~=4.0.7",
"flask-restful~=0.3.10",
"flask-sqlalchemy~=3.1.1",
"gevent~=24.11.1",
"gmpy2~=2.2.1",
"google-api-core==2.18.0",
"google-api-python-client==2.90.0",
"google-auth==2.29.0",
"google-auth-httplib2==0.2.0",
"google-cloud-aiplatform==1.49.0",
"googleapis-common-protos==1.63.0",
"gunicorn~=23.0.0",
"httpx[socks]~=0.27.0",
"jieba==0.42.1",
"langfuse~=2.51.3",
"langsmith~=0.1.77",
"mailchimp-transactional~=1.0.50",
"markdown~=3.5.1",
"numpy~=1.26.4",
"oci~=2.135.1",
"openai~=1.61.0",
"openpyxl~=3.1.5",
"opik~=1.3.4",
"opentelemetry-api==1.27.0",
"opentelemetry-distro==0.48b0",
"opentelemetry-exporter-otlp==1.27.0",
"opentelemetry-exporter-otlp-proto-common==1.27.0",
"opentelemetry-exporter-otlp-proto-grpc==1.27.0",
"opentelemetry-exporter-otlp-proto-http==1.27.0",
"opentelemetry-instrumentation==0.48b0",
"opentelemetry-instrumentation-celery==0.48b0",
"opentelemetry-instrumentation-flask==0.48b0",
"opentelemetry-instrumentation-sqlalchemy==0.48b0",
"opentelemetry-propagator-b3==1.27.0",
# opentelemetry-proto1.28.0 depends on protobuf (>=5.0,<6.0),
# which is conflict with googleapis-common-protos (1.63.0)
"opentelemetry-proto==1.27.0",
"opentelemetry-sdk==1.27.0",
"opentelemetry-semantic-conventions==0.48b0",
"opentelemetry-util-http==0.48b0",
"pandas-stubs~=2.2.3.241009",
"pandas[excel,output-formatting,performance]~=2.2.2",
"pandoc~=2.4",
"psycogreen~=1.0.2",
"psycopg2-binary~=2.9.6",
"pycryptodome==3.19.1",
"pydantic~=2.9.2",
"pydantic-extra-types~=2.9.0",
"pydantic-settings~=2.6.0",
"pyjwt~=2.8.0",
"pypdfium2~=4.30.0",
"python-docx~=1.1.0",
"python-dotenv==1.0.1",
"pyyaml~=6.0.1",
"readabilipy==0.2.0",
"redis[hiredis]~=5.0.3",
"resend~=0.7.0",
"sentry-sdk[flask]~=1.44.1",
"sqlalchemy~=2.0.29",
"starlette==0.41.0",
"tiktoken~=0.8.0",
"tokenizers~=0.15.0",
"transformers~=4.35.0",
"unstructured[docx,epub,md,ppt,pptx]~=0.16.1",
"validators==0.21.0",
"yarl~=1.18.3",
]
# Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group.
[tool.poetry] [tool.uv]
package-mode = false default-groups = ["storage", "tools", "vdb"]
############################################################ [dependency-groups]
# [ Main ] Dependency group
############################################################
[tool.poetry.dependencies]
authlib = "1.3.1"
azure-identity = "1.16.1"
beautifulsoup4 = "4.12.2"
boto3 = "1.35.99"
bs4 = "~0.0.1"
cachetools = "~5.3.0"
celery = "~5.4.0"
chardet = "~5.1.0"
flask = "~3.1.0"
flask-compress = "~1.17"
flask-cors = "~4.0.0"
flask-login = "~0.6.3"
flask-migrate = "~4.0.7"
flask-restful = "~0.3.10"
flask-sqlalchemy = "~3.1.1"
gevent = "~24.11.1"
gmpy2 = "~2.2.1"
google-api-core = "2.18.0"
google-api-python-client = "2.90.0"
google-auth = "2.29.0"
google-auth-httplib2 = "0.2.0"
google-cloud-aiplatform = "1.49.0"
googleapis-common-protos = "1.63.0"
gunicorn = "~23.0.0"
httpx = { version = "~0.27.0", extras = ["socks"] }
jieba = "0.42.1"
json-repair = "~0.40.0"
langfuse = "~2.51.3"
langsmith = "~0.1.77"
mailchimp-transactional = "~1.0.50"
markdown = "~3.5.1"
numpy = "~1.26.4"
oci = "~2.135.1"
openai = "~1.61.0"
openpyxl = "~3.1.5"
opentelemetry-api = "1.27.0"
opentelemetry-distro = "0.48b0"
opentelemetry-exporter-otlp = "1.27.0"
opentelemetry-exporter-otlp-proto-common = "1.27.0"
opentelemetry-exporter-otlp-proto-grpc = "1.27.0"
opentelemetry-exporter-otlp-proto-http = "1.27.0"
opentelemetry-instrumentation = "0.48b0"
opentelemetry-instrumentation-flask = "0.48b0"
opentelemetry-instrumentation-sqlalchemy = "0.48b0"
opentelemetry-propagator-b3 = "1.27.0"
opentelemetry-proto = "1.27.0" # 1.28.0 depends on protobuf (>=5.0,<6.0), conflict with googleapis-common-protos (1.63.0)
opentelemetry-sdk = "1.27.0"
opentelemetry-semantic-conventions = "0.48b0"
opentelemetry-util-http = "0.48b0"
opik = "~1.3.4"
pandas = { version = "~2.2.2", extras = [
"performance",
"excel",
"output-formatting",
] }
pandas-stubs = "~2.2.3.241009"
pandoc = "~2.4"
psycogreen = "~1.0.2"
psycopg2-binary = "~2.9.6"
pycryptodome = "3.19.1"
pydantic = "~2.9.2"
pydantic-settings = "~2.6.0"
pydantic_extra_types = "~2.9.0"
pyjwt = "~2.8.0"
pypdfium2 = "~4.30.0"
python = ">=3.11,<3.13"
python-docx = "~1.1.0"
python-dotenv = "1.0.1"
pyyaml = "~6.0.1"
readabilipy = "0.2.0"
redis = { version = "~5.0.3", extras = ["hiredis"] }
resend = "~0.7.0"
sentry-sdk = { version = "~1.44.1", extras = ["flask"] }
sqlalchemy = "~2.0.29"
starlette = "0.41.0"
tiktoken = "~0.8.0"
tokenizers = "~0.15.0"
transformers = "~4.35.0"
unstructured = { version = "~0.16.1", extras = [
"docx",
"epub",
"md",
"ppt",
"pptx",
] }
validators = "0.21.0"
yarl = "~1.18.3"
# Before adding new dependency, consider place it in alphabet order (a-z) and suitable group.
############################################################
# [ Indirect ] dependency group
# Related transparent dependencies with pinned version
# required by main implementations
############################################################
[tool.poetry.group.indirect.dependencies]
kaleido = "0.2.1"
rank-bm25 = "~0.2.2"
safetensors = "~0.4.3"
############################################################
# [ Tools ] dependency group
############################################################
[tool.poetry.group.tools.dependencies]
cloudscraper = "1.2.71"
nltk = "3.9.1"
############################################################
# [ Storage ] dependency group
# Required for storage clients
############################################################
[tool.poetry.group.storage.dependencies]
azure-storage-blob = "12.13.0"
bce-python-sdk = "~0.9.23"
cos-python-sdk-v5 = "1.9.30"
esdk-obs-python = "3.24.6.1"
google-cloud-storage = "2.16.0"
opendal = "~0.45.16"
oss2 = "2.18.5"
supabase = "~2.8.1"
tos = "~2.7.1"
############################################################
# [ VDB ] dependency group
# Required by vector store clients
############################################################
[tool.poetry.group.vdb.dependencies]
alibabacloud_gpdb20160503 = "~3.8.0"
alibabacloud_tea_openapi = "~0.3.9"
chromadb = "0.5.20"
clickhouse-connect = "~0.7.16"
couchbase = "~4.3.0"
elasticsearch = "8.14.0"
opensearch-py = "2.4.0"
oracledb = "~2.2.1"
pgvecto-rs = { version = "~0.2.1", extras = ['sqlalchemy'] }
pgvector = "0.2.5"
pymilvus = "~2.5.0"
pymochow = "1.3.1"
pyobvector = "~0.1.6"
qdrant-client = "1.7.3"
tablestore = "6.1.0"
tcvectordb = "~1.6.4"
tidb-vector = "0.0.9"
upstash-vector = "0.6.0"
volcengine-compat = "~1.0.156"
weaviate-client = "~3.21.0"
xinference-client = "~1.2.2"
############################################################ ############################################################
# [ Dev ] dependency group # [ Dev ] dependency group
# Required for development and running tests # Required for development and running tests
############################################################ ############################################################
[tool.poetry.group.dev] dev = [
optional = true "coverage~=7.2.4",
[tool.poetry.group.dev.dependencies] "dotenv-linter~=0.5.0",
coverage = "~7.2.4" "faker~=32.1.0",
faker = "~32.1.0" "lxml-stubs~=0.5.1",
lxml-stubs = "~0.5.1" "mypy~=1.15.0",
mypy = "~1.15.0" "ruff~=0.11.5",
pytest = "~8.3.2" "pytest~=8.3.2",
pytest-benchmark = "~4.0.0" "pytest-benchmark~=4.0.0",
pytest-env = "~1.1.3" "pytest-cov~=4.1.0",
pytest-mock = "~3.14.0" "pytest-env~=1.1.3",
types-aiofiles = "~24.1.0" "pytest-mock~=3.14.0",
types-beautifulsoup4 = "~4.12.0" "types-aiofiles~=24.1.0",
types-cachetools = "~5.5.0" "types-beautifulsoup4~=4.12.0",
types-colorama = "~0.4.15" "types-cachetools~=5.5.0",
types-defusedxml = "~0.7.0" "types-colorama~=0.4.15",
types-deprecated = "~1.2.15" "types-defusedxml~=0.7.0",
types-docutils = "~0.21.0" "types-deprecated~=1.2.15",
types-flask-cors = "~5.0.0" "types-docutils~=0.21.0",
types-flask-migrate = "~4.1.0" "types-flask-cors~=5.0.0",
types-gevent = "~24.11.0" "types-flask-migrate~=4.1.0",
types-greenlet = "~3.1.0" "types-gevent~=24.11.0",
types-html5lib = "~1.1.11" "types-greenlet~=3.1.0",
types-markdown = "~3.7.0" "types-html5lib~=1.1.11",
types-oauthlib = "~3.2.0" "types-markdown~=3.7.0",
types-objgraph = "~3.6.0" "types-oauthlib~=3.2.0",
types-olefile = "~0.47.0" "types-objgraph~=3.6.0",
types-openpyxl = "~3.1.5" "types-olefile~=0.47.0",
types-pexpect = "~4.9.0" "types-openpyxl~=3.1.5",
types-protobuf = "~5.29.1" "types-pexpect~=4.9.0",
types-psutil = "~7.0.0" "types-protobuf~=5.29.1",
types-psycopg2 = "~2.9.21" "types-psutil~=7.0.0",
types-pygments = "~2.19.0" "types-psycopg2~=2.9.21",
types-pymysql = "~1.1.0" "types-pygments~=2.19.0",
types-python-dateutil = "~2.9.0" "types-pymysql~=1.1.0",
types-pywin32 = "~310.0.0" "types-python-dateutil~=2.9.0",
types-pyyaml = "~6.0.12" "types-pywin32~=310.0.0",
types-regex = "~2024.11.6" "types-pyyaml~=6.0.12",
types-requests = "~2.32.0" "types-regex~=2024.11.6",
types-requests-oauthlib = "~2.0.0" "types-requests~=2.32.0",
types-shapely = "~2.0.0" "types-requests-oauthlib~=2.0.0",
types-simplejson = "~3.20.0" "types-shapely~=2.0.0",
types-six = "~1.17.0" "types-simplejson~=3.20.0",
types-tensorflow = "~2.18.0" "types-six~=1.17.0",
types-tqdm = "~4.67.0" "types-tensorflow~=2.18.0",
types-ujson = "~5.10.0" "types-tqdm~=4.67.0",
"types-ujson~=5.10.0",
]
############################################################ ############################################################
# [ Lint ] dependency group # [ Storage ] dependency group
# Required for code style linting # Required for storage clients
############################################################ ############################################################
[tool.poetry.group.lint] storage = [
optional = true "azure-storage-blob==12.13.0",
[tool.poetry.group.lint.dependencies] "bce-python-sdk~=0.9.23",
dotenv-linter = "~0.5.0" "cos-python-sdk-v5==1.9.30",
ruff = "~0.11.0" "esdk-obs-python==3.24.6.1",
"google-cloud-storage==2.16.0",
"opendal~=0.45.16",
"oss2==2.18.5",
"supabase~=2.8.1",
"tos~=2.7.1",
]
############################################################
# [ Tools ] dependency group
############################################################
tools = ["cloudscraper~=1.2.71", "nltk~=3.9.1"]
############################################################
# [ VDB ] dependency group
# Required by vector store clients
############################################################
vdb = [
"alibabacloud_gpdb20160503~=3.8.0",
"alibabacloud_tea_openapi~=0.3.9",
"chromadb==0.5.20",
"clickhouse-connect~=0.7.16",
"couchbase~=4.3.0",
"elasticsearch==8.14.0",
"opensearch-py==2.4.0",
"oracledb~=2.2.1",
"pgvecto-rs[sqlalchemy]~=0.2.1",
"pgvector==0.2.5",
"pymilvus~=2.5.0",
"pymochow==1.3.1",
"pyobvector~=0.1.6",
"qdrant-client==1.7.3",
"tablestore==6.1.0",
"tcvectordb~=1.6.4",
"tidb-vector==0.0.9",
"upstash-vector==0.6.0",
"volcengine-compat~=1.0.156",
"weaviate-client~=3.21.0",
"xinference-client~=1.2.2",
]

View File

@ -1,5 +1,6 @@
[pytest] [pytest]
continue-on-collection-errors = true continue-on-collection-errors = true
addopts = --cov=./api --cov-report=json --cov-report=xml
env = env =
ANTHROPIC_API_KEY = sk-ant-api11-IamNotARealKeyJustForMockTestKawaiiiiiiiiii-NotBaka-ASkksz ANTHROPIC_API_KEY = sk-ant-api11-IamNotARealKeyJustForMockTestKawaiiiiiiiiii-NotBaka-ASkksz
AZURE_OPENAI_API_BASE = https://difyai-openai.openai.azure.com AZURE_OPENAI_API_BASE = https://difyai-openai.openai.azure.com

View File

@ -553,7 +553,7 @@ class DocumentService:
{"id": "remove_extra_spaces", "enabled": True}, {"id": "remove_extra_spaces", "enabled": True},
{"id": "remove_urls_emails", "enabled": False}, {"id": "remove_urls_emails", "enabled": False},
], ],
"segmentation": {"delimiter": "\n", "max_tokens": 500, "chunk_overlap": 50}, "segmentation": {"delimiter": "\n", "max_tokens": 1024, "chunk_overlap": 50},
}, },
"limits": { "limits": {
"indexing_max_segmentation_tokens_length": dify_config.INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH, "indexing_max_segmentation_tokens_length": dify_config.INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH,
@ -2175,7 +2175,13 @@ class SegmentService:
@classmethod @classmethod
def get_segments( def get_segments(
cls, document_id: str, tenant_id: str, status_list: list[str] | None = None, keyword: str | None = None cls,
document_id: str,
tenant_id: str,
status_list: list[str] | None = None,
keyword: str | None = None,
page: int = 1,
limit: int = 20,
): ):
"""Get segments for a document with optional filtering.""" """Get segments for a document with optional filtering."""
query = DocumentSegment.query.filter( query = DocumentSegment.query.filter(
@ -2188,10 +2194,11 @@ class SegmentService:
if keyword: if keyword:
query = query.filter(DocumentSegment.content.ilike(f"%{keyword}%")) query = query.filter(DocumentSegment.content.ilike(f"%{keyword}%"))
segments = query.order_by(DocumentSegment.position.asc()).all() paginated_segments = query.order_by(DocumentSegment.position.asc()).paginate(
total = len(segments) page=page, per_page=limit, max_per_page=100, error_out=False
)
return segments, total return paginated_segments.items, paginated_segments.total
@classmethod @classmethod
def update_segment_by_id( def update_segment_by_id(

View File

@ -1,49 +0,0 @@
from typing import Any
import toml # type: ignore
def load_api_poetry_configs() -> dict[str, Any]:
pyproject_toml = toml.load("api/pyproject.toml")
return pyproject_toml["tool"]["poetry"]
def load_all_dependency_groups() -> dict[str, dict[str, dict[str, Any]]]:
configs = load_api_poetry_configs()
configs_by_group = {"main": configs}
for group_name in configs["group"]:
configs_by_group[group_name] = configs["group"][group_name]
dependencies_by_group = {group_name: base["dependencies"] for group_name, base in configs_by_group.items()}
return dependencies_by_group
def test_group_dependencies_sorted():
for group_name, dependencies in load_all_dependency_groups().items():
dependency_names = list(dependencies.keys())
expected_dependency_names = sorted(set(dependency_names))
section = f"tool.poetry.group.{group_name}.dependencies" if group_name else "tool.poetry.dependencies"
assert expected_dependency_names == dependency_names, (
f"Dependencies in group {group_name} are not sorted. "
f"Check and fix [{section}] section in pyproject.toml file"
)
def test_group_dependencies_version_operator():
for group_name, dependencies in load_all_dependency_groups().items():
for dependency_name, specification in dependencies.items():
version_spec = specification if isinstance(specification, str) else specification["version"]
assert not version_spec.startswith("^"), (
f"Please replace '{dependency_name} = {version_spec}' with '{dependency_name} = ~{version_spec[1:]}' "
f"'^' operator is too wide and not allowed in the version specification."
)
def test_duplicated_dependency_crossing_groups() -> None:
all_dependency_names: list[str] = []
for dependencies in load_all_dependency_groups().values():
dependency_names = list(dependencies.keys())
all_dependency_names.extend(dependency_names)
expected_all_dependency_names = set(all_dependency_names)
assert sorted(expected_all_dependency_names) == sorted(all_dependency_names), (
"Duplicated dependencies crossing groups are found"
)

6286
api/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

7
dev/mypy-check Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
set -x
# run mypy checks
uv run --directory api --dev \
python -m mypy --install-types --non-interactive .

View File

@ -2,20 +2,14 @@
set -x set -x
# style checks rely on commands in path
if ! command -v ruff &> /dev/null || ! command -v dotenv-linter &> /dev/null; then
echo "Installing linting tools (Ruff, dotenv-linter ...) ..."
poetry install -C api --only lint
fi
# run ruff linter # run ruff linter
poetry run -C api ruff check --fix ./ uv run --directory api --dev ruff check --fix ./
# run ruff formatter # run ruff formatter
poetry run -C api ruff format ./ uv run --directory api --dev ruff format ./
# run dotenv-linter linter # run dotenv-linter linter
poetry run -P api dotenv-linter ./api/.env.example ./web/.env.example uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example
# run mypy check # run mypy check
dev/run-mypy dev/mypy-check

View File

@ -1,11 +0,0 @@
#!/bin/bash
set -x
if ! command -v mypy &> /dev/null; then
poetry install -C api --with dev
fi
# run mypy checks
poetry run -C api \
python -m mypy --install-types --non-interactive .

View File

@ -1,18 +0,0 @@
#!/bin/bash
# rely on `poetry` in path
if ! command -v poetry &> /dev/null; then
echo "Installing Poetry ..."
pip install poetry
fi
# check poetry.lock in sync with pyproject.toml
poetry check -C api --lock
if [ $? -ne 0 ]; then
# update poetry.lock
# refreshing lockfile only without updating locked versions
echo "poetry.lock is outdated, refreshing without updating locked versions ..."
poetry lock -C api
else
echo "poetry.lock is ready."
fi

10
dev/sync-uv Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# rely on `uv` in path
if ! command -v uv &> /dev/null; then
echo "Installing uv ..."
pip install uv
fi
# check uv.lock in sync with pyproject.toml
uv lock --project api

View File

@ -1,13 +0,0 @@
#!/bin/bash
# rely on `poetry` in path
if ! command -v poetry &> /dev/null; then
echo "Installing Poetry ..."
pip install poetry
fi
# refreshing lockfile, updating locked versions
poetry update -C api
# check poetry.lock in sync with pyproject.toml
poetry check -C api --lock

22
dev/update-uv Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
# Update dependencies in dify/api project using uv
set -e
set -o pipefail
SCRIPT_DIR="$(dirname "$0")"
REPO_ROOT="$(dirname "${SCRIPT_DIR}")"
# rely on `poetry` in path
if ! command -v uv &> /dev/null; then
echo "Installing uv ..."
pip install uv
fi
cd "${REPO_ROOT}"
# refreshing lockfile, updating locked versions
uv lock --project api --upgrade
# check uv.lock in sync with pyproject.toml
uv lock --project api --check

View File

@ -174,6 +174,12 @@ CELERY_MIN_WORKERS=
API_TOOL_DEFAULT_CONNECT_TIMEOUT=10 API_TOOL_DEFAULT_CONNECT_TIMEOUT=10
API_TOOL_DEFAULT_READ_TIMEOUT=60 API_TOOL_DEFAULT_READ_TIMEOUT=60
# -------------------------------
# Datasource Configuration
# --------------------------------
ENABLE_WEBSITE_JINAREADER=true
ENABLE_WEBSITE_FIRECRAWL=true
ENABLE_WEBSITE_WATERCRAWL=true
# ------------------------------ # ------------------------------
# Database Configuration # Database Configuration

View File

@ -17,8 +17,10 @@ services:
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800} PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1} INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
depends_on: depends_on:
- db db:
- redis condition: service_healthy
redis:
condition: service_started
volumes: volumes:
# Mount the storage directory to the container, for storing user files. # Mount the storage directory to the container, for storing user files.
- ./volumes/app/storage:/app/api/storage - ./volumes/app/storage:/app/api/storage
@ -42,8 +44,10 @@ services:
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800} PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1} INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
depends_on: depends_on:
- db db:
- redis condition: service_healthy
redis:
condition: service_started
volumes: volumes:
# Mount the storage directory to the container, for storing user files. # Mount the storage directory to the container, for storing user files.
- ./volumes/app/storage:/app/api/storage - ./volumes/app/storage:/app/api/storage
@ -71,7 +75,9 @@ services:
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5}
ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true}
ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true}
ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true}
# The postgres database. # The postgres database.
db: db:
image: postgres:15-alpine image: postgres:15-alpine
@ -163,7 +169,7 @@ services:
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-}
PAWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-} AWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-}
AWS_REGION: ${PLUGIN_AWS_REGION:-} AWS_REGION: ${PLUGIN_AWS_REGION:-}
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-} AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-} AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}

View File

@ -107,7 +107,7 @@ services:
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-}
PAWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-} AWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-}
AWS_REGION: ${PLUGIN_AWS_REGION:-} AWS_REGION: ${PLUGIN_AWS_REGION:-}
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-} AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-} AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}

View File

@ -43,6 +43,9 @@ x-shared-env: &shared-api-worker-env
CELERY_MIN_WORKERS: ${CELERY_MIN_WORKERS:-} CELERY_MIN_WORKERS: ${CELERY_MIN_WORKERS:-}
API_TOOL_DEFAULT_CONNECT_TIMEOUT: ${API_TOOL_DEFAULT_CONNECT_TIMEOUT:-10} API_TOOL_DEFAULT_CONNECT_TIMEOUT: ${API_TOOL_DEFAULT_CONNECT_TIMEOUT:-10}
API_TOOL_DEFAULT_READ_TIMEOUT: ${API_TOOL_DEFAULT_READ_TIMEOUT:-60} API_TOOL_DEFAULT_READ_TIMEOUT: ${API_TOOL_DEFAULT_READ_TIMEOUT:-60}
ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true}
ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true}
ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true}
DB_USERNAME: ${DB_USERNAME:-postgres} DB_USERNAME: ${DB_USERNAME:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-difyai123456} DB_PASSWORD: ${DB_PASSWORD:-difyai123456}
DB_HOST: ${DB_HOST:-db} DB_HOST: ${DB_HOST:-db}
@ -486,8 +489,10 @@ services:
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800} PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1} INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
depends_on: depends_on:
- db db:
- redis condition: service_healthy
redis:
condition: service_started
volumes: volumes:
# Mount the storage directory to the container, for storing user files. # Mount the storage directory to the container, for storing user files.
- ./volumes/app/storage:/app/api/storage - ./volumes/app/storage:/app/api/storage
@ -511,8 +516,10 @@ services:
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800} PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1} INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
depends_on: depends_on:
- db db:
- redis condition: service_healthy
redis:
condition: service_started
volumes: volumes:
# Mount the storage directory to the container, for storing user files. # Mount the storage directory to the container, for storing user files.
- ./volumes/app/storage:/app/api/storage - ./volumes/app/storage:/app/api/storage
@ -540,7 +547,9 @@ services:
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5}
ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true}
ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true}
ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true}
# The postgres database. # The postgres database.
db: db:
image: postgres:15-alpine image: postgres:15-alpine
@ -632,7 +641,7 @@ services:
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-}
PAWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-} AWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-}
AWS_REGION: ${PLUGIN_AWS_REGION:-} AWS_REGION: ${PLUGIN_AWS_REGION:-}
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-} AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-} AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}

View File

@ -49,3 +49,8 @@ NEXT_PUBLIC_MAX_PARALLEL_LIMIT=10
# The maximum number of iterations for agent setting # The maximum number of iterations for agent setting
NEXT_PUBLIC_MAX_ITERATIONS_NUM=5 NEXT_PUBLIC_MAX_ITERATIONS_NUM=5
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER=true
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL=true
NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL=true

View File

@ -27,17 +27,11 @@ done
if $api_modified; then if $api_modified; then
echo "Running Ruff linter on api module" echo "Running Ruff linter on api module"
# python style checks rely on `ruff` in path
if ! command -v ruff > /dev/null 2>&1; then
echo "Installing linting tools (Ruff, dotenv-linter ...) ..."
poetry install -C api --only lint
fi
# run Ruff linter auto-fixing # run Ruff linter auto-fixing
ruff check --fix ./api uv run --project api --dev ruff check --fix ./api
# run Ruff linter checks # run Ruff linter checks
ruff check ./api || status=$? uv run --project api --dev ruff check ./api || status=$?
status=${status:-0} status=${status:-0}

View File

@ -16,6 +16,7 @@ import AppsContext, { useAppContext } from '@/context/app-context'
import type { HtmlContentProps } from '@/app/components/base/popover' import type { HtmlContentProps } from '@/app/components/base/popover'
import CustomPopover from '@/app/components/base/popover' import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import { basePath } from '@/utils/var'
import { getRedirection } from '@/utils/app-redirection' import { getRedirection } from '@/utils/app-redirection'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
@ -216,7 +217,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
try { try {
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {} const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
if (installed_apps?.length > 0) if (installed_apps?.length > 0)
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank') window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank')
else else
throw new Error('No app found in Explore') throw new Error('No app found in Explore')
} }

View File

@ -11,7 +11,7 @@ import { useQuery } from '@tanstack/react-query'
import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel' import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel'
import Datasets from './Datasets' import Datasets from './Datasets'
import DatasetFooter from './DatasetFooter' import DatasetFooter from './DatasetFooter'
import ApiServer from './ApiServer' import ApiServer from '../../components/develop/ApiServer'
import Doc from './Doc' import Doc from './Doc'
import TabSliderNew from '@/app/components/base/tab-slider-new' import TabSliderNew from '@/app/components/base/tab-slider-new'
import TagManagementModal from '@/app/components/base/tag-management' import TagManagementModal from '@/app/components/base/tag-management'

View File

@ -9,6 +9,9 @@ import TemplateZh from './template/template.zh.mdx'
import TemplateJa from './template/template.ja.mdx' import TemplateJa from './template/template.ja.mdx'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language' import { LanguagesSupported } from '@/i18n/language'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'
type DocProps = { type DocProps = {
apiBaseUrl: string apiBaseUrl: string
@ -19,6 +22,7 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [toc, setToc] = useState<Array<{ href: string; text: string }>>([]) const [toc, setToc] = useState<Array<{ href: string; text: string }>>([])
const [isTocExpanded, setIsTocExpanded] = useState(false) const [isTocExpanded, setIsTocExpanded] = useState(false)
const { theme } = useTheme()
// Set initial TOC expanded state based on screen width // Set initial TOC expanded state based on screen width
useEffect(() => { useEffect(() => {
@ -83,12 +87,12 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
<div className={`fixed right-20 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}> <div className={`fixed right-20 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
{isTocExpanded {isTocExpanded
? ( ? (
<nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg bg-gray-50 p-4 shadow-md"> <nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg bg-components-panel-bg p-4 shadow-md">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">{t('appApi.develop.toc')}</h3> <h3 className="text-lg font-semibold text-text-primary">{t('appApi.develop.toc')}</h3>
<button <button
onClick={() => setIsTocExpanded(false)} onClick={() => setIsTocExpanded(false)}
className="text-gray-500 hover:text-gray-700" className="text-text-tertiary hover:text-text-secondary"
> >
</button> </button>
@ -98,7 +102,7 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
<li key={index}> <li key={index}>
<a <a
href={item.href} href={item.href}
className="text-gray-600 transition-colors duration-200 hover:text-gray-900 hover:underline" className="text-text-secondary transition-colors duration-200 hover:text-text-primary hover:underline"
onClick={e => handleTocClick(e, item)} onClick={e => handleTocClick(e, item)}
> >
{item.text} {item.text}
@ -111,13 +115,13 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
: ( : (
<button <button
onClick={() => setIsTocExpanded(true)} onClick={() => setIsTocExpanded(true)}
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 shadow-md transition-colors duration-200 hover:bg-gray-100" className="flex h-10 w-10 items-center justify-center rounded-full bg-components-button-secondary-bg shadow-md transition-colors duration-200 hover:bg-components-button-secondary-bg-hover"
> >
<RiListUnordered className="h-6 w-6" /> <RiListUnordered className="h-6 w-6 text-components-button-secondary-text" />
</button> </button>
)} )}
</div> </div>
<article className='prose-xl prose mx-1 rounded-t-xl bg-white px-4 pt-16 sm:mx-12'> <article className={cn('prose-xl prose mx-1 rounded-t-xl bg-background-default px-4 pt-16 sm:mx-12', theme === Theme.dark && 'dark:prose-invert')}>
{Template} {Template}
</article> </article>
</div> </div>

View File

@ -1,5 +1,6 @@
'use client' 'use client'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { basePath } from '@/utils/var'
import { import {
RiAddLine, RiAddLine,
RiArrowRightLine, RiArrowRightLine,
@ -17,7 +18,7 @@ const CreateAppCard = (
<div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px] <div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px]
border-components-panel-border transition-all duration-200 ease-in-out' border-components-panel-border transition-all duration-200 ease-in-out'
> >
<a ref={ref} className='group flex grow cursor-pointer items-start p-4' href='/datasets/create'> <a ref={ref} className='group flex grow cursor-pointer items-start p-4' href={`${basePath}/datasets/create`}>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter
p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge' p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge'
@ -28,7 +29,7 @@ const CreateAppCard = (
</div> </div>
</a> </a>
<div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div> <div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div>
<a className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href='/datasets/connect'> <a className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href={`${basePath}/datasets/connect`}>
<div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div> <div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div>
<RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' /> <RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' />
</a> </a>

View File

@ -24,6 +24,7 @@ import {
PortalToFollowElemContent, PortalToFollowElemContent,
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import { basePath } from '@/utils/var'
import { fetchInstalledAppList } from '@/service/explore' import { fetchInstalledAppList } from '@/service/explore'
import EmbeddedModal from '@/app/components/app/overview/embedded' import EmbeddedModal from '@/app/components/app/overview/embedded'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
@ -75,7 +76,7 @@ const AppPublisher = ({
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {} const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
const appURL = `${appBaseURL}/${appMode}/${accessToken}` const appURL = `${appBaseURL}/${basePath}/${appMode}/${accessToken}`
const isChatApp = ['chat', 'agent-chat', 'completion'].includes(appDetail?.mode || '') const isChatApp = ['chat', 'agent-chat', 'completion'].includes(appDetail?.mode || '')
const language = useGetLanguage() const language = useGetLanguage()
@ -120,7 +121,7 @@ const AppPublisher = ({
try { try {
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {} const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
if (installed_apps?.length > 0) if (installed_apps?.length > 0)
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank') window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank')
else else
throw new Error('No app found in Explore') throw new Error('No app found in Explore')
} }

View File

@ -21,6 +21,7 @@ import { useFeatures } from '@/app/components/base/features/hooks'
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils' import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
import type { InputForm } from '@/app/components/base/chat/chat/type' import type { InputForm } from '@/app/components/base/chat/chat/type'
import { canFindTool } from '@/utils' import { canFindTool } from '@/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
type DebugWithSingleModelProps = { type DebugWithSingleModelProps = {
checkCanSend?: () => boolean checkCanSend?: () => boolean
@ -125,10 +126,14 @@ const DebugWithSingleModel = (
) )
}, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList]) }, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => { const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)! const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(editedQuestion ? editedQuestion.message : question.content,
editedQuestion ? editedQuestion.files : question.message_files,
true,
isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
)
}, [chatList, doSend]) }, [chatList, doSend])
const allToolIcons = useMemo(() => { const allToolIcons = useMemo(() => {

View File

@ -2,6 +2,7 @@
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { basePath } from '@/utils/var'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
@ -503,6 +504,12 @@ const Configuration: FC = () => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const collectionList = await fetchCollectionList() const collectionList = await fetchCollectionList()
if (basePath) {
collectionList.forEach((item) => {
if (typeof item.icon == 'string' && !item.icon.includes(basePath))
item.icon = `${basePath}${item.icon}`
})
}
setCollectionList(collectionList) setCollectionList(collectionList)
fetchAppDetail({ url: '/apps', id: appId }).then(async (res: any) => { fetchAppDetail({ url: '/apps', id: appId }).then(async (res: any) => {
setMode(res.mode) setMode(res.mode)

View File

@ -14,6 +14,7 @@ import type { AppIconSelection } from '../../base/app-icon-picker'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
import AppsContext, { useAppContext } from '@/context/app-context' import AppsContext, { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
@ -352,11 +353,11 @@ function AppScreenShot({ mode, show }: { mode: AppMode; show: boolean }) {
'workflow': 'Workflow', 'workflow': 'Workflow',
} }
return <picture> return <picture>
<source media="(resolution: 1x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}.png`} /> <source media="(resolution: 1x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`} />
<source media="(resolution: 2x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}@2x.png`} /> <source media="(resolution: 2x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}@2x.png`} />
<source media="(resolution: 3x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}@3x.png`} /> <source media="(resolution: 3x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}@3x.png`} />
<Image className={show ? '' : 'hidden'} <Image className={show ? '' : 'hidden'}
src={`/screenshots/${theme}/${modeToImageMap[mode]}.png`} src={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`}
alt='App Screen Shot' alt='App Screen Shot'
width={664} height={448} /> width={664} height={448} />
</picture> </picture>

View File

@ -7,6 +7,7 @@ import { usePathname } from 'next/navigation'
import { useDebounce } from 'ahooks' import { useDebounce } from 'ahooks'
import { omit } from 'lodash-es' import { omit } from 'lodash-es'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { basePath } from '@/utils/var'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import List from './list' import List from './list'
import Filter, { TIME_PERIOD_MAPPING } from './filter' import Filter, { TIME_PERIOD_MAPPING } from './filter'
@ -109,7 +110,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
? <Loading type='app' /> ? <Loading type='app' />
: total > 0 : total > 0
? <List logs={isChatMode ? chatConversations : completionConversations} appDetail={appDetail} onRefresh={isChatMode ? mutateChatList : mutateCompletionList} /> ? <List logs={isChatMode ? chatConversations : completionConversations} appDetail={appDetail} onRefresh={isChatMode ? mutateChatList : mutateCompletionList} />
: <EmptyElement appUrl={`${appDetail.site.app_base_url}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} /> : <EmptyElement appUrl={`${appDetail.site.app_base_url}${basePath}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
} }
{/* Show Pagination only if the total is more than the limit */} {/* Show Pagination only if the total is more than the limit */}
{(total && total > APP_PAGE_LIMIT) {(total && total > APP_PAGE_LIMIT)

View File

@ -17,6 +17,7 @@ import type { ConfigParams } from './settings'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import AppBasic from '@/app/components/app-sidebar/basic' import AppBasic from '@/app/components/app-sidebar/basic'
import { asyncRunSafe, randomString } from '@/utils' import { asyncRunSafe, randomString } from '@/utils'
import { basePath } from '@/utils/var'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
@ -88,7 +89,7 @@ function AppCard({
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
const { app_base_url, access_token } = appInfo.site ?? {} const { app_base_url, access_token } = appInfo.site ?? {}
const appMode = (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') ? 'chat' : appInfo.mode const appMode = (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') ? 'chat' : appInfo.mode
const appUrl = `${app_base_url}/${appMode}/${access_token}` const appUrl = `${app_base_url}${basePath}/${appMode}/${access_token}`
const apiUrl = appInfo?.api_base_url const apiUrl = appInfo?.api_base_url
const genClickFuncByName = (opName: string) => { const genClickFuncByName = (opName: string) => {

View File

@ -13,6 +13,7 @@ import { IS_CE_EDITION } from '@/config'
import type { SiteInfo } from '@/models/share' import type { SiteInfo } from '@/models/share'
import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context' import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import { basePath } from '@/utils/var'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
@ -28,7 +29,7 @@ const OPTION_MAP = {
iframe: { iframe: {
getContent: (url: string, token: string) => getContent: (url: string, token: string) =>
`<iframe `<iframe
src="${url}/chatbot/${token}" src="${url}${basePath}/chatbot/${token}"
style="width: 100%; height: 100%; min-height: 700px" style="width: 100%; height: 100%; min-height: 700px"
frameborder="0" frameborder="0"
allow="microphone"> allow="microphone">
@ -41,17 +42,17 @@ const OPTION_MAP = {
token: '${token}'${isTestEnv token: '${token}'${isTestEnv
? `, ? `,
isDev: true` isDev: true`
: ''}${IS_CE_EDITION : ''}${IS_CE_EDITION
? `, ? `,
baseUrl: '${url}'` baseUrl: '${url}${basePath}'`
: ''}, : ''},
systemVariables: { systemVariables: {
// user_id: 'YOU CAN DEFINE USER ID HERE', // user_id: 'YOU CAN DEFINE USER ID HERE',
}, },
} }
</script> </script>
<script <script
src="${url}/embed.min.js" src="${url}${basePath}/embed.min.js"
id="${token}" id="${token}"
defer> defer>
</script> </script>
@ -66,7 +67,7 @@ const OPTION_MAP = {
</style>`, </style>`,
}, },
chromePlugin: { chromePlugin: {
getContent: (url: string, token: string) => `ChatBot URL: ${url}/chatbot/${token}`, getContent: (url: string, token: string) => `ChatBot URL: ${url}${basePath}/chatbot/${token}`,
}, },
} }
const prefixEmbedded = 'appOverview.overview.appInfo.embedded' const prefixEmbedded = 'appOverview.overview.appInfo.embedded'

View File

@ -11,6 +11,7 @@ import timezone from 'dayjs/plugin/timezone'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import Link from 'next/link' import Link from 'next/link'
import List from './list' import List from './list'
import { basePath } from '@/utils/var'
import Filter, { TIME_PERIOD_MAPPING } from './filter' import Filter, { TIME_PERIOD_MAPPING } from './filter'
import Pagination from '@/app/components/base/pagination' import Pagination from '@/app/components/base/pagination'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
@ -100,7 +101,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
? <Loading type='app' /> ? <Loading type='app' />
: total > 0 : total > 0
? <List logs={workflowLogs} appDetail={appDetail} onRefresh={mutate} /> ? <List logs={workflowLogs} appDetail={appDetail} onRefresh={mutate} />
: <EmptyElement appUrl={`${appDetail.site.app_base_url}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} /> : <EmptyElement appUrl={`${appDetail.site.app_base_url}${basePath}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
} }
{/* Show Pagination only if the total is more than the limit */} {/* Show Pagination only if the total is more than the limit */}
{(total && total > APP_PAGE_LIMIT) {(total && total > APP_PAGE_LIMIT)

View File

@ -22,6 +22,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions' import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
@ -139,22 +140,16 @@ const ChatWrapper = () => {
isPublicAPI: !isInstalledApp, isPublicAPI: !isInstalledApp,
}, },
) )
}, [ }, [chatList, handleNewConversationCompleted, handleSend, currentConversationId, currentConversationInputs, newConversationInputs, isInstalledApp, appId])
chatList,
handleNewConversationCompleted,
handleSend,
currentConversationId,
currentConversationItem,
currentConversationInputs,
newConversationInputs,
isInstalledApp,
appId,
])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => { const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)! const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(editedQuestion ? editedQuestion.message : question.content,
editedQuestion ? editedQuestion.files : question.message_files,
true,
isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
)
}, [chatList, doSend]) }, [chatList, doSend])
const messageList = useMemo(() => { const messageList = useMemo(() => {

View File

@ -52,7 +52,7 @@ function getFormattedChatList(messages: any[]) {
id: `question-${item.id}`, id: `question-${item.id}`,
content: item.query, content: item.query,
isAnswer: false, isAnswer: false,
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))), message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id, upload_file_id: item.upload_file_id }))),
parentMessageId: item.parent_message_id || undefined, parentMessageId: item.parent_message_id || undefined,
}) })
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [] const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
@ -63,7 +63,7 @@ function getFormattedChatList(messages: any[]) {
feedback: item.feedback, feedback: item.feedback,
isAnswer: true, isAnswer: true,
citation: item.retriever_resources, citation: item.retriever_resources,
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))), message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id, upload_file_id: item.upload_file_id }))),
parentMessageId: `question-${item.id}`, parentMessageId: `question-${item.id}`,
}) })
}) })

View File

@ -2,7 +2,7 @@ import type {
FC, FC,
ReactNode, ReactNode,
} from 'react' } from 'react'
import { memo, useEffect, useRef, useState } from 'react' import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
ChatConfig, ChatConfig,
@ -19,9 +19,9 @@ import Citation from '@/app/components/base/chat/chat/citation'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
import type { AppData } from '@/models/share' import type { AppData } from '@/models/share'
import AnswerIcon from '@/app/components/base/answer-icon' import AnswerIcon from '@/app/components/base/answer-icon'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { FileList } from '@/app/components/base/file-uploader' import { FileList } from '@/app/components/base/file-uploader'
import ContentSwitch from '../content-switch'
type AnswerProps = { type AnswerProps = {
item: ChatItem item: ChatItem
@ -100,12 +100,19 @@ const Answer: FC<AnswerProps> = ({
} }
}, []) }, [])
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
if (direction === 'prev')
item.prevSibling && switchSibling?.(item.prevSibling)
else
item.nextSibling && switchSibling?.(item.nextSibling)
}, [switchSibling, item.prevSibling, item.nextSibling])
return ( return (
<div className='mb-2 flex last:mb-0'> <div className='mb-2 flex last:mb-0'>
<div className='relative h-10 w-10 shrink-0'> <div className='relative h-10 w-10 shrink-0'>
{answerIcon || <AnswerIcon />} {answerIcon || <AnswerIcon />}
{responding && ( {responding && (
<div className='absolute -left-[3px] -top-[3px] flex h-4 w-4 items-center rounded-full border-[0.5px] border-divider-subtle bg-background-section-burn pl-[6px] shadow-xs'> <div className='absolute left-[-3px] top-[-3px] flex h-4 w-4 items-center rounded-full border-[0.5px] border-divider-subtle bg-background-section-burn pl-[6px] shadow-xs'>
<LoadingAnim type='avatar' /> <LoadingAnim type='avatar' />
</div> </div>
)} )}
@ -208,23 +215,17 @@ const Answer: FC<AnswerProps> = ({
<Citation data={citation} showHitInfo={config?.supportCitationHitInfo} /> <Citation data={citation} showHitInfo={config?.supportCitationHitInfo} />
) )
} }
{item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && <div className="flex items-center justify-center pt-3.5 text-sm"> {
<button item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && (
className={`${item.prevSibling ? 'opacity-100' : 'opacity-30'}`} <ContentSwitch
disabled={!item.prevSibling} count={item.siblingCount}
onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)} currentIndex={item.siblingIndex}
> prevDisabled={!item.prevSibling}
<ChevronRight className="h-[14px] w-[14px] rotate-180 text-text-primary" /> nextDisabled={!item.nextSibling}
</button> switchSibling={handleSwitchSibling}
<span className="px-2 text-xs text-text-primary">{item.siblingIndex + 1} / {item.siblingCount}</span> />
<button )
className={`${item.nextSibling ? 'opacity-100' : 'opacity-30'}`} }
disabled={!item.nextSibling}
onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)}
>
<ChevronRight className="h-[14px] w-[14px] text-text-primary" />
</button>
</div>}
</div> </div>
</div> </div>
<More more={more} /> <More more={more} />

View File

@ -2,8 +2,6 @@ import type { FC } from 'react'
import { memo } from 'react' import { memo } from 'react'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
import { useChatContext } from '../context' import { useChatContext } from '../context'
import Button from '@/app/components/base/button'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
type SuggestedQuestionsProps = { type SuggestedQuestionsProps = {
item: ChatItem item: ChatItem
@ -12,9 +10,6 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
item, item,
}) => { }) => {
const { onSend } = useChatContext() const { onSend } = useChatContext()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const klassName = `mr-1 mt-1 ${isMobile ? 'block overflow-hidden text-ellipsis' : ''} max-w-full shrink-0 last:mr-0`
const { const {
isOpeningStatement, isOpeningStatement,
@ -27,14 +22,13 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
return ( return (
<div className='flex flex-wrap'> <div className='flex flex-wrap'>
{suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => ( {suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
<Button <div
key={index} key={index}
variant='secondary-accent' className='system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
className={klassName}
onClick={() => onSend?.(question)} onClick={() => onSend?.(question)}
> >
{question} {question}
</Button>), </div>),
)} )}
</div> </div>
) )

View File

@ -0,0 +1,39 @@
import { ChevronRight } from '../../icons/src/vender/line/arrows'
export default function ContentSwitch({
count,
currentIndex,
prevDisabled,
nextDisabled,
switchSibling,
}: {
count?: number
currentIndex?: number
prevDisabled: boolean
nextDisabled: boolean
switchSibling: (direction: 'prev' | 'next') => void
}) {
return (
count && count > 1 && currentIndex !== undefined && (
<div className="flex items-center justify-center pt-3.5 text-sm">
<button
className={`${prevDisabled ? 'opacity-30' : 'opacity-100'}`}
disabled={prevDisabled}
onClick={() => !prevDisabled && switchSibling('prev')}
>
<ChevronRight className="h-[14px] w-[14px] rotate-180 text-text-primary" />
</button>
<span className="px-2 text-xs text-text-primary">
{currentIndex + 1} / {count}
</span>
<button
className={`${nextDisabled ? 'opacity-30' : 'opacity-100'}`}
disabled={nextDisabled}
onClick={() => !nextDisabled && switchSibling('next')}
>
<ChevronRight className="h-[14px] w-[14px] text-text-primary" />
</button>
</div>
)
)
}

View File

@ -208,7 +208,7 @@ const Chat: FC<ChatProps> = ({
useEffect(() => { useEffect(() => {
if (!sidebarCollapseState) if (!sidebarCollapseState)
setTimeout(() => handleWindowResize(), 200) setTimeout(() => handleWindowResize(), 200)
}, [sidebarCollapseState]) }, [handleWindowResize, sidebarCollapseState])
const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
@ -265,6 +265,7 @@ const Chat: FC<ChatProps> = ({
item={item} item={item}
questionIcon={questionIcon} questionIcon={questionIcon}
theme={themeBuilder?.theme} theme={themeBuilder?.theme}
switchSibling={switchSibling}
/> />
) )
}) })

View File

@ -4,46 +4,137 @@ import type {
} from 'react' } from 'react'
import { import {
memo, memo,
useCallback,
useState,
} from 'react' } from 'react'
import type { ChatItem } from '../types' import type { ChatItem } from '../types'
import type { Theme } from '../embedded-chatbot/theme/theme-context' import type { Theme } from '../embedded-chatbot/theme/theme-context'
import { CssTransform } from '../embedded-chatbot/theme/utils' import { CssTransform } from '../embedded-chatbot/theme/utils'
import ContentSwitch from './content-switch'
import { User } from '@/app/components/base/icons/src/public/avatar' import { User } from '@/app/components/base/icons/src/public/avatar'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import { FileList } from '@/app/components/base/file-uploader' import { FileList } from '@/app/components/base/file-uploader'
import ActionButton from '../../action-button'
import { RiClipboardLine, RiEditLine } from '@remixicon/react'
import Toast from '../../toast'
import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import Textarea from 'react-textarea-autosize'
import Button from '../../button'
import { useChatContext } from './context'
type QuestionProps = { type QuestionProps = {
item: ChatItem item: ChatItem
questionIcon?: ReactNode questionIcon?: ReactNode
theme: Theme | null | undefined theme: Theme | null | undefined
switchSibling?: (siblingMessageId: string) => void
} }
const Question: FC<QuestionProps> = ({ const Question: FC<QuestionProps> = ({
item, item,
questionIcon, questionIcon,
theme, theme,
switchSibling,
}) => { }) => {
const { t } = useTranslation()
const { const {
content, content,
message_files, message_files,
} = item } = item
const {
onRegenerate,
} = useChatContext()
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState(content)
const handleEdit = useCallback(() => {
setIsEditing(true)
setEditedContent(content)
}, [content])
const handleResend = useCallback(() => {
setIsEditing(false)
onRegenerate?.(item, { message: editedContent, files: message_files })
}, [editedContent, message_files, item, onRegenerate])
const handleCancelEditing = useCallback(() => {
setIsEditing(false)
setEditedContent(content)
}, [content])
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
if (direction === 'prev')
item.prevSibling && switchSibling?.(item.prevSibling)
else
item.nextSibling && switchSibling?.(item.nextSibling)
}, [switchSibling, item.prevSibling, item.nextSibling])
return ( return (
<div className='mb-2 flex justify-end pl-14 last:mb-0'> <div className='mb-2 flex justify-end pl-14 last:mb-0'>
<div className='group relative mr-4 max-w-full'> <div className={cn('group relative mr-4 flex max-w-full items-start', isEditing && 'flex-1')}>
<div className={cn('mr-2 gap-1', isEditing ? 'hidden' : 'flex')}>
<div className="
absolutegap-0.5 hidden rounded-[10px] border-[0.5px] border-components-actionbar-border
bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex
">
<ActionButton onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<RiClipboardLine className='h-4 w-4' />
</ActionButton>
<ActionButton onClick={handleEdit}>
<RiEditLine className='h-4 w-4' />
</ActionButton>
</div>
</div>
<div <div
className='rounded-2xl bg-[#D1E9FF]/50 px-4 py-3 text-sm text-gray-900' className='w-full rounded-2xl bg-[#D1E9FF]/50 px-4 py-3 text-sm text-gray-900'
style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}} style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}}
> >
{ {
!!message_files?.length && ( !!message_files?.length && (
<FileList <FileList
className='mb-2'
files={message_files} files={message_files}
showDeleteAction={false} showDeleteAction={false}
showDownloadAction={true} showDownloadAction={true}
/> />
) )
} }
<Markdown content={content} /> { !isEditing
? <Markdown content={content} />
: <div className="
flex flex-col gap-2 rounded-xl
border border-components-chat-input-border bg-components-panel-bg-blur p-[9px] shadow-md
">
<div className="max-h-[158px] overflow-y-auto overflow-x-hidden">
<Textarea
className={cn(
'body-lg-regular w-full p-1 leading-6 text-text-tertiary outline-none',
)}
autoFocus
minRows={1}
value={editedContent}
onChange={e => setEditedContent(e.target.value)}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant='ghost' onClick={handleCancelEditing}>{t('common.operation.cancel')}</Button>
<Button variant='primary' onClick={handleResend}>{t('common.chat.resend')}</Button>
</div>
</div> }
{ !isEditing && <ContentSwitch
count={item.siblingCount}
currentIndex={item.siblingIndex}
prevDisabled={!item.prevSibling}
nextDisabled={!item.nextSibling}
switchSibling={handleSwitchSibling}
/>}
</div> </div>
<div className='mt-1 h-[18px]' /> <div className='mt-1 h-[18px]' />
</div> </div>

View File

@ -24,6 +24,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions' import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
@ -140,10 +141,14 @@ const ChatWrapper = () => {
) )
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted]) }, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => { const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)! const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(editedQuestion ? editedQuestion.message : question.content,
editedQuestion ? editedQuestion.files : question.message_files,
true,
isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
)
}, [chatList, doSend]) }, [chatList, doSend])
const messageList = useMemo(() => { const messageList = useMemo(() => {
@ -179,7 +184,7 @@ const ChatWrapper = () => {
return null return null
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) { if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
return ( return (
<div className='flex h-[50vh] items-center justify-center px-4 py-12'> <div className={cn('flex items-center justify-center px-4 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
<div className='flex max-w-[720px] grow gap-4'> <div className='flex max-w-[720px] grow gap-4'>
<AppIcon <AppIcon
size='xl' size='xl'
@ -197,7 +202,7 @@ const ChatWrapper = () => {
) )
} }
return ( return (
<div className={cn('flex h-[50vh] flex-col items-center justify-center gap-3 py-12')}> <div className={cn('flex h-[50vh] flex-col items-center justify-center gap-3 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
<AppIcon <AppIcon
size='xl' size='xl'
iconType={appData?.site.icon_type} iconType={appData?.site.icon_type}

View File

@ -134,7 +134,7 @@ export const getProcessedFilesFromResponse = (files: FileResponse[]) => {
progress: 100, progress: 100,
transferMethod: fileItem.transfer_method, transferMethod: fileItem.transfer_method,
supportFileType: fileItem.type, supportFileType: fileItem.type,
uploadedId: fileItem.related_id, uploadedId: fileItem.upload_file_id || fileItem.related_id,
url: fileItem.url, url: fileItem.url,
} }
}) })

View File

@ -1,5 +1,6 @@
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import type { FC } from 'react' import type { FC } from 'react'
import { basePath } from '@/utils/var'
type LogoEmbeddedChatHeaderProps = { type LogoEmbeddedChatHeaderProps = {
className?: string className?: string
@ -13,7 +14,7 @@ const LogoEmbeddedChatHeader: FC<LogoEmbeddedChatHeaderProps> = ({
<source media="(resolution: 2x)" srcSet='/logo/logo-embedded-chat-header@2x.png' /> <source media="(resolution: 2x)" srcSet='/logo/logo-embedded-chat-header@2x.png' />
<source media="(resolution: 3x)" srcSet='/logo/logo-embedded-chat-header@3x.png' /> <source media="(resolution: 3x)" srcSet='/logo/logo-embedded-chat-header@3x.png' />
<img <img
src='/logo/logo-embedded-chat-header.png' src={`${basePath}/logo/logo-embedded-chat-header.png`}
alt='logo' alt='logo'
className={classNames('block h-6 w-auto', className)} className={classNames('block h-6 w-auto', className)}
/> />

View File

@ -1,5 +1,6 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { basePath } from '@/utils/var'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
type LogoSiteProps = { type LogoSiteProps = {
@ -11,7 +12,7 @@ const LogoSite: FC<LogoSiteProps> = ({
}) => { }) => {
return ( return (
<img <img
src={'/logo/logo.png'} src={`${basePath}/logo/logo.png`}
className={classNames('block w-[22.651px] h-[24.5px]', className)} className={classNames('block w-[22.651px] h-[24.5px]', className)}
alt='logo' alt='logo'
/> />

View File

@ -20,7 +20,7 @@ import { useProviderContext } from '@/context/provider-context'
import VectorSpaceFull from '@/app/components/billing/vector-space-full' import VectorSpaceFull from '@/app/components/billing/vector-space-full'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others' import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
type IStepOneProps = { type IStepOneProps = {
datasetId?: string datasetId?: string
dataSourceType?: DataSourceType dataSourceType?: DataSourceType
@ -126,9 +126,7 @@ const StepOne = ({
return true return true
if (files.some(file => !file.file.id)) if (files.some(file => !file.file.id))
return true return true
if (isShowVectorSpaceFull) return isShowVectorSpaceFull
return true
return false
}, [files, isShowVectorSpaceFull]) }, [files, isShowVectorSpaceFull])
return ( return (
@ -193,7 +191,8 @@ const StepOne = ({
{t('datasetCreation.stepOne.dataSourceType.notion')} {t('datasetCreation.stepOne.dataSourceType.notion')}
</span> </span>
</div> </div>
<div {(ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL) && (
<div
className={cn( className={cn(
s.dataSourceItem, s.dataSourceItem,
'system-sm-medium', 'system-sm-medium',
@ -201,7 +200,7 @@ const StepOne = ({
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled, dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
)} )}
onClick={() => changeType(DataSourceType.WEB)} onClick={() => changeType(DataSourceType.WEB)}
> >
<span className={cn(s.datasetIcon, s.web)} /> <span className={cn(s.datasetIcon, s.web)} />
<span <span
title={t('datasetCreation.stepOne.dataSourceType.web')} title={t('datasetCreation.stepOne.dataSourceType.web')}
@ -209,7 +208,8 @@ const StepOne = ({
> >
{t('datasetCreation.stepOne.dataSourceType.web')} {t('datasetCreation.stepOne.dataSourceType.web')}
</span> </span>
</div> </div>
)}
</div> </div>
) )
} }

View File

@ -97,7 +97,7 @@ export enum IndexingType {
} }
const DEFAULT_SEGMENT_IDENTIFIER = '\\n\\n' const DEFAULT_SEGMENT_IDENTIFIER = '\\n\\n'
const DEFAULT_MAXIMUM_CHUNK_LENGTH = 500 const DEFAULT_MAXIMUM_CHUNK_LENGTH = 1024
const DEFAULT_OVERLAP = 50 const DEFAULT_OVERLAP = 50
const MAXIMUM_CHUNK_TOKEN_LENGTH = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000', 10) const MAXIMUM_CHUNK_TOKEN_LENGTH = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000', 10)
@ -117,11 +117,11 @@ const defaultParentChildConfig: ParentChildConfig = {
chunkForContext: 'paragraph', chunkForContext: 'paragraph',
parent: { parent: {
delimiter: '\\n\\n', delimiter: '\\n\\n',
maxLength: 500, maxLength: 1024,
}, },
child: { child: {
delimiter: '\\n', delimiter: '\\n',
maxLength: 200, maxLength: 512,
}, },
} }
@ -623,12 +623,12 @@ const StepTwo = ({
onChange={e => setSegmentIdentifier(e.target.value, true)} onChange={e => setSegmentIdentifier(e.target.value, true)}
/> />
<MaxLengthInput <MaxLengthInput
unit='tokens' unit='characters'
value={maxChunkLength} value={maxChunkLength}
onChange={setMaxChunkLength} onChange={setMaxChunkLength}
/> />
<OverlapInput <OverlapInput
unit='tokens' unit='characters'
value={overlap} value={overlap}
min={1} min={1}
onChange={setOverlap} onChange={setOverlap}
@ -756,7 +756,7 @@ const StepTwo = ({
})} })}
/> />
<MaxLengthInput <MaxLengthInput
unit='tokens' unit='characters'
value={parentChildConfig.parent.maxLength} value={parentChildConfig.parent.maxLength}
onChange={value => setParentChildConfig({ onChange={value => setParentChildConfig({
...parentChildConfig, ...parentChildConfig,
@ -803,7 +803,7 @@ const StepTwo = ({
})} })}
/> />
<MaxLengthInput <MaxLengthInput
unit='tokens' unit='characters'
value={parentChildConfig.child.maxLength} value={parentChildConfig.child.maxLength}
onChange={value => setParentChildConfig({ onChange={value => setParentChildConfig({
...parentChildConfig, ...parentChildConfig,

View File

@ -12,6 +12,7 @@ import { useModalContext } from '@/context/modal-context'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets' import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { fetchDataSources } from '@/service/datasets' import { fetchDataSources } from '@/service/datasets'
import { type DataSourceItem, DataSourceProvider } from '@/models/common' import { type DataSourceItem, DataSourceProvider } from '@/models/common'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
type Props = { type Props = {
onPreview: (payload: CrawlResultItem) => void onPreview: (payload: CrawlResultItem) => void
@ -84,7 +85,7 @@ const Website: FC<Props> = ({
{t('datasetCreation.stepOne.website.chooseProvider')} {t('datasetCreation.stepOne.website.chooseProvider')}
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<button {ENABLE_WEBSITE_JINAREADER && <button
className={cn('flex items-center justify-center rounded-lg px-4 py-2', className={cn('flex items-center justify-center rounded-lg px-4 py-2',
selectedProvider === DataSourceProvider.jinaReader selectedProvider === DataSourceProvider.jinaReader
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary' ? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
@ -95,8 +96,8 @@ const Website: FC<Props> = ({
> >
<span className={cn(s.jinaLogo, 'mr-2')}/> <span className={cn(s.jinaLogo, 'mr-2')}/>
<span>Jina Reader</span> <span>Jina Reader</span>
</button> </button>}
<button {ENABLE_WEBSITE_FIRECRAWL && <button
className={cn('rounded-lg px-4 py-2', className={cn('rounded-lg px-4 py-2',
selectedProvider === DataSourceProvider.fireCrawl selectedProvider === DataSourceProvider.fireCrawl
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary' ? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
@ -106,8 +107,8 @@ const Website: FC<Props> = ({
onClick={() => setSelectedProvider(DataSourceProvider.fireCrawl)} onClick={() => setSelectedProvider(DataSourceProvider.fireCrawl)}
> >
🔥 Firecrawl 🔥 Firecrawl
</button> </button>}
<button {ENABLE_WEBSITE_WATERCRAWL && <button
className={cn('flex items-center justify-center rounded-lg px-4 py-2', className={cn('flex items-center justify-center rounded-lg px-4 py-2',
selectedProvider === DataSourceProvider.waterCrawl selectedProvider === DataSourceProvider.waterCrawl
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary' ? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
@ -118,7 +119,7 @@ const Website: FC<Props> = ({
> >
<span className={cn(s.watercrawlLogo, 'mr-2')}/> <span className={cn(s.watercrawlLogo, 'mr-2')}/>
<span>WaterCrawl</span> <span>WaterCrawl</span>
</button> </button>}
</div> </div>
</div> </div>
{source && selectedProvider === DataSourceProvider.fireCrawl && ( {source && selectedProvider === DataSourceProvider.fireCrawl && (

View File

@ -6,6 +6,7 @@ import s from './index.module.css'
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others' import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { DataSourceProvider } from '@/models/common' import { DataSourceProvider } from '@/models/common'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
const I18N_PREFIX = 'datasetCreation.stepOne.website' const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -16,29 +17,30 @@ type Props = {
const NoData: FC<Props> = ({ const NoData: FC<Props> = ({
onConfig, onConfig,
provider,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const providerConfig = { const providerConfig = {
[DataSourceProvider.jinaReader]: { [DataSourceProvider.jinaReader]: ENABLE_WEBSITE_JINAREADER ? {
emoji: <span className={s.jinaLogo} />, emoji: <span className={s.jinaLogo} />,
title: t(`${I18N_PREFIX}.jinaReaderNotConfigured`), title: t(`${I18N_PREFIX}.jinaReaderNotConfigured`),
description: t(`${I18N_PREFIX}.jinaReaderNotConfiguredDescription`), description: t(`${I18N_PREFIX}.jinaReaderNotConfiguredDescription`),
}, } : null,
[DataSourceProvider.fireCrawl]: { [DataSourceProvider.fireCrawl]: ENABLE_WEBSITE_FIRECRAWL ? {
emoji: '🔥', emoji: '🔥',
title: t(`${I18N_PREFIX}.fireCrawlNotConfigured`), title: t(`${I18N_PREFIX}.fireCrawlNotConfigured`),
description: t(`${I18N_PREFIX}.fireCrawlNotConfiguredDescription`), description: t(`${I18N_PREFIX}.fireCrawlNotConfiguredDescription`),
}, } : null,
[DataSourceProvider.waterCrawl]: { [DataSourceProvider.waterCrawl]: ENABLE_WEBSITE_WATERCRAWL ? {
emoji: <span className={s.watercrawlLogo} />, emoji: '💧',
title: t(`${I18N_PREFIX}.waterCrawlNotConfigured`), title: t(`${I18N_PREFIX}.waterCrawlNotConfigured`),
description: t(`${I18N_PREFIX}.waterCrawlNotConfiguredDescription`), description: t(`${I18N_PREFIX}.waterCrawlNotConfiguredDescription`),
}, } : null,
} }
const currentProvider = providerConfig[provider] const currentProvider = Object.values(providerConfig).find(provider => provider !== null) || providerConfig[DataSourceProvider.jinaReader]
if (!currentProvider) return null
return ( return (
<> <>

View File

@ -4,7 +4,6 @@ import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import CopyFeedback from '@/app/components/base/copy-feedback' import CopyFeedback from '@/app/components/base/copy-feedback'
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button' import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
import { randomString } from '@/utils'
type ApiServerProps = { type ApiServerProps = {
apiBaseUrl: string apiBaseUrl: string
@ -16,21 +15,17 @@ const ApiServer: FC<ApiServerProps> = ({
return ( return (
<div className='flex flex-wrap items-center gap-y-2'> <div className='flex flex-wrap items-center gap-y-2'>
<div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-white bg-white/80 pl-1.5 pr-1 leading-5'> <div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal pl-1.5 pr-1 leading-5'>
<div className='mr-0.5 h-5 shrink-0 rounded-md border border-gray-200 px-1.5 text-[11px] text-gray-500'>{t('appApi.apiServer')}</div> <div className='mr-0.5 h-5 shrink-0 rounded-md border border-divider-subtle px-1.5 text-[11px] text-text-tertiary'>{t('appApi.apiServer')}</div>
<div className='w-fit truncate px-1 text-[13px] font-medium text-gray-800 sm:w-[248px]'>{apiBaseUrl}</div> <div className='w-fit truncate px-1 text-[13px] font-medium text-text-secondary sm:w-[248px]'>{apiBaseUrl}</div>
<div className='mx-1 h-[14px] w-[1px] bg-gray-200'></div> <div className='mx-1 h-[14px] w-[1px] bg-divider-regular'></div>
<CopyFeedback <CopyFeedback content={apiBaseUrl}/>
content={apiBaseUrl}
selectorId={randomString(8)}
className={'!h-6 !w-6 hover:bg-gray-200'}
/>
</div> </div>
<div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-[#D1FADF] bg-[#ECFDF3] px-3 text-xs font-semibold text-[#039855]'> <div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-[#D1FADF] bg-[#ECFDF3] px-3 text-xs font-semibold text-[#039855]'>
{t('appApi.ok')} {t('appApi.ok')}
</div> </div>
<SecretKeyButton <SecretKeyButton
className='!h-8 shrink-0 bg-white' className='!h-8 shrink-0'
/> />
</div> </div>
) )

View File

@ -17,6 +17,9 @@ import TemplateChatZh from './template/template_chat.zh.mdx'
import TemplateChatJa from './template/template_chat.ja.mdx' import TemplateChatJa from './template/template_chat.ja.mdx'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language' import { LanguagesSupported } from '@/i18n/language'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'
type IDocProps = { type IDocProps = {
appDetail: any appDetail: any
@ -27,6 +30,7 @@ const Doc = ({ appDetail }: IDocProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [toc, setToc] = useState<Array<{ href: string; text: string }>>([]) const [toc, setToc] = useState<Array<{ href: string; text: string }>>([])
const [isTocExpanded, setIsTocExpanded] = useState(false) const [isTocExpanded, setIsTocExpanded] = useState(false)
const { theme } = useTheme()
const variables = appDetail?.model_config?.configs?.prompt_variables || [] const variables = appDetail?.model_config?.configs?.prompt_variables || []
const inputs = variables.reduce((res: any, variable: any) => { const inputs = variables.reduce((res: any, variable: any) => {
@ -83,12 +87,12 @@ const Doc = ({ appDetail }: IDocProps) => {
<div className={`fixed right-8 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}> <div className={`fixed right-8 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
{isTocExpanded {isTocExpanded
? ( ? (
<nav className="toc w-full rounded-lg bg-gray-50 p-4 shadow-md"> <nav className="toc w-full rounded-lg bg-components-panel-bg p-4 shadow-md">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">{t('appApi.develop.toc')}</h3> <h3 className="text-lg font-semibold text-text-primary">{t('appApi.develop.toc')}</h3>
<button <button
onClick={() => setIsTocExpanded(false)} onClick={() => setIsTocExpanded(false)}
className="text-gray-500 hover:text-gray-700" className="text-text-tertiary hover:text-text-secondary"
> >
</button> </button>
@ -98,7 +102,7 @@ const Doc = ({ appDetail }: IDocProps) => {
<li key={index}> <li key={index}>
<a <a
href={item.href} href={item.href}
className="text-gray-600 transition-colors duration-200 hover:text-gray-900 hover:underline" className="text-text-secondary transition-colors duration-200 hover:text-text-primary hover:underline"
onClick={e => handleTocClick(e, item)} onClick={e => handleTocClick(e, item)}
> >
{item.text} {item.text}
@ -111,13 +115,13 @@ const Doc = ({ appDetail }: IDocProps) => {
: ( : (
<button <button
onClick={() => setIsTocExpanded(true)} onClick={() => setIsTocExpanded(true)}
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 shadow-md transition-colors duration-200 hover:bg-gray-100" className="flex h-10 w-10 items-center justify-center rounded-full bg-components-button-secondary-bg shadow-md transition-colors duration-200 hover:bg-components-button-secondary-bg-hover"
> >
<RiListUnordered className="h-6 w-6" /> <RiListUnordered className="h-6 w-6 text-components-button-secondary-text" />
</button> </button>
)} )}
</div> </div>
<article className="prose-xl prose" > <article className={cn('prose-xl prose', theme === Theme.dark && 'dark:prose-invert')} >
{(appDetail?.mode === 'chat' || appDetail?.mode === 'agent-chat') && ( {(appDetail?.mode === 'chat' || appDetail?.mode === 'agent-chat') && (
(() => { (() => {
switch (locale) { switch (locale) {

View File

@ -1,10 +1,7 @@
'use client' 'use client'
import { useTranslation } from 'react-i18next'
import s from './secret-key/style.module.css'
import Doc from '@/app/components/develop/doc' import Doc from '@/app/components/develop/doc'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import InputCopy from '@/app/components/develop/secret-key/input-copy' import ApiServer from '@/app/components/develop/ApiServer'
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
type IDevelopMainProps = { type IDevelopMainProps = {
@ -13,11 +10,10 @@ type IDevelopMainProps = {
const DevelopMain = ({ appId }: IDevelopMainProps) => { const DevelopMain = ({ appId }: IDevelopMainProps) => {
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const { t } = useTranslation()
if (!appDetail) { if (!appDetail) {
return ( return (
<div className='flex h-full items-center justify-center bg-white'> <div className='flex h-full items-center justify-center bg-background-default'>
<Loading /> <Loading />
</div> </div>
) )
@ -25,21 +21,9 @@ const DevelopMain = ({ appId }: IDevelopMainProps) => {
return ( return (
<div className='relative flex h-full flex-col overflow-hidden'> <div className='relative flex h-full flex-col overflow-hidden'>
<div className='flex shrink-0 items-center justify-between border-b border-solid border-b-gray-100 px-6 py-2'> <div className='flex shrink-0 items-center justify-between border-b border-solid border-b-divider-regular px-6 py-2'>
<div className='text-lg font-medium text-gray-900'></div> <div className='text-lg font-medium text-text-primary'></div>
<div className='flex flex-wrap items-center gap-y-1'> <ApiServer apiBaseUrl={appDetail.api_base_url} />
<InputCopy className='mr-1 w-52 shrink-0 sm:w-80' value={appDetail.api_base_url}>
<div className={`ml-2 shrink-0 rounded-[6px] border border-solid border-gray-200 px-2 py-0.5 text-[0.625rem] text-gray-500 ${s.customApi}`}>
{t('appApi.apiServer')}
</div>
</InputCopy>
<div className={`mr-2 flex h-9 items-center rounded-lg
px-3 text-[13px] font-normal ${appDetail.enable_api ? 'bg-green-50 text-green-500' : 'bg-yellow-50 text-yellow-500'}`}>
<div className='mr-1'>{t('appApi.status')}</div>
<div className='font-semibold'>{appDetail.enable_api ? `${t('appApi.ok')}` : `${t('appApi.disabled')}`}</div>
</div>
<SecretKeyButton className='shrink-0' appId={appId} />
</div>
</div> </div>
<div className='grow overflow-auto px-4 py-4 sm:px-10'> <div className='grow overflow-auto px-4 py-4 sm:px-10'>
<Doc appDetail={appDetail} /> <Doc appDetail={appDetail} />

View File

@ -12,7 +12,7 @@ type IChildrenProps = {
type IHeaderingProps = { type IHeaderingProps = {
url: string url: string
method: 'PUT' | 'DELETE' | 'GET' | 'POST' method: 'PUT' | 'DELETE' | 'GET' | 'POST' | 'PATCH'
title: string title: string
name: string name: string
} }
@ -34,6 +34,9 @@ export const Heading = function H2({
case 'POST': case 'POST':
style = 'ring-sky-300 bg-sky-400/10 text-sky-500 dark:ring-sky-400/30 dark:bg-sky-400/10 dark:text-sky-400' style = 'ring-sky-300 bg-sky-400/10 text-sky-500 dark:ring-sky-400/30 dark:bg-sky-400/10 dark:text-sky-400'
break break
case 'PATCH':
style = 'ring-violet-300 bg-violet-400/10 text-violet-500 dark:ring-violet-400/30 dark:bg-violet-400/10 dark:text-violet-400'
break
default: default:
style = 'ring-emerald-300 dark:ring-emerald-400/30 bg-emerald-400/10 text-emerald-500 dark:text-emerald-400' style = 'ring-emerald-300 dark:ring-emerald-400/30 bg-emerald-400/10 text-emerald-500 dark:text-emerald-400'
break break

View File

@ -1,16 +1,17 @@
'use client' 'use client'
import { import {
useEffect,
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine } from '@remixicon/react'
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid' import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
import useSWR, { useSWRConfig } from 'swr' import useSWR, { useSWRConfig } from 'swr'
import copy from 'copy-to-clipboard'
import SecretKeyGenerateModal from './secret-key-generate' import SecretKeyGenerateModal from './secret-key-generate'
import s from './style.module.css' import s from './style.module.css'
import ActionButton from '@/app/components/base/action-button'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import CopyFeedback from '@/app/components/base/copy-feedback'
import { import {
createApikey as createAppApikey, createApikey as createAppApikey,
delApikey as delAppApikey, delApikey as delAppApikey,
@ -22,7 +23,6 @@ import {
fetchApiKeysList as fetchDatasetApiKeysList, fetchApiKeysList as fetchDatasetApiKeysList,
} from '@/service/datasets' } from '@/service/datasets'
import type { CreateApiKeyResponse } from '@/models/app' import type { CreateApiKeyResponse } from '@/models/app'
import Tooltip from '@/app/components/base/tooltip'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import useTimestamp from '@/hooks/use-timestamp' import useTimestamp from '@/hooks/use-timestamp'
@ -54,20 +54,6 @@ const SecretKeyModal = ({
const [delKeyID, setDelKeyId] = useState('') const [delKeyID, setDelKeyId] = useState('')
const [copyValue, setCopyValue] = useState('')
useEffect(() => {
if (copyValue) {
const timeout = setTimeout(() => {
setCopyValue('')
}, 1000)
return () => {
clearTimeout(timeout)
}
}
}, [copyValue])
const onDel = async () => { const onDel = async () => {
setShowConfirmDelete(false) setShowConfirmDelete(false)
if (!delKeyID) if (!delKeyID)
@ -104,7 +90,7 @@ const SecretKeyModal = ({
{ {
!!apiKeysList?.data?.length && ( !!apiKeysList?.data?.length && (
<div className='mt-4 flex grow flex-col overflow-hidden'> <div className='mt-4 flex grow flex-col overflow-hidden'>
<div className='flex h-9 shrink-0 items-center border-b border-solid text-xs font-semibold text-text-tertiary'> <div className='flex h-9 shrink-0 items-center border-b border-divider-regular text-xs font-semibold text-text-tertiary'>
<div className='w-64 shrink-0 px-3'>{t('appApi.apiKeyModal.secretKey')}</div> <div className='w-64 shrink-0 px-3'>{t('appApi.apiKeyModal.secretKey')}</div>
<div className='w-[200px] shrink-0 px-3'>{t('appApi.apiKeyModal.created')}</div> <div className='w-[200px] shrink-0 px-3'>{t('appApi.apiKeyModal.created')}</div>
<div className='w-[200px] shrink-0 px-3'>{t('appApi.apiKeyModal.lastUsed')}</div> <div className='w-[200px] shrink-0 px-3'>{t('appApi.apiKeyModal.lastUsed')}</div>
@ -112,28 +98,22 @@ const SecretKeyModal = ({
</div> </div>
<div className='grow overflow-auto'> <div className='grow overflow-auto'>
{apiKeysList.data.map(api => ( {apiKeysList.data.map(api => (
<div className='flex h-9 items-center border-b border-solid text-sm font-normal text-text-secondary' key={api.id}> <div className='flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary' key={api.id}>
<div className='w-64 shrink-0 truncate px-3 font-mono'>{generateToken(api.token)}</div> <div className='w-64 shrink-0 truncate px-3 font-mono'>{generateToken(api.token)}</div>
<div className='w-[200px] shrink-0 truncate px-3'>{formatTime(Number(api.created_at), t('appLog.dateTimeFormat') as string)}</div> <div className='w-[200px] shrink-0 truncate px-3'>{formatTime(Number(api.created_at), t('appLog.dateTimeFormat') as string)}</div>
<div className='w-[200px] shrink-0 truncate px-3'>{api.last_used_at ? formatTime(Number(api.last_used_at), t('appLog.dateTimeFormat') as string) : t('appApi.never')}</div> <div className='w-[200px] shrink-0 truncate px-3'>{api.last_used_at ? formatTime(Number(api.last_used_at), t('appLog.dateTimeFormat') as string) : t('appApi.never')}</div>
<div className='flex grow px-3'> <div className='flex grow space-x-2 px-3'>
<Tooltip <CopyFeedback content={api.token} />
popupContent={copyValue === api.token ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} {isCurrentWorkspaceManager && (
popupClassName='mr-1' <ActionButton
> onClick={() => {
<div className={`mr-1 flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover ${s.copyIcon} ${copyValue === api.token ? s.copied : ''}`} onClick={() => { setDelKeyId(api.id)
// setIsCopied(true) setShowConfirmDelete(true)
copy(api.token) }}
setCopyValue(api.token) >
}}></div> <RiDeleteBinLine className='h-4 w-4' />
</Tooltip> </ActionButton>
{isCurrentWorkspaceManager )}
&& <div className={`flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg ${s.trashIcon}`} onClick={() => {
setDelKeyId(api.id)
setShowConfirmDelete(true)
}}>
</div>
}
</div> </div>
</div> </div>
))} ))}

View File

@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react' import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react'
import { RiArrowDownSLine } from '@remixicon/react' import { RiArrowDownSLine } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
import PlanBadge from '@/app/components/header/plan-badge' import PlanBadge from '@/app/components/header/plan-badge'
import { switchWorkspace } from '@/service/common' import { switchWorkspace } from '@/service/common'
import { useWorkspacesContext } from '@/context/workspace-context' import { useWorkspacesContext } from '@/context/workspace-context'
@ -22,7 +23,7 @@ const WorkplaceSelector = () => {
return return
await switchWorkspace({ url: '/workspaces/switch', body: { tenant_id } }) await switchWorkspace({ url: '/workspaces/switch', body: { tenant_id } })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
location.assign(`${location.origin}`) location.assign(`${location.origin}${basePath}`)
} }
catch { catch {
notify({ type: 'error', message: t('common.provider.saveFailed') }) notify({ type: 'error', message: t('common.provider.saveFailed') })

View File

@ -3,6 +3,7 @@ import DataSourceNotion from './data-source-notion'
import DataSourceWebsite from './data-source-website' import DataSourceWebsite from './data-source-website'
import { fetchDataSource } from '@/service/common' import { fetchDataSource } from '@/service/common'
import { DataSourceProvider } from '@/models/common' import { DataSourceProvider } from '@/models/common'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
export default function DataSourcePage() { export default function DataSourcePage() {
const { data } = useSWR({ url: 'data-source/integrates' }, fetchDataSource) const { data } = useSWR({ url: 'data-source/integrates' }, fetchDataSource)
@ -11,9 +12,9 @@ export default function DataSourcePage() {
return ( return (
<div className='mb-8'> <div className='mb-8'>
<DataSourceNotion workspaces={notionWorkspaces} /> <DataSourceNotion workspaces={notionWorkspaces} />
<DataSourceWebsite provider={DataSourceProvider.jinaReader} /> {ENABLE_WEBSITE_JINAREADER && <DataSourceWebsite provider={DataSourceProvider.jinaReader} />}
<DataSourceWebsite provider={DataSourceProvider.fireCrawl} /> {ENABLE_WEBSITE_FIRECRAWL && <DataSourceWebsite provider={DataSourceProvider.fireCrawl} />}
<DataSourceWebsite provider={DataSourceProvider.waterCrawl} /> {ENABLE_WEBSITE_WATERCRAWL && <DataSourceWebsite provider={DataSourceProvider.waterCrawl} />}
</div> </div>
) )
} }

View File

@ -1,5 +1,6 @@
'use client' 'use client'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { basePath } from '@/utils/var'
import { t } from 'i18next' import { t } from 'i18next'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import s from './index.module.css' import s from './index.module.css'
@ -18,7 +19,7 @@ const InvitationLink = ({
const selector = useRef(`invite-link-${randomString(4)}`) const selector = useRef(`invite-link-${randomString(4)}`)
const copyHandle = useCallback(() => { const copyHandle = useCallback(() => {
copy(`${!value.url.startsWith('http') ? window.location.origin : ''}${value.url}`) copy(`${!value.url.startsWith('http') ? window.location.origin : ''}${basePath}${value.url}`)
setIsCopied(true) setIsCopied(true)
}, [value]) }, [value])
@ -41,7 +42,7 @@ const InvitationLink = ({
<Tooltip <Tooltip
popupContent={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} popupContent={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
> >
<div className='r-0 absolute left-0 top-0 w-full cursor-pointer truncate pl-2 pr-2' onClick={copyHandle}>{value.url}</div> <div className='r-0 absolute left-0 top-0 w-full cursor-pointer truncate pl-2 pr-2' onClick={copyHandle}>{basePath + value.url}</div>
</Tooltip> </Tooltip>
</div> </div>
<div className="h-4 shrink-0 border bg-divider-regular" /> <div className="h-4 shrink-0 border bg-divider-regular" />

View File

@ -3,6 +3,7 @@ import type {
Model, Model,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { basePath } from '@/utils/var'
import { useLanguage } from '../hooks' import { useLanguage } from '../hooks'
import { Group } from '@/app/components/base/icons/src/vender/other' import { Group } from '@/app/components/base/icons/src/vender/other'
import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm' import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
@ -30,7 +31,7 @@ const ModelIcon: FC<ModelIconProps> = ({
if (provider?.icon_small) { if (provider?.icon_small) {
return ( return (
<div className={cn('flex h-5 w-5 items-center justify-center', isDeprecated && 'opacity-50', className)}> <div className={cn('flex h-5 w-5 items-center justify-center', isDeprecated && 'opacity-50', className)}>
<img alt='model-icon' src={renderI18nObject(provider.icon_small, language)}/> <img alt='model-icon' src={basePath + renderI18nObject(provider.icon_small, language)}/>
</div> </div>
) )
} }

View File

@ -1,5 +1,6 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { ModelProvider } from '../declarations' import type { ModelProvider } from '../declarations'
import { basePath } from '@/utils/var'
import { useLanguage } from '../hooks' import { useLanguage } from '../hooks'
import { Openai } from '@/app/components/base/icons/src/vender/other' import { Openai } from '@/app/components/base/icons/src/vender/other'
import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm' import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm'
@ -40,7 +41,7 @@ const ProviderIcon: FC<ProviderIconProps> = ({
<div className={cn('inline-flex items-center gap-2', className)}> <div className={cn('inline-flex items-center gap-2', className)}>
<img <img
alt='provider-icon' alt='provider-icon'
src={renderI18nObject(provider.icon_small, language)} src={basePath + renderI18nObject(provider.icon_small, language)}
className='h-6 w-6' className='h-6 w-6'
/> />
<div className='system-md-semibold text-text-primary'> <div className='system-md-semibold text-text-primary'>

View File

@ -104,7 +104,10 @@ const PluginItem: FC<Props> = ({
{!isDifyVersionCompatible && <Tooltip popupContent={ {!isDifyVersionCompatible && <Tooltip popupContent={
t('plugin.difyVersionNotCompatible', { minimalDifyVersion: declarationMeta.minimum_dify_version }) t('plugin.difyVersionNotCompatible', { minimalDifyVersion: declarationMeta.minimum_dify_version })
}><RiErrorWarningLine color='red' className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /></Tooltip>} }><RiErrorWarningLine color='red' className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /></Tooltip>}
<Badge className='ml-1 shrink-0' text={source === PluginSource.github ? plugin.meta!.version : plugin.version} /> <Badge className='ml-1 shrink-0'
text={source === PluginSource.github ? plugin.meta!.version : plugin.version}
hasRedCornerMark={(source === PluginSource.marketplace) && !!plugin.latest_unique_identifier && plugin.latest_unique_identifier !== plugin_unique_identifier}
/>
</div> </div>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<Description text={descriptionText} descriptionLineRows={1}></Description> <Description text={descriptionText} descriptionLineRows={1}></Description>

View File

@ -406,8 +406,7 @@ export type VersionProps = {
export type StrategyParamItem = { export type StrategyParamItem = {
name: string name: string
label: Record<Locale, string> label: Record<Locale, string>
human_description: Record<Locale, string> help: Record<Locale, string>
llm_description: string
placeholder: Record<Locale, string> placeholder: Record<Locale, string>
type: string type: string
scope: string scope: string

View File

@ -14,6 +14,7 @@ import Type from './type'
import Category from './category' import Category from './category'
import Tools from './tools' import Tools from './tools'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import Drawer from '@/app/components/base/drawer' import Drawer from '@/app/components/base/drawer'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
@ -57,6 +58,12 @@ const AddToolModal: FC<Props> = ({
const getAllTools = async () => { const getAllTools = async () => {
setListLoading(true) setListLoading(true)
const buildInTools = await fetchAllBuiltInTools() const buildInTools = await fetchAllBuiltInTools()
if (basePath) {
buildInTools.forEach((item) => {
if (typeof item.icon == 'string' && !item.icon.includes(basePath))
item.icon = `${basePath}${item.icon}`
})
}
const customTools = await fetchAllCustomTools() const customTools = await fetchAllCustomTools()
const workflowTools = await fetchAllWorkflowTools() const workflowTools = await fetchAllWorkflowTools()
const mergedToolList = [ const mergedToolList = [

View File

@ -2,6 +2,7 @@ import {
memo, memo,
useCallback, useCallback,
} from 'react' } from 'react'
import { basePath } from '@/utils/var'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiAddLine, RiAddLine,
@ -53,7 +54,7 @@ const Blocks = ({
> >
<div className='flex h-[22px] w-full items-center justify-between pl-3 pr-1 text-xs font-medium text-gray-500'> <div className='flex h-[22px] w-full items-center justify-between pl-3 pr-1 text-xs font-medium text-gray-500'>
{toolWithProvider.label[language]} {toolWithProvider.label[language]}
<a className='hidden cursor-pointer items-center group-hover:flex' href={`/tools?category=${toolWithProvider.type}`} target='_blank'>{t('tools.addToolModal.manageInTools')}<ArrowUpRight className='ml-0.5 h-3 w-3' /></a> <a className='hidden cursor-pointer items-center group-hover:flex' href={`${basePath}/tools?category=${toolWithProvider.type}`} target='_blank'>{t('tools.addToolModal.manageInTools')}<ArrowUpRight className='ml-0.5 h-3 w-3' /></a>
</div> </div>
{list.map((tool) => { {list.map((tool) => {
const labelContent = (() => { const labelContent = (() => {

View File

@ -6,6 +6,7 @@ import {
RiCloseLine, RiCloseLine,
} from '@remixicon/react' } from '@remixicon/react'
import { AuthHeaderPrefix, AuthType, CollectionType } from '../types' import { AuthHeaderPrefix, AuthType, CollectionType } from '../types'
import { basePath } from '@/utils/var'
import type { Collection, CustomCollectionBackend, Tool, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '../types' import type { Collection, CustomCollectionBackend, Tool, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '../types'
import ToolItem from './tool-item' import ToolItem from './tool-item'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -276,7 +277,7 @@ const ProviderDetail = ({
variant='primary' variant='primary'
className={cn('my-3 w-[183px] shrink-0')} className={cn('my-3 w-[183px] shrink-0')}
> >
<a className='flex items-center' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'> <a className='flex items-center' href={`${basePath}/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
<div className='system-sm-medium'>{t('tools.openInStudio')}</div> <div className='system-sm-medium'>{t('tools.openInStudio')}</div>
<LinkExternal02 className='ml-1 h-4 w-4' /> <LinkExternal02 className='ml-1 h-4 w-4' />
</a> </a>

View File

@ -59,6 +59,7 @@ import { CollectionType } from '@/app/components/tools/types'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants' import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants' import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
import { useWorkflowConfig } from '@/service/use-workflow' import { useWorkflowConfig } from '@/service/use-workflow'
import { basePath } from '@/utils/var'
import { canFindTool } from '@/utils' import { canFindTool } from '@/utils'
export const useIsChatMode = () => { export const useIsChatMode = () => {
@ -446,6 +447,12 @@ export const useFetchToolsData = () => {
if (type === 'builtin') { if (type === 'builtin') {
const buildInTools = await fetchAllBuiltInTools() const buildInTools = await fetchAllBuiltInTools()
if (basePath) {
buildInTools.forEach((item) => {
if (typeof item.icon == 'string' && !item.icon.includes(basePath))
item.icon = `${basePath}${item.icon}`
})
}
workflowStore.setState({ workflowStore.setState({
buildInTools: buildInTools || [], buildInTools: buildInTools || [],
}) })

View File

@ -65,7 +65,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
switch (schema.type) { switch (schema.type) {
case FormTypeEnum.textInput: { case FormTypeEnum.textInput: {
const def = schema as CredentialFormSchemaTextInput const def = schema as CredentialFormSchemaTextInput
const value = props.value[schema.variable] const value = props.value[schema.variable] || schema.default
const onChange = (value: string) => { const onChange = (value: string) => {
props.onChange({ ...props.value, [schema.variable]: value }) props.onChange({ ...props.value, [schema.variable]: value })
} }

View File

@ -27,6 +27,7 @@ export function strategyParamToCredientialForm(param: StrategyParamItem): Creden
variable: param.name, variable: param.name,
show_on: [], show_on: [],
type: toType(param.type), type: toType(param.type),
tooltip: param.help,
} }
} }
@ -53,6 +54,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
outputSchema, outputSchema,
handleMemoryChange, handleMemoryChange,
} = useConfig(props.id, props.data) } = useConfig(props.id, props.data)
console.log('currentStrategy', currentStrategy)
const { t } = useTranslation() const { t } = useTranslation()
const nodeInfo = useMemo(() => { const nodeInfo = useMemo(() => {
if (!runResult) if (!runResult)

View File

@ -20,6 +20,7 @@ import {
} from '@/service/debug' } from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils' import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
type ChatWrapperProps = { type ChatWrapperProps = {
showConversationVariableModal: boolean showConversationVariableModal: boolean
@ -94,10 +95,14 @@ const ChatWrapper = (
) )
}, [handleSend, workflowStore, conversationId, chatList, appDetail]) }, [handleSend, workflowStore, conversationId, chatList, appDetail])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => { const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)! const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(editedQuestion ? editedQuestion.message : question.content,
editedQuestion ? editedQuestion.files : question.message_files,
true,
isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
)
}, [chatList, doSend]) }, [chatList, doSend])
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {

View File

@ -3,6 +3,7 @@ import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useSWR from 'swr' import useSWR from 'swr'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import { basePath } from '@/utils/var'
import cn from 'classnames' import cn from 'classnames'
import { CheckCircleIcon } from '@heroicons/react/24/solid' import { CheckCircleIcon } from '@heroicons/react/24/solid'
import Input from '../components/base/input' import Input from '../components/base/input'
@ -163,7 +164,7 @@ const ChangePasswordForm = () => {
</div> </div>
<div className="mx-auto mt-6 w-full"> <div className="mx-auto mt-6 w-full">
<Button variant='primary' className='w-full'> <Button variant='primary' className='w-full'>
<a href="/signin">{t('login.passwordChanged')}</a> <a href={`${basePath}/signin`}>{t('login.passwordChanged')}</a>
</Button> </Button>
</div> </div>
</div> </div>

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