mirror of https://github.com/langgenius/dify.git
Merge remote-tracking branch 'origin/main' into feat/queue-based-graph-engine
This commit is contained in:
commit
22ee318cf8
|
|
@ -1,6 +1,7 @@
|
|||
name: Run Pytest
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
name: DB Migration Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
name: Main CI Pipeline
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
checks: write
|
||||
|
||||
concurrency:
|
||||
group: main-ci-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# First, run autofix if needed
|
||||
autofix:
|
||||
name: Auto-fix code issues
|
||||
if: github.repository == 'langgenius/dify'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changes-made: ${{ steps.check-changes.outputs.changes }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Run Python fixes
|
||||
run: |
|
||||
cd api
|
||||
uv sync --dev
|
||||
# Fix lint errors
|
||||
uv run ruff check --fix-only .
|
||||
# Format code
|
||||
uv run ruff format .
|
||||
|
||||
- name: Run ast-grep
|
||||
run: |
|
||||
uvx --from ast-grep-cli sg --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all
|
||||
|
||||
- name: Run mdformat
|
||||
run: |
|
||||
uvx mdformat .
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
if [ -n "$(git diff --name-only)" ]; then
|
||||
echo "changes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.check-changes.outputs.changes == 'true'
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add -A
|
||||
git commit -m "Auto-fix: Apply code formatting and linting fixes"
|
||||
git push
|
||||
|
||||
# Check which paths were changed to determine which tests to run
|
||||
check-changes:
|
||||
name: Check Changed Files
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
api-changed: ${{ steps.changes.outputs.api }}
|
||||
web-changed: ${{ steps.changes.outputs.web }}
|
||||
vdb-changed: ${{ steps.changes.outputs.vdb }}
|
||||
migration-changed: ${{ steps.changes.outputs.migration }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
api:
|
||||
- 'api/**'
|
||||
- 'docker/**'
|
||||
- '.github/workflows/api-tests.yml'
|
||||
web:
|
||||
- 'web/**'
|
||||
vdb:
|
||||
- 'api/core/rag/datasource/**'
|
||||
- 'docker/**'
|
||||
- '.github/workflows/vdb-tests.yml'
|
||||
- 'api/uv.lock'
|
||||
- 'api/pyproject.toml'
|
||||
migration:
|
||||
- 'api/migrations/**'
|
||||
- '.github/workflows/db-migration-test.yml'
|
||||
|
||||
# After autofix completes (or if no changes needed), run tests in parallel
|
||||
api-tests:
|
||||
name: API Tests
|
||||
needs: [autofix, check-changes]
|
||||
if: always() && !cancelled() && needs.check-changes.outputs.api-changed == 'true'
|
||||
uses: ./.github/workflows/api-tests.yml
|
||||
|
||||
web-tests:
|
||||
name: Web Tests
|
||||
needs: [autofix, check-changes]
|
||||
if: always() && !cancelled() && needs.check-changes.outputs.web-changed == 'true'
|
||||
uses: ./.github/workflows/web-tests.yml
|
||||
|
||||
style-check:
|
||||
name: Style Check
|
||||
needs: autofix
|
||||
if: always() && !cancelled()
|
||||
uses: ./.github/workflows/style.yml
|
||||
|
||||
vdb-tests:
|
||||
name: VDB Tests
|
||||
needs: [autofix, check-changes]
|
||||
if: always() && !cancelled() && needs.check-changes.outputs.vdb-changed == 'true'
|
||||
uses: ./.github/workflows/vdb-tests.yml
|
||||
|
||||
db-migration-test:
|
||||
name: DB Migration Test
|
||||
needs: [autofix, check-changes]
|
||||
if: always() && !cancelled() && needs.check-changes.outputs.migration-changed == 'true'
|
||||
uses: ./.github/workflows/db-migration-test.yml
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
name: Style check
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
name: Run VDB Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
name: Web Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ from .app import (
|
|||
)
|
||||
|
||||
# Import auth controllers
|
||||
from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth
|
||||
from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth, oauth_server
|
||||
|
||||
# Import billing controllers
|
||||
from .billing import billing, compliance
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
from functools import wraps
|
||||
from typing import cast
|
||||
|
||||
import flask_login
|
||||
from flask import request
|
||||
from flask_restx import Resource, reqparse
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from libs.login import login_required
|
||||
from models.account import Account
|
||||
from models.model import OAuthProviderApp
|
||||
from services.oauth_server import OAUTH_ACCESS_TOKEN_EXPIRES_IN, OAuthGrantType, OAuthServerService
|
||||
|
||||
from .. import api
|
||||
|
||||
|
||||
def oauth_server_client_id_required(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("client_id", type=str, required=True, location="json")
|
||||
parsed_args = parser.parse_args()
|
||||
client_id = parsed_args.get("client_id")
|
||||
if not client_id:
|
||||
raise BadRequest("client_id is required")
|
||||
|
||||
oauth_provider_app = OAuthServerService.get_oauth_provider_app(client_id)
|
||||
if not oauth_provider_app:
|
||||
raise NotFound("client_id is invalid")
|
||||
|
||||
kwargs["oauth_provider_app"] = oauth_provider_app
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def oauth_server_access_token_required(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
oauth_provider_app = kwargs.get("oauth_provider_app")
|
||||
if not oauth_provider_app or not isinstance(oauth_provider_app, OAuthProviderApp):
|
||||
raise BadRequest("Invalid oauth_provider_app")
|
||||
|
||||
if not request.headers.get("Authorization"):
|
||||
raise BadRequest("Authorization is required")
|
||||
|
||||
authorization_header = request.headers.get("Authorization")
|
||||
if not authorization_header:
|
||||
raise BadRequest("Authorization header is required")
|
||||
|
||||
parts = authorization_header.split(" ")
|
||||
if len(parts) != 2:
|
||||
raise BadRequest("Invalid Authorization header format")
|
||||
|
||||
token_type = parts[0]
|
||||
if token_type != "Bearer":
|
||||
raise BadRequest("token_type is invalid")
|
||||
|
||||
access_token = parts[1]
|
||||
if not access_token:
|
||||
raise BadRequest("access_token is required")
|
||||
|
||||
account = OAuthServerService.validate_oauth_access_token(oauth_provider_app.client_id, access_token)
|
||||
if not account:
|
||||
raise BadRequest("access_token or client_id is invalid")
|
||||
|
||||
kwargs["account"] = account
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
class OAuthServerAppApi(Resource):
|
||||
@setup_required
|
||||
@oauth_server_client_id_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("redirect_uri", type=str, required=True, location="json")
|
||||
parsed_args = parser.parse_args()
|
||||
redirect_uri = parsed_args.get("redirect_uri")
|
||||
|
||||
# check if redirect_uri is valid
|
||||
if redirect_uri not in oauth_provider_app.redirect_uris:
|
||||
raise BadRequest("redirect_uri is invalid")
|
||||
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"app_icon": oauth_provider_app.app_icon,
|
||||
"app_label": oauth_provider_app.app_label,
|
||||
"scope": oauth_provider_app.scope,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OAuthServerUserAuthorizeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@oauth_server_client_id_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp):
|
||||
account = cast(Account, flask_login.current_user)
|
||||
user_account_id = account.id
|
||||
|
||||
code = OAuthServerService.sign_oauth_authorization_code(oauth_provider_app.client_id, user_account_id)
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"code": code,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OAuthServerUserTokenApi(Resource):
|
||||
@setup_required
|
||||
@oauth_server_client_id_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("grant_type", type=str, required=True, location="json")
|
||||
parser.add_argument("code", type=str, required=False, location="json")
|
||||
parser.add_argument("client_secret", type=str, required=False, location="json")
|
||||
parser.add_argument("redirect_uri", type=str, required=False, location="json")
|
||||
parser.add_argument("refresh_token", type=str, required=False, location="json")
|
||||
parsed_args = parser.parse_args()
|
||||
|
||||
grant_type = OAuthGrantType(parsed_args["grant_type"])
|
||||
|
||||
if grant_type == OAuthGrantType.AUTHORIZATION_CODE:
|
||||
if not parsed_args["code"]:
|
||||
raise BadRequest("code is required")
|
||||
|
||||
if parsed_args["client_secret"] != oauth_provider_app.client_secret:
|
||||
raise BadRequest("client_secret is invalid")
|
||||
|
||||
if parsed_args["redirect_uri"] not in oauth_provider_app.redirect_uris:
|
||||
raise BadRequest("redirect_uri is invalid")
|
||||
|
||||
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
|
||||
grant_type, code=parsed_args["code"], client_id=oauth_provider_app.client_id
|
||||
)
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
)
|
||||
elif grant_type == OAuthGrantType.REFRESH_TOKEN:
|
||||
if not parsed_args["refresh_token"]:
|
||||
raise BadRequest("refresh_token is required")
|
||||
|
||||
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
|
||||
grant_type, refresh_token=parsed_args["refresh_token"], client_id=oauth_provider_app.client_id
|
||||
)
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise BadRequest("invalid grant_type")
|
||||
|
||||
|
||||
class OAuthServerUserAccountApi(Resource):
|
||||
@setup_required
|
||||
@oauth_server_client_id_required
|
||||
@oauth_server_access_token_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp, account: Account):
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"name": account.name,
|
||||
"email": account.email,
|
||||
"avatar": account.avatar,
|
||||
"interface_language": account.interface_language,
|
||||
"timezone": account.timezone,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
api.add_resource(OAuthServerAppApi, "/oauth/provider")
|
||||
api.add_resource(OAuthServerUserAuthorizeApi, "/oauth/provider/authorize")
|
||||
api.add_resource(OAuthServerUserTokenApi, "/oauth/provider/token")
|
||||
api.add_resource(OAuthServerUserAccountApi, "/oauth/provider/account")
|
||||
|
|
@ -318,10 +318,6 @@ class DatasetApi(DatasetApiResource):
|
|||
except services.errors.account.NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
data = marshal(dataset, dataset_detail_fields)
|
||||
if data.get("permission") == "partial_members":
|
||||
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
|
||||
data.update({"partial_member_list": part_users_list})
|
||||
|
||||
# check embedding setting
|
||||
provider_manager = ProviderManager()
|
||||
assert isinstance(current_user, Account)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Callable, Generator, Mapping
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -368,7 +369,7 @@ class AdvancedChatAppGenerateTaskPipeline:
|
|||
) -> Generator[StreamResponse, None, None]:
|
||||
"""Handle node succeeded events."""
|
||||
# Record files if it's an answer node or end node
|
||||
if event.node_type in [NodeType.ANSWER, NodeType.END]:
|
||||
if event.node_type in [NodeType.ANSWER, NodeType.END, NodeType.LLM]:
|
||||
self._recorded_files.extend(
|
||||
self._workflow_response_converter.fetch_files_from_node_outputs(event.outputs or {})
|
||||
)
|
||||
|
|
@ -843,7 +844,14 @@ class AdvancedChatAppGenerateTaskPipeline:
|
|||
|
||||
def _save_message(self, *, session: Session, graph_runtime_state: Optional[GraphRuntimeState] = None) -> None:
|
||||
message = self._get_message(session=session)
|
||||
message.answer = self._task_state.answer
|
||||
|
||||
# If there are assistant files, remove markdown image links from answer
|
||||
answer_text = self._task_state.answer
|
||||
if self._recorded_files:
|
||||
# Remove markdown image links since we're storing files separately
|
||||
answer_text = re.sub(r"!\[.*?\]\(.*?\)", "", answer_text).strip()
|
||||
|
||||
message.answer = answer_text
|
||||
message.updated_at = naive_utc_now()
|
||||
message.provider_response_latency = time.perf_counter() - self._base_task_pipeline._start_at
|
||||
message.message_metadata = self._task_state.metadata.model_dump_json()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union, final
|
||||
|
||||
|
|
@ -14,6 +13,7 @@ from core.workflow.repositories.draft_variable_repository import (
|
|||
NoopDraftVariableSaver,
|
||||
)
|
||||
from factories import file_factory
|
||||
from libs.orjson import orjson_dumps
|
||||
from services.workflow_draft_variable_service import DraftVariableSaver as DraftVariableSaverImpl
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -174,7 +174,7 @@ class BaseAppGenerator:
|
|||
def gen():
|
||||
for message in generator:
|
||||
if isinstance(message, Mapping | dict):
|
||||
yield f"data: {json.dumps(message)}\n\n"
|
||||
yield f"data: {orjson_dumps(message)}\n\n"
|
||||
else:
|
||||
yield f"event: {message}\n\n"
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ def to_prompt_message_content(
|
|||
"url": _to_url(f) if dify_config.MULTIMODAL_SEND_FORMAT == "url" else "",
|
||||
"format": f.extension.removeprefix("."),
|
||||
"mime_type": f.mime_type,
|
||||
"filename": f.filename or "",
|
||||
}
|
||||
if f.type == FileType.IMAGE:
|
||||
params["detail"] = image_detail_config or ImagePromptMessageContent.DETAIL.LOW
|
||||
|
|
|
|||
|
|
@ -32,6 +32,65 @@ class TokenBufferMemory:
|
|||
self.conversation = conversation
|
||||
self.model_instance = model_instance
|
||||
|
||||
def _build_prompt_message_with_files(
|
||||
self, message_files: list[MessageFile], text_content: str, message: Message, app_record, is_user_message: bool
|
||||
) -> PromptMessage:
|
||||
"""
|
||||
Build prompt message with files.
|
||||
:param message_files: list of MessageFile objects
|
||||
:param text_content: text content of the message
|
||||
:param message: Message object
|
||||
:param app_record: app record
|
||||
:param is_user_message: whether this is a user message
|
||||
:return: PromptMessage
|
||||
"""
|
||||
if self.conversation.mode in {AppMode.AGENT_CHAT, AppMode.COMPLETION, AppMode.CHAT}:
|
||||
file_extra_config = FileUploadConfigManager.convert(self.conversation.model_config)
|
||||
elif self.conversation.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||
workflow_run = db.session.scalar(select(WorkflowRun).where(WorkflowRun.id == message.workflow_run_id))
|
||||
if not workflow_run:
|
||||
raise ValueError(f"Workflow run not found: {message.workflow_run_id}")
|
||||
workflow = db.session.scalar(select(Workflow).where(Workflow.id == workflow_run.workflow_id))
|
||||
if not workflow:
|
||||
raise ValueError(f"Workflow not found: {workflow_run.workflow_id}")
|
||||
file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False)
|
||||
else:
|
||||
raise AssertionError(f"Invalid app mode: {self.conversation.mode}")
|
||||
|
||||
detail = ImagePromptMessageContent.DETAIL.HIGH
|
||||
if file_extra_config and app_record:
|
||||
# Build files directly without filtering by belongs_to
|
||||
file_objs = [
|
||||
file_factory.build_from_message_file(
|
||||
message_file=message_file, tenant_id=app_record.tenant_id, config=file_extra_config
|
||||
)
|
||||
for message_file in message_files
|
||||
]
|
||||
if file_extra_config.image_config and file_extra_config.image_config.detail:
|
||||
detail = file_extra_config.image_config.detail
|
||||
else:
|
||||
file_objs = []
|
||||
|
||||
if not file_objs:
|
||||
if is_user_message:
|
||||
return UserPromptMessage(content=text_content)
|
||||
else:
|
||||
return AssistantPromptMessage(content=text_content)
|
||||
else:
|
||||
prompt_message_contents: list[PromptMessageContentUnionTypes] = []
|
||||
for file in file_objs:
|
||||
prompt_message = file_manager.to_prompt_message_content(
|
||||
file,
|
||||
image_detail_config=detail,
|
||||
)
|
||||
prompt_message_contents.append(prompt_message)
|
||||
prompt_message_contents.append(TextPromptMessageContent(data=text_content))
|
||||
|
||||
if is_user_message:
|
||||
return UserPromptMessage(content=prompt_message_contents)
|
||||
else:
|
||||
return AssistantPromptMessage(content=prompt_message_contents)
|
||||
|
||||
def get_history_prompt_messages(
|
||||
self, max_token_limit: int = 2000, message_limit: Optional[int] = None
|
||||
) -> Sequence[PromptMessage]:
|
||||
|
|
@ -40,82 +99,73 @@ class TokenBufferMemory:
|
|||
:param max_token_limit: max token limit
|
||||
:param message_limit: message limit
|
||||
"""
|
||||
with Session(db.engine) as session:
|
||||
app_record = self.conversation.app
|
||||
app_record = self.conversation.app
|
||||
|
||||
# fetch limited messages, and return reversed
|
||||
stmt = (
|
||||
select(Message)
|
||||
.where(Message.conversation_id == self.conversation.id)
|
||||
.order_by(Message.created_at.desc())
|
||||
# fetch limited messages, and return reversed
|
||||
stmt = (
|
||||
select(Message).where(Message.conversation_id == self.conversation.id).order_by(Message.created_at.desc())
|
||||
)
|
||||
|
||||
if message_limit and message_limit > 0:
|
||||
message_limit = min(message_limit, 500)
|
||||
else:
|
||||
message_limit = 500
|
||||
|
||||
stmt = stmt.limit(message_limit)
|
||||
|
||||
messages = db.session.scalars(stmt).all()
|
||||
|
||||
# instead of all messages from the conversation, we only need to extract messages
|
||||
# that belong to the thread of last message
|
||||
thread_messages = extract_thread_messages(messages)
|
||||
|
||||
# for newly created message, its answer is temporarily empty, we don't need to add it to memory
|
||||
if thread_messages and not thread_messages[0].answer and thread_messages[0].answer_tokens == 0:
|
||||
thread_messages.pop(0)
|
||||
|
||||
messages = list(reversed(thread_messages))
|
||||
|
||||
prompt_messages: list[PromptMessage] = []
|
||||
for message in messages:
|
||||
# Process user message with files
|
||||
user_files = (
|
||||
db.session.query(MessageFile)
|
||||
.where(
|
||||
MessageFile.message_id == message.id,
|
||||
(MessageFile.belongs_to == "user") | (MessageFile.belongs_to.is_(None)),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if message_limit and message_limit > 0:
|
||||
message_limit = min(message_limit, 500)
|
||||
if user_files:
|
||||
user_prompt_message = self._build_prompt_message_with_files(
|
||||
message_files=user_files,
|
||||
text_content=message.query,
|
||||
message=message,
|
||||
app_record=app_record,
|
||||
is_user_message=True,
|
||||
)
|
||||
prompt_messages.append(user_prompt_message)
|
||||
else:
|
||||
message_limit = 500
|
||||
prompt_messages.append(UserPromptMessage(content=message.query))
|
||||
|
||||
stmt = stmt.limit(message_limit)
|
||||
|
||||
messages = session.scalars(stmt).all()
|
||||
|
||||
# instead of all messages from the conversation, we only need to extract messages
|
||||
# that belong to the thread of last message
|
||||
thread_messages = extract_thread_messages(messages)
|
||||
|
||||
# for newly created message, its answer is temporarily empty, we don't need to add it to memory
|
||||
if thread_messages and not thread_messages[0].answer and thread_messages[0].answer_tokens == 0:
|
||||
thread_messages.pop(0)
|
||||
|
||||
messages = list(reversed(thread_messages))
|
||||
|
||||
prompt_messages: list[PromptMessage] = []
|
||||
for message in messages:
|
||||
files = session.query(MessageFile).where(MessageFile.message_id == message.id).all()
|
||||
if files:
|
||||
file_extra_config = None
|
||||
if self.conversation.mode in {AppMode.AGENT_CHAT, AppMode.COMPLETION, AppMode.CHAT}:
|
||||
file_extra_config = FileUploadConfigManager.convert(self.conversation.model_config)
|
||||
elif self.conversation.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||
workflow_run = session.scalar(
|
||||
select(WorkflowRun).where(WorkflowRun.id == message.workflow_run_id)
|
||||
)
|
||||
if not workflow_run:
|
||||
raise ValueError(f"Workflow run not found: {message.workflow_run_id}")
|
||||
workflow = session.scalar(select(Workflow).where(Workflow.id == workflow_run.workflow_id))
|
||||
if not workflow:
|
||||
raise ValueError(f"Workflow not found: {workflow_run.workflow_id}")
|
||||
file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False)
|
||||
else:
|
||||
raise AssertionError(f"Invalid app mode: {self.conversation.mode}")
|
||||
|
||||
detail = ImagePromptMessageContent.DETAIL.LOW
|
||||
if file_extra_config and app_record:
|
||||
file_objs = file_factory.build_from_message_files(
|
||||
message_files=files, tenant_id=app_record.tenant_id, config=file_extra_config
|
||||
)
|
||||
if file_extra_config.image_config and file_extra_config.image_config.detail:
|
||||
detail = file_extra_config.image_config.detail
|
||||
else:
|
||||
file_objs = []
|
||||
|
||||
if not file_objs:
|
||||
prompt_messages.append(UserPromptMessage(content=message.query))
|
||||
else:
|
||||
prompt_message_contents: list[PromptMessageContentUnionTypes] = []
|
||||
for file in file_objs:
|
||||
prompt_message = file_manager.to_prompt_message_content(
|
||||
file,
|
||||
image_detail_config=detail,
|
||||
)
|
||||
prompt_message_contents.append(prompt_message)
|
||||
prompt_message_contents.append(TextPromptMessageContent(data=message.query))
|
||||
|
||||
prompt_messages.append(UserPromptMessage(content=prompt_message_contents))
|
||||
|
||||
else:
|
||||
prompt_messages.append(UserPromptMessage(content=message.query))
|
||||
# Process assistant message with files
|
||||
assistant_files = (
|
||||
db.session.query(MessageFile)
|
||||
.where(MessageFile.message_id == message.id, MessageFile.belongs_to == "assistant")
|
||||
.all()
|
||||
)
|
||||
|
||||
if assistant_files:
|
||||
assistant_prompt_message = self._build_prompt_message_with_files(
|
||||
message_files=assistant_files,
|
||||
text_content=message.answer,
|
||||
message=message,
|
||||
app_record=app_record,
|
||||
is_user_message=False,
|
||||
)
|
||||
prompt_messages.append(assistant_prompt_message)
|
||||
else:
|
||||
prompt_messages.append(AssistantPromptMessage(content=message.answer))
|
||||
|
||||
if not prompt_messages:
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ class MultiModalPromptMessageContent(PromptMessageContent):
|
|||
base64_data: str = Field(default="", description="the base64 data of multi-modal file")
|
||||
url: str = Field(default="", description="the url of multi-modal file")
|
||||
mime_type: str = Field(default=..., description="the mime type of multi-modal file")
|
||||
filename: str = Field(default="", description="the filename of multi-modal file")
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
|
|
|
|||
|
|
@ -41,8 +41,14 @@ def build_from_message_file(
|
|||
"url": message_file.url,
|
||||
"id": message_file.id,
|
||||
"type": message_file.type,
|
||||
"upload_file_id": message_file.upload_file_id,
|
||||
}
|
||||
|
||||
# Set the correct ID field based on transfer method
|
||||
if message_file.transfer_method == FileTransferMethod.TOOL_FILE.value:
|
||||
mapping["tool_file_id"] = message_file.upload_file_id
|
||||
else:
|
||||
mapping["upload_file_id"] = message_file.upload_file_id
|
||||
|
||||
return build_from_mapping(
|
||||
mapping=mapping,
|
||||
tenant_id=tenant_id,
|
||||
|
|
@ -318,6 +324,11 @@ def _is_file_valid_with_config(
|
|||
file_transfer_method: FileTransferMethod,
|
||||
config: FileUploadConfig,
|
||||
) -> bool:
|
||||
# FIXME(QIN2DIM): Always allow tool files (files generated by the assistant/model)
|
||||
# These are internally generated and should bypass user upload restrictions
|
||||
if file_transfer_method == FileTransferMethod.TOOL_FILE:
|
||||
return True
|
||||
|
||||
if (
|
||||
config.allowed_file_types
|
||||
and input_file_type not in config.allowed_file_types
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
from typing import Any, Optional
|
||||
|
||||
import orjson
|
||||
|
||||
|
||||
def orjson_dumps(
|
||||
obj: Any,
|
||||
encoding: str = "utf-8",
|
||||
option: Optional[int] = None,
|
||||
) -> str:
|
||||
return orjson.dumps(obj, option=option).decode(encoding)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 8d289573e1da
|
||||
Revises: fa8b0fa6f407
|
||||
Create Date: 2025-08-20 17:47:17.015695
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import models as models
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8d289573e1da'
|
||||
down_revision = '0e154742a5fa'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('oauth_provider_apps',
|
||||
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('app_icon', sa.String(length=255), nullable=False),
|
||||
sa.Column('app_label', sa.JSON(), server_default='{}', nullable=False),
|
||||
sa.Column('client_id', sa.String(length=255), nullable=False),
|
||||
sa.Column('client_secret', sa.String(length=255), nullable=False),
|
||||
sa.Column('redirect_uris', sa.JSON(), server_default='[]', nullable=False),
|
||||
sa.Column('scope', sa.String(length=255), server_default=sa.text("'read:name read:email read:avatar read:interface_language read:timezone'"), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name='oauth_provider_app_pkey')
|
||||
)
|
||||
with op.batch_alter_table('oauth_provider_apps', schema=None) as batch_op:
|
||||
batch_op.create_index('oauth_provider_app_client_id_idx', ['client_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oauth_provider_apps', schema=None) as batch_op:
|
||||
batch_op.drop_index('oauth_provider_app_client_id_idx')
|
||||
|
||||
op.drop_table('oauth_provider_apps')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -580,6 +580,32 @@ class InstalledApp(Base):
|
|||
return tenant
|
||||
|
||||
|
||||
class OAuthProviderApp(Base):
|
||||
"""
|
||||
Globally shared OAuth provider app information.
|
||||
Only for Dify Cloud.
|
||||
"""
|
||||
|
||||
__tablename__ = "oauth_provider_apps"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="oauth_provider_app_pkey"),
|
||||
sa.Index("oauth_provider_app_client_id_idx", "client_id"),
|
||||
)
|
||||
|
||||
id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
|
||||
app_icon = mapped_column(String(255), nullable=False)
|
||||
app_label = mapped_column(sa.JSON, nullable=False, server_default="{}")
|
||||
client_id = mapped_column(String(255), nullable=False)
|
||||
client_secret = mapped_column(String(255), nullable=False)
|
||||
redirect_uris = mapped_column(sa.JSON, nullable=False, server_default="[]")
|
||||
scope = mapped_column(
|
||||
String(255),
|
||||
nullable=False,
|
||||
server_default=sa.text("'read:name read:email read:avatar read:interface_language read:timezone'"),
|
||||
)
|
||||
created_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)"))
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
__tablename__ = "conversations"
|
||||
__table_args__ = (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
import enum
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.account import Account
|
||||
from models.model import OAuthProviderApp
|
||||
from services.account_service import AccountService
|
||||
|
||||
|
||||
class OAuthGrantType(enum.StrEnum):
|
||||
AUTHORIZATION_CODE = "authorization_code"
|
||||
REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
|
||||
OAUTH_AUTHORIZATION_CODE_REDIS_KEY = "oauth_provider:{client_id}:authorization_code:{code}"
|
||||
OAUTH_ACCESS_TOKEN_REDIS_KEY = "oauth_provider:{client_id}:access_token:{token}"
|
||||
OAUTH_ACCESS_TOKEN_EXPIRES_IN = 60 * 60 * 12 # 12 hours
|
||||
OAUTH_REFRESH_TOKEN_REDIS_KEY = "oauth_provider:{client_id}:refresh_token:{token}"
|
||||
OAUTH_REFRESH_TOKEN_EXPIRES_IN = 60 * 60 * 24 * 30 # 30 days
|
||||
|
||||
|
||||
class OAuthServerService:
|
||||
@staticmethod
|
||||
def get_oauth_provider_app(client_id: str) -> OAuthProviderApp | None:
|
||||
query = select(OAuthProviderApp).where(OAuthProviderApp.client_id == client_id)
|
||||
|
||||
with Session(db.engine) as session:
|
||||
return session.execute(query).scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
def sign_oauth_authorization_code(client_id: str, user_account_id: str) -> str:
|
||||
code = str(uuid.uuid4())
|
||||
redis_key = OAUTH_AUTHORIZATION_CODE_REDIS_KEY.format(client_id=client_id, code=code)
|
||||
redis_client.set(redis_key, user_account_id, ex=60 * 10) # 10 minutes
|
||||
return code
|
||||
|
||||
@staticmethod
|
||||
def sign_oauth_access_token(
|
||||
grant_type: OAuthGrantType,
|
||||
code: str = "",
|
||||
client_id: str = "",
|
||||
refresh_token: str = "",
|
||||
) -> tuple[str, str]:
|
||||
match grant_type:
|
||||
case OAuthGrantType.AUTHORIZATION_CODE:
|
||||
redis_key = OAUTH_AUTHORIZATION_CODE_REDIS_KEY.format(client_id=client_id, code=code)
|
||||
user_account_id = redis_client.get(redis_key)
|
||||
if not user_account_id:
|
||||
raise BadRequest("invalid code")
|
||||
|
||||
# delete code
|
||||
redis_client.delete(redis_key)
|
||||
|
||||
access_token = OAuthServerService._sign_oauth_access_token(client_id, user_account_id)
|
||||
refresh_token = OAuthServerService._sign_oauth_refresh_token(client_id, user_account_id)
|
||||
return access_token, refresh_token
|
||||
case OAuthGrantType.REFRESH_TOKEN:
|
||||
redis_key = OAUTH_REFRESH_TOKEN_REDIS_KEY.format(client_id=client_id, token=refresh_token)
|
||||
user_account_id = redis_client.get(redis_key)
|
||||
if not user_account_id:
|
||||
raise BadRequest("invalid refresh token")
|
||||
|
||||
access_token = OAuthServerService._sign_oauth_access_token(client_id, user_account_id)
|
||||
return access_token, refresh_token
|
||||
|
||||
@staticmethod
|
||||
def _sign_oauth_access_token(client_id: str, user_account_id: str) -> str:
|
||||
token = str(uuid.uuid4())
|
||||
redis_key = OAUTH_ACCESS_TOKEN_REDIS_KEY.format(client_id=client_id, token=token)
|
||||
redis_client.set(redis_key, user_account_id, ex=OAUTH_ACCESS_TOKEN_EXPIRES_IN)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
def _sign_oauth_refresh_token(client_id: str, user_account_id: str) -> str:
|
||||
token = str(uuid.uuid4())
|
||||
redis_key = OAUTH_REFRESH_TOKEN_REDIS_KEY.format(client_id=client_id, token=token)
|
||||
redis_client.set(redis_key, user_account_id, ex=OAUTH_REFRESH_TOKEN_EXPIRES_IN)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
def validate_oauth_access_token(client_id: str, token: str) -> Account | None:
|
||||
redis_key = OAUTH_ACCESS_TOKEN_REDIS_KEY.format(client_id=client_id, token=token)
|
||||
user_account_id = redis_client.get(redis_key)
|
||||
if not user_account_id:
|
||||
return None
|
||||
|
||||
user_id_str = user_account_id.decode("utf-8")
|
||||
|
||||
return AccountService.load_user(user_id_str)
|
||||
64
api/uv.lock
64
api/uv.lock
|
|
@ -3963,40 +3963,40 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.10.18"
|
||||
version = "3.11.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c7/c54a948ce9a4278794f669a353551ce7db4ffb656c69a6e1f2264d563e50/orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8", size = 248929 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/60/a9c674ef1dd8ab22b5b10f9300e7e70444d4e3cda4b8258d6c2488c32143/orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d", size = 133364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/4e/f7d1bdd983082216e414e6d7ef897b0c2957f99c545826c06f371d52337e/orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7", size = 136995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/89/46b9181ba0ea251c9243b0c8ce29ff7c9796fa943806a9c8b02592fce8ea/orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a", size = 132894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/dd/7bce6fcc5b8c21aef59ba3c67f2166f0a1a9b0317dcca4a9d5bd7934ecfd/orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679", size = 137016 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/4a/b8aea1c83af805dcd31c1f03c95aabb3e19a016b2a4645dd822c5686e94d/orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947", size = 138290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d6/7eb05c85d987b688707f45dcf83c91abc2251e0dd9fb4f7be96514f838b1/orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4", size = 142829 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/78/ddd3ee7873f2b5f90f016bc04062713d567435c53ecc8783aab3a4d34915/orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334", size = 132805 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/09/c8e047f73d2c5d21ead9c180203e111cddeffc0848d5f0f974e346e21c8e/orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17", size = 135008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/4b/dccbf5055ef8fb6eda542ab271955fc1f9bf0b941a058490293f8811122b/orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e", size = 413419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f3/1eac0c5e2d6d6790bd2025ebfbefcbd37f0d097103d76f9b3f9302af5a17/orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b", size = 153292 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/b4/ef0abf64c8f1fabf98791819ab502c2c8c1dc48b786646533a93637d8999/orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7", size = 137182 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/a3/6ea878e7b4a0dc5c888d0370d7752dcb23f402747d10e2257478d69b5e63/orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1", size = 142695 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/2a/4048700a3233d562f0e90d5572a849baa18ae4e5ce4c3ba6247e4ece57b0/orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a", size = 134603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/45/10d934535a4993d27e1c84f1810e79ccf8b1b7418cef12151a22fe9bb1e1/orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5", size = 131400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/360674cd817faef32e49276187922a946468579fcaf37afdfb6c07046e92/orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f", size = 238238, upload-time = "2025-08-26T17:44:54.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/3d/5fa9ea4b34c1a13be7d9046ba98d06e6feb1d8853718992954ab59d16625/orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91", size = 127713, upload-time = "2025-08-26T17:44:55.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/5f/e18367823925e00b1feec867ff5f040055892fc474bf5f7875649ecfa586/orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904", size = 123241, upload-time = "2025-08-26T17:44:57.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/bd/3c66b91c4564759cf9f473251ac1650e446c7ba92a7c0f9f56ed54f9f0e6/orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6", size = 127895, upload-time = "2025-08-26T17:44:58.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/b5/dc8dcd609db4766e2967a85f63296c59d4722b39503e5b0bf7fd340d387f/orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d", size = 130303, upload-time = "2025-08-26T17:44:59.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c2/d58ec5fd1270b2aa44c862171891adc2e1241bd7dab26c8f46eb97c6c6f1/orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038", size = 132366, upload-time = "2025-08-26T17:45:00.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/87/0ef7e22eb8dd1ef940bfe3b9e441db519e692d62ed1aae365406a16d23d0/orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb", size = 135180, upload-time = "2025-08-26T17:45:02.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/6a/e5bf7b70883f374710ad74faf99bacfc4b5b5a7797c1d5e130350e0e28a3/orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2", size = 132741, upload-time = "2025-08-26T17:45:03.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/0c/4577fd860b6386ffaa56440e792af01c7882b56d2766f55384b5b0e9d39b/orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55", size = 131104, upload-time = "2025-08-26T17:45:04.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/4b/83e92b2d67e86d1c33f2ea9411742a714a26de63641b082bdbf3d8e481af/orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1", size = 403887, upload-time = "2025-08-26T17:45:06.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/e5/9eea6a14e9b5ceb4a271a1fd2e1dec5f2f686755c0fab6673dc6ff3433f4/orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824", size = 145855, upload-time = "2025-08-26T17:45:08.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/78/8d4f5ad0c80ba9bf8ac4d0fc71f93a7d0dc0844989e645e2074af376c307/orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f", size = 135361, upload-time = "2025-08-26T17:45:09.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5f/16386970370178d7a9b438517ea3d704efcf163d286422bae3b37b88dbb5/orjson-3.11.3-cp311-cp311-win32.whl", hash = "sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204", size = 136190, upload-time = "2025-08-26T17:45:10.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/60/db16c6f7a41dd8ac9fb651f66701ff2aeb499ad9ebc15853a26c7c152448/orjson-3.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b", size = 131389, upload-time = "2025-08-26T17:45:12.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/2a/bb811ad336667041dea9b8565c7c9faf2f59b47eb5ab680315eea612ef2e/orjson-3.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e", size = 126120, upload-time = "2025-08-26T17:45:13.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", size = 238259, upload-time = "2025-08-26T17:45:15.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", size = 127633, upload-time = "2025-08-26T17:45:16.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", size = 123061, upload-time = "2025-08-26T17:45:17.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", size = 127956, upload-time = "2025-08-26T17:45:19.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", size = 130790, upload-time = "2025-08-26T17:45:20.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", size = 132385, upload-time = "2025-08-26T17:45:22.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", size = 135305, upload-time = "2025-08-26T17:45:23.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", size = 132875, upload-time = "2025-08-26T17:45:25.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", size = 130940, upload-time = "2025-08-26T17:45:27.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", size = 403852, upload-time = "2025-08-26T17:45:28.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", size = 146293, upload-time = "2025-08-26T17:45:29.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", size = 135470, upload-time = "2025-08-26T17:45:31.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", size = 136248, upload-time = "2025-08-26T17:45:32.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", size = 131437, upload-time = "2025-08-26T17:45:34.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", size = 125978, upload-time = "2025-08-26T17:45:36.422Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT
|
||||
# The deployment edition, SELF_HOSTED
|
||||
NEXT_PUBLIC_EDITION=SELF_HOSTED
|
||||
# The base path for the application
|
||||
NEXT_PUBLIC_BASE_PATH=
|
||||
# The base URL of console application, refers to the Console base URL of WEB service if console domain is
|
||||
# different from api or web app domain.
|
||||
# example: http://cloud.dify.ai/console/api
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ RUN apk add --no-cache tzdata
|
|||
RUN corepack enable
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
ENV NEXT_PUBLIC_BASE_PATH=
|
||||
|
||||
|
||||
# install packages
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightUpLine, RiRobot2Line } from '@remixicon/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Button from '../components/base/button'
|
||||
import Avatar from './avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import { useCallback } from 'react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import Avatar from './avatar'
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
'use client'
|
||||
import Header from '@/app/signin/_header'
|
||||
|
||||
import cn from '@/utils/classnames'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export default function SignInLayout({ children }: any) {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
useDocumentTitle('')
|
||||
const isLoggedIn = useMemo(() => {
|
||||
try {
|
||||
return Boolean(localStorage.getItem('console_token') && localStorage.getItem('refresh_token'))
|
||||
}
|
||||
catch { return false }
|
||||
}, [])
|
||||
return <>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
<div className={cn('flex w-full shrink-0 flex-col items-center rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||
<Header />
|
||||
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
|
||||
<div className='flex flex-col md:w-[400px]'>
|
||||
{isLoggedIn ? <AppContextProvider>
|
||||
{children}
|
||||
</AppContextProvider>
|
||||
: children}
|
||||
</div>
|
||||
</div>
|
||||
{systemFeatures.branding.enabled === false && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
|
||||
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
|
||||
import {
|
||||
RiAccountCircleLine,
|
||||
RiGlobalLine,
|
||||
RiInfoCardLine,
|
||||
RiMailLine,
|
||||
RiTranslate2,
|
||||
} from '@remixicon/react'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export const OAUTH_AUTHORIZE_PENDING_KEY = 'oauth_authorize_pending'
|
||||
export const REDIRECT_URL_KEY = 'oauth_redirect_url'
|
||||
|
||||
const OAUTH_AUTHORIZE_PENDING_TTL = 60 * 3
|
||||
|
||||
function setItemWithExpiry(key: string, value: string, ttl: number) {
|
||||
const item = {
|
||||
value,
|
||||
expiry: dayjs().add(ttl, 'seconds').unix(),
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(item))
|
||||
}
|
||||
|
||||
function buildReturnUrl(pathname: string, search: string) {
|
||||
try {
|
||||
const base = `${globalThis.location.origin}${pathname}${search}`
|
||||
return base
|
||||
}
|
||||
catch {
|
||||
return pathname + search
|
||||
}
|
||||
}
|
||||
|
||||
export default function OAuthAuthorize() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const SCOPE_INFO_MAP: Record<string, { icon: React.ComponentType<{ className?: string }>, label: string }> = {
|
||||
'read:name': {
|
||||
icon: RiInfoCardLine,
|
||||
label: t('oauth.scopes.name'),
|
||||
},
|
||||
'read:email': {
|
||||
icon: RiMailLine,
|
||||
label: t('oauth.scopes.email'),
|
||||
},
|
||||
'read:avatar': {
|
||||
icon: RiAccountCircleLine,
|
||||
label: t('oauth.scopes.avatar'),
|
||||
},
|
||||
'read:interface_language': {
|
||||
icon: RiTranslate2,
|
||||
label: t('oauth.scopes.languagePreference'),
|
||||
},
|
||||
'read:timezone': {
|
||||
icon: RiGlobalLine,
|
||||
label: t('oauth.scopes.timezone'),
|
||||
},
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const language = useLanguage()
|
||||
const searchParams = useSearchParams()
|
||||
const client_id = decodeURIComponent(searchParams.get('client_id') || '')
|
||||
const redirect_uri = decodeURIComponent(searchParams.get('redirect_uri') || '')
|
||||
const { userProfile } = useAppContext()
|
||||
const { data: authAppInfo, isLoading, isError } = useOAuthAppInfo(client_id, redirect_uri)
|
||||
const { mutateAsync: authorize, isPending: authorizing } = useAuthorizeOAuthApp()
|
||||
const hasNotifiedRef = useRef(false)
|
||||
|
||||
const isLoggedIn = useMemo(() => {
|
||||
try {
|
||||
return Boolean(localStorage.getItem('console_token') && localStorage.getItem('refresh_token'))
|
||||
}
|
||||
catch { return false }
|
||||
}, [])
|
||||
|
||||
const onLoginSwitchClick = () => {
|
||||
try {
|
||||
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
|
||||
setItemWithExpiry(OAUTH_AUTHORIZE_PENDING_KEY, returnUrl, OAUTH_AUTHORIZE_PENDING_TTL)
|
||||
router.push(`/signin?${REDIRECT_URL_KEY}=${encodeURIComponent(returnUrl)}`)
|
||||
}
|
||||
catch {
|
||||
router.push('/signin')
|
||||
}
|
||||
}
|
||||
|
||||
const onAuthorize = async () => {
|
||||
if (!client_id || !redirect_uri)
|
||||
return
|
||||
try {
|
||||
const { code } = await authorize({ client_id })
|
||||
const url = new URL(redirect_uri)
|
||||
url.searchParams.set('code', code)
|
||||
globalThis.location.href = url.toString()
|
||||
}
|
||||
catch (err: any) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: `${t('oauth.error.authorizeFailed')}: ${err.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const invalidParams = !client_id || !redirect_uri
|
||||
if ((invalidParams || isError) && !hasNotifiedRef.current) {
|
||||
hasNotifiedRef.current = true
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: invalidParams ? t('oauth.error.invalidParams') : t('oauth.error.authAppInfoFetchFailed'),
|
||||
duration: 0,
|
||||
})
|
||||
}
|
||||
}, [client_id, redirect_uri, isError])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='bg-background-default-subtle'>
|
||||
<Loading type='app' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-background-default-subtle'>
|
||||
{authAppInfo?.app_icon && (
|
||||
<div className='w-max rounded-2xl border-[0.5px] border-components-panel-border bg-text-primary-on-surface p-3 shadow-lg'>
|
||||
<img src={authAppInfo.app_icon} alt='app icon' className='h-10 w-10 rounded' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
|
||||
<div className='title-4xl-semi-bold'>
|
||||
{isLoggedIn && <div className='text-text-primary'>{t('oauth.connect')}</div>}
|
||||
<div className='text-[var(--color-saas-dify-blue-inverted)]'>{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')}</div>
|
||||
{!isLoggedIn && <div className='text-text-primary'>{t('oauth.tips.notLoggedIn')}</div>}
|
||||
</div>
|
||||
<div className='body-md-regular text-text-secondary'>{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')} ${t('oauth.tips.loggedIn')}` : t('oauth.tips.needLogin')}</div>
|
||||
</div>
|
||||
|
||||
{isLoggedIn && userProfile && (
|
||||
<div className='flex items-center justify-between rounded-xl bg-background-section-burn-inverted p-3'>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
<div>
|
||||
<div className='system-md-semi-bold text-text-secondary'>{userProfile.name}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{userProfile.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant='tertiary' size='small' onClick={onLoginSwitchClick}>{t('oauth.switchAccount')}</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoggedIn && Boolean(authAppInfo?.scope) && (
|
||||
<div className='mt-2 flex flex-col gap-2.5 rounded-xl bg-background-section-burn-inverted px-[22px] py-5 text-text-secondary'>
|
||||
{authAppInfo!.scope.split(/\s+/).filter(Boolean).map((scope: string) => {
|
||||
const Icon = SCOPE_INFO_MAP[scope]
|
||||
return (
|
||||
<div key={scope} className='body-sm-medium flex items-center gap-2 text-text-secondary'>
|
||||
{Icon ? <Icon.icon className='h-4 w-4' /> : <RiAccountCircleLine className='h-4 w-4' />}
|
||||
{Icon.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex flex-col items-center gap-2 pt-4'>
|
||||
{!isLoggedIn ? (
|
||||
<Button variant='primary' size='large' className='w-full' onClick={onLoginSwitchClick}>{t('oauth.login')}</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button variant='primary' size='large' className='w-full' onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('oauth.continue')}</Button>
|
||||
<Button size='large' className='w-full' onClick={() => router.push('/apps')}>{t('common.operation.cancel')}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-4 py-2'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="1" viewBox="0 0 400 1" fill="none">
|
||||
<path d="M0 0.5H400" stroke="url(#paint0_linear_2_5904)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2_5904" x1="400" y1="9.49584" x2="0.000228929" y2="9.17666" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.01" />
|
||||
<stop offset="0.505" stop-color="#101828" stop-opacity="0.08" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.01" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-3 text-text-tertiary'>{t('oauth.tips.common')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import {
|
|||
FloatingPortal,
|
||||
autoUpdate,
|
||||
flip,
|
||||
hide,
|
||||
offset,
|
||||
shift,
|
||||
size,
|
||||
|
|
@ -39,7 +40,7 @@ export function usePortalToFollowElem({
|
|||
triggerPopupSameWidth,
|
||||
}: PortalToFollowElemOptions = {}) {
|
||||
const setOpen = setControlledOpen
|
||||
|
||||
const container = document.getElementById('workflow-container') || document.body
|
||||
const data = useFloating({
|
||||
placement,
|
||||
open,
|
||||
|
|
@ -50,9 +51,17 @@ export function usePortalToFollowElem({
|
|||
flip({
|
||||
crossAxis: placement.includes('-'),
|
||||
fallbackAxisSideDirection: 'start',
|
||||
padding: 5,
|
||||
padding: 8,
|
||||
}),
|
||||
shift({
|
||||
padding: 8,
|
||||
boundary: container,
|
||||
altBoundary: true,
|
||||
}),
|
||||
hide({
|
||||
// hide when the reference element is not visible
|
||||
boundary: container,
|
||||
}),
|
||||
shift({ padding: 5 }),
|
||||
size({
|
||||
apply({ rects, elements }) {
|
||||
if (triggerPopupSameWidth)
|
||||
|
|
@ -133,9 +142,9 @@ export const PortalToFollowElemTrigger = (
|
|||
context.getReferenceProps({
|
||||
ref,
|
||||
...props,
|
||||
...children.props,
|
||||
...(children.props || {}),
|
||||
'data-state': context.open ? 'open' : 'closed',
|
||||
}),
|
||||
} as React.HTMLProps<HTMLElement>),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -177,6 +186,7 @@ export const PortalToFollowElemContent = (
|
|||
style={{
|
||||
...context.floatingStyles,
|
||||
...style,
|
||||
visibility: context.middlewareData.hide?.referenceHidden ? 'hidden' : 'visible',
|
||||
}}
|
||||
{...context.getFloatingProps(props)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ type TagInputProps = {
|
|||
customizedConfirmKey?: 'Enter' | 'Tab'
|
||||
isInWorkflow?: boolean
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
const TagInput: FC<TagInputProps> = ({
|
||||
|
|
@ -24,6 +25,7 @@ const TagInput: FC<TagInputProps> = ({
|
|||
customizedConfirmKey = 'Enter',
|
||||
isInWorkflow,
|
||||
placeholder,
|
||||
required = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
|
|
@ -42,7 +44,8 @@ const TagInput: FC<TagInputProps> = ({
|
|||
const handleNewTag = useCallback((value: string) => {
|
||||
const valueTrimmed = value.trim()
|
||||
if (!valueTrimmed) {
|
||||
notify({ type: 'error', message: t('datasetDocuments.segment.keywordEmpty') })
|
||||
if (required)
|
||||
notify({ type: 'error', message: t('datasetDocuments.segment.keywordEmpty') })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +63,7 @@ const TagInput: FC<TagInputProps> = ({
|
|||
setTimeout(() => {
|
||||
setValue('')
|
||||
})
|
||||
}, [items, onChange, notify, t])
|
||||
}, [items, onChange, notify, t, required])
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (isSpecialMode && e.key === 'Enter')
|
||||
|
|
|
|||
|
|
@ -56,12 +56,11 @@ const Toast = ({
|
|||
'top-0',
|
||||
'right-0',
|
||||
)}>
|
||||
<div className={`absolute inset-0 -z-10 opacity-40 ${
|
||||
(type === 'success' && 'bg-toast-success-bg')
|
||||
<div className={`absolute inset-0 -z-10 opacity-40 ${(type === 'success' && 'bg-toast-success-bg')
|
||||
|| (type === 'warning' && 'bg-toast-warning-bg')
|
||||
|| (type === 'error' && 'bg-toast-error-bg')
|
||||
|| (type === 'info' && 'bg-toast-info-bg')
|
||||
}`}
|
||||
}`}
|
||||
/>
|
||||
<div className={`flex ${size === 'md' ? 'gap-1' : 'gap-0.5'}`}>
|
||||
<div className={`flex items-center justify-center ${size === 'md' ? 'p-0.5' : 'p-1'}`}>
|
||||
|
|
@ -162,7 +161,9 @@ Toast.notify = ({
|
|||
</ToastContext.Provider>,
|
||||
)
|
||||
document.body.appendChild(holder)
|
||||
setTimeout(toastHandler.clear, duration || defaultDuring)
|
||||
const d = duration ?? defaultDuring
|
||||
if (d > 0)
|
||||
setTimeout(toastHandler.clear, d)
|
||||
}
|
||||
|
||||
return toastHandler
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ const ParameterItem: FC<ParameterItemProps> = ({
|
|||
onChange={handleTagChange}
|
||||
customizedConfirmKey='Tab'
|
||||
isInWorkflow={isInWorkflow}
|
||||
required={parameterRule.required}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
} from '@/app/education-apply/constants'
|
||||
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
|
||||
|
||||
type SwrInitializerProps = {
|
||||
children: ReactNode
|
||||
|
|
@ -63,7 +64,11 @@ const SwrInitializer = ({
|
|||
if (searchParams.has('access_token') || searchParams.has('refresh_token')) {
|
||||
consoleToken && localStorage.setItem('console_token', consoleToken)
|
||||
refreshToken && localStorage.setItem('refresh_token', refreshToken)
|
||||
router.replace(pathname)
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
if (redirectUrl)
|
||||
location.replace(redirectUrl)
|
||||
else
|
||||
router.replace(pathname)
|
||||
}
|
||||
|
||||
setInit(true)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const Field: FC<Props> = ({
|
|||
<div className={cn(className, inline && 'flex w-full items-center justify-between')}>
|
||||
<div
|
||||
onClick={() => supportFold && toggleFold()}
|
||||
className={cn('flex items-center justify-between', supportFold && 'cursor-pointer')}>
|
||||
className={cn('sticky top-0 z-10 flex items-center justify-between bg-components-panel-bg', supportFold && 'cursor-pointer')}>
|
||||
<div className='flex h-6 items-center'>
|
||||
<div className={cn(isSubTitle ? 'system-xs-medium-uppercase text-text-tertiary' : 'system-sm-semibold-uppercase text-text-secondary')}>
|
||||
{title} {required && <span className='text-text-destructive'>*</span>}
|
||||
|
|
|
|||
|
|
@ -418,9 +418,8 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
}
|
||||
<Split />
|
||||
</div>
|
||||
|
||||
{tabType === TabType.settings && (
|
||||
<>
|
||||
<div className='flex-1 overflow-y-auto'>
|
||||
<div>
|
||||
{cloneElement(children as any, {
|
||||
id,
|
||||
|
|
@ -465,7 +464,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tabType === TabType.lastRun && (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Input from '@/app/components/base/input'
|
|||
import Toast from '@/app/components/base/toast'
|
||||
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
|
||||
export default function CheckCode() {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -43,7 +44,13 @@ export default function CheckCode() {
|
|||
if (ret.result === 'success') {
|
||||
localStorage.setItem('console_token', ret.data.access_token)
|
||||
localStorage.setItem('refresh_token', ret.data.refresh_token)
|
||||
router.replace(invite_token ? `/signin/invite-settings?${searchParams.toString()}` : '/apps')
|
||||
if (invite_token) {
|
||||
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
||||
}
|
||||
else {
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/apps')
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) { console.error(error) }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { login } from '@/service/common'
|
|||
import Input from '@/app/components/base/input'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { noop } from 'lodash-es'
|
||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
|
||||
type MailAndPasswordAuthProps = {
|
||||
isInvite: boolean
|
||||
|
|
@ -74,7 +75,8 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
|||
else {
|
||||
localStorage.setItem('console_token', res.data.access_token)
|
||||
localStorage.setItem('refresh_token', res.data.refresh_token)
|
||||
router.replace('/apps')
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/apps')
|
||||
}
|
||||
}
|
||||
else if (res.code === 'account_not_found') {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import Loading from '@/app/components/base/loading'
|
|||
import Toast from '@/app/components/base/toast'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
|
||||
export default function InviteSettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -60,7 +61,8 @@ export default function InviteSettingsPage() {
|
|||
localStorage.setItem('console_token', res.data.access_token)
|
||||
localStorage.setItem('refresh_token', res.data.refresh_token)
|
||||
await setLocaleOnClient(language, false)
|
||||
router.replace('/apps')
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/apps')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default function SignInLayout({ children }: any) {
|
|||
useDocumentTitle('')
|
||||
return <>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||
<div className={cn('flex w-full shrink-0 flex-col items-center rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||
<Header />
|
||||
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
|
||||
<div className='flex flex-col md:w-[400px]'>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { LicenseStatus } from '@/types/feature'
|
|||
import Toast from '@/app/components/base/toast'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { resolvePostLoginRedirect } from './utils/post-login-redirect'
|
||||
|
||||
const NormalForm = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -37,7 +38,8 @@ const NormalForm = () => {
|
|||
if (consoleToken && refreshToken) {
|
||||
localStorage.setItem('console_token', consoleToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
router.replace('/apps')
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/apps')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { OAUTH_AUTHORIZE_PENDING_KEY, REDIRECT_URL_KEY } from '@/app/account/oauth/authorize/page'
|
||||
import dayjs from 'dayjs'
|
||||
import type { ReadonlyURLSearchParams } from 'next/navigation'
|
||||
|
||||
function getItemWithExpiry(key: string): string | null {
|
||||
const itemStr = localStorage.getItem(key)
|
||||
if (!itemStr)
|
||||
return null
|
||||
|
||||
try {
|
||||
const item = JSON.parse(itemStr)
|
||||
localStorage.removeItem(key)
|
||||
if (!item?.value) return null
|
||||
|
||||
return dayjs().unix() > item.expiry ? null : item.value
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const resolvePostLoginRedirect = (searchParams: ReadonlyURLSearchParams) => {
|
||||
const redirectUrl = searchParams.get(REDIRECT_URL_KEY)
|
||||
if (redirectUrl) {
|
||||
try {
|
||||
localStorage.removeItem(OAUTH_AUTHORIZE_PENDING_KEY)
|
||||
return decodeURIComponent(redirectUrl)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to decode redirect URL:', e)
|
||||
return redirectUrl
|
||||
}
|
||||
}
|
||||
|
||||
return getItemWithExpiry(OAUTH_AUTHORIZE_PENDING_KEY)
|
||||
}
|
||||
|
|
@ -24,13 +24,13 @@ export type AppContextValue = {
|
|||
}
|
||||
|
||||
const userProfilePlaceholder = {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: false,
|
||||
}
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: false,
|
||||
}
|
||||
|
||||
const initialLangGeniusVersionInfo = {
|
||||
current_env: '',
|
||||
|
|
@ -96,13 +96,13 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
|||
const versionData = await fetchLangGeniusVersion({ url: '/version', params: { current_version } })
|
||||
setLangGeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
|
||||
}
|
||||
catch (error) {
|
||||
catch (error) {
|
||||
console.error('Failed to update user profile:', error)
|
||||
if (userProfile.id === '')
|
||||
setUserProfile(userProfilePlaceholder)
|
||||
}
|
||||
}
|
||||
else if (userProfileError && userProfile.id === '') {
|
||||
else if (userProfileError && userProfile.id === '') {
|
||||
setUserProfile(userProfilePlaceholder)
|
||||
}
|
||||
}, [userProfileResponse, userProfileError, userProfile.id])
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ set -e
|
|||
|
||||
export NEXT_PUBLIC_DEPLOY_ENV=${DEPLOY_ENV}
|
||||
export NEXT_PUBLIC_EDITION=${EDITION}
|
||||
export NEXT_PUBLIC_BASE_PATH=${NEXT_PUBLIC_BASE_PATH}
|
||||
export NEXT_PUBLIC_API_PREFIX=${CONSOLE_API_URL}/console/api
|
||||
export NEXT_PUBLIC_PUBLIC_API_PREFIX=${APP_API_URL}/api
|
||||
export NEXT_PUBLIC_MARKETPLACE_API_PREFIX=${MARKETPLACE_API_URL}/api/v1
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const NAMESPACES = [
|
|||
'explore',
|
||||
'layout',
|
||||
'login',
|
||||
'oauth',
|
||||
'plugin-tags',
|
||||
'plugin',
|
||||
'register',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
common: 'Wir respektieren Ihre Privatsphäre und werden diese Informationen nur verwenden, um Ihre Erfahrung mit unseren Entwickler-Tools zu verbessern.',
|
||||
notLoggedIn: 'möchte auf Ihr Dify Cloud-Konto zugreifen',
|
||||
loggedIn: 'möchte auf die folgenden Informationen aus Ihrem Dify Cloud-Konto zugreifen.',
|
||||
needLogin: 'Bitte melden Sie sich an, um zu autorisieren.',
|
||||
},
|
||||
scopes: {
|
||||
avatar: 'Avatar',
|
||||
timezone: 'Zeitzone',
|
||||
name: 'Name',
|
||||
email: 'E-Mail',
|
||||
languagePreference: 'Sprachauswahl',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'Ungültige Parameter',
|
||||
authAppInfoFetchFailed: 'Fehler beim Abrufen der App-Informationen für die Autorisierung',
|
||||
authorizeFailed: 'Autorisierung fehlgeschlagen',
|
||||
},
|
||||
switchAccount: 'Konto wechseln',
|
||||
login: 'Anmelden',
|
||||
unknownApp: 'Unbekannte App',
|
||||
continue: 'Fortsetzen',
|
||||
connect: 'Verbinde zu',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
loggedIn: 'wants to access the following information from your Dify Cloud account.',
|
||||
notLoggedIn: 'wants to access your Dify Cloud account',
|
||||
needLogin: 'Please log in to authorize',
|
||||
common: 'We respect your privacy and will only use this information to enhance your experience with our developer tools.',
|
||||
},
|
||||
connect: 'Connect to',
|
||||
continue: 'Continue',
|
||||
switchAccount: 'Switch Account',
|
||||
login: 'Login',
|
||||
scopes: {
|
||||
name: 'Name',
|
||||
email: 'Email',
|
||||
avatar: 'Avatar',
|
||||
languagePreference: 'Language Preference',
|
||||
timezone: 'Timezone',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'Invalid parameters',
|
||||
authorizeFailed: 'Authorize failed',
|
||||
authAppInfoFetchFailed: 'Failed to fetch app info for authorization',
|
||||
},
|
||||
unknownApp: 'Unknown App',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'Por favor inicie sesión para autorizar',
|
||||
notLoggedIn: 'quiere acceder a su cuenta de Dify Cloud',
|
||||
loggedIn: 'quiere acceder a la siguiente información de su cuenta de Dify Cloud.',
|
||||
common: 'Respetamos su privacidad y solo utilizaremos esta información para mejorar su experiencia con nuestras herramientas para desarrolladores.',
|
||||
},
|
||||
scopes: {
|
||||
avatar: 'Avatar',
|
||||
name: 'Nombre',
|
||||
timezone: 'Zona horaria',
|
||||
languagePreference: 'Preferencia de idioma',
|
||||
email: 'Correo electrónico',
|
||||
},
|
||||
error: {
|
||||
authAppInfoFetchFailed: 'No se pudo obtener la información de la aplicación para la autorización',
|
||||
authorizeFailed: 'La autorización falló',
|
||||
invalidParams: 'Parámetros inválidos',
|
||||
},
|
||||
continue: 'Continuar',
|
||||
unknownApp: 'Aplicación Desconocida',
|
||||
switchAccount: 'Cambiar de cuenta',
|
||||
login: 'Iniciar sesión',
|
||||
connect: 'Conectar a',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'لطفاً برای تأیید وارد شوید',
|
||||
notLoggedIn: 'میخواهد به حساب Dify Cloud شما دسترسی پیدا کند',
|
||||
loggedIn: 'میخواهد به اطلاعات زیر از حساب ابر دیفی شما دسترسی پیدا کند.',
|
||||
common: 'ما به حریم خصوصی شما احترام میگذاریم و تنها از این اطلاعات برای بهبود تجربه شما با ابزارهای توسعهدهندهمان استفاده خواهیم کرد.',
|
||||
},
|
||||
scopes: {
|
||||
name: 'نام',
|
||||
avatar: 'آواتار',
|
||||
timezone: 'منطقه زمانی',
|
||||
email: 'ایمیل',
|
||||
languagePreference: 'ترجیحات زبانی',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'پارامترهای نامعتبر',
|
||||
authAppInfoFetchFailed: 'عدم موفقیت در دریافت اطلاعات برنامه برای مجوز',
|
||||
authorizeFailed: 'احراز هویت ناموفق بود',
|
||||
},
|
||||
login: 'ورود',
|
||||
connect: 'متصل به',
|
||||
continue: 'ادامه دهید',
|
||||
unknownApp: 'برنامه نامشخص',
|
||||
switchAccount: 'تغییر حساب',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'Veuillez vous connecter pour autoriser',
|
||||
notLoggedIn: 'veut accéder à votre compte Dify Cloud',
|
||||
common: 'Nous respectons votre vie privée et n\'utiliserons ces informations que pour améliorer votre expérience avec nos outils de développement.',
|
||||
loggedIn: 'veut accéder aux informations suivantes de votre compte Dify Cloud.',
|
||||
},
|
||||
scopes: {
|
||||
email: 'E-mail',
|
||||
name: 'Nom',
|
||||
timezone: 'Fuseau horaire',
|
||||
avatar: 'Avatar',
|
||||
languagePreference: 'Préférence de langue',
|
||||
},
|
||||
error: {
|
||||
authAppInfoFetchFailed: 'Échec de la récupération des informations de l\'application pour l\'autorisation',
|
||||
invalidParams: 'Paramètres invalides',
|
||||
authorizeFailed: 'Autorisation échouée',
|
||||
},
|
||||
switchAccount: 'Changer de compte',
|
||||
login: 'Connexion',
|
||||
unknownApp: 'Application inconnue',
|
||||
continue: 'Continuer',
|
||||
connect: 'Se connecter à',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'कृपया प्राधिकरण के लिए लॉग इन करें',
|
||||
notLoggedIn: 'आप आपके Dify Cloud खाते तक पहुंचना चाहते हैं',
|
||||
common: 'हम आपकी गोपनीयता का सम्मान करते हैं और इस जानकारी का उपयोग केवल आपके हमारे विकास उपकरणों के साथ अनुभव को बेहतर बनाने के लिए करेंगे।',
|
||||
loggedIn: 'आप आपके Dify Cloud खाते से निम्नलिखित जानकारी तक पहुंचना चाहते हैं।',
|
||||
},
|
||||
scopes: {
|
||||
name: 'नाम',
|
||||
avatar: 'अवतार',
|
||||
email: 'ईमेल',
|
||||
languagePreference: 'भाषा चयन',
|
||||
timezone: 'समय क्षेत्र',
|
||||
},
|
||||
error: {
|
||||
authorizeFailed: 'अनु autorización विफल',
|
||||
invalidParams: 'अमान्य पैरामीटर',
|
||||
authAppInfoFetchFailed: 'प्राधिकरण के लिए ऐप जानकारी प्राप्त करने में असफल हुआ',
|
||||
},
|
||||
connect: 'संयोजित करें',
|
||||
switchAccount: 'खाता बदलें',
|
||||
unknownApp: 'अनजान ऐप',
|
||||
login: 'लॉगइन',
|
||||
continue: 'जारी रखें',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
notLoggedIn: 'vuole accedere al tuo account Dify Cloud',
|
||||
loggedIn: 'vuole accedere alle seguenti informazioni dal tuo account Dify Cloud.',
|
||||
common: 'Rispettiamo la tua privacy e utilizzeremo queste informazioni solo per migliorare la tua esperienza con i nostri strumenti per sviluppatori.',
|
||||
needLogin: 'Per favore, accedi per autorizzare',
|
||||
},
|
||||
scopes: {
|
||||
email: 'Email',
|
||||
languagePreference: 'Preferenza Linguistica',
|
||||
name: 'Nome',
|
||||
timezone: 'Fuso orario',
|
||||
avatar: 'Avatar',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'Parametri non validi',
|
||||
authorizeFailed: 'Autorizzazione fallita',
|
||||
authAppInfoFetchFailed: 'Impossibile recuperare le informazioni sull\'app per l\'autorizzazione',
|
||||
},
|
||||
switchAccount: 'Cambia account',
|
||||
login: 'Accesso',
|
||||
unknownApp: 'App sconosciuta',
|
||||
connect: 'Connetti a',
|
||||
continue: 'Continua',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
notLoggedIn: 'あなたのDify Cloudアカウントにアクセスしたいです',
|
||||
needLogin: 'ログインして認証してください',
|
||||
loggedIn: 'あなたのDify Cloudアカウントから以下の情報にアクセスしたいと思っています。',
|
||||
common: '私たちはあなたのプライバシーを尊重し、この情報を私たちの開発者ツールによる体験を向上させるためにのみ使用します。',
|
||||
},
|
||||
scopes: {
|
||||
email: 'メール',
|
||||
languagePreference: '言語の好み',
|
||||
timezone: 'タイムゾーン',
|
||||
name: '名前',
|
||||
avatar: 'アバター',
|
||||
},
|
||||
error: {
|
||||
authorizeFailed: '認証に失敗しました',
|
||||
invalidParams: '無効なパラメータ',
|
||||
authAppInfoFetchFailed: '認証のためのアプリ情報の取得に失敗しました',
|
||||
},
|
||||
unknownApp: '未知のアプリ',
|
||||
login: 'ログイン',
|
||||
switchAccount: 'アカウントを切り替える',
|
||||
continue: '続けてください',
|
||||
connect: '接続する',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: '로그인하여 인증해 주세요.',
|
||||
notLoggedIn: 'Dify Cloud 계정에 접근하고 싶어합니다.',
|
||||
loggedIn: '다음 정보를 귀하의 Dify Cloud 계정에서 액세스하려고 합니다.',
|
||||
common: '우리는 귀하의 개인 정보를 존중하며, 이 정보를 개발자 도구를 통한 귀하의 경험 향상에만 사용할 것입니다.',
|
||||
},
|
||||
scopes: {
|
||||
avatar: '아바타',
|
||||
email: '이메일',
|
||||
name: '이름',
|
||||
languagePreference: '언어 선호',
|
||||
timezone: '시간대',
|
||||
},
|
||||
error: {
|
||||
invalidParams: '유효하지 않은 매개변수',
|
||||
authorizeFailed: '권한 부여 실패',
|
||||
authAppInfoFetchFailed: '인증을 위한 앱 정보를 가져오지 못했습니다.',
|
||||
},
|
||||
continue: '계속하다',
|
||||
unknownApp: '알 수 없는 앱',
|
||||
switchAccount: '계정 전환',
|
||||
login: '로그인',
|
||||
connect: '연결하다',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'Proszę się zalogować, aby autoryzować',
|
||||
notLoggedIn: 'chce uzyskać dostęp do twojego konta Dify Cloud',
|
||||
common: 'Szanujemy Twoją prywatność i będziemy wykorzystywać te informacje tylko w celu ulepszenia Twojego doświadczenia z naszymi narzędziami deweloperskimi.',
|
||||
loggedIn: 'chce uzyskać dostęp do następujących informacji z twojego konta Dify Cloud.',
|
||||
},
|
||||
scopes: {
|
||||
timezone: 'Strefa czasowa',
|
||||
name: 'Imię',
|
||||
avatar: 'Avatar',
|
||||
languagePreference: 'Preferencje językowe',
|
||||
email: 'Email',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'Nieprawidłowe parametry',
|
||||
authorizeFailed: 'Autoryzacja nie powiodła się',
|
||||
authAppInfoFetchFailed: 'Nie udało się pobrać informacji o aplikacji w celu autoryzacji',
|
||||
},
|
||||
unknownApp: 'Nieznana aplikacja',
|
||||
continue: 'Kontynuuj',
|
||||
login: 'Zaloguj się',
|
||||
connect: 'Połącz z',
|
||||
switchAccount: 'Zmień konto',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
notLoggedIn: 'quer acessar sua conta do Dify Cloud',
|
||||
loggedIn: 'quer acessar as seguintes informações da sua conta Dify Cloud.',
|
||||
common: 'Respeitamos sua privacidade e usaremos essas informações apenas para melhorar sua experiência com nossas ferramentas de desenvolvedor.',
|
||||
needLogin: 'Por favor, faça login para autorizar',
|
||||
},
|
||||
scopes: {
|
||||
email: 'Email',
|
||||
avatar: 'Avatar',
|
||||
languagePreference: 'Preferência de Idioma',
|
||||
timezone: 'Fuso horário',
|
||||
name: 'Nome',
|
||||
},
|
||||
error: {
|
||||
authorizeFailed: 'Autorização falhou',
|
||||
authAppInfoFetchFailed: 'Falha ao buscar informações do aplicativo para autorização',
|
||||
invalidParams: 'Parâmetros inválidos',
|
||||
},
|
||||
login: 'Entrar',
|
||||
switchAccount: 'Mudar Conta',
|
||||
unknownApp: 'Aplicativo Desconhecido',
|
||||
continue: 'Continue',
|
||||
connect: 'Conectar a',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'Vă rugăm să vă conectați pentru a autoriza',
|
||||
loggedIn: 'vrea să acceseze următoarele informații din contul tău Dify Cloud.',
|
||||
notLoggedIn: 'vrea să acceseze contul tău Dify Cloud',
|
||||
common: 'Respectăm confidențialitatea dvs. și vom folosi aceste informații doar pentru a îmbunătăți experiența dvs. cu instrumentele noastre pentru dezvoltatori.',
|
||||
},
|
||||
scopes: {
|
||||
name: 'Nume',
|
||||
avatar: 'Avatar',
|
||||
languagePreference: 'Preferință lingvistică',
|
||||
email: 'Email',
|
||||
timezone: 'Fus orar',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'Parametrii invalizi',
|
||||
authorizeFailed: 'Autorizarea a eșuat',
|
||||
authAppInfoFetchFailed: 'Nu s-au putut obține informațiile aplicației pentru autorizare',
|
||||
},
|
||||
continue: 'Continuați',
|
||||
connect: 'Conectează la',
|
||||
unknownApp: 'Aplicație necunoscută',
|
||||
login: 'Conectare',
|
||||
switchAccount: 'Schimbă contul',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'Пожалуйста, войдите, чтобы авторизоваться',
|
||||
notLoggedIn: 'хочет получить доступ к вашей учетной записи Dify Cloud',
|
||||
loggedIn: 'хочет получить следующую информацию из вашего аккаунта Dify Cloud.',
|
||||
common: 'Мы уважаем вашу конфиденциальность и будем использовать эту информацию только для улучшения вашего опыта с нашими инструментами разработчика.',
|
||||
},
|
||||
scopes: {
|
||||
languagePreference: 'Предпочтение языка',
|
||||
email: 'Электронная почта',
|
||||
avatar: 'Аватар',
|
||||
name: 'Имя',
|
||||
timezone: 'Часовой пояс',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'Неверные параметры',
|
||||
authorizeFailed: 'Авторизация не удалась',
|
||||
authAppInfoFetchFailed: 'Не удалось получить информацию об приложении для авторизации',
|
||||
},
|
||||
continue: 'Продолжайте',
|
||||
connect: 'Подключиться к',
|
||||
switchAccount: 'Сменить аккаунт',
|
||||
unknownApp: 'Неизвестное приложение',
|
||||
login: 'Вход',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
notLoggedIn: 'želi dostopati do vašega Dify Cloud računa',
|
||||
loggedIn: 'želi dostopati do naslednjih informacij iz vašega računa Dify Cloud.',
|
||||
common: 'Soočamo se z vašo zasebnostjo in te informacije bomo uporabili le za izboljšanje vaših izkušenj z našimi orodji za razvijalce.',
|
||||
needLogin: 'Prosimo, prijavite se za avtorizacijo',
|
||||
},
|
||||
scopes: {
|
||||
timezone: 'Časovni pas',
|
||||
email: 'Email',
|
||||
languagePreference: 'Jezikovna prednost',
|
||||
avatar: 'Avatar',
|
||||
name: 'Ime',
|
||||
},
|
||||
error: {
|
||||
authAppInfoFetchFailed: 'Pridobivanje informacij o aplikaciji za avtorizacijo ni uspelo',
|
||||
authorizeFailed: 'Avtentikacija je spodletela',
|
||||
invalidParams: 'Neveljavni parametri',
|
||||
},
|
||||
login: 'Prijava',
|
||||
unknownApp: 'Nepoznana aplikacija',
|
||||
continue: 'Nadaljuj',
|
||||
switchAccount: 'Preklopi račun',
|
||||
connect: 'Poveži se z',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'โปรดเข้าสู่ระบบเพื่ออนุญาต',
|
||||
notLoggedIn: 'ต้องการเข้าถึงบัญชี Dify Cloud ของคุณ',
|
||||
loggedIn: 'ต้องการเข้าถึงข้อมูลต่อไปนี้จากบัญชี Dify Cloud ของคุณ.',
|
||||
common: 'เรามีความเคารพต่อความเป็นส่วนตัวของคุณและจะใช้ข้อมูลนี้เพื่อปรับปรุงประสบการณ์ของคุณกับเครื่องมือนักพัฒนาของเราเท่านั้น.',
|
||||
},
|
||||
scopes: {
|
||||
email: 'อีเมล',
|
||||
languagePreference: 'ความชอบภาษา',
|
||||
timezone: 'เขตเวลา',
|
||||
name: 'ชื่อ',
|
||||
avatar: 'อวตาร',
|
||||
},
|
||||
error: {
|
||||
authorizeFailed: 'การอนุญาตล้มเหลว',
|
||||
authAppInfoFetchFailed: 'ไม่สามารถดึงข้อมูลแอปเพื่อการอนุญาตได้',
|
||||
invalidParams: 'พารามิเตอร์ไม่ถูกต้อง',
|
||||
},
|
||||
login: 'เข้าสู่ระบบ',
|
||||
continue: 'ดำเนินต่อไป',
|
||||
connect: 'เชื่อมต่อกับ',
|
||||
unknownApp: 'แอปพลิเคชันที่ไม่รู้จัก',
|
||||
switchAccount: 'เปลี่ยนบัญชี',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
notLoggedIn: 'Dify Cloud hesabınıza erişmek istiyor',
|
||||
common: 'Gizliliğinize saygı gösteriyoruz ve bu bilgiyi yalnızca geliştirici araçlarımızla deneyiminizi geliştirmek için kullanacağız.',
|
||||
loggedIn: 'Dify Cloud hesabınızdaki aşağıdaki bilgilere erişmek istiyor.',
|
||||
needLogin: 'Lütfen yetkilendirmek için giriş yapın',
|
||||
},
|
||||
scopes: {
|
||||
timezone: 'Saat Dilimi',
|
||||
name: 'İsim',
|
||||
email: 'E-posta',
|
||||
avatar: 'Avatar',
|
||||
languagePreference: 'Dil Tercihi',
|
||||
},
|
||||
error: {
|
||||
authorizeFailed: 'Yetkilendirme başarısız',
|
||||
authAppInfoFetchFailed: 'Yetkilendirme için uygulama bilgisi alınamadı',
|
||||
invalidParams: 'Geçersiz parametreler',
|
||||
},
|
||||
continue: 'Devam et',
|
||||
connect: 'Bağlan',
|
||||
unknownApp: 'Bilinmeyen Uygulama',
|
||||
login: 'Giriş',
|
||||
switchAccount: 'Hesabı Değiştir',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
notLoggedIn: 'хоче отримати доступ до вашого облікового запису Dify Cloud',
|
||||
needLogin: 'Будь ласка, увійдіть, щоб авторизуватися.',
|
||||
loggedIn: 'хоче отримати доступ до наступної інформації з вашого облікового запису Dify Cloud.',
|
||||
common: 'Ми поважаємо вашу конфіденційність і використовуватимемо цю інформацію лише для покращення вашого досвіду з нашими інструментами для розробників.',
|
||||
},
|
||||
scopes: {
|
||||
languagePreference: 'Перевага мови',
|
||||
name: 'Ім\'я',
|
||||
email: 'Електронна пошта',
|
||||
avatar: 'Аватар',
|
||||
timezone: 'Часовий пояс',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'Недійсні параметри',
|
||||
authorizeFailed: 'Авторизація не вдалася',
|
||||
authAppInfoFetchFailed: 'Не вдалося отримати інформацію про додаток для авторизації',
|
||||
},
|
||||
login: 'Увійти',
|
||||
unknownApp: 'Невідома програма',
|
||||
continue: 'Продовжувати',
|
||||
switchAccount: 'Переключити акаунт',
|
||||
connect: 'Підключитися до',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
needLogin: 'Vui lòng đăng nhập để xác thực',
|
||||
notLoggedIn: 'muốn truy cập vào tài khoản Dify Cloud của bạn',
|
||||
loggedIn: 'muốn truy cập thông tin sau từ tài khoản Dify Cloud của bạn.',
|
||||
common: 'Chúng tôi tôn trọng quyền riêng tư của bạn và sẽ chỉ sử dụng thông tin này để cải thiện trải nghiệm của bạn với các công cụ phát triển của chúng tôi.',
|
||||
},
|
||||
scopes: {
|
||||
timezone: 'Múi giờ',
|
||||
languagePreference: 'Sở thích ngôn ngữ',
|
||||
name: 'Tên',
|
||||
email: 'Email',
|
||||
avatar: 'Avatar',
|
||||
},
|
||||
error: {
|
||||
authorizeFailed: 'Ủy quyền không thành công',
|
||||
authAppInfoFetchFailed: 'Không thể lấy thông tin ứng dụng để xác thực',
|
||||
invalidParams: 'Tham số không hợp lệ',
|
||||
},
|
||||
login: 'Đăng nhập',
|
||||
switchAccount: 'Chuyển tài khoản',
|
||||
connect: 'Kết nối với',
|
||||
continue: 'Tiếp tục',
|
||||
unknownApp: 'Ứng dụng không xác định',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
loggedIn: '想要访问您的 Dify Cloud 账号中的以下信息。',
|
||||
notLoggedIn: '想要访问您的 Dify Cloud 账号',
|
||||
needLogin: '请先登录以授权',
|
||||
common: '我们尊重您的隐私,并仅使用此信息来增强您对我们开发工具的使用体验。',
|
||||
},
|
||||
connect: '连接到',
|
||||
continue: '继续',
|
||||
switchAccount: '切换账号',
|
||||
login: '登录',
|
||||
scopes: {
|
||||
name: '名称',
|
||||
email: '邮箱',
|
||||
avatar: '头像',
|
||||
languagePreference: '语言偏好',
|
||||
timezone: '时区',
|
||||
},
|
||||
error: {
|
||||
invalidParams: '无效的参数',
|
||||
authorizeFailed: '授权失败',
|
||||
authAppInfoFetchFailed: '获取待授权应用的信息失败',
|
||||
},
|
||||
unknownApp: '未知应用',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
notLoggedIn: '想要訪問您的 Dify 雲端帳戶',
|
||||
loggedIn: '想要訪問您 Dify Cloud 帳戶中的以下資訊。',
|
||||
common: '我們尊重您的隱私,只會使用這些信息來提升您使用我們開發者工具的體驗。',
|
||||
needLogin: '請登錄以進行授權',
|
||||
},
|
||||
scopes: {
|
||||
timezone: '時區',
|
||||
languagePreference: '語言偏好',
|
||||
email: '電子郵件',
|
||||
name: '名字',
|
||||
avatar: '阿凡達',
|
||||
},
|
||||
error: {
|
||||
invalidParams: '無效的參數',
|
||||
authAppInfoFetchFailed: '無法獲取應用程式授權信息',
|
||||
authorizeFailed: '授權失敗',
|
||||
},
|
||||
login: '登入',
|
||||
connect: '連接到',
|
||||
switchAccount: '切換帳戶',
|
||||
unknownApp: '未知應用',
|
||||
continue: '繼續',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { post } from './base'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
|
||||
const NAME_SPACE = 'oauth-provider'
|
||||
|
||||
export type OAuthAppInfo = {
|
||||
app_icon: string
|
||||
app_label: Record<string, string>
|
||||
scope: string
|
||||
}
|
||||
|
||||
export type OAuthAuthorizeResponse = {
|
||||
code: string
|
||||
}
|
||||
|
||||
export const useOAuthAppInfo = (client_id: string, redirect_uri: string) => {
|
||||
return useQuery<OAuthAppInfo>({
|
||||
queryKey: [NAME_SPACE, 'authAppInfo', client_id, redirect_uri],
|
||||
queryFn: () => post<OAuthAppInfo>('/oauth/provider', { body: { client_id, redirect_uri } }, { silent: true }),
|
||||
enabled: Boolean(client_id && redirect_uri),
|
||||
})
|
||||
}
|
||||
|
||||
export const useAuthorizeOAuthApp = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'authorize'],
|
||||
mutationFn: (payload: { client_id: string }) => post<OAuthAuthorizeResponse>('/oauth/provider/authorize', { body: payload }),
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// export basePath to next.config.js
|
||||
// same as the one exported from var.ts
|
||||
module.exports = {
|
||||
basePath: '',
|
||||
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
|
||||
assetPrefix: '',
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue