mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
Merge remote-tracking branch 'upstream/feat/hitl-form-enhancement' into feat/hitl-form-enhancement
This commit is contained in:
commit
f1833fdb08
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: frontend-query-mutation
|
name: frontend-query-mutation
|
||||||
description: Guide for implementing Dify frontend query and mutation patterns with TanStack Query and oRPC. Trigger when creating or updating contracts in web/contract, wiring router composition, consuming consoleQuery or marketplaceQuery in components or services, deciding whether to call queryOptions() directly or extract a helper or use-* hook, handling conditional queries, cache invalidation, mutation error handling, or migrating legacy service calls to contract-first query and mutation helpers.
|
description: Guide for implementing Dify frontend query and mutation patterns with TanStack Query and oRPC. Trigger when creating or updating contracts in web/contract, wiring router composition, consuming consoleQuery or marketplaceQuery in components or services, deciding whether to call queryOptions()/mutationOptions() directly or extract a helper or use-* hook, configuring oRPC experimental_defaults/default options, handling conditional queries, cache updates/invalidation, mutation error handling, or migrating legacy service calls to contract-first query and mutation helpers.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Frontend Query & Mutation
|
# Frontend Query & Mutation
|
||||||
@ -9,22 +9,24 @@ description: Guide for implementing Dify frontend query and mutation patterns wi
|
|||||||
|
|
||||||
- Keep contract as the single source of truth in `web/contract/*`.
|
- Keep contract as the single source of truth in `web/contract/*`.
|
||||||
- Prefer contract-shaped `queryOptions()` and `mutationOptions()`.
|
- Prefer contract-shaped `queryOptions()` and `mutationOptions()`.
|
||||||
- Keep invalidation and mutation flow knowledge in the service layer.
|
- Keep default cache behavior with `consoleQuery`/`marketplaceQuery` setup, and keep business orchestration in feature vertical hooks when direct contract calls are not enough.
|
||||||
|
- Treat `web/service/use-*` query or mutation wrappers as legacy migration targets, not the preferred destination.
|
||||||
- Keep abstractions minimal to preserve TypeScript inference.
|
- Keep abstractions minimal to preserve TypeScript inference.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Identify the change surface.
|
1. Identify the change surface.
|
||||||
- Read `references/contract-patterns.md` for contract files, router composition, client helpers, and query or mutation call-site shape.
|
- Read `references/contract-patterns.md` for contract files, router composition, client helpers, and query or mutation call-site shape.
|
||||||
- Read `references/runtime-rules.md` for conditional queries, invalidation, error handling, and legacy migrations.
|
- Read `references/runtime-rules.md` for conditional queries, default options, cache updates/invalidation, error handling, and legacy migrations.
|
||||||
- Read both references when a task spans contract shape and runtime behavior.
|
- Read both references when a task spans contract shape and runtime behavior.
|
||||||
2. Implement the smallest abstraction that fits the task.
|
2. Implement the smallest abstraction that fits the task.
|
||||||
- Default to direct `useQuery(...)` or `useMutation(...)` calls with oRPC helpers at the call site.
|
- Default to direct `useQuery(...)` or `useMutation(...)` calls with oRPC helpers at the call site.
|
||||||
- Extract a small shared query helper only when multiple call sites share the same extra options.
|
- Extract a small shared query helper only when multiple call sites share the same extra options.
|
||||||
- Create `web/service/use-{domain}.ts` only for orchestration or shared domain behavior.
|
- Create or keep feature hooks only for real orchestration or shared domain behavior.
|
||||||
|
- When touching thin `web/service/use-*` wrappers, migrate them away when feasible.
|
||||||
3. Preserve Dify conventions.
|
3. Preserve Dify conventions.
|
||||||
- Keep contract inputs in `{ params, query?, body? }` shape.
|
- Keep contract inputs in `{ params, query?, body? }` shape.
|
||||||
- Bind invalidation in the service-layer mutation definition.
|
- Bind default cache updates/invalidation in `createTanstackQueryUtils(...experimental_defaults...)`; use feature hooks only for workflows that cannot be expressed as default operation behavior.
|
||||||
- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required.
|
- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required.
|
||||||
|
|
||||||
## Files Commonly Touched
|
## Files Commonly Touched
|
||||||
@ -33,7 +35,7 @@ description: Guide for implementing Dify frontend query and mutation patterns wi
|
|||||||
- `web/contract/marketplace.ts`
|
- `web/contract/marketplace.ts`
|
||||||
- `web/contract/router.ts`
|
- `web/contract/router.ts`
|
||||||
- `web/service/client.ts`
|
- `web/service/client.ts`
|
||||||
- `web/service/use-*.ts`
|
- legacy `web/service/use-*.ts` files when migrating wrappers away
|
||||||
- component and hook call sites using `consoleQuery` or `marketplaceQuery`
|
- component and hook call sites using `consoleQuery` or `marketplaceQuery`
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "Frontend Query & Mutation"
|
display_name: "Frontend Query & Mutation"
|
||||||
short_description: "Dify TanStack Query and oRPC patterns"
|
short_description: "Dify TanStack Query, oRPC, and default option patterns"
|
||||||
default_prompt: "Use this skill when implementing or reviewing Dify frontend contracts, query and mutation call sites, conditional queries, invalidation, or legacy query/mutation migrations."
|
default_prompt: "Use this skill when implementing or reviewing Dify frontend contracts, query and mutation call sites, oRPC default options, conditional queries, cache updates/invalidation, or legacy query/mutation migrations."
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
- Core workflow
|
- Core workflow
|
||||||
- Query usage decision rule
|
- Query usage decision rule
|
||||||
- Mutation usage decision rule
|
- Mutation usage decision rule
|
||||||
|
- Thin hook decision rule
|
||||||
- Anti-patterns
|
- Anti-patterns
|
||||||
- Contract rules
|
- Contract rules
|
||||||
- Type export
|
- Type export
|
||||||
@ -55,9 +56,13 @@ const invoiceQuery = useQuery(consoleQuery.billing.invoices.queryOptions({
|
|||||||
|
|
||||||
1. Default to direct `*.queryOptions(...)` usage at the call site.
|
1. Default to direct `*.queryOptions(...)` usage at the call site.
|
||||||
2. If 3 or more call sites share the same extra options, extract a small query helper, not a `use-*` passthrough hook.
|
2. If 3 or more call sites share the same extra options, extract a small query helper, not a `use-*` passthrough hook.
|
||||||
3. Create `web/service/use-{domain}.ts` only for orchestration.
|
3. Create or keep feature hooks only for orchestration.
|
||||||
- Combine multiple queries or mutations.
|
- Combine multiple queries or mutations.
|
||||||
- Share domain-level derived state or invalidation helpers.
|
- Share domain-level derived state or invalidation helpers.
|
||||||
|
- Prefer `web/features/{domain}/hooks/*` for feature-owned workflows.
|
||||||
|
4. Treat `web/service/use-{domain}.ts` as legacy.
|
||||||
|
- Do not create new thin service wrappers for oRPC contracts.
|
||||||
|
- When touching existing wrappers, inline direct `consoleQuery` or `marketplaceQuery` consumption when the wrapper is only a passthrough.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const invoicesBaseQueryOptions = () =>
|
const invoicesBaseQueryOptions = () =>
|
||||||
@ -74,11 +79,37 @@ const invoiceQuery = useQuery({
|
|||||||
1. Default to mutation helpers from `consoleQuery` or `marketplaceQuery`, for example `useMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...))`.
|
1. Default to mutation helpers from `consoleQuery` or `marketplaceQuery`, for example `useMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...))`.
|
||||||
2. If the mutation flow is heavily custom, use oRPC clients as `mutationFn`, for example `consoleClient.xxx` or `marketplaceClient.xxx`, instead of handwritten non-oRPC mutation logic.
|
2. If the mutation flow is heavily custom, use oRPC clients as `mutationFn`, for example `consoleClient.xxx` or `marketplaceClient.xxx`, instead of handwritten non-oRPC mutation logic.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const createTagMutation = useMutation(consoleQuery.tags.create.mutationOptions())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Thin Hook Decision Rule
|
||||||
|
|
||||||
|
Remove thin hooks when they only rename a single oRPC query or mutation helper.
|
||||||
|
Keep hooks when they orchestrate business behavior across multiple operations, own local workflow state, or normalize a feature-specific API.
|
||||||
|
Prefer feature vertical hooks for kept orchestration. Do not move new contract-first wrappers into `web/service/use-*`.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const deleteTagMutation = useMutation(consoleQuery.tags.delete.mutationOptions())
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const applyTagBindingsMutation = useApplyTagBindingsMutation()
|
||||||
|
```
|
||||||
|
|
||||||
|
`useApplyTagBindingsMutation` is acceptable because it coordinates bind and unbind requests, computes deltas, and exposes a feature-level workflow rather than a single endpoint passthrough.
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- Do not wrap `useQuery` with `options?: Partial<UseQueryOptions>`.
|
- Do not wrap `useQuery` with `options?: Partial<UseQueryOptions>`.
|
||||||
- Do not split local `queryKey` and `queryFn` when oRPC `queryOptions` already exists and fits the use case.
|
- Do not split local `queryKey` and `queryFn` when oRPC `queryOptions` already exists and fits the use case.
|
||||||
- Do not create thin `use-*` passthrough hooks for a single endpoint.
|
- Do not create thin `use-*` passthrough hooks for a single endpoint.
|
||||||
|
- Do not create business-layer helpers whose only purpose is to call `consoleQuery.xxx.mutationOptions()` or `queryOptions()`.
|
||||||
|
- Do not introduce new `web/service/use-*` files for oRPC contract passthroughs.
|
||||||
- These patterns can degrade inference, especially around `throwOnError` and `select`, and add unnecessary indirection.
|
- These patterns can degrade inference, especially around `throwOnError` and `select`, and add unnecessary indirection.
|
||||||
|
|
||||||
## Contract Rules
|
## Contract Rules
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- Conditional queries
|
- Conditional queries
|
||||||
|
- oRPC default options
|
||||||
- Cache invalidation
|
- Cache invalidation
|
||||||
- Key API guide
|
- Key API guide
|
||||||
- `mutate` vs `mutateAsync`
|
- `mutate` vs `mutateAsync`
|
||||||
@ -35,9 +36,50 @@ function useBadAccessMode(appId: string | undefined) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## oRPC Default Options
|
||||||
|
|
||||||
|
Use `experimental_defaults` in `createTanstackQueryUtils` when a contract operation should always carry shared TanStack Query behavior, such as default stale time, mutation cache writes, or invalidation.
|
||||||
|
|
||||||
|
Place defaults at the query utility creation point in `web/service/client.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const consoleQuery = createTanstackQueryUtils(consoleClient, {
|
||||||
|
path: ['console'],
|
||||||
|
experimental_defaults: {
|
||||||
|
tags: {
|
||||||
|
create: {
|
||||||
|
mutationOptions: {
|
||||||
|
onSuccess: (tag, _variables, _result, context) => {
|
||||||
|
context.client.setQueryData(
|
||||||
|
consoleQuery.tags.list.queryKey({
|
||||||
|
input: {
|
||||||
|
query: {
|
||||||
|
type: tag.type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
(oldTags: Tag[] | undefined) => oldTags ? [tag, ...oldTags] : oldTags,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Keep defaults inline in the `consoleQuery` or `marketplaceQuery` initialization when they need sibling oRPC key builders.
|
||||||
|
- Do not create a wrapper function solely to host `createTanstackQueryUtils`.
|
||||||
|
- Do not split defaults into a vertical feature file if that forces handwritten operation paths such as `generateOperationKey(['console', ...])`.
|
||||||
|
- Keep feature-level orchestration in the feature vertical; keep query utility lifecycle defaults with the query utility.
|
||||||
|
- Prefer call-site callbacks for UI feedback only; shared cache behavior belongs in oRPC defaults when it is tied to a contract operation.
|
||||||
|
|
||||||
## Cache Invalidation
|
## Cache Invalidation
|
||||||
|
|
||||||
Bind invalidation in the service-layer mutation definition.
|
Bind shared invalidation in oRPC defaults when it is tied to a contract operation.
|
||||||
|
Use feature vertical hooks only for multi-operation workflows or domain orchestration that cannot live in a single operation default.
|
||||||
Components may add UI feedback in call-site callbacks, but they should not decide which queries to invalidate.
|
Components may add UI feedback in call-site callbacks, but they should not decide which queries to invalidate.
|
||||||
|
|
||||||
Use:
|
Use:
|
||||||
@ -49,7 +91,7 @@ Use:
|
|||||||
Do not use deprecated `useInvalid` from `use-base.ts`.
|
Do not use deprecated `useInvalid` from `use-base.ts`.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Service layer owns cache invalidation.
|
// Feature orchestration owns cache invalidation only when defaults are not enough.
|
||||||
export const useUpdateAccessMode = () => {
|
export const useUpdateAccessMode = () => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
@ -124,7 +166,7 @@ When touching old code, migrate it toward these rules:
|
|||||||
|
|
||||||
| Old pattern | New pattern |
|
| Old pattern | New pattern |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `useInvalid(key)` in service layer | `queryClient.invalidateQueries(...)` inside mutation `onSuccess` |
|
| `useInvalid(key)` in service wrappers | oRPC defaults, or a feature vertical hook for real orchestration |
|
||||||
| component-triggered invalidation after mutation | move invalidation into the service-layer mutation definition |
|
| component-triggered invalidation after mutation | move invalidation into oRPC defaults or a feature vertical hook |
|
||||||
| imperative fetch plus manual invalidation | wrap it in `useMutation(...mutationOptions(...))` |
|
| imperative fetch plus manual invalidation | wrap it in `useMutation(...mutationOptions(...))` |
|
||||||
| `await mutateAsync()` without `try/catch` | switch to `mutate(...)` or add `try/catch` |
|
| `await mutateAsync()` without `try/catch` | switch to `mutate(...)` or add `try/catch` |
|
||||||
|
|||||||
2
.github/workflows/api-tests.yml
vendored
2
.github/workflows/api-tests.yml
vendored
@ -99,7 +99,7 @@ jobs:
|
|||||||
- name: Set up dotenvs
|
- name: Set up dotenvs
|
||||||
run: |
|
run: |
|
||||||
cp docker/.env.example docker/.env
|
cp docker/.env.example docker/.env
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
cp docker/envs/middleware.env.example docker/middleware.env
|
||||||
|
|
||||||
- name: Expose Service Ports
|
- name: Expose Service Ports
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
run: sh .github/workflows/expose_service_ports.sh
|
||||||
|
|||||||
4
.github/workflows/db-migration-test.yml
vendored
4
.github/workflows/db-migration-test.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Prepare middleware env
|
- name: Prepare middleware env
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
cp middleware.env.example middleware.env
|
cp envs/middleware.env.example middleware.env
|
||||||
|
|
||||||
- name: Set up Middlewares
|
- name: Set up Middlewares
|
||||||
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
||||||
@ -87,7 +87,7 @@ jobs:
|
|||||||
- name: Prepare middleware env for MySQL
|
- name: Prepare middleware env for MySQL
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
cp middleware.env.example middleware.env
|
cp envs/middleware.env.example middleware.env
|
||||||
sed -i 's/DB_TYPE=postgresql/DB_TYPE=mysql/' middleware.env
|
sed -i 's/DB_TYPE=postgresql/DB_TYPE=mysql/' middleware.env
|
||||||
sed -i 's/DB_HOST=db_postgres/DB_HOST=db_mysql/' middleware.env
|
sed -i 's/DB_HOST=db_postgres/DB_HOST=db_mysql/' middleware.env
|
||||||
sed -i 's/DB_PORT=5432/DB_PORT=3306/' middleware.env
|
sed -i 's/DB_PORT=5432/DB_PORT=3306/' middleware.env
|
||||||
|
|||||||
8
.github/workflows/main-ci.yml
vendored
8
.github/workflows/main-ci.yml
vendored
@ -57,7 +57,7 @@ jobs:
|
|||||||
- '.github/workflows/api-tests.yml'
|
- '.github/workflows/api-tests.yml'
|
||||||
- '.github/workflows/expose_service_ports.sh'
|
- '.github/workflows/expose_service_ports.sh'
|
||||||
- 'docker/.env.example'
|
- 'docker/.env.example'
|
||||||
- 'docker/middleware.env.example'
|
- 'docker/envs/middleware.env.example'
|
||||||
- 'docker/docker-compose.middleware.yaml'
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
- 'docker/docker-compose-template.yaml'
|
- 'docker/docker-compose-template.yaml'
|
||||||
- 'docker/generate_docker_compose'
|
- 'docker/generate_docker_compose'
|
||||||
@ -84,7 +84,7 @@ jobs:
|
|||||||
- 'pnpm-workspace.yaml'
|
- 'pnpm-workspace.yaml'
|
||||||
- '.nvmrc'
|
- '.nvmrc'
|
||||||
- 'docker/docker-compose.middleware.yaml'
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
- 'docker/middleware.env.example'
|
- 'docker/envs/middleware.env.example'
|
||||||
- '.github/workflows/web-e2e.yml'
|
- '.github/workflows/web-e2e.yml'
|
||||||
- '.github/actions/setup-web/**'
|
- '.github/actions/setup-web/**'
|
||||||
vdb:
|
vdb:
|
||||||
@ -94,7 +94,7 @@ jobs:
|
|||||||
- '.github/workflows/vdb-tests.yml'
|
- '.github/workflows/vdb-tests.yml'
|
||||||
- '.github/workflows/expose_service_ports.sh'
|
- '.github/workflows/expose_service_ports.sh'
|
||||||
- 'docker/.env.example'
|
- 'docker/.env.example'
|
||||||
- 'docker/middleware.env.example'
|
- 'docker/envs/middleware.env.example'
|
||||||
- 'docker/docker-compose.yaml'
|
- 'docker/docker-compose.yaml'
|
||||||
- 'docker/docker-compose-template.yaml'
|
- 'docker/docker-compose-template.yaml'
|
||||||
- 'docker/generate_docker_compose'
|
- 'docker/generate_docker_compose'
|
||||||
@ -116,7 +116,7 @@ jobs:
|
|||||||
- '.github/workflows/db-migration-test.yml'
|
- '.github/workflows/db-migration-test.yml'
|
||||||
- '.github/workflows/expose_service_ports.sh'
|
- '.github/workflows/expose_service_ports.sh'
|
||||||
- 'docker/.env.example'
|
- 'docker/.env.example'
|
||||||
- 'docker/middleware.env.example'
|
- 'docker/envs/middleware.env.example'
|
||||||
- 'docker/docker-compose.middleware.yaml'
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
- 'docker/docker-compose-template.yaml'
|
- 'docker/docker-compose-template.yaml'
|
||||||
- 'docker/generate_docker_compose'
|
- 'docker/generate_docker_compose'
|
||||||
|
|||||||
2
.github/workflows/vdb-tests-full.yml
vendored
2
.github/workflows/vdb-tests-full.yml
vendored
@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Set up dotenvs
|
- name: Set up dotenvs
|
||||||
run: |
|
run: |
|
||||||
cp docker/.env.example docker/.env
|
cp docker/.env.example docker/.env
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
cp docker/envs/middleware.env.example docker/middleware.env
|
||||||
|
|
||||||
- name: Expose Service Ports
|
- name: Expose Service Ports
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
run: sh .github/workflows/expose_service_ports.sh
|
||||||
|
|||||||
2
.github/workflows/vdb-tests.yml
vendored
2
.github/workflows/vdb-tests.yml
vendored
@ -48,7 +48,7 @@ jobs:
|
|||||||
- name: Set up dotenvs
|
- name: Set up dotenvs
|
||||||
run: |
|
run: |
|
||||||
cp docker/.env.example docker/.env
|
cp docker/.env.example docker/.env
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
cp docker/envs/middleware.env.example docker/middleware.env
|
||||||
|
|
||||||
- name: Expose Service Ports
|
- name: Expose Service Ports
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
run: sh .github/workflows/expose_service_ports.sh
|
||||||
|
|||||||
@ -76,11 +76,10 @@ The easiest way to start the Dify server is through [Docker Compose](docker/dock
|
|||||||
```bash
|
```bash
|
||||||
cd dify
|
cd dify
|
||||||
cd docker
|
cd docker
|
||||||
./dify-compose up -d
|
cp .env.example .env
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
On Windows PowerShell, run `.\dify-compose.ps1 up -d` from the `docker` directory.
|
|
||||||
|
|
||||||
After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization process.
|
After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization process.
|
||||||
|
|
||||||
#### Seeking help
|
#### Seeking help
|
||||||
@ -138,7 +137,7 @@ Star Dify on GitHub and be instantly notified of new releases.
|
|||||||
|
|
||||||
### Custom configurations
|
### Custom configurations
|
||||||
|
|
||||||
If you need to customize the configuration, add only the values you want to override to `docker/.env`. The default values live in [`docker/.env.default`](docker/.env.default), and the full reference remains in [`docker/.env.example`](docker/.env.example). After making any changes, re-run `./dify-compose up -d` or `.\dify-compose.ps1 up -d` from the `docker` directory. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
If you need to customize the configuration, edit `docker/.env`. The essential startup defaults live in [`docker/.env.example`](docker/.env.example), and optional advanced variables are split under `docker/envs/` by theme. After making any changes, re-run `docker compose up -d` from the `docker` directory. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
||||||
|
|
||||||
### Metrics Monitoring with Grafana
|
### Metrics Monitoring with Grafana
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,8 @@ DB_DATABASE=dify
|
|||||||
|
|
||||||
SQLALCHEMY_POOL_PRE_PING=true
|
SQLALCHEMY_POOL_PRE_PING=true
|
||||||
SQLALCHEMY_POOL_TIMEOUT=30
|
SQLALCHEMY_POOL_TIMEOUT=30
|
||||||
|
# Connection pool reset behavior on return
|
||||||
|
SQLALCHEMY_POOL_RESET_ON_RETURN=rollback
|
||||||
|
|
||||||
# Storage configuration
|
# Storage configuration
|
||||||
# use for store upload files, private keys...
|
# use for store upload files, private keys...
|
||||||
@ -381,7 +383,7 @@ VIKINGDB_ACCESS_KEY=your-ak
|
|||||||
VIKINGDB_SECRET_KEY=your-sk
|
VIKINGDB_SECRET_KEY=your-sk
|
||||||
VIKINGDB_REGION=cn-shanghai
|
VIKINGDB_REGION=cn-shanghai
|
||||||
VIKINGDB_HOST=api-vikingdb.xxx.volces.com
|
VIKINGDB_HOST=api-vikingdb.xxx.volces.com
|
||||||
VIKINGDB_SCHEMA=http
|
VIKINGDB_SCHEME=http
|
||||||
VIKINGDB_CONNECTION_TIMEOUT=30
|
VIKINGDB_CONNECTION_TIMEOUT=30
|
||||||
VIKINGDB_SOCKET_TIMEOUT=30
|
VIKINGDB_SOCKET_TIMEOUT=30
|
||||||
|
|
||||||
@ -432,8 +434,6 @@ UPLOAD_FILE_EXTENSION_BLACKLIST=
|
|||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
MULTIMODAL_SEND_FORMAT=base64
|
MULTIMODAL_SEND_FORMAT=base64
|
||||||
PROMPT_GENERATION_MAX_TOKENS=512
|
|
||||||
CODE_GENERATION_MAX_TOKENS=1024
|
|
||||||
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
||||||
|
|
||||||
# Mail configuration, support: resend, smtp, sendgrid
|
# Mail configuration, support: resend, smtp, sendgrid
|
||||||
|
|||||||
@ -114,7 +114,7 @@ class SQLAlchemyEngineOptionsDict(TypedDict):
|
|||||||
pool_pre_ping: bool
|
pool_pre_ping: bool
|
||||||
connect_args: dict[str, str]
|
connect_args: dict[str, str]
|
||||||
pool_use_lifo: bool
|
pool_use_lifo: bool
|
||||||
pool_reset_on_return: None
|
pool_reset_on_return: Literal["commit", "rollback", None]
|
||||||
pool_timeout: int
|
pool_timeout: int
|
||||||
|
|
||||||
|
|
||||||
@ -223,6 +223,11 @@ class DatabaseConfig(BaseSettings):
|
|||||||
default=30,
|
default=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SQLALCHEMY_POOL_RESET_ON_RETURN: Literal["commit", "rollback", None] = Field(
|
||||||
|
description="Connection pool reset behavior on return. Options: 'commit', 'rollback', or None",
|
||||||
|
default="rollback",
|
||||||
|
)
|
||||||
|
|
||||||
RETRIEVAL_SERVICE_EXECUTORS: NonNegativeInt = Field(
|
RETRIEVAL_SERVICE_EXECUTORS: NonNegativeInt = Field(
|
||||||
description="Number of processes for the retrieval service, default to CPU cores.",
|
description="Number of processes for the retrieval service, default to CPU cores.",
|
||||||
default=os.cpu_count() or 1,
|
default=os.cpu_count() or 1,
|
||||||
@ -252,7 +257,7 @@ class DatabaseConfig(BaseSettings):
|
|||||||
"pool_pre_ping": self.SQLALCHEMY_POOL_PRE_PING,
|
"pool_pre_ping": self.SQLALCHEMY_POOL_PRE_PING,
|
||||||
"connect_args": connect_args,
|
"connect_args": connect_args,
|
||||||
"pool_use_lifo": self.SQLALCHEMY_POOL_USE_LIFO,
|
"pool_use_lifo": self.SQLALCHEMY_POOL_USE_LIFO,
|
||||||
"pool_reset_on_return": None,
|
"pool_reset_on_return": self.SQLALCHEMY_POOL_RESET_ON_RETURN,
|
||||||
"pool_timeout": self.SQLALCHEMY_POOL_TIMEOUT,
|
"pool_timeout": self.SQLALCHEMY_POOL_TIMEOUT,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
"name": "Website Generator"
|
"name": "Website Generator"
|
||||||
},
|
},
|
||||||
"app_id": "b53545b1-79ea-4da3-b31a-c39391c6f041",
|
"app_id": "b53545b1-79ea-4da3-b31a-c39391c6f041",
|
||||||
"category": "Programming",
|
"categories": ["Programming"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"name": "Investment Analysis Report Copilot"
|
"name": "Investment Analysis Report Copilot"
|
||||||
},
|
},
|
||||||
"app_id": "a23b57fa-85da-49c0-a571-3aff375976c1",
|
"app_id": "a23b57fa-85da-49c0-a571-3aff375976c1",
|
||||||
"category": "Agent",
|
"categories": ["Agent"],
|
||||||
"copyright": "Dify.AI",
|
"copyright": "Dify.AI",
|
||||||
"description": "Welcome to your personalized Investment Analysis Copilot service, where we delve into the depths of stock analysis to provide you with comprehensive insights. \n",
|
"description": "Welcome to your personalized Investment Analysis Copilot service, where we delve into the depths of stock analysis to provide you with comprehensive insights. \n",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"name": "Workflow Planning Assistant "
|
"name": "Workflow Planning Assistant "
|
||||||
},
|
},
|
||||||
"app_id": "f3303a7d-a81c-404e-b401-1f8711c998c1",
|
"app_id": "f3303a7d-a81c-404e-b401-1f8711c998c1",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "An assistant that helps you plan and select the right node for a workflow (V0.6.0). ",
|
"description": "An assistant that helps you plan and select the right node for a workflow (V0.6.0). ",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -67,7 +67,7 @@
|
|||||||
"name": "Automated Email Reply "
|
"name": "Automated Email Reply "
|
||||||
},
|
},
|
||||||
"app_id": "e9d92058-7d20-4904-892f-75d90bef7587",
|
"app_id": "e9d92058-7d20-4904-892f-75d90bef7587",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "Reply emails using Gmail API. It will automatically retrieve email in your inbox and create a response in Gmail. \nConfigure your Gmail API in Google Cloud Console. ",
|
"description": "Reply emails using Gmail API. It will automatically retrieve email in your inbox and create a response in Gmail. \nConfigure your Gmail API in Google Cloud Console. ",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -83,7 +83,7 @@
|
|||||||
"name": "Book Translation "
|
"name": "Book Translation "
|
||||||
},
|
},
|
||||||
"app_id": "98b87f88-bd22-4d86-8b74-86beba5e0ed4",
|
"app_id": "98b87f88-bd22-4d86-8b74-86beba5e0ed4",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "A workflow designed to translate a full book up to 15000 tokens per run. Uses Code node to separate text into chunks and Iteration to translate each chunk. ",
|
"description": "A workflow designed to translate a full book up to 15000 tokens per run. Uses Code node to separate text into chunks and Iteration to translate each chunk. ",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -99,7 +99,7 @@
|
|||||||
"name": "Python bug fixer"
|
"name": "Python bug fixer"
|
||||||
},
|
},
|
||||||
"app_id": "cae337e6-aec5-4c7b-beca-d6f1a808bd5e",
|
"app_id": "cae337e6-aec5-4c7b-beca-d6f1a808bd5e",
|
||||||
"category": "Programming",
|
"categories": ["Programming"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"name": "Code Interpreter"
|
"name": "Code Interpreter"
|
||||||
},
|
},
|
||||||
"app_id": "d077d587-b072-4f2c-b631-69ed1e7cdc0f",
|
"app_id": "d077d587-b072-4f2c-b631-69ed1e7cdc0f",
|
||||||
"category": "Programming",
|
"categories": ["Programming"],
|
||||||
"copyright": "Copyright 2023 Dify",
|
"copyright": "Copyright 2023 Dify",
|
||||||
"description": "Code interpreter, clarifying the syntax and semantics of the code.",
|
"description": "Code interpreter, clarifying the syntax and semantics of the code.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -131,7 +131,7 @@
|
|||||||
"name": "SVG Logo Design "
|
"name": "SVG Logo Design "
|
||||||
},
|
},
|
||||||
"app_id": "73fbb5f1-c15d-4d74-9cc8-46d9db9b2cca",
|
"app_id": "73fbb5f1-c15d-4d74-9cc8-46d9db9b2cca",
|
||||||
"category": "Agent",
|
"categories": ["Agent"],
|
||||||
"copyright": "Dify.AI",
|
"copyright": "Dify.AI",
|
||||||
"description": "Hello, I am your creative partner in bringing ideas to vivid life! I can assist you in creating stunning designs by leveraging abilities of DALL·E 3. ",
|
"description": "Hello, I am your creative partner in bringing ideas to vivid life! I can assist you in creating stunning designs by leveraging abilities of DALL·E 3. ",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -147,7 +147,7 @@
|
|||||||
"name": "Long Story Generator (Iteration) "
|
"name": "Long Story Generator (Iteration) "
|
||||||
},
|
},
|
||||||
"app_id": "5efb98d7-176b-419c-b6ef-50767391ab62",
|
"app_id": "5efb98d7-176b-419c-b6ef-50767391ab62",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "A workflow demonstrating how to use Iteration node to generate long article that is longer than the context length of LLMs. ",
|
"description": "A workflow demonstrating how to use Iteration node to generate long article that is longer than the context length of LLMs. ",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -163,7 +163,7 @@
|
|||||||
"name": "Text Summarization Workflow"
|
"name": "Text Summarization Workflow"
|
||||||
},
|
},
|
||||||
"app_id": "f00c4531-6551-45ee-808f-1d7903099515",
|
"app_id": "f00c4531-6551-45ee-808f-1d7903099515",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "Based on users' choice, retrieve external knowledge to more accurately summarize articles.",
|
"description": "Based on users' choice, retrieve external knowledge to more accurately summarize articles.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -179,7 +179,7 @@
|
|||||||
"name": "YouTube Channel Data Analysis"
|
"name": "YouTube Channel Data Analysis"
|
||||||
},
|
},
|
||||||
"app_id": "be591209-2ca8-410f-8f3b-ca0e530dd638",
|
"app_id": "be591209-2ca8-410f-8f3b-ca0e530dd638",
|
||||||
"category": "Agent",
|
"categories": ["Agent"],
|
||||||
"copyright": "Dify.AI",
|
"copyright": "Dify.AI",
|
||||||
"description": "I am a YouTube Channel Data Analysis Copilot, I am here to provide expert data analysis tailored to your needs. ",
|
"description": "I am a YouTube Channel Data Analysis Copilot, I am here to provide expert data analysis tailored to your needs. ",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -195,7 +195,7 @@
|
|||||||
"name": "Article Grading Bot"
|
"name": "Article Grading Bot"
|
||||||
},
|
},
|
||||||
"app_id": "a747f7b4-c48b-40d6-b313-5e628232c05f",
|
"app_id": "a747f7b4-c48b-40d6-b313-5e628232c05f",
|
||||||
"category": "Writing",
|
"categories": ["Writing"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "Assess the quality of articles and text based on user defined criteria. ",
|
"description": "Assess the quality of articles and text based on user defined criteria. ",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -211,7 +211,7 @@
|
|||||||
"name": "SEO Blog Generator"
|
"name": "SEO Blog Generator"
|
||||||
},
|
},
|
||||||
"app_id": "18f3bd03-524d-4d7a-8374-b30dbe7c69d5",
|
"app_id": "18f3bd03-524d-4d7a-8374-b30dbe7c69d5",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "Workflow for retrieving information from the internet, followed by segmented generation of SEO blogs.",
|
"description": "Workflow for retrieving information from the internet, followed by segmented generation of SEO blogs.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -227,7 +227,7 @@
|
|||||||
"name": "SQL Creator"
|
"name": "SQL Creator"
|
||||||
},
|
},
|
||||||
"app_id": "050ef42e-3e0c-40c1-a6b6-a64f2c49d744",
|
"app_id": "050ef42e-3e0c-40c1-a6b6-a64f2c49d744",
|
||||||
"category": "Programming",
|
"categories": ["Programming"],
|
||||||
"copyright": "Copyright 2023 Dify",
|
"copyright": "Copyright 2023 Dify",
|
||||||
"description": "Write SQL from natural language by pasting in your schema with the request.Please describe your query requirements in natural language and select the target database type.",
|
"description": "Write SQL from natural language by pasting in your schema with the request.Please describe your query requirements in natural language and select the target database type.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -243,7 +243,7 @@
|
|||||||
"name": "Sentiment Analysis "
|
"name": "Sentiment Analysis "
|
||||||
},
|
},
|
||||||
"app_id": "f06bf86b-d50c-4895-a942-35112dbe4189",
|
"app_id": "f06bf86b-d50c-4895-a942-35112dbe4189",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "Batch sentiment analysis of text, followed by JSON output of sentiment classification along with scores.",
|
"description": "Batch sentiment analysis of text, followed by JSON output of sentiment classification along with scores.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -259,7 +259,7 @@
|
|||||||
"name": "Strategic Consulting Expert"
|
"name": "Strategic Consulting Expert"
|
||||||
},
|
},
|
||||||
"app_id": "7e8ca1ae-02f2-4b5f-979e-62d19133bee2",
|
"app_id": "7e8ca1ae-02f2-4b5f-979e-62d19133bee2",
|
||||||
"category": "Assistant",
|
"categories": ["Assistant"],
|
||||||
"copyright": "Copyright 2023 Dify",
|
"copyright": "Copyright 2023 Dify",
|
||||||
"description": "I can answer your questions related to strategic marketing.",
|
"description": "I can answer your questions related to strategic marketing.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -275,7 +275,7 @@
|
|||||||
"name": "Code Converter"
|
"name": "Code Converter"
|
||||||
},
|
},
|
||||||
"app_id": "4006c4b2-0735-4f37-8dbb-fb1a8c5bd87a",
|
"app_id": "4006c4b2-0735-4f37-8dbb-fb1a8c5bd87a",
|
||||||
"category": "Programming",
|
"categories": ["Programming"],
|
||||||
"copyright": "Copyright 2023 Dify",
|
"copyright": "Copyright 2023 Dify",
|
||||||
"description": "This is an application that provides the ability to convert code snippets in multiple programming languages. You can input the code you wish to convert, select the target programming language, and get the desired output.",
|
"description": "This is an application that provides the ability to convert code snippets in multiple programming languages. You can input the code you wish to convert, select the target programming language, and get the desired output.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -291,7 +291,7 @@
|
|||||||
"name": "Question Classifier + Knowledge + Chatbot "
|
"name": "Question Classifier + Knowledge + Chatbot "
|
||||||
},
|
},
|
||||||
"app_id": "d9f6b733-e35d-4a40-9f38-ca7bbfa009f7",
|
"app_id": "d9f6b733-e35d-4a40-9f38-ca7bbfa009f7",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "Basic Workflow Template, a chatbot capable of identifying intents alongside with a knowledge base.",
|
"description": "Basic Workflow Template, a chatbot capable of identifying intents alongside with a knowledge base.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -307,7 +307,7 @@
|
|||||||
"name": "AI Front-end interviewer"
|
"name": "AI Front-end interviewer"
|
||||||
},
|
},
|
||||||
"app_id": "127efead-8944-4e20-ba9d-12402eb345e0",
|
"app_id": "127efead-8944-4e20-ba9d-12402eb345e0",
|
||||||
"category": "HR",
|
"categories": ["HR"],
|
||||||
"copyright": "Copyright 2023 Dify",
|
"copyright": "Copyright 2023 Dify",
|
||||||
"description": "A simulated front-end interviewer that tests the skill level of front-end development through questioning.",
|
"description": "A simulated front-end interviewer that tests the skill level of front-end development through questioning.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -323,7 +323,7 @@
|
|||||||
"name": "Knowledge Retrieval + Chatbot "
|
"name": "Knowledge Retrieval + Chatbot "
|
||||||
},
|
},
|
||||||
"app_id": "e9870913-dd01-4710-9f06-15d4180ca1ce",
|
"app_id": "e9870913-dd01-4710-9f06-15d4180ca1ce",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "Basic Workflow Template, A chatbot with a knowledge base. ",
|
"description": "Basic Workflow Template, A chatbot with a knowledge base. ",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -339,7 +339,7 @@
|
|||||||
"name": "Email Assistant Workflow "
|
"name": "Email Assistant Workflow "
|
||||||
},
|
},
|
||||||
"app_id": "dd5b6353-ae9b-4bce-be6a-a681a12cf709",
|
"app_id": "dd5b6353-ae9b-4bce-be6a-a681a12cf709",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "A multifunctional email assistant capable of summarizing, replying, composing, proofreading, and checking grammar.",
|
"description": "A multifunctional email assistant capable of summarizing, replying, composing, proofreading, and checking grammar.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
@ -355,7 +355,7 @@
|
|||||||
"name": "Customer Review Analysis Workflow "
|
"name": "Customer Review Analysis Workflow "
|
||||||
},
|
},
|
||||||
"app_id": "9c0cd31f-4b62-4005-adf5-e3888d08654a",
|
"app_id": "9c0cd31f-4b62-4005-adf5-e3888d08654a",
|
||||||
"category": "Workflow",
|
"categories": ["Workflow"],
|
||||||
"copyright": null,
|
"copyright": null,
|
||||||
"description": "Utilize LLM (Large Language Models) to classify customer reviews and forward them to the internal system.",
|
"description": "Utilize LLM (Large Language Models) to classify customer reviews and forward them to the internal system.",
|
||||||
"is_listed": true,
|
"is_listed": true,
|
||||||
|
|||||||
@ -25,6 +25,7 @@ from controllers.console.wraps import (
|
|||||||
is_admin_or_owner_required,
|
is_admin_or_owner_required,
|
||||||
setup_required,
|
setup_required,
|
||||||
)
|
)
|
||||||
|
from core.db.session_factory import session_factory
|
||||||
from core.ops.ops_trace_manager import OpsTraceManager
|
from core.ops.ops_trace_manager import OpsTraceManager
|
||||||
from core.rag.entities import PreProcessingRule, Rule, Segmentation
|
from core.rag.entities import PreProcessingRule, Rule, Segmentation
|
||||||
from core.rag.retrieval.retrieval_methods import RetrievalMethod
|
from core.rag.retrieval.retrieval_methods import RetrievalMethod
|
||||||
@ -841,7 +842,8 @@ class AppTraceApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self, app_id):
|
def get(self, app_id):
|
||||||
"""Get app trace"""
|
"""Get app trace"""
|
||||||
app_trace_config = OpsTraceManager.get_app_tracing_config(app_id=app_id)
|
with session_factory.create_session() as session:
|
||||||
|
app_trace_config = OpsTraceManager.get_app_tracing_config(app_id, session)
|
||||||
|
|
||||||
return app_trace_config
|
return app_trace_config
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class RecommendedAppResponse(ResponseModel):
|
|||||||
copyright: str | None = None
|
copyright: str | None = None
|
||||||
privacy_policy: str | None = None
|
privacy_policy: str | None = None
|
||||||
custom_disclaimer: str | None = None
|
custom_disclaimer: str | None = None
|
||||||
category: str | None = None
|
categories: list[str] = Field(default_factory=list)
|
||||||
position: int | None = None
|
position: int | None = None
|
||||||
is_listed: bool | None = None
|
is_listed: bool | None = None
|
||||||
can_trial: bool | None = None
|
can_trial: bool | None = None
|
||||||
|
|||||||
@ -876,10 +876,10 @@ class ToolBuiltinProviderSetDefaultApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, provider):
|
def post(self, provider):
|
||||||
current_user, current_tenant_id = current_account_with_tenant()
|
_, current_tenant_id = current_account_with_tenant()
|
||||||
payload = BuiltinProviderDefaultCredentialPayload.model_validate(console_ns.payload or {})
|
payload = BuiltinProviderDefaultCredentialPayload.model_validate(console_ns.payload or {})
|
||||||
return BuiltinToolManageService.set_default_provider(
|
return BuiltinToolManageService.set_default_provider(
|
||||||
tenant_id=current_tenant_id, user_id=current_user.id, provider=provider, id=payload.id
|
tenant_id=current_tenant_id, provider=provider, id=payload.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ from controllers.web.wraps import WebApiResource
|
|||||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||||
from graphon.model_runtime.errors.invoke import InvokeError
|
from graphon.model_runtime.errors.invoke import InvokeError
|
||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
from models.model import App
|
from models.model import App, EndUser
|
||||||
from services.audio_service import AudioService
|
from services.audio_service import AudioService
|
||||||
from services.errors.audio import (
|
from services.errors.audio import (
|
||||||
AudioTooLargeServiceError,
|
AudioTooLargeServiceError,
|
||||||
@ -69,12 +69,12 @@ class AudioApi(WebApiResource):
|
|||||||
500: "Internal Server Error",
|
500: "Internal Server Error",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def post(self, app_model: App, end_user):
|
def post(self, app_model: App, end_user: EndUser):
|
||||||
"""Convert audio to text"""
|
"""Convert audio to text"""
|
||||||
file = request.files["file"]
|
file = request.files["file"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=end_user)
|
response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=end_user.external_user_id)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except services.errors.app_model_config.AppModelConfigBrokenError:
|
except services.errors.app_model_config.AppModelConfigBrokenError:
|
||||||
@ -117,7 +117,7 @@ class TextApi(WebApiResource):
|
|||||||
500: "Internal Server Error",
|
500: "Internal Server Error",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def post(self, app_model: App, end_user):
|
def post(self, app_model: App, end_user: EndUser):
|
||||||
"""Convert text to audio"""
|
"""Convert text to audio"""
|
||||||
try:
|
try:
|
||||||
payload = TextToAudioPayload.model_validate(web_ns.payload or {})
|
payload = TextToAudioPayload.model_validate(web_ns.payload or {})
|
||||||
|
|||||||
@ -844,24 +844,24 @@ class WorkflowResponseConverter:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
files: list[Mapping[str, Any]] = []
|
files: list[Mapping[str, Any]] = []
|
||||||
if isinstance(value, FileSegment):
|
match value:
|
||||||
files.append(value.value.to_dict())
|
case FileSegment():
|
||||||
elif isinstance(value, ArrayFileSegment):
|
files.append(value.value.to_dict())
|
||||||
files.extend([i.to_dict() for i in value.value])
|
case ArrayFileSegment():
|
||||||
elif isinstance(value, File):
|
files.extend([i.to_dict() for i in value.value])
|
||||||
files.append(value.to_dict())
|
case File():
|
||||||
elif isinstance(value, list):
|
files.append(value.to_dict())
|
||||||
for item in value:
|
case list():
|
||||||
file = cls._get_file_var_from_value(item)
|
for item in value:
|
||||||
|
file = cls._get_file_var_from_value(item)
|
||||||
|
if file:
|
||||||
|
files.append(file)
|
||||||
|
case dict():
|
||||||
|
file = cls._get_file_var_from_value(value)
|
||||||
if file:
|
if file:
|
||||||
files.append(file)
|
files.append(file)
|
||||||
elif isinstance(
|
case _:
|
||||||
value,
|
pass
|
||||||
dict,
|
|
||||||
):
|
|
||||||
file = cls._get_file_var_from_value(value)
|
|
||||||
if file:
|
|
||||||
files.append(file)
|
|
||||||
|
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|||||||
@ -569,13 +569,13 @@ class OpsTraceManager:
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_app_tracing_config(cls, app_id: str):
|
def get_app_tracing_config(cls, app_id: str, session: Session):
|
||||||
"""
|
"""
|
||||||
Get app tracing config
|
Get app tracing config
|
||||||
:param app_id: app id
|
:param app_id: app id
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
app: App | None = db.session.get(App, app_id)
|
app: App | None = session.get(App, app_id)
|
||||||
if not app:
|
if not app:
|
||||||
raise ValueError("App not found")
|
raise ValueError("App not found")
|
||||||
if not app.tracing:
|
if not app.tracing:
|
||||||
|
|||||||
@ -53,24 +53,27 @@ class PromptMessageUtil:
|
|||||||
files = []
|
files = []
|
||||||
if isinstance(prompt_message.content, list):
|
if isinstance(prompt_message.content, list):
|
||||||
for content in prompt_message.content:
|
for content in prompt_message.content:
|
||||||
if isinstance(content, TextPromptMessageContent):
|
match content:
|
||||||
text += content.data
|
case TextPromptMessageContent():
|
||||||
elif isinstance(content, ImagePromptMessageContent):
|
text += content.data
|
||||||
files.append(
|
case ImagePromptMessageContent():
|
||||||
{
|
files.append(
|
||||||
"type": "image",
|
{
|
||||||
"data": content.data[:10] + "...[TRUNCATED]..." + content.data[-10:],
|
"type": "image",
|
||||||
"detail": content.detail.value,
|
"data": content.data[:10] + "...[TRUNCATED]..." + content.data[-10:],
|
||||||
}
|
"detail": content.detail.value,
|
||||||
)
|
}
|
||||||
elif isinstance(content, AudioPromptMessageContent):
|
)
|
||||||
files.append(
|
case AudioPromptMessageContent():
|
||||||
{
|
files.append(
|
||||||
"type": "audio",
|
{
|
||||||
"data": content.data[:10] + "...[TRUNCATED]..." + content.data[-10:],
|
"type": "audio",
|
||||||
"format": content.format,
|
"data": content.data[:10] + "...[TRUNCATED]..." + content.data[-10:],
|
||||||
}
|
"format": content.format,
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
text = cast(str, prompt_message.content)
|
text = cast(str, prompt_message.content)
|
||||||
|
|
||||||
|
|||||||
@ -23,36 +23,37 @@ _TOOL_FILE_URL_PATTERN = re.compile(r"(?:^|/+)files/tools/(?P<tool_file_id>[^/?#
|
|||||||
|
|
||||||
|
|
||||||
def safe_json_value(v):
|
def safe_json_value(v):
|
||||||
if isinstance(v, datetime):
|
match v:
|
||||||
tz_name = "UTC"
|
case datetime():
|
||||||
if isinstance(current_user, Account) and current_user.timezone is not None:
|
tz_name = "UTC"
|
||||||
tz_name = current_user.timezone
|
if isinstance(current_user, Account) and current_user.timezone is not None:
|
||||||
return v.astimezone(pytz.timezone(tz_name)).isoformat()
|
tz_name = current_user.timezone
|
||||||
elif isinstance(v, date):
|
return v.astimezone(pytz.timezone(tz_name)).isoformat()
|
||||||
return v.isoformat()
|
case date():
|
||||||
elif isinstance(v, UUID):
|
return v.isoformat()
|
||||||
return str(v)
|
case UUID():
|
||||||
elif isinstance(v, Decimal):
|
return str(v)
|
||||||
return float(v)
|
case Decimal():
|
||||||
elif isinstance(v, bytes):
|
return float(v)
|
||||||
try:
|
case bytes():
|
||||||
return v.decode("utf-8")
|
try:
|
||||||
except UnicodeDecodeError:
|
return v.decode("utf-8")
|
||||||
return v.hex()
|
except UnicodeDecodeError:
|
||||||
elif isinstance(v, memoryview):
|
return v.hex()
|
||||||
return v.tobytes().hex()
|
case memoryview():
|
||||||
elif isinstance(v, np.integer):
|
return v.tobytes().hex()
|
||||||
return int(v)
|
case np.integer():
|
||||||
elif isinstance(v, np.floating):
|
return int(v)
|
||||||
return float(v)
|
case np.floating():
|
||||||
elif isinstance(v, np.ndarray):
|
return float(v)
|
||||||
return v.tolist()
|
case np.ndarray():
|
||||||
elif isinstance(v, dict):
|
return v.tolist()
|
||||||
return safe_json_dict(v)
|
case dict():
|
||||||
elif isinstance(v, list | tuple | set):
|
return safe_json_dict(v)
|
||||||
return [safe_json_value(i) for i in v]
|
case list() | tuple() | set():
|
||||||
else:
|
return [safe_json_value(i) for i in v]
|
||||||
return v
|
case _:
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
def safe_json_dict(d: dict[str, Any]):
|
def safe_json_dict(d: dict[str, Any]):
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
"""add recommended app categories
|
||||||
|
|
||||||
|
Revision ID: a4f2d8c9b731
|
||||||
|
Revises: 227822d22895
|
||||||
|
Create Date: 2026-04-29 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "a4f2d8c9b731"
|
||||||
|
down_revision = "227822d22895"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("categories", sa.JSON(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("categories")
|
||||||
@ -878,6 +878,7 @@ class RecommendedApp(TypeBase):
|
|||||||
copyright: Mapped[str] = mapped_column(String(255), nullable=False)
|
copyright: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
privacy_policy: Mapped[str] = mapped_column(String(255), nullable=False)
|
privacy_policy: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
category: Mapped[str] = mapped_column(String(255), nullable=False)
|
category: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
categories: Mapped[list[str] | None] = mapped_column(sa.JSON, nullable=True, default=None)
|
||||||
custom_disclaimer: Mapped[str] = mapped_column(LongText, default="")
|
custom_disclaimer: Mapped[str] = mapped_column(LongText, default="")
|
||||||
position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
|
position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
|
||||||
is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
|
is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
|
||||||
|
|||||||
49
api/services/recommend_app/category_order.py
Normal file
49
api/services/recommend_app/category_order.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""Apply Redis-backed category ordering for DB-backed Explore apps."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from collections.abc import Collection
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from extensions.ext_redis import redis_client
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
EXPLORE_APP_CATEGORY_ORDER_KEY_PREFIX = "explore:apps:category_order"
|
||||||
|
|
||||||
|
|
||||||
|
def _category_order_key(language: str) -> str:
|
||||||
|
return f"{EXPLORE_APP_CATEGORY_ORDER_KEY_PREFIX}:{language}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_explore_app_category_order(language: str) -> list[str]:
|
||||||
|
try:
|
||||||
|
raw_categories = redis_client.get(_category_order_key(language))
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to read explore app category order from Redis.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not raw_categories:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(raw_categories, bytes):
|
||||||
|
raw_categories = raw_categories.decode("utf-8")
|
||||||
|
|
||||||
|
try:
|
||||||
|
categories: Any = json.loads(raw_categories)
|
||||||
|
except (TypeError, json.JSONDecodeError):
|
||||||
|
logger.warning("Invalid explore app category order payload for language %s.", language)
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not isinstance(categories, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [category for category in categories if isinstance(category, str)]
|
||||||
|
|
||||||
|
|
||||||
|
def order_categories(categories: Collection[str], language: str) -> list[str]:
|
||||||
|
configured_order = get_explore_app_category_order(language)
|
||||||
|
if configured_order:
|
||||||
|
return configured_order
|
||||||
|
|
||||||
|
return sorted(categories)
|
||||||
@ -6,6 +6,7 @@ from constants.languages import languages
|
|||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.model import App, RecommendedApp
|
from models.model import App, RecommendedApp
|
||||||
from services.app_dsl_service import AppDslService
|
from services.app_dsl_service import AppDslService
|
||||||
|
from services.recommend_app.category_order import order_categories
|
||||||
from services.recommend_app.recommend_app_base import RecommendAppRetrievalBase
|
from services.recommend_app.recommend_app_base import RecommendAppRetrievalBase
|
||||||
from services.recommend_app.recommend_app_type import RecommendAppType
|
from services.recommend_app.recommend_app_type import RecommendAppType
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ class RecommendedAppItemDict(TypedDict):
|
|||||||
copyright: Any
|
copyright: Any
|
||||||
privacy_policy: Any
|
privacy_policy: Any
|
||||||
custom_disclaimer: str
|
custom_disclaimer: str
|
||||||
category: str
|
categories: list[str]
|
||||||
position: int
|
position: int
|
||||||
is_listed: bool
|
is_listed: bool
|
||||||
|
|
||||||
@ -80,6 +81,7 @@ class DatabaseRecommendAppRetrieval(RecommendAppRetrievalBase):
|
|||||||
if not site:
|
if not site:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
app_categories = recommended_app.categories or []
|
||||||
recommended_app_result: RecommendedAppItemDict = {
|
recommended_app_result: RecommendedAppItemDict = {
|
||||||
"id": recommended_app.id,
|
"id": recommended_app.id,
|
||||||
"app": recommended_app.app,
|
"app": recommended_app.app,
|
||||||
@ -88,15 +90,18 @@ class DatabaseRecommendAppRetrieval(RecommendAppRetrievalBase):
|
|||||||
"copyright": site.copyright,
|
"copyright": site.copyright,
|
||||||
"privacy_policy": site.privacy_policy,
|
"privacy_policy": site.privacy_policy,
|
||||||
"custom_disclaimer": site.custom_disclaimer,
|
"custom_disclaimer": site.custom_disclaimer,
|
||||||
"category": recommended_app.category,
|
"categories": app_categories,
|
||||||
"position": recommended_app.position,
|
"position": recommended_app.position,
|
||||||
"is_listed": recommended_app.is_listed,
|
"is_listed": recommended_app.is_listed,
|
||||||
}
|
}
|
||||||
recommended_apps_result.append(recommended_app_result)
|
recommended_apps_result.append(recommended_app_result)
|
||||||
|
|
||||||
categories.add(recommended_app.category)
|
categories.update(app_categories)
|
||||||
|
|
||||||
return RecommendedAppsResultDict(recommended_apps=recommended_apps_result, categories=sorted(categories))
|
return RecommendedAppsResultDict(
|
||||||
|
recommended_apps=recommended_apps_result,
|
||||||
|
categories=order_categories(categories, language),
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fetch_recommended_app_detail_from_db(cls, app_id: str) -> RecommendedAppDetailDict | None:
|
def fetch_recommended_app_detail_from_db(cls, app_id: str) -> RecommendedAppDetailDict | None:
|
||||||
|
|||||||
@ -408,7 +408,7 @@ class BuiltinToolManageService:
|
|||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_default_provider(tenant_id: str, user_id: str, provider: str, id: str):
|
def set_default_provider(tenant_id: str, provider: str, id: str):
|
||||||
"""
|
"""
|
||||||
set default provider
|
set default provider
|
||||||
"""
|
"""
|
||||||
@ -422,12 +422,11 @@ class BuiltinToolManageService:
|
|||||||
if target_provider is None:
|
if target_provider is None:
|
||||||
raise ValueError("provider not found")
|
raise ValueError("provider not found")
|
||||||
|
|
||||||
# clear default provider
|
# clear default provider (tenant-scoped: only one default per provider per workspace)
|
||||||
session.execute(
|
session.execute(
|
||||||
update(BuiltinToolProvider)
|
update(BuiltinToolProvider)
|
||||||
.where(
|
.where(
|
||||||
BuiltinToolProvider.tenant_id == tenant_id,
|
BuiltinToolProvider.tenant_id == tenant_id,
|
||||||
BuiltinToolProvider.user_id == user_id,
|
|
||||||
BuiltinToolProvider.provider == provider,
|
BuiltinToolProvider.provider == provider,
|
||||||
BuiltinToolProvider.is_default.is_(True),
|
BuiltinToolProvider.is_default.is_(True),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -194,14 +194,15 @@ class VariableTruncator(BaseTruncator):
|
|||||||
|
|
||||||
result: _PartResult[Any]
|
result: _PartResult[Any]
|
||||||
# Apply type-specific truncation with target size
|
# Apply type-specific truncation with target size
|
||||||
if isinstance(segment, ArraySegment):
|
match segment:
|
||||||
result = self._truncate_array(segment.value, target_size)
|
case ArraySegment():
|
||||||
elif isinstance(segment, StringSegment):
|
result = self._truncate_array(segment.value, target_size)
|
||||||
result = self._truncate_string(segment.value, target_size)
|
case StringSegment():
|
||||||
elif isinstance(segment, ObjectSegment):
|
result = self._truncate_string(segment.value, target_size)
|
||||||
result = self._truncate_object(segment.value, target_size)
|
case ObjectSegment():
|
||||||
else:
|
result = self._truncate_object(segment.value, target_size)
|
||||||
raise AssertionError("this should be unreachable.")
|
case _:
|
||||||
|
raise AssertionError("this should be unreachable.")
|
||||||
|
|
||||||
return _PartResult(
|
return _PartResult(
|
||||||
value=segment.model_copy(update={"value": result.value}),
|
value=segment.model_copy(update={"value": result.value}),
|
||||||
@ -219,40 +220,41 @@ class VariableTruncator(BaseTruncator):
|
|||||||
return VariableTruncator.calculate_json_size(value.model_dump(), depth=depth + 1)
|
return VariableTruncator.calculate_json_size(value.model_dump(), depth=depth + 1)
|
||||||
if depth > _MAX_DEPTH:
|
if depth > _MAX_DEPTH:
|
||||||
raise MaxDepthExceededError()
|
raise MaxDepthExceededError()
|
||||||
if isinstance(value, str):
|
match value:
|
||||||
# Ideally, the size of strings should be calculated based on their utf-8 encoded length.
|
case str():
|
||||||
# However, this adds complexity as we would need to compute encoded sizes consistently
|
# Ideally, the size of strings should be calculated based on their utf-8 encoded length.
|
||||||
# throughout the code. Therefore, we approximate the size using the string's length.
|
# However, this adds complexity as we would need to compute encoded sizes consistently
|
||||||
# Rough estimate: number of characters, plus 2 for quotes
|
# throughout the code. Therefore, we approximate the size using the string's length.
|
||||||
return len(value) + 2
|
# Rough estimate: number of characters, plus 2 for quotes
|
||||||
elif isinstance(value, (int, float)):
|
return len(value) + 2
|
||||||
return len(str(value))
|
case bool():
|
||||||
elif isinstance(value, bool):
|
return 4 if value else 5 # "true" or "false"
|
||||||
return 4 if value else 5 # "true" or "false"
|
case int() | float():
|
||||||
elif value is None:
|
return len(str(value))
|
||||||
return 4 # "null"
|
case None:
|
||||||
elif isinstance(value, list):
|
return 4 # "null"
|
||||||
# Size = sum of elements + separators + brackets
|
case list():
|
||||||
total = 2 # "[]"
|
# Size = sum of elements + separators + brackets
|
||||||
for i, item in enumerate(value):
|
total = 2 # "[]"
|
||||||
if i > 0:
|
for i, item in enumerate(value):
|
||||||
total += 1 # ","
|
if i > 0:
|
||||||
total += VariableTruncator.calculate_json_size(item, depth=depth + 1)
|
total += 1 # ","
|
||||||
return total
|
total += VariableTruncator.calculate_json_size(item, depth=depth + 1)
|
||||||
elif isinstance(value, dict):
|
return total
|
||||||
# Size = sum of keys + values + separators + brackets
|
case dict():
|
||||||
total = 2 # "{}"
|
# Size = sum of keys + values + separators + brackets
|
||||||
for index, key in enumerate(value.keys()):
|
total = 2 # "{}"
|
||||||
if index > 0:
|
for index, key in enumerate(value.keys()):
|
||||||
total += 1 # ","
|
if index > 0:
|
||||||
total += VariableTruncator.calculate_json_size(str(key), depth=depth + 1) # Key as string
|
total += 1 # ","
|
||||||
total += 1 # ":"
|
total += VariableTruncator.calculate_json_size(str(key), depth=depth + 1) # Key as string
|
||||||
total += VariableTruncator.calculate_json_size(value[key], depth=depth + 1)
|
total += 1 # ":"
|
||||||
return total
|
total += VariableTruncator.calculate_json_size(value[key], depth=depth + 1)
|
||||||
elif isinstance(value, File):
|
return total
|
||||||
return VariableTruncator.calculate_json_size(value.model_dump(), depth=depth + 1)
|
case File():
|
||||||
else:
|
return VariableTruncator.calculate_json_size(value.model_dump(), depth=depth + 1)
|
||||||
raise UnknownTypeError(f"got unknown type {type(value)}")
|
case _:
|
||||||
|
raise UnknownTypeError(f"got unknown type {type(value)}")
|
||||||
|
|
||||||
def _truncate_string(self, value: str, target_size: int) -> _PartResult[str]:
|
def _truncate_string(self, value: str, target_size: int) -> _PartResult[str]:
|
||||||
if (size := self.calculate_json_size(value)) < target_size:
|
if (size := self.calculate_json_size(value)) < target_size:
|
||||||
@ -419,22 +421,23 @@ class VariableTruncator(BaseTruncator):
|
|||||||
target_size: int,
|
target_size: int,
|
||||||
) -> _PartResult[Any]:
|
) -> _PartResult[Any]:
|
||||||
"""Truncate a value within an object to fit within budget."""
|
"""Truncate a value within an object to fit within budget."""
|
||||||
if isinstance(val, UpdatedVariable):
|
match val:
|
||||||
# TODO(Workflow): push UpdatedVariable normalization closer to its producer.
|
case UpdatedVariable():
|
||||||
return self._truncate_object(val.model_dump(), target_size)
|
# TODO(Workflow): push UpdatedVariable normalization closer to its producer.
|
||||||
elif isinstance(val, str):
|
return self._truncate_object(val.model_dump(), target_size)
|
||||||
return self._truncate_string(val, target_size)
|
case str():
|
||||||
elif isinstance(val, list):
|
return self._truncate_string(val, target_size)
|
||||||
return self._truncate_array(val, target_size)
|
case list():
|
||||||
elif isinstance(val, dict):
|
return self._truncate_array(val, target_size)
|
||||||
return self._truncate_object(val, target_size)
|
case dict():
|
||||||
elif isinstance(val, File):
|
return self._truncate_object(val, target_size)
|
||||||
# File objects should not be truncated, return as-is
|
case File():
|
||||||
return _PartResult(val, self.calculate_json_size(val), False)
|
# File objects should not be truncated, return as-is
|
||||||
elif val is None or isinstance(val, (bool, int, float)):
|
return _PartResult(val, self.calculate_json_size(val), False)
|
||||||
return _PartResult(val, self.calculate_json_size(val), False)
|
case None | bool() | int() | float():
|
||||||
else:
|
return _PartResult(val, self.calculate_json_size(val), False)
|
||||||
raise AssertionError("this statement should be unreachable.")
|
case _:
|
||||||
|
raise AssertionError("this statement should be unreachable.")
|
||||||
|
|
||||||
|
|
||||||
class DummyVariableTruncator(BaseTruncator):
|
class DummyVariableTruncator(BaseTruncator):
|
||||||
|
|||||||
@ -47,6 +47,7 @@ def _create_recommended_app(
|
|||||||
*,
|
*,
|
||||||
app_id: str,
|
app_id: str,
|
||||||
category: str = "chat",
|
category: str = "chat",
|
||||||
|
categories: list[str] | None = None,
|
||||||
language: str = "en-US",
|
language: str = "en-US",
|
||||||
is_listed: bool = True,
|
is_listed: bool = True,
|
||||||
position: int = 1,
|
position: int = 1,
|
||||||
@ -57,6 +58,7 @@ def _create_recommended_app(
|
|||||||
copyright="copy",
|
copyright="copy",
|
||||||
privacy_policy="pp",
|
privacy_policy="pp",
|
||||||
category=category,
|
category=category,
|
||||||
|
categories=[category] if categories is None else categories,
|
||||||
language=language,
|
language=language,
|
||||||
is_listed=is_listed,
|
is_listed=is_listed,
|
||||||
position=position,
|
position=position,
|
||||||
@ -113,6 +115,53 @@ class TestFetchRecommendedAppsFromDb:
|
|||||||
assert "assistant" in result["categories"]
|
assert "assistant" in result["categories"]
|
||||||
assert "writing" in result["categories"]
|
assert "writing" in result["categories"]
|
||||||
|
|
||||||
|
def test_returns_multiple_categories_for_one_app(
|
||||||
|
self, flask_app_with_containers, db_session_with_containers: Session
|
||||||
|
):
|
||||||
|
tenant_id = str(uuid4())
|
||||||
|
created_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
|
||||||
|
_create_site(db_session_with_containers, app_id=created_app.id)
|
||||||
|
_create_recommended_app(
|
||||||
|
db_session_with_containers,
|
||||||
|
app_id=created_app.id,
|
||||||
|
category="writing",
|
||||||
|
categories=["writing", "assistant"],
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session_with_containers.expire_all()
|
||||||
|
|
||||||
|
result = DatabaseRecommendAppRetrieval.fetch_recommended_apps_from_db("en-US")
|
||||||
|
|
||||||
|
recommended_app = next(item for item in result["recommended_apps"] if item["app_id"] == created_app.id)
|
||||||
|
assert recommended_app["categories"] == ["writing", "assistant"]
|
||||||
|
assert "writing" in result["categories"]
|
||||||
|
assert "assistant" in result["categories"]
|
||||||
|
|
||||||
|
def test_ignores_legacy_category_when_categories_are_empty(
|
||||||
|
self,
|
||||||
|
flask_app_with_containers,
|
||||||
|
db_session_with_containers: Session,
|
||||||
|
):
|
||||||
|
legacy_category = f"legacy-empty-{uuid4()}"
|
||||||
|
tenant_id = str(uuid4())
|
||||||
|
created_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
|
||||||
|
_create_site(db_session_with_containers, app_id=created_app.id)
|
||||||
|
_create_recommended_app(
|
||||||
|
db_session_with_containers,
|
||||||
|
app_id=created_app.id,
|
||||||
|
category=legacy_category,
|
||||||
|
categories=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session_with_containers.expire_all()
|
||||||
|
|
||||||
|
result = DatabaseRecommendAppRetrieval.fetch_recommended_apps_from_db("en-US")
|
||||||
|
|
||||||
|
recommended_app = next(item for item in result["recommended_apps"] if item["app_id"] == created_app.id)
|
||||||
|
assert "category" not in recommended_app
|
||||||
|
assert recommended_app["categories"] == []
|
||||||
|
assert legacy_category not in result["categories"]
|
||||||
|
|
||||||
def test_falls_back_to_default_language_when_empty(
|
def test_falls_back_to_default_language_when_empty(
|
||||||
self, flask_app_with_containers, db_session_with_containers: Session
|
self, flask_app_with_containers, db_session_with_containers: Session
|
||||||
):
|
):
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@ -71,6 +71,7 @@ def _pending_yaml_content(version: str = "99.0.0") -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def _app_stub(**overrides: Any) -> App:
|
def _app_stub(**overrides: Any) -> App:
|
||||||
|
"""Create a stub App object for testing without hitting the database."""
|
||||||
defaults = {
|
defaults = {
|
||||||
"id": str(uuid4()),
|
"id": str(uuid4()),
|
||||||
"tenant_id": _DEFAULT_TENANT_ID,
|
"tenant_id": _DEFAULT_TENANT_ID,
|
||||||
@ -83,7 +84,10 @@ def _app_stub(**overrides: Any) -> App:
|
|||||||
"use_icon_as_answer_icon": False,
|
"use_icon_as_answer_icon": False,
|
||||||
"app_model_config": None,
|
"app_model_config": None,
|
||||||
}
|
}
|
||||||
return cast(App, SimpleNamespace(**(defaults | overrides)))
|
app = MagicMock(spec=App)
|
||||||
|
for key, value in (defaults | overrides).items():
|
||||||
|
object.__setattr__(app, key, value)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
class TestAppDslService:
|
class TestAppDslService:
|
||||||
|
|||||||
@ -114,8 +114,8 @@ def test_flask_configs(monkeypatch: pytest.MonkeyPatch):
|
|||||||
"pool_recycle": 3600,
|
"pool_recycle": 3600,
|
||||||
"pool_size": 30,
|
"pool_size": 30,
|
||||||
"pool_use_lifo": False,
|
"pool_use_lifo": False,
|
||||||
"pool_reset_on_return": None,
|
|
||||||
"pool_timeout": 30,
|
"pool_timeout": 30,
|
||||||
|
"pool_reset_on_return": "rollback",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert config["CONSOLE_WEB_URL"] == "https://example.com"
|
assert config["CONSOLE_WEB_URL"] == "https://example.com"
|
||||||
|
|||||||
@ -126,7 +126,7 @@ class TestRecommendedAppResponseModels:
|
|||||||
},
|
},
|
||||||
"app_id": "app-1",
|
"app_id": "app-1",
|
||||||
"description": "desc",
|
"description": "desc",
|
||||||
"category": "cat",
|
"categories": ["cat", "other"],
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_listed": True,
|
"is_listed": True,
|
||||||
"can_trial": False,
|
"can_trial": False,
|
||||||
@ -137,4 +137,5 @@ class TestRecommendedAppResponseModels:
|
|||||||
).model_dump(mode="json")
|
).model_dump(mode="json")
|
||||||
|
|
||||||
assert response["recommended_apps"][0]["app_id"] == "app-1"
|
assert response["recommended_apps"][0]["app_id"] == "app-1"
|
||||||
|
assert response["recommended_apps"][0]["categories"] == ["cat", "other"]
|
||||||
assert response["categories"] == ["cat"]
|
assert response["categories"] == ["cat"]
|
||||||
|
|||||||
@ -407,18 +407,18 @@ def test_update_app_tracing_config_success(mock_db):
|
|||||||
def test_get_app_tracing_config_errors_when_missing(mock_db):
|
def test_get_app_tracing_config_errors_when_missing(mock_db):
|
||||||
mock_db.get.return_value = None
|
mock_db.get.return_value = None
|
||||||
with pytest.raises(ValueError, match="App not found"):
|
with pytest.raises(ValueError, match="App not found"):
|
||||||
OpsTraceManager.get_app_tracing_config("app")
|
OpsTraceManager.get_app_tracing_config("app", mock_db)
|
||||||
|
|
||||||
|
|
||||||
def test_get_app_tracing_config_returns_defaults(mock_db):
|
def test_get_app_tracing_config_returns_defaults(mock_db):
|
||||||
mock_db.get.return_value = SimpleNamespace(tracing=None)
|
mock_db.get.return_value = SimpleNamespace(tracing=None)
|
||||||
assert OpsTraceManager.get_app_tracing_config("app-id") == {"enabled": False, "tracing_provider": None}
|
assert OpsTraceManager.get_app_tracing_config("app-id", mock_db) == {"enabled": False, "tracing_provider": None}
|
||||||
|
|
||||||
|
|
||||||
def test_get_app_tracing_config_returns_payload(mock_db):
|
def test_get_app_tracing_config_returns_payload(mock_db):
|
||||||
payload = {"enabled": True, "tracing_provider": "dummy"}
|
payload = {"enabled": True, "tracing_provider": "dummy"}
|
||||||
mock_db.get.return_value = SimpleNamespace(tracing=json.dumps(payload))
|
mock_db.get.return_value = SimpleNamespace(tracing=json.dumps(payload))
|
||||||
assert OpsTraceManager.get_app_tracing_config("app-id") == payload
|
assert OpsTraceManager.get_app_tracing_config("app-id", mock_db) == payload
|
||||||
|
|
||||||
|
|
||||||
def test_check_and_project_helpers(monkeypatch):
|
def test_check_and_project_helpers(monkeypatch):
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from services.recommend_app.category_order import get_explore_app_category_order, order_categories
|
||||||
|
|
||||||
|
|
||||||
|
@patch("services.recommend_app.category_order.redis_client.get")
|
||||||
|
def test_get_explore_app_category_order_returns_redis_list(mock_get):
|
||||||
|
mock_get.return_value = json.dumps(["C", "A", "B"]).encode()
|
||||||
|
|
||||||
|
assert get_explore_app_category_order("en-US") == ["C", "A", "B"]
|
||||||
|
mock_get.assert_called_once_with("explore:apps:category_order:en-US")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("services.recommend_app.category_order.redis_client.get")
|
||||||
|
def test_order_categories_uses_redis_order_as_source_of_truth(mock_get):
|
||||||
|
mock_get.return_value = json.dumps(["C", "A", "B"]).encode()
|
||||||
|
|
||||||
|
assert order_categories({"A", "B", "C", "D"}, "en-US") == ["C", "A", "B"]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("services.recommend_app.category_order.redis_client.get")
|
||||||
|
def test_order_categories_falls_back_to_sorted_categories_without_redis_order(mock_get):
|
||||||
|
mock_get.return_value = None
|
||||||
|
|
||||||
|
assert order_categories({"B", "A", "C"}, "en-US") == ["A", "B", "C"]
|
||||||
@ -180,7 +180,7 @@ class TestSetDefaultProvider:
|
|||||||
session.scalar.return_value = None
|
session.scalar.return_value = None
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="provider not found"):
|
with pytest.raises(ValueError, match="provider not found"):
|
||||||
BuiltinToolManageService.set_default_provider("t", "u", "p", "id")
|
BuiltinToolManageService.set_default_provider("t", "p", "id")
|
||||||
|
|
||||||
@patch(f"{MODULE}.sessionmaker")
|
@patch(f"{MODULE}.sessionmaker")
|
||||||
@patch(f"{MODULE}.db")
|
@patch(f"{MODULE}.db")
|
||||||
@ -189,11 +189,29 @@ class TestSetDefaultProvider:
|
|||||||
target = MagicMock()
|
target = MagicMock()
|
||||||
session.scalar.return_value = target
|
session.scalar.return_value = target
|
||||||
|
|
||||||
result = BuiltinToolManageService.set_default_provider("t", "u", "p", "id")
|
result = BuiltinToolManageService.set_default_provider("t", "p", "id")
|
||||||
|
|
||||||
assert result == {"result": "success"}
|
assert result == {"result": "success"}
|
||||||
assert target.is_default is True
|
assert target.is_default is True
|
||||||
|
|
||||||
|
@patch(f"{MODULE}.sessionmaker")
|
||||||
|
@patch(f"{MODULE}.db")
|
||||||
|
def test_clear_default_is_tenant_scoped_not_user_scoped(self, mock_db, mock_sm_cls):
|
||||||
|
# Regression: clearing prior defaults must NOT filter by user_id, otherwise
|
||||||
|
# two workspace members can each leave their own credential as default at
|
||||||
|
# the same time (the default flag is tenant-scoped, not per-user).
|
||||||
|
session = _mock_sessionmaker(mock_sm_cls)
|
||||||
|
session.scalar.return_value = MagicMock()
|
||||||
|
|
||||||
|
BuiltinToolManageService.set_default_provider("tenant-1", "google", "cred-id")
|
||||||
|
|
||||||
|
session.execute.assert_called_once()
|
||||||
|
update_stmt = session.execute.call_args.args[0]
|
||||||
|
compiled = str(update_stmt.compile(compile_kwargs={"literal_binds": True}))
|
||||||
|
assert "user_id" not in compiled
|
||||||
|
assert "tenant_id" in compiled
|
||||||
|
assert "provider" in compiled
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateBuiltinToolProvider:
|
class TestUpdateBuiltinToolProvider:
|
||||||
@patch(f"{MODULE}.sessionmaker")
|
@patch(f"{MODULE}.sessionmaker")
|
||||||
|
|||||||
@ -93,10 +93,16 @@ BASE_API_AND_DOCKER_COMPOSE_CONFIG_SET_DIFF: frozenset[str] = frozenset(
|
|||||||
|
|
||||||
API_CONFIG_SET = set(dotenv_values(Path("api") / Path(".env.example")).keys())
|
API_CONFIG_SET = set(dotenv_values(Path("api") / Path(".env.example")).keys())
|
||||||
DOCKER_CONFIG_SET = set(dotenv_values(Path("docker") / Path(".env.example")).keys())
|
DOCKER_CONFIG_SET = set(dotenv_values(Path("docker") / Path(".env.example")).keys())
|
||||||
DOCKER_COMPOSE_CONFIG_SET = set()
|
DOCKER_COMPOSE_CONFIG_SET = set(DOCKER_CONFIG_SET)
|
||||||
|
|
||||||
with open(Path("docker") / Path("docker-compose.yaml")) as f:
|
# Read environment variables from the split env files used by docker-compose
|
||||||
DOCKER_COMPOSE_CONFIG_SET = set(yaml.safe_load(f.read())["x-shared-env"].keys())
|
# Walk through all .env.example files in subdirectories (per-module structure)
|
||||||
|
envs_dir = Path("docker") / Path("envs")
|
||||||
|
if envs_dir.exists():
|
||||||
|
for env_file_path in envs_dir.rglob("*.env.example"):
|
||||||
|
env_keys = set(dotenv_values(env_file_path).keys())
|
||||||
|
DOCKER_CONFIG_SET.update(env_keys)
|
||||||
|
DOCKER_COMPOSE_CONFIG_SET.update(env_keys)
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_config():
|
def test_yaml_config():
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
# ------------------------------------------------------------------
|
|
||||||
# Minimal defaults for Docker Compose deployments.
|
|
||||||
#
|
|
||||||
# Keep local changes in .env. Use .env.example as the full reference
|
|
||||||
# for advanced and service-specific settings.
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Public URLs used when Dify generates links. Change these together when
|
|
||||||
# exposing Dify under another hostname, IP address, or port.
|
|
||||||
CONSOLE_WEB_URL=http://localhost
|
|
||||||
SERVICE_API_URL=http://localhost
|
|
||||||
APP_WEB_URL=http://localhost
|
|
||||||
FILES_URL=http://localhost
|
|
||||||
INTERNAL_FILES_URL=http://api:5001
|
|
||||||
TRIGGER_URL=http://localhost
|
|
||||||
ENDPOINT_URL_TEMPLATE=http://localhost/e/{hook_id}
|
|
||||||
NEXT_PUBLIC_SOCKET_URL=ws://localhost
|
|
||||||
EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
|
|
||||||
EXPOSE_PLUGIN_DEBUGGING_PORT=5003
|
|
||||||
|
|
||||||
# Built-in metadata database defaults.
|
|
||||||
DB_TYPE=postgresql
|
|
||||||
DB_USERNAME=postgres
|
|
||||||
DB_PASSWORD=difyai123456
|
|
||||||
DB_HOST=db_postgres
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_DATABASE=dify
|
|
||||||
|
|
||||||
# Built-in Redis defaults.
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=difyai123456
|
|
||||||
|
|
||||||
# Default file storage.
|
|
||||||
STORAGE_TYPE=opendal
|
|
||||||
OPENDAL_SCHEME=fs
|
|
||||||
OPENDAL_FS_ROOT=storage
|
|
||||||
|
|
||||||
# Default vector database.
|
|
||||||
VECTOR_STORE=weaviate
|
|
||||||
|
|
||||||
# Internal service authentication. Paired values must match.
|
|
||||||
PLUGIN_DAEMON_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi
|
|
||||||
PLUGIN_DIFY_INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
|
||||||
|
|
||||||
# Host ports.
|
|
||||||
EXPOSE_NGINX_PORT=80
|
|
||||||
EXPOSE_NGINX_SSL_PORT=443
|
|
||||||
|
|
||||||
# Docker Compose profiles for bundled services.
|
|
||||||
COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql}
|
|
||||||
1588
docker/.env.example
1588
docker/.env.example
File diff suppressed because it is too large
Load Diff
3
docker/.gitignore
vendored
Normal file
3
docker/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Ignore actual .env files (keep only .env.example files in git)
|
||||||
|
*.env
|
||||||
|
!*.env.example
|
||||||
@ -7,29 +7,31 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
|
|||||||
- **Certbot Container**: `docker-compose.yaml` now contains `certbot` for managing SSL certificates. This container automatically renews certificates and ensures secure HTTPS connections.\
|
- **Certbot Container**: `docker-compose.yaml` now contains `certbot` for managing SSL certificates. This container automatically renews certificates and ensures secure HTTPS connections.\
|
||||||
For more information, refer `docker/certbot/README.md`.
|
For more information, refer `docker/certbot/README.md`.
|
||||||
|
|
||||||
- **Persistent Environment Variables**: Default environment variables are managed through `.env.default`, while local overrides are stored in `.env`, ensuring that your configurations persist across deployments.
|
- **Persistent Environment Variables**: Essential startup defaults are provided in `.env.example`, while local values are stored in `.env`, ensuring that your configurations persist across deployments.
|
||||||
|
|
||||||
> What is `.env`? </br> </br>
|
> What is `.env`? </br> </br>
|
||||||
> The `.env` file is a local override file. Keep it small by adding only the values that differ from `.env.default`. Use `.env.example` as the full reference when you need advanced configuration.
|
> The `.env` file is the local startup file. Copy it from `.env.example` for a default deployment. Optional advanced settings live in `envs/*.env.example` files.
|
||||||
|
|
||||||
- **Unified Vector Database Services**: All vector database services are now managed from a single Docker Compose file `docker-compose.yaml`. You can switch between different vector databases by setting the `VECTOR_STORE` environment variable in your `.env` file.
|
- **Unified Vector Database Services**: All vector database services are now managed from a single Docker Compose file `docker-compose.yaml`. You can switch between different vector databases by setting the `VECTOR_STORE` environment variable in your `.env` file.
|
||||||
|
|
||||||
- **Local .env Overrides**: The `dify-compose` and `dify-compose.ps1` wrappers create `.env` if it is missing and generate a persistent `SECRET_KEY` for this deployment.
|
|
||||||
|
|
||||||
### How to Deploy Dify with `docker-compose.yaml`
|
### How to Deploy Dify with `docker-compose.yaml`
|
||||||
|
|
||||||
1. **Prerequisites**: Ensure Docker and Docker Compose are installed on your system.
|
1. **Prerequisites**: Ensure Docker and Docker Compose are installed on your system.
|
||||||
1. **Environment Setup**:
|
1. **Environment Setup**:
|
||||||
- Navigate to the `docker` directory.
|
- Navigate to the `docker` directory.
|
||||||
- No copy step is required. The `dify-compose` wrappers create `.env` if it is missing and write a generated `SECRET_KEY` to it.
|
- Copy `.env.example` to `.env`.
|
||||||
- When prompted on first run, press Enter to use the default deployment, or answer `y` to stop and edit `.env` first.
|
- Customize `.env` when you need to change essential startup defaults. Copy optional files from `envs/` without the `.example` suffix when you need advanced settings.
|
||||||
- Customize `.env` only when you need to override defaults from `.env.default`. Refer to `.env.example` for the full list of available variables.
|
|
||||||
- **Optional (for advanced deployments)**:
|
- **Optional (for advanced deployments)**:
|
||||||
If you maintain a full `.env` file copied from `.env.example`, you may use the environment synchronization tool to keep it aligned with the latest `.env.example` updates while preserving your custom settings.
|
If you maintain a full `.env` file copied from `.env.example`, you may use the environment synchronization tool to keep it aligned with the latest `.env.example` updates while preserving your custom settings.
|
||||||
See the [Environment Variables Synchronization](#environment-variables-synchronization) section below.
|
See the [Environment Variables Synchronization](#environment-variables-synchronization) section below.
|
||||||
1. **Running the Services**:
|
1. **Running the Services**:
|
||||||
- Execute `./dify-compose up -d` from the `docker` directory to start the services. On Windows PowerShell, run `.\dify-compose.ps1 up -d`.
|
- Execute `docker compose up -d` from the `docker` directory to start the services.
|
||||||
- To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`.
|
- To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`.
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
1. **SSL Certificate Setup**:
|
1. **SSL Certificate Setup**:
|
||||||
- Refer `docker/certbot/README.md` to set up SSL certificates using Certbot.
|
- Refer `docker/certbot/README.md` to set up SSL certificates using Certbot.
|
||||||
1. **OpenTelemetry Collector Setup**:
|
1. **OpenTelemetry Collector Setup**:
|
||||||
@ -41,7 +43,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
|
|||||||
1. **Middleware Setup**:
|
1. **Middleware Setup**:
|
||||||
- Use the `docker-compose.middleware.yaml` for setting up essential middleware services like databases and caches.
|
- Use the `docker-compose.middleware.yaml` for setting up essential middleware services like databases and caches.
|
||||||
- Navigate to the `docker` directory.
|
- Navigate to the `docker` directory.
|
||||||
- Ensure the `middleware.env` file is created by running `cp middleware.env.example middleware.env` (refer to the `middleware.env.example` file).
|
- Ensure the `middleware.env` file is created by running `cp envs/middleware.env.example middleware.env` (refer to the `envs/middleware.env.example` file).
|
||||||
1. **Running Middleware Services**:
|
1. **Running Middleware Services**:
|
||||||
- Navigate to the `docker` directory.
|
- Navigate to the `docker` directory.
|
||||||
- Execute `docker compose --env-file middleware.env -f docker-compose.middleware.yaml -p dify up -d` to start PostgreSQL/MySQL (per `DB_TYPE`) plus the bundled Weaviate instance.
|
- Execute `docker compose --env-file middleware.env -f docker-compose.middleware.yaml -p dify up -d` to start PostgreSQL/MySQL (per `DB_TYPE`) plus the bundled Weaviate instance.
|
||||||
@ -58,13 +60,13 @@ For users migrating from the `docker-legacy` setup:
|
|||||||
1. **Data Migration**:
|
1. **Data Migration**:
|
||||||
- Ensure that data from services like databases and caches is backed up and migrated appropriately to the new structure if necessary.
|
- Ensure that data from services like databases and caches is backed up and migrated appropriately to the new structure if necessary.
|
||||||
|
|
||||||
### Overview of `.env.default`, `.env`, and `.env.example`
|
### Overview of `.env`, `.env.example`, and `envs/`
|
||||||
|
|
||||||
- `.env.default` contains the minimal default configuration for Docker Compose deployments.
|
- `.env.example` contains the essential default configuration for Docker Compose deployments.
|
||||||
- `.env` contains the generated `SECRET_KEY` plus any local overrides.
|
- `.env` contains local startup values copied from `.env.example` and any local changes.
|
||||||
- `.env.example` is the full reference for advanced configuration.
|
- `envs/*.env.example` files contain optional advanced configuration grouped by theme.
|
||||||
|
|
||||||
The `dify-compose` wrappers merge `.env.default` and `.env` into a temporary environment file, append paired internal service keys when needed, and remove the temporary file after Docker Compose starts.
|
Docker Compose reads `envs/*.env` files when present, then reads `.env` last so values in `.env` take precedence.
|
||||||
|
|
||||||
#### Key Modules and Customization
|
#### Key Modules and Customization
|
||||||
|
|
||||||
@ -74,7 +76,7 @@ The `dify-compose` wrappers merge `.env.default` and `.env` into a temporary env
|
|||||||
|
|
||||||
#### Other notable variables
|
#### Other notable variables
|
||||||
|
|
||||||
The `.env.example` file provided in the Docker setup is extensive and covers a wide range of configuration options. It is structured into several sections, each pertaining to different aspects of the application and its services. Here are some of the key sections and variables:
|
The root `.env.example` file contains the essential startup settings. Optional and provider-specific settings are grouped in `envs/*.env.example` files. Here are some of the key sections and variables:
|
||||||
|
|
||||||
1. **Common Variables**:
|
1. **Common Variables**:
|
||||||
|
|
||||||
@ -102,7 +104,7 @@ The `.env.example` file provided in the Docker setup is extensive and covers a w
|
|||||||
|
|
||||||
1. **Storage Configuration**:
|
1. **Storage Configuration**:
|
||||||
|
|
||||||
- `STORAGE_TYPE`, `S3_BUCKET_NAME`, `AZURE_BLOB_ACCOUNT_NAME`: Settings for file storage options like local, S3, Azure Blob, etc.
|
- `STORAGE_TYPE`, `OPENDAL_SCHEME`, `OPENDAL_FS_ROOT`: Default local file storage settings. Optional storage backends are configured from the files under `envs/`.
|
||||||
|
|
||||||
1. **Vector Database Configuration**:
|
1. **Vector Database Configuration**:
|
||||||
|
|
||||||
@ -124,11 +126,11 @@ The `.env.example` file provided in the Docker setup is extensive and covers a w
|
|||||||
|
|
||||||
### Environment Variables Synchronization
|
### Environment Variables Synchronization
|
||||||
|
|
||||||
When upgrading Dify or pulling the latest changes, new environment variables may be introduced in `.env.default` or `.env.example`.
|
When upgrading Dify or pulling the latest changes, new environment variables may be introduced in `.env.example` or the optional files under `envs/`.
|
||||||
|
|
||||||
If you use the default override-only workflow, review `.env.default` and add only the values you need to override to `.env`.
|
If you use the default workflow, review `.env.example` and keep your `.env` aligned with essential startup values.
|
||||||
|
|
||||||
If you maintain a full `.env` file copied from `.env.example`, an optional environment variables synchronization tool is provided.
|
If you maintain a customized `.env` file copied from `.env.example`, an optional environment variables synchronization tool is provided.
|
||||||
|
|
||||||
> This tool performs a **one-way synchronization** from `.env.example` to `.env`.
|
> This tool performs a **one-way synchronization** from `.env.example` to `.env`.
|
||||||
> Existing values in `.env` are never overwritten automatically.
|
> Existing values in `.env` are never overwritten automatically.
|
||||||
|
|||||||
@ -1,334 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
DEFAULT_ENV_FILE=".env.default"
|
|
||||||
USER_ENV_FILE=".env"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '%s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf 'Error: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_compose() {
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_CMD=(docker compose)
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v docker-compose >/dev/null 2>&1; then
|
|
||||||
COMPOSE_CMD=(docker-compose)
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
die "Docker Compose is not available. Install Docker Compose, then run this command again."
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_secret_key() {
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
openssl rand -base64 42
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v dd >/dev/null 2>&1 && command -v base64 >/dev/null 2>&1; then
|
|
||||||
dd if=/dev/urandom bs=42 count=1 2>/dev/null | base64 | tr -d '\n'
|
|
||||||
printf '\n'
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_env_files() {
|
|
||||||
[[ -f "$DEFAULT_ENV_FILE" ]] || die "$DEFAULT_ENV_FILE is missing."
|
|
||||||
|
|
||||||
if [[ -f "$USER_ENV_FILE" ]]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
: >"$USER_ENV_FILE"
|
|
||||||
|
|
||||||
if [[ ! -t 0 ]]; then
|
|
||||||
log "Created $USER_ENV_FILE for local overrides."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf 'Created %s for local overrides.\n' "$USER_ENV_FILE"
|
|
||||||
printf 'Do you need a custom deployment now? (Most users can press Enter to skip.) [y/N] '
|
|
||||||
read -r answer
|
|
||||||
|
|
||||||
case "${answer:-}" in
|
|
||||||
y | Y | yes | YES | Yes)
|
|
||||||
cat <<'EOF'
|
|
||||||
Edit .env with the settings you want to override, using .env.example as the full reference.
|
|
||||||
Run ./dify-compose up -d again when you are ready.
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
user_env_value() {
|
|
||||||
local key="$1"
|
|
||||||
awk -F= -v target="$key" '
|
|
||||||
/^[[:space:]]*#/ || !/=/{ next }
|
|
||||||
{
|
|
||||||
key = $1
|
|
||||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
|
||||||
if (key == target) {
|
|
||||||
value = substr($0, index($0, "=") + 1)
|
|
||||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
||||||
if ((value ~ /^".*"$/) || (value ~ /^'\''.*'\''$/)) {
|
|
||||||
value = substr(value, 2, length(value) - 2)
|
|
||||||
}
|
|
||||||
result = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END { print result }
|
|
||||||
' "$USER_ENV_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
set_user_env_value() {
|
|
||||||
local key="$1"
|
|
||||||
local value="$2"
|
|
||||||
local temp_file
|
|
||||||
|
|
||||||
temp_file="$(mktemp "${TMPDIR:-/tmp}/dify-env.XXXXXX")"
|
|
||||||
awk -F= -v target="$key" -v replacement="$key=$value" '
|
|
||||||
BEGIN { replaced = 0 }
|
|
||||||
/^[[:space:]]*#/ || !/=/{ print; next }
|
|
||||||
{
|
|
||||||
key = $1
|
|
||||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
|
||||||
if (key == target) {
|
|
||||||
if (!replaced) {
|
|
||||||
print replacement
|
|
||||||
replaced = 1
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
print
|
|
||||||
}
|
|
||||||
END {
|
|
||||||
if (!replaced) {
|
|
||||||
print replacement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$USER_ENV_FILE" >"$temp_file"
|
|
||||||
mv "$temp_file" "$USER_ENV_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_secret_key() {
|
|
||||||
local current_secret_key
|
|
||||||
local secret_key
|
|
||||||
|
|
||||||
current_secret_key="$(user_env_value SECRET_KEY)"
|
|
||||||
if [[ -n "$current_secret_key" ]]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
secret_key="$(generate_secret_key)" || die "Unable to generate SECRET_KEY. Install openssl or configure SECRET_KEY in .env."
|
|
||||||
set_user_env_value SECRET_KEY "$secret_key"
|
|
||||||
log "Generated SECRET_KEY in $USER_ENV_FILE."
|
|
||||||
}
|
|
||||||
|
|
||||||
env_value() {
|
|
||||||
local key="$1"
|
|
||||||
awk -F= -v target="$key" '
|
|
||||||
/^[[:space:]]*#/ || !/=/{ next }
|
|
||||||
{
|
|
||||||
key = $1
|
|
||||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
|
||||||
if (key == target) {
|
|
||||||
value = substr($0, index($0, "=") + 1)
|
|
||||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
||||||
if ((value ~ /^".*"$/) || (value ~ /^'\''.*'\''$/)) {
|
|
||||||
value = substr(value, 2, length(value) - 2)
|
|
||||||
}
|
|
||||||
result = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END { print result }
|
|
||||||
' "$DEFAULT_ENV_FILE" "$USER_ENV_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
user_overrides() {
|
|
||||||
local key="$1"
|
|
||||||
grep -Eq "^[[:space:]]*${key}[[:space:]]*=" "$USER_ENV_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
write_merged_env() {
|
|
||||||
awk '
|
|
||||||
function trim(s) {
|
|
||||||
sub(/^[[:space:]]+/, "", s)
|
|
||||||
sub(/[[:space:]]+$/, "", s)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
/^[[:space:]]*#/ || !/=/{ next }
|
|
||||||
|
|
||||||
{
|
|
||||||
key = $0
|
|
||||||
sub(/=.*/, "", key)
|
|
||||||
key = trim(key)
|
|
||||||
if (key == "") {
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
value = substr($0, index($0, "=") + 1)
|
|
||||||
value = trim(value)
|
|
||||||
|
|
||||||
if (!(key in seen)) {
|
|
||||||
order[++count] = key
|
|
||||||
seen[key] = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
values[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
END {
|
|
||||||
for (i = 1; i <= count; i++) {
|
|
||||||
key = order[i]
|
|
||||||
print key "=" values[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$DEFAULT_ENV_FILE" "$USER_ENV_FILE" >"$MERGED_ENV_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
set_merged_env_value() {
|
|
||||||
local key="$1"
|
|
||||||
local value="$2"
|
|
||||||
local temp_file
|
|
||||||
|
|
||||||
temp_file="$(mktemp "${TMPDIR:-/tmp}/dify-compose-env.XXXXXX")"
|
|
||||||
awk -F= -v target="$key" -v replacement="$key=$value" '
|
|
||||||
BEGIN { replaced = 0 }
|
|
||||||
/^[[:space:]]*#/ || !/=/{ print; next }
|
|
||||||
{
|
|
||||||
key = $1
|
|
||||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
|
||||||
if (key == target) {
|
|
||||||
if (!replaced) {
|
|
||||||
print replacement
|
|
||||||
replaced = 1
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
print
|
|
||||||
}
|
|
||||||
END {
|
|
||||||
if (!replaced) {
|
|
||||||
print replacement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$MERGED_ENV_FILE" >"$temp_file"
|
|
||||||
mv "$temp_file" "$MERGED_ENV_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
set_if_not_overridden() {
|
|
||||||
local key="$1"
|
|
||||||
local value="$2"
|
|
||||||
|
|
||||||
if user_overrides "$key"; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
set_merged_env_value "$key" "$value"
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata_db_host() {
|
|
||||||
case "$1" in
|
|
||||||
mysql) printf 'db_mysql' ;;
|
|
||||||
postgresql | '') printf 'db_postgres' ;;
|
|
||||||
*) printf '%s' "$(env_value DB_HOST)" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata_db_port() {
|
|
||||||
case "$1" in
|
|
||||||
mysql) printf '3306' ;;
|
|
||||||
postgresql | '') printf '5432' ;;
|
|
||||||
*) printf '%s' "$(env_value DB_PORT)" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata_db_user() {
|
|
||||||
case "$1" in
|
|
||||||
mysql) printf 'root' ;;
|
|
||||||
postgresql | '') printf 'postgres' ;;
|
|
||||||
*) printf '%s' "$(env_value DB_USERNAME)" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
build_merged_env() {
|
|
||||||
MERGED_ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/dify-compose.XXXXXX")"
|
|
||||||
trap 'rm -f "$MERGED_ENV_FILE"' EXIT
|
|
||||||
|
|
||||||
write_merged_env
|
|
||||||
|
|
||||||
local db_type
|
|
||||||
local redis_host
|
|
||||||
local redis_port
|
|
||||||
local redis_username
|
|
||||||
local redis_password
|
|
||||||
local redis_auth
|
|
||||||
local code_execution_api_key
|
|
||||||
local weaviate_api_key
|
|
||||||
|
|
||||||
db_type="$(env_value DB_TYPE)"
|
|
||||||
|
|
||||||
set_if_not_overridden DB_HOST "$(metadata_db_host "$db_type")"
|
|
||||||
set_if_not_overridden DB_PORT "$(metadata_db_port "$db_type")"
|
|
||||||
set_if_not_overridden DB_USERNAME "$(metadata_db_user "$db_type")"
|
|
||||||
|
|
||||||
if ! user_overrides CELERY_BROKER_URL; then
|
|
||||||
redis_host="$(env_value REDIS_HOST)"
|
|
||||||
redis_port="$(env_value REDIS_PORT)"
|
|
||||||
redis_username="$(env_value REDIS_USERNAME)"
|
|
||||||
redis_password="$(env_value REDIS_PASSWORD)"
|
|
||||||
redis_auth=""
|
|
||||||
|
|
||||||
if [[ -n "$redis_username" && -n "$redis_password" ]]; then
|
|
||||||
redis_auth="${redis_username}:${redis_password}@"
|
|
||||||
elif [[ -n "$redis_password" ]]; then
|
|
||||||
redis_auth=":${redis_password}@"
|
|
||||||
elif [[ -n "$redis_username" ]]; then
|
|
||||||
redis_auth="${redis_username}@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
set_merged_env_value CELERY_BROKER_URL "redis://${redis_auth}${redis_host:-redis}:${redis_port:-6379}/1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! user_overrides SANDBOX_API_KEY; then
|
|
||||||
code_execution_api_key="$(env_value CODE_EXECUTION_API_KEY)"
|
|
||||||
set_if_not_overridden SANDBOX_API_KEY "${code_execution_api_key:-dify-sandbox}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! user_overrides WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS; then
|
|
||||||
weaviate_api_key="$(env_value WEAVIATE_API_KEY)"
|
|
||||||
set_if_not_overridden WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS \
|
|
||||||
"${weaviate_api_key:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
detect_compose
|
|
||||||
ensure_env_files
|
|
||||||
ensure_secret_key
|
|
||||||
build_merged_env
|
|
||||||
|
|
||||||
if [[ "$#" -eq 0 ]]; then
|
|
||||||
set -- up -d
|
|
||||||
fi
|
|
||||||
|
|
||||||
"${COMPOSE_CMD[@]}" --env-file "$MERGED_ENV_FILE" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@ -1,317 +0,0 @@
|
|||||||
$ErrorActionPreference = "Stop"
|
|
||||||
Set-StrictMode -Version Latest
|
|
||||||
|
|
||||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
||||||
Set-Location $ScriptDir
|
|
||||||
|
|
||||||
$DefaultEnvFile = ".env.default"
|
|
||||||
$UserEnvFile = ".env"
|
|
||||||
$MergedEnvFile = $null
|
|
||||||
$Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList $false
|
|
||||||
|
|
||||||
function Write-Info {
|
|
||||||
param([string]$Message)
|
|
||||||
[Console]::Error.WriteLine($Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Fail {
|
|
||||||
param([string]$Message)
|
|
||||||
[Console]::Error.WriteLine("Error: $Message")
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-CommandSuccess {
|
|
||||||
param([string[]]$Command)
|
|
||||||
|
|
||||||
try {
|
|
||||||
$Executable = $Command[0]
|
|
||||||
$CommandArgs = @()
|
|
||||||
if ($Command.Length -gt 1) {
|
|
||||||
$CommandArgs = @($Command[1..($Command.Length - 1)])
|
|
||||||
}
|
|
||||||
|
|
||||||
& $Executable @CommandArgs *> $null
|
|
||||||
return $LASTEXITCODE -eq 0
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-ComposeCommand {
|
|
||||||
if (Test-CommandSuccess @("docker", "compose", "version")) {
|
|
||||||
return @("docker", "compose")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Get-Command "docker-compose" -ErrorAction SilentlyContinue) -and (Test-CommandSuccess @("docker-compose", "version"))) {
|
|
||||||
return @("docker-compose")
|
|
||||||
}
|
|
||||||
|
|
||||||
Fail "Docker Compose is not available. Install Docker Compose, then run this command again."
|
|
||||||
}
|
|
||||||
|
|
||||||
function New-SecretKey {
|
|
||||||
$Bytes = New-Object byte[] 42
|
|
||||||
$Generator = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
|
||||||
|
|
||||||
try {
|
|
||||||
$Generator.GetBytes($Bytes)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$Generator.Dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
return [Convert]::ToBase64String($Bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Ensure-EnvFiles {
|
|
||||||
if (-not (Test-Path $DefaultEnvFile -PathType Leaf)) {
|
|
||||||
Fail "$DefaultEnvFile is missing."
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $UserEnvFile -PathType Leaf) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -ItemType File -Path $UserEnvFile | Out-Null
|
|
||||||
|
|
||||||
if ([Console]::IsInputRedirected) {
|
|
||||||
Write-Info "Created $UserEnvFile for local overrides."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Info "Created $UserEnvFile for local overrides."
|
|
||||||
$Answer = Read-Host "Do you need a custom deployment now? (Most users can press Enter to skip.) [y/N]"
|
|
||||||
|
|
||||||
if ($Answer -match "^(y|yes)$") {
|
|
||||||
Write-Output "Edit .env with the settings you want to override, using .env.example as the full reference."
|
|
||||||
Write-Output "Run .\dify-compose.ps1 up -d again when you are ready."
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Read-EnvFile {
|
|
||||||
param([string]$Path)
|
|
||||||
|
|
||||||
$Values = [ordered]@{}
|
|
||||||
|
|
||||||
if (-not (Test-Path $Path -PathType Leaf)) {
|
|
||||||
return $Values
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($Line in Get-Content -Path $Path) {
|
|
||||||
if ($Line -match "^\s*#" -or $Line -notmatch "=") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$SeparatorIndex = $Line.IndexOf("=")
|
|
||||||
$Key = $Line.Substring(0, $SeparatorIndex).Trim()
|
|
||||||
$Value = $Line.Substring($SeparatorIndex + 1).Trim()
|
|
||||||
|
|
||||||
if (($Value.StartsWith('"') -and $Value.EndsWith('"')) -or ($Value.StartsWith("'") -and $Value.EndsWith("'"))) {
|
|
||||||
$Value = $Value.Substring(1, $Value.Length - 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Key.Length -gt 0) {
|
|
||||||
$Values[$Key] = $Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $Values
|
|
||||||
}
|
|
||||||
|
|
||||||
function Set-UserEnvValue {
|
|
||||||
param(
|
|
||||||
[string]$Key,
|
|
||||||
[string]$Value
|
|
||||||
)
|
|
||||||
|
|
||||||
$Path = [string](Resolve-Path $UserEnvFile)
|
|
||||||
$Lines = [System.IO.File]::ReadAllLines($Path, [System.Text.Encoding]::UTF8)
|
|
||||||
$Output = New-Object System.Collections.Generic.List[string]
|
|
||||||
$Replaced = $false
|
|
||||||
|
|
||||||
foreach ($Line in $Lines) {
|
|
||||||
if ($Line -match "^\s*#" -or $Line -notmatch "=") {
|
|
||||||
$Output.Add($Line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$SeparatorIndex = $Line.IndexOf("=")
|
|
||||||
$CurrentKey = $Line.Substring(0, $SeparatorIndex).Trim()
|
|
||||||
|
|
||||||
if ($CurrentKey -eq $Key) {
|
|
||||||
if (-not $Replaced) {
|
|
||||||
$Output.Add("$Key=$Value")
|
|
||||||
$Replaced = $true
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$Output.Add($Line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $Replaced) {
|
|
||||||
$Output.Add("$Key=$Value")
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllLines($Path, $Output, $Utf8NoBom)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Ensure-SecretKey {
|
|
||||||
$Values = Read-EnvFile $UserEnvFile
|
|
||||||
|
|
||||||
if ($Values.Contains("SECRET_KEY") -and $Values["SECRET_KEY"]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Set-UserEnvValue "SECRET_KEY" (New-SecretKey)
|
|
||||||
Write-Info "Generated SECRET_KEY in $UserEnvFile."
|
|
||||||
}
|
|
||||||
|
|
||||||
function Merge-EnvValues {
|
|
||||||
$Values = [ordered]@{}
|
|
||||||
|
|
||||||
foreach ($Entry in (Read-EnvFile $DefaultEnvFile).GetEnumerator()) {
|
|
||||||
$Values[$Entry.Key] = $Entry.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($Entry in (Read-EnvFile $UserEnvFile).GetEnumerator()) {
|
|
||||||
$Values[$Entry.Key] = $Entry.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
return $Values
|
|
||||||
}
|
|
||||||
|
|
||||||
function User-Overrides {
|
|
||||||
param([string]$Key)
|
|
||||||
|
|
||||||
if (-not (Test-Path $UserEnvFile -PathType Leaf)) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
return [bool](Select-String -Path $UserEnvFile -Pattern "^\s*$([regex]::Escape($Key))\s*=" -Quiet)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Metadata-DbHost {
|
|
||||||
param([string]$DbType, $Values)
|
|
||||||
|
|
||||||
switch ($DbType) {
|
|
||||||
"mysql" { return "db_mysql" }
|
|
||||||
"postgresql" { return "db_postgres" }
|
|
||||||
"" { return "db_postgres" }
|
|
||||||
default { return $Values["DB_HOST"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Metadata-DbPort {
|
|
||||||
param([string]$DbType, $Values)
|
|
||||||
|
|
||||||
switch ($DbType) {
|
|
||||||
"mysql" { return "3306" }
|
|
||||||
"postgresql" { return "5432" }
|
|
||||||
"" { return "5432" }
|
|
||||||
default { return $Values["DB_PORT"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Metadata-DbUser {
|
|
||||||
param([string]$DbType, $Values)
|
|
||||||
|
|
||||||
switch ($DbType) {
|
|
||||||
"mysql" { return "root" }
|
|
||||||
"postgresql" { return "postgres" }
|
|
||||||
"" { return "postgres" }
|
|
||||||
default { return $Values["DB_USERNAME"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-MergedEnv {
|
|
||||||
param($Values)
|
|
||||||
|
|
||||||
$Output = New-Object System.Collections.Generic.List[string]
|
|
||||||
|
|
||||||
foreach ($Entry in $Values.GetEnumerator()) {
|
|
||||||
$Output.Add("$($Entry.Key)=$($Entry.Value)")
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllLines($MergedEnvFile, $Output, $Utf8NoBom)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-MergedEnv {
|
|
||||||
$Values = Merge-EnvValues
|
|
||||||
$script:MergedEnvFile = [System.IO.Path]::GetTempFileName()
|
|
||||||
|
|
||||||
$DbType = if ($Values.Contains("DB_TYPE")) { $Values["DB_TYPE"] } else { "postgresql" }
|
|
||||||
|
|
||||||
if (-not (User-Overrides "DB_HOST")) {
|
|
||||||
$Values["DB_HOST"] = Metadata-DbHost $DbType $Values
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (User-Overrides "DB_PORT")) {
|
|
||||||
$Values["DB_PORT"] = Metadata-DbPort $DbType $Values
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (User-Overrides "DB_USERNAME")) {
|
|
||||||
$Values["DB_USERNAME"] = Metadata-DbUser $DbType $Values
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (User-Overrides "CELERY_BROKER_URL")) {
|
|
||||||
$RedisHost = if ($Values.Contains("REDIS_HOST") -and $Values["REDIS_HOST"]) { $Values["REDIS_HOST"] } else { "redis" }
|
|
||||||
$RedisPort = if ($Values.Contains("REDIS_PORT") -and $Values["REDIS_PORT"]) { $Values["REDIS_PORT"] } else { "6379" }
|
|
||||||
$RedisUsername = if ($Values.Contains("REDIS_USERNAME")) { $Values["REDIS_USERNAME"] } else { "" }
|
|
||||||
$RedisPassword = if ($Values.Contains("REDIS_PASSWORD")) { $Values["REDIS_PASSWORD"] } else { "" }
|
|
||||||
$RedisAuth = ""
|
|
||||||
|
|
||||||
if ($RedisUsername -and $RedisPassword) {
|
|
||||||
$RedisAuth = "${RedisUsername}:${RedisPassword}@"
|
|
||||||
}
|
|
||||||
elseif ($RedisPassword) {
|
|
||||||
$RedisAuth = ":${RedisPassword}@"
|
|
||||||
}
|
|
||||||
elseif ($RedisUsername) {
|
|
||||||
$RedisAuth = "${RedisUsername}@"
|
|
||||||
}
|
|
||||||
|
|
||||||
$Values["CELERY_BROKER_URL"] = "redis://$RedisAuth${RedisHost}:${RedisPort}/1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (User-Overrides "SANDBOX_API_KEY")) {
|
|
||||||
$CodeExecutionApiKey = if ($Values.Contains("CODE_EXECUTION_API_KEY") -and $Values["CODE_EXECUTION_API_KEY"]) { $Values["CODE_EXECUTION_API_KEY"] } else { "dify-sandbox" }
|
|
||||||
$Values["SANDBOX_API_KEY"] = $CodeExecutionApiKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (User-Overrides "WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS")) {
|
|
||||||
$WeaviateApiKey = if ($Values.Contains("WEAVIATE_API_KEY") -and $Values["WEAVIATE_API_KEY"]) { $Values["WEAVIATE_API_KEY"] } else { "WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih" }
|
|
||||||
$Values["WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS"] = $WeaviateApiKey
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-MergedEnv $Values
|
|
||||||
}
|
|
||||||
|
|
||||||
$ComposeCommand = Get-ComposeCommand
|
|
||||||
|
|
||||||
try {
|
|
||||||
Ensure-EnvFiles
|
|
||||||
Ensure-SecretKey
|
|
||||||
Build-MergedEnv
|
|
||||||
|
|
||||||
$ComposeArgs = @($args)
|
|
||||||
if ($ComposeArgs.Count -eq 0) {
|
|
||||||
$ComposeArgs = @("up", "-d")
|
|
||||||
}
|
|
||||||
|
|
||||||
$ComposeCommandArgs = @()
|
|
||||||
if ($ComposeCommand.Length -gt 1) {
|
|
||||||
$ComposeCommandArgs = @($ComposeCommand[1..($ComposeCommand.Length - 1)])
|
|
||||||
}
|
|
||||||
|
|
||||||
$ComposeExecutable = $ComposeCommand[0]
|
|
||||||
& $ComposeExecutable @ComposeCommandArgs --env-file $MergedEnvFile @ComposeArgs
|
|
||||||
exit $LASTEXITCODE
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if ($MergedEnvFile -and (Test-Path $MergedEnvFile -PathType Leaf)) {
|
|
||||||
Remove-Item -Force $MergedEnvFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,202 @@
|
|||||||
x-shared-env: &shared-api-worker-env
|
# Shared configuration using YAML anchors and env_file
|
||||||
|
x-shared-api-worker-config: &shared-api-worker-config
|
||||||
|
env_file:
|
||||||
|
- path: ./envs/core-services/shared.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/core-services/api.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/security.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/db-postgres.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/db-mysql.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/redis.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/weaviate.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/qdrant.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/oceanbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/seekdb.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/couchbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/pgvector.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/vastbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/pgvecto-rs.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/chroma.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/iris.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/oracle.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/opengauss.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/myscale.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/matrixone.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/elasticsearch.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/opensearch.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/milvus.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/nginx.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/certbot.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/ssrf-proxy.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/etcd.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/minio.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/milvus-standalone.env
|
||||||
|
required: false
|
||||||
|
- ./.env
|
||||||
|
networks:
|
||||||
|
- ssrf_proxy_network
|
||||||
|
- default
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
x-shared-worker-config: &shared-worker-config
|
||||||
|
env_file:
|
||||||
|
- path: ./envs/core-services/shared.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/core-services/worker.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/security.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/db-postgres.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/db-mysql.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/redis.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/weaviate.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/qdrant.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/oceanbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/seekdb.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/couchbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/pgvector.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/vastbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/pgvecto-rs.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/chroma.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/iris.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/oracle.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/opengauss.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/myscale.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/matrixone.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/elasticsearch.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/opensearch.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/milvus.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/nginx.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/certbot.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/ssrf-proxy.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/etcd.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/minio.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/milvus-standalone.env
|
||||||
|
required: false
|
||||||
|
- ./.env
|
||||||
|
networks:
|
||||||
|
- ssrf_proxy_network
|
||||||
|
- default
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
x-shared-worker-beat-config: &shared-worker-beat-config
|
||||||
|
env_file:
|
||||||
|
- path: ./envs/core-services/shared.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/core-services/worker-beat.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/security.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/db-postgres.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/db-mysql.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/redis.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/weaviate.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/qdrant.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/oceanbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/seekdb.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/couchbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/pgvector.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/vastbase.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/pgvecto-rs.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/chroma.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/iris.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/oracle.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/opengauss.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/myscale.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/matrixone.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/elasticsearch.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/opensearch.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/vectorstores/milvus.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/nginx.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/certbot.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/ssrf-proxy.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/etcd.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/minio.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/infrastructure/milvus-standalone.env
|
||||||
|
required: false
|
||||||
|
- ./.env
|
||||||
|
networks:
|
||||||
|
- ssrf_proxy_network
|
||||||
|
- default
|
||||||
|
restart: always
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# Init container to fix permissions
|
# Init container to fix permissions
|
||||||
init_permissions:
|
init_permissions:
|
||||||
@ -21,12 +219,9 @@ services:
|
|||||||
|
|
||||||
# API service
|
# API service
|
||||||
api:
|
api:
|
||||||
|
<<: *shared-api-worker-config
|
||||||
image: langgenius/dify-api:1.14.0
|
image: langgenius/dify-api:1.14.0
|
||||||
restart: always
|
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
|
||||||
<<: *shared-api-worker-env
|
|
||||||
# Startup mode, 'api' starts the API server.
|
|
||||||
MODE: api
|
MODE: api
|
||||||
SENTRY_DSN: ${API_SENTRY_DSN:-}
|
SENTRY_DSN: ${API_SENTRY_DSN:-}
|
||||||
SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0}
|
SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0}
|
||||||
@ -69,12 +264,9 @@ services:
|
|||||||
# worker service
|
# worker service
|
||||||
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
|
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
|
||||||
worker:
|
worker:
|
||||||
|
<<: *shared-worker-config
|
||||||
image: langgenius/dify-api:1.14.0
|
image: langgenius/dify-api:1.14.0
|
||||||
restart: always
|
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
|
||||||
<<: *shared-api-worker-env
|
|
||||||
# Startup mode, 'worker' starts the Celery worker for processing all queues.
|
|
||||||
MODE: worker
|
MODE: worker
|
||||||
SENTRY_DSN: ${API_SENTRY_DSN:-}
|
SENTRY_DSN: ${API_SENTRY_DSN:-}
|
||||||
SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0}
|
SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0}
|
||||||
@ -115,12 +307,9 @@ services:
|
|||||||
# worker_beat service
|
# worker_beat service
|
||||||
# Celery beat for scheduling periodic tasks.
|
# Celery beat for scheduling periodic tasks.
|
||||||
worker_beat:
|
worker_beat:
|
||||||
|
<<: *shared-worker-beat-config
|
||||||
image: langgenius/dify-api:1.14.0
|
image: langgenius/dify-api:1.14.0
|
||||||
restart: always
|
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
|
||||||
<<: *shared-api-worker-env
|
|
||||||
# Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks.
|
|
||||||
MODE: beat
|
MODE: beat
|
||||||
depends_on:
|
depends_on:
|
||||||
init_permissions:
|
init_permissions:
|
||||||
@ -154,6 +343,12 @@ services:
|
|||||||
web:
|
web:
|
||||||
image: langgenius/dify-web:1.14.0
|
image: langgenius/dify-web:1.14.0
|
||||||
restart: always
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- path: ./envs/core-services/web.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/security.env
|
||||||
|
required: false
|
||||||
|
- ./.env
|
||||||
environment:
|
environment:
|
||||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||||
APP_API_URL: ${APP_API_URL:-}
|
APP_API_URL: ${APP_API_URL:-}
|
||||||
@ -228,7 +423,7 @@ services:
|
|||||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-difyai123456}
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-difyai123456}
|
||||||
MYSQL_DATABASE: ${DB_DATABASE:-dify}
|
MYSQL_DATABASE: ${DB_DATABASE:-dify}
|
||||||
command: >
|
command: >
|
||||||
--max_connections=1000
|
--max_connections=${MYSQL_MAX_CONNECTIONS:-1000}
|
||||||
--innodb_buffer_pool_size=${MYSQL_INNODB_BUFFER_POOL_SIZE:-512M}
|
--innodb_buffer_pool_size=${MYSQL_INNODB_BUFFER_POOL_SIZE:-512M}
|
||||||
--innodb_log_file_size=${MYSQL_INNODB_LOG_FILE_SIZE:-128M}
|
--innodb_log_file_size=${MYSQL_INNODB_LOG_FILE_SIZE:-128M}
|
||||||
--innodb_flush_log_at_trx_commit=${MYSQL_INNODB_FLUSH_LOG_AT_TRX_COMMIT:-2}
|
--innodb_flush_log_at_trx_commit=${MYSQL_INNODB_FLUSH_LOG_AT_TRX_COMMIT:-2}
|
||||||
@ -270,6 +465,12 @@ services:
|
|||||||
sandbox:
|
sandbox:
|
||||||
image: langgenius/dify-sandbox:0.2.15
|
image: langgenius/dify-sandbox:0.2.15
|
||||||
restart: always
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- path: ./envs/core-services/sandbox.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/security.env
|
||||||
|
required: false
|
||||||
|
- ./.env
|
||||||
environment:
|
environment:
|
||||||
# The DifySandbox configurations
|
# The DifySandbox configurations
|
||||||
# Make sure you are changing this key for your deployment with a strong key.
|
# Make sure you are changing this key for your deployment with a strong key.
|
||||||
@ -294,9 +495,24 @@ services:
|
|||||||
plugin_daemon:
|
plugin_daemon:
|
||||||
image: langgenius/dify-plugin-daemon:0.6.0-local
|
image: langgenius/dify-plugin-daemon:0.6.0-local
|
||||||
restart: always
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- path: ./envs/core-services/shared.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/core-services/plugin-daemon.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/security.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/db-postgres.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/db-mysql.env
|
||||||
|
required: false
|
||||||
|
- path: ./envs/databases/redis.env
|
||||||
|
required: false
|
||||||
|
- ./.env
|
||||||
|
networks:
|
||||||
|
- ssrf_proxy_network
|
||||||
|
- default
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
|
||||||
<<: *shared-api-worker-env
|
|
||||||
DB_DATABASE: ${DB_PLUGIN_DATABASE:-dify_plugin}
|
DB_DATABASE: ${DB_PLUGIN_DATABASE:-dify_plugin}
|
||||||
DB_SSL_MODE: ${DB_SSL_MODE:-disable}
|
DB_SSL_MODE: ${DB_SSL_MODE:-disable}
|
||||||
SERVER_PORT: ${PLUGIN_DAEMON_PORT:-5002}
|
SERVER_PORT: ${PLUGIN_DAEMON_PORT:-5002}
|
||||||
|
|||||||
@ -51,7 +51,7 @@ services:
|
|||||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-difyai123456}
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-difyai123456}
|
||||||
MYSQL_DATABASE: ${DB_DATABASE:-dify}
|
MYSQL_DATABASE: ${DB_DATABASE:-dify}
|
||||||
command: >
|
command: >
|
||||||
--max_connections=1000
|
--max_connections=${MYSQL_MAX_CONNECTIONS:-1000}
|
||||||
--innodb_buffer_pool_size=${MYSQL_INNODB_BUFFER_POOL_SIZE:-512M}
|
--innodb_buffer_pool_size=${MYSQL_INNODB_BUFFER_POOL_SIZE:-512M}
|
||||||
--innodb_log_file_size=${MYSQL_INNODB_LOG_FILE_SIZE:-128M}
|
--innodb_log_file_size=${MYSQL_INNODB_LOG_FILE_SIZE:-128M}
|
||||||
--innodb_flush_log_at_trx_commit=${MYSQL_INNODB_FLUSH_LOG_AT_TRX_COMMIT:-2}
|
--innodb_flush_log_at_trx_commit=${MYSQL_INNODB_FLUSH_LOG_AT_TRX_COMMIT:-2}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
13
docker/envs/core-services/api.env.example
Normal file
13
docker/envs/core-services/api.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Api Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
MODE=api
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_TRACES_SAMPLE_RATE=1.0
|
||||||
|
SENTRY_PROFILES_SAMPLE_RATE=1.0
|
||||||
|
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
||||||
|
PLUGIN_REMOTE_INSTALL_PORT=5003
|
||||||
|
PLUGIN_MAX_PACKAGE_SIZE=52428800
|
||||||
|
PLUGIN_DAEMON_TIMEOUT=600.0
|
||||||
|
INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||||
23
docker/envs/core-services/plugin-daemon.env.example
Normal file
23
docker/envs/core-services/plugin-daemon.env.example
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Plugin Daemon Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
DB_PLUGIN_DATABASE=dify_plugin
|
||||||
|
PLUGIN_DAEMON_URL=http://plugin_daemon:5002
|
||||||
|
PLUGIN_PPROF_ENABLED=false
|
||||||
|
PLUGIN_DIFY_INNER_API_URL=http://api:5001
|
||||||
|
FORCE_VERIFYING_SIGNATURE=true
|
||||||
|
PLUGIN_STDIO_BUFFER_SIZE=1024
|
||||||
|
PLUGIN_STDIO_MAX_BUFFER_SIZE=5242880
|
||||||
|
PLUGIN_PYTHON_ENV_INIT_TIMEOUT=120
|
||||||
|
PLUGIN_MAX_EXECUTION_TIMEOUT=600
|
||||||
|
PLUGIN_DEBUGGING_HOST=0.0.0.0
|
||||||
|
PLUGIN_DEBUGGING_PORT=5003
|
||||||
|
PLUGIN_DAEMON_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi
|
||||||
|
PLUGIN_DIFY_INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||||
|
PLUGIN_DAEMON_PORT=5002
|
||||||
|
CELERY_WORKER_CLASS=
|
||||||
|
PLUGIN_STORAGE_TYPE=local
|
||||||
|
PLUGIN_STORAGE_LOCAL_ROOT=/app/storage
|
||||||
|
PLUGIN_WORKING_PATH=/app/storage/cwd
|
||||||
|
PLUGIN_STORAGE_OSS_BUCKET=
|
||||||
17
docker/envs/core-services/sandbox.env.example
Normal file
17
docker/envs/core-services/sandbox.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Sandbox Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
SANDBOX_HTTP_PROXY=http://ssrf_proxy:3128
|
||||||
|
SANDBOX_HTTPS_PROXY=http://ssrf_proxy:3128
|
||||||
|
SANDBOX_PORT=8194
|
||||||
|
PIP_MIRROR_URL=
|
||||||
|
SANDBOX_API_KEY=dify-sandbox
|
||||||
|
SANDBOX_GIN_MODE=release
|
||||||
|
SANDBOX_WORKER_TIMEOUT=15
|
||||||
|
SANDBOX_ENABLE_NETWORK=true
|
||||||
|
SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=21
|
||||||
|
SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE=1000
|
||||||
|
SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL=200
|
||||||
|
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
|
||||||
|
SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL=90000
|
||||||
469
docker/envs/core-services/shared.env.example
Normal file
469
docker/envs/core-services/shared.env.example
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Shared API/Worker Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
CONSOLE_WEB_URL=
|
||||||
|
SERVICE_API_URL=
|
||||||
|
TRIGGER_URL=http://localhost
|
||||||
|
APP_WEB_URL=
|
||||||
|
FILES_URL=
|
||||||
|
INTERNAL_FILES_URL=
|
||||||
|
LANG=C.UTF-8
|
||||||
|
LC_ALL=C.UTF-8
|
||||||
|
PYTHONIOENCODING=utf-8
|
||||||
|
UV_CACHE_DIR=/tmp/.uv-cache
|
||||||
|
CHECK_UPDATE_URL=https://updates.dify.ai
|
||||||
|
OPENAI_API_BASE=https://api.openai.com/v1
|
||||||
|
MIGRATION_ENABLED=true
|
||||||
|
FILES_ACCESS_TIMEOUT=300
|
||||||
|
ENABLE_COLLABORATION_MODE=false
|
||||||
|
CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1
|
||||||
|
CELERY_TASK_ANNOTATIONS=null
|
||||||
|
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
|
||||||
|
SUPABASE_URL=your-server-url
|
||||||
|
TIDB_ON_QDRANT_URL=http://127.0.0.1
|
||||||
|
TIDB_ON_QDRANT_API_KEY=dify
|
||||||
|
TIDB_API_URL=http://127.0.0.1
|
||||||
|
TIDB_IAM_API_URL=http://127.0.0.1
|
||||||
|
TIDB_REGION=regions/aws-us-east-1
|
||||||
|
TIDB_PROJECT_ID=dify
|
||||||
|
TIDB_SPEND_LIMIT=100
|
||||||
|
TENCENT_VECTOR_DB_URL=http://127.0.0.1
|
||||||
|
TENCENT_VECTOR_DB_API_KEY=dify
|
||||||
|
LINDORM_URL=http://localhost:30070
|
||||||
|
LINDORM_USERNAME=admin
|
||||||
|
UPSTASH_VECTOR_URL=https://xxx-vector.upstash.io
|
||||||
|
UPLOAD_FILE_SIZE_LIMIT=15
|
||||||
|
UPLOAD_FILE_BATCH_LIMIT=5
|
||||||
|
UPLOAD_FILE_EXTENSION_BLACKLIST=
|
||||||
|
SINGLE_CHUNK_ATTACHMENT_LIMIT=10
|
||||||
|
IMAGE_FILE_BATCH_LIMIT=10
|
||||||
|
ATTACHMENT_IMAGE_FILE_SIZE_LIMIT=2
|
||||||
|
ATTACHMENT_IMAGE_DOWNLOAD_TIMEOUT=60
|
||||||
|
ETL_TYPE=dify
|
||||||
|
UNSTRUCTURED_API_URL=
|
||||||
|
MULTIMODAL_SEND_FORMAT=base64
|
||||||
|
UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
|
||||||
|
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
|
||||||
|
UPLOAD_AUDIO_FILE_SIZE_LIMIT=50
|
||||||
|
API_SENTRY_DSN=
|
||||||
|
API_SENTRY_TRACES_SAMPLE_RATE=1.0
|
||||||
|
API_SENTRY_PROFILES_SAMPLE_RATE=1.0
|
||||||
|
WEB_SENTRY_DSN=
|
||||||
|
PLUGIN_SENTRY_ENABLED=false
|
||||||
|
PLUGIN_SENTRY_DSN=
|
||||||
|
NOTION_INTEGRATION_TYPE=public
|
||||||
|
RESEND_API_URL=https://api.resend.com
|
||||||
|
SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128
|
||||||
|
SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128
|
||||||
|
PGDATA=/var/lib/postgresql/data/pgdata
|
||||||
|
PLUGIN_MAX_PACKAGE_SIZE=52428800
|
||||||
|
PLUGIN_MODEL_SCHEMA_CACHE_TTL=3600
|
||||||
|
ENDPOINT_URL_TEMPLATE=http://localhost/e/{hook_id}
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
LOG_OUTPUT_FORMAT=text
|
||||||
|
LOG_FILE=/app/logs/server.log
|
||||||
|
LOG_FILE_MAX_SIZE=20
|
||||||
|
LOG_FILE_BACKUP_COUNT=5
|
||||||
|
LOG_DATEFORMAT=%Y-%m-%d %H:%M:%S
|
||||||
|
LOG_TZ=UTC
|
||||||
|
DEBUG=false
|
||||||
|
FLASK_DEBUG=false
|
||||||
|
ENABLE_REQUEST_LOGGING=False
|
||||||
|
WORKFLOW_LOG_CLEANUP_ENABLED=false
|
||||||
|
WORKFLOW_LOG_RETENTION_DAYS=30
|
||||||
|
WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100
|
||||||
|
WORKFLOW_LOG_CLEANUP_SPECIFIC_WORKFLOW_IDS=
|
||||||
|
EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
|
||||||
|
EXPOSE_PLUGIN_DEBUGGING_PORT=5003
|
||||||
|
DEPLOY_ENV=PRODUCTION
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||||
|
APP_DEFAULT_ACTIVE_REQUESTS=0
|
||||||
|
APP_MAX_ACTIVE_REQUESTS=0
|
||||||
|
APP_MAX_EXECUTION_TIME=1200
|
||||||
|
DIFY_BIND_ADDRESS=0.0.0.0
|
||||||
|
DIFY_PORT=5001
|
||||||
|
SERVER_WORKER_AMOUNT=1
|
||||||
|
SERVER_WORKER_CLASS=gevent
|
||||||
|
SERVER_WORKER_CONNECTIONS=10
|
||||||
|
CELERY_SENTINEL_PASSWORD=
|
||||||
|
S3_ACCESS_KEY=
|
||||||
|
S3_SECRET_KEY=
|
||||||
|
ARCHIVE_STORAGE_ACCESS_KEY=
|
||||||
|
ARCHIVE_STORAGE_SECRET_KEY=
|
||||||
|
AZURE_BLOB_ACCOUNT_KEY=difyai
|
||||||
|
ALIYUN_OSS_ACCESS_KEY=your-access-key
|
||||||
|
ALIYUN_OSS_SECRET_KEY=your-secret-key
|
||||||
|
TENCENT_COS_SECRET_KEY=your-secret-key
|
||||||
|
TENCENT_COS_SECRET_ID=your-secret-id
|
||||||
|
OCI_ACCESS_KEY=your-access-key
|
||||||
|
OCI_SECRET_KEY=your-secret-key
|
||||||
|
HUAWEI_OBS_SECRET_KEY=your-secret-key
|
||||||
|
HUAWEI_OBS_ACCESS_KEY=your-access-key
|
||||||
|
VOLCENGINE_TOS_SECRET_KEY=your-secret-key
|
||||||
|
VOLCENGINE_TOS_ACCESS_KEY=your-access-key
|
||||||
|
BAIDU_OBS_SECRET_KEY=your-secret-key
|
||||||
|
BAIDU_OBS_ACCESS_KEY=your-access-key
|
||||||
|
SUPABASE_API_KEY=your-access-key
|
||||||
|
ALIBABACLOUD_MYSQL_PASSWORD=difyai123456
|
||||||
|
RELYT_PASSWORD=difyai123456
|
||||||
|
LINDORM_PASSWORD=admin
|
||||||
|
LINDORM_USING_UGC=True
|
||||||
|
LINDORM_QUERY_TIMEOUT=1
|
||||||
|
HUAWEI_CLOUD_PASSWORD=admin
|
||||||
|
UPSTASH_VECTOR_TOKEN=dify
|
||||||
|
TABLESTORE_ACCESS_KEY_ID=xxx
|
||||||
|
TABLESTORE_ACCESS_KEY_SECRET=xxx
|
||||||
|
TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE=false
|
||||||
|
CLICKZETTA_PASSWORD=
|
||||||
|
CLICKZETTA_INSTANCE=
|
||||||
|
CLICKZETTA_SERVICE=api.clickzetta.com
|
||||||
|
CLICKZETTA_WORKSPACE=quick_start
|
||||||
|
CLICKZETTA_VCLUSTER=default_ap
|
||||||
|
CLICKZETTA_SCHEMA=dify
|
||||||
|
CLICKZETTA_BATCH_SIZE=100
|
||||||
|
CLICKZETTA_ENABLE_INVERTED_INDEX=true
|
||||||
|
CLICKZETTA_ANALYZER_TYPE=chinese
|
||||||
|
CLICKZETTA_ANALYZER_MODE=smart
|
||||||
|
UNSTRUCTURED_API_KEY=
|
||||||
|
SCARF_NO_ANALYTICS=true
|
||||||
|
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
||||||
|
NOTION_CLIENT_SECRET=
|
||||||
|
NOTION_CLIENT_ID=
|
||||||
|
NOTION_INTERNAL_SECRET=
|
||||||
|
MAIL_TYPE=resend
|
||||||
|
MAIL_DEFAULT_SEND_FROM=
|
||||||
|
RESEND_API_KEY=your-resend-api-key
|
||||||
|
SMTP_SERVER=
|
||||||
|
SMTP_PORT=465
|
||||||
|
SMTP_USERNAME=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_USE_TLS=true
|
||||||
|
SMTP_OPPORTUNISTIC_TLS=false
|
||||||
|
SMTP_LOCAL_HOSTNAME=
|
||||||
|
SENDGRID_API_KEY=
|
||||||
|
INVITE_EXPIRY_HOURS=72
|
||||||
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
CODE_EXECUTION_ENDPOINT=http://sandbox:8194
|
||||||
|
CODE_EXECUTION_API_KEY=dify-sandbox
|
||||||
|
CODE_EXECUTION_SSL_VERIFY=True
|
||||||
|
CODE_EXECUTION_POOL_MAX_CONNECTIONS=100
|
||||||
|
CODE_EXECUTION_POOL_MAX_KEEPALIVE_CONNECTIONS=20
|
||||||
|
CODE_EXECUTION_POOL_KEEPALIVE_EXPIRY=5.0
|
||||||
|
CODE_MAX_NUMBER=9223372036854775807
|
||||||
|
CODE_MIN_NUMBER=-9223372036854775808
|
||||||
|
CODE_MAX_DEPTH=5
|
||||||
|
CODE_MAX_PRECISION=20
|
||||||
|
CODE_MAX_STRING_LENGTH=400000
|
||||||
|
CODE_MAX_STRING_ARRAY_LENGTH=30
|
||||||
|
CODE_MAX_OBJECT_ARRAY_LENGTH=30
|
||||||
|
CODE_MAX_NUMBER_ARRAY_LENGTH=1000
|
||||||
|
CODE_EXECUTION_CONNECT_TIMEOUT=10
|
||||||
|
CODE_EXECUTION_READ_TIMEOUT=60
|
||||||
|
CODE_EXECUTION_WRITE_TIMEOUT=10
|
||||||
|
TEMPLATE_TRANSFORM_MAX_LENGTH=400000
|
||||||
|
WORKFLOW_MAX_EXECUTION_STEPS=500
|
||||||
|
WORKFLOW_MAX_EXECUTION_TIME=1200
|
||||||
|
WORKFLOW_CALL_MAX_DEPTH=5
|
||||||
|
MAX_VARIABLE_SIZE=204800
|
||||||
|
WORKFLOW_FILE_UPLOAD_LIMIT=10
|
||||||
|
GRAPH_ENGINE_MIN_WORKERS=1
|
||||||
|
GRAPH_ENGINE_MAX_WORKERS=10
|
||||||
|
GRAPH_ENGINE_SCALE_UP_THRESHOLD=3
|
||||||
|
GRAPH_ENGINE_SCALE_DOWN_IDLE_TIME=5.0
|
||||||
|
ALIYUN_SLS_ACCESS_KEY_ID=
|
||||||
|
ALIYUN_SLS_ACCESS_KEY_SECRET=
|
||||||
|
WEBHOOK_REQUEST_BODY_MAX_SIZE=10485760
|
||||||
|
RESPECT_XFORWARD_HEADERS_ENABLED=false
|
||||||
|
SSRF_HTTP_PORT=3128
|
||||||
|
SSRF_COREDUMP_DIR=/var/spool/squid
|
||||||
|
SSRF_REVERSE_PROXY_PORT=8194
|
||||||
|
SSRF_SANDBOX_HOST=sandbox
|
||||||
|
SSRF_DEFAULT_TIME_OUT=5
|
||||||
|
SSRF_DEFAULT_CONNECT_TIME_OUT=5
|
||||||
|
SSRF_DEFAULT_READ_TIME_OUT=5
|
||||||
|
SSRF_DEFAULT_WRITE_TIME_OUT=5
|
||||||
|
SSRF_POOL_MAX_CONNECTIONS=100
|
||||||
|
SSRF_POOL_MAX_KEEPALIVE_CONNECTIONS=20
|
||||||
|
SSRF_POOL_KEEPALIVE_EXPIRY=5.0
|
||||||
|
PLUGIN_AWS_ACCESS_KEY=
|
||||||
|
PLUGIN_AWS_SECRET_KEY=
|
||||||
|
PLUGIN_AWS_REGION=
|
||||||
|
PLUGIN_TENCENT_COS_SECRET_KEY=
|
||||||
|
PLUGIN_TENCENT_COS_SECRET_ID=
|
||||||
|
PLUGIN_ALIYUN_OSS_ACCESS_KEY_ID=
|
||||||
|
PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET=
|
||||||
|
PLUGIN_VOLCENGINE_TOS_ACCESS_KEY=
|
||||||
|
PLUGIN_VOLCENGINE_TOS_SECRET_KEY=
|
||||||
|
OTLP_API_KEY=
|
||||||
|
OTEL_EXPORTER_OTLP_PROTOCOL=
|
||||||
|
OTEL_EXPORTER_TYPE=otlp
|
||||||
|
OTEL_SAMPLING_RATE=0.1
|
||||||
|
OTEL_BATCH_EXPORT_SCHEDULE_DELAY=5000
|
||||||
|
OTEL_MAX_QUEUE_SIZE=2048
|
||||||
|
OTEL_MAX_EXPORT_BATCH_SIZE=512
|
||||||
|
OTEL_METRIC_EXPORT_INTERVAL=60000
|
||||||
|
OTEL_BATCH_EXPORT_TIMEOUT=10000
|
||||||
|
OTEL_METRIC_EXPORT_TIMEOUT=30000
|
||||||
|
QUEUE_MONITOR_THRESHOLD=200
|
||||||
|
QUEUE_MONITOR_ALERT_EMAILS=
|
||||||
|
QUEUE_MONITOR_INTERVAL=30
|
||||||
|
SWAGGER_UI_ENABLED=false
|
||||||
|
SWAGGER_UI_PATH=/swagger-ui.html
|
||||||
|
DSL_EXPORT_ENCRYPT_DATASET_ID=true
|
||||||
|
DATASET_MAX_SEGMENTS_PER_REQUEST=0
|
||||||
|
ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false
|
||||||
|
ENABLE_CLEAN_UNUSED_DATASETS_TASK=false
|
||||||
|
ENABLE_CREATE_TIDB_SERVERLESS_TASK=false
|
||||||
|
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false
|
||||||
|
ENABLE_CLEAN_MESSAGES=false
|
||||||
|
ENABLE_WORKFLOW_RUN_CLEANUP_TASK=false
|
||||||
|
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false
|
||||||
|
ENABLE_DATASETS_QUEUE_MONITOR=false
|
||||||
|
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true
|
||||||
|
ENABLE_WORKFLOW_SCHEDULE_POLLER_TASK=true
|
||||||
|
WORKFLOW_SCHEDULE_POLLER_INTERVAL=1
|
||||||
|
WORKFLOW_SCHEDULE_POLLER_BATCH_SIZE=100
|
||||||
|
WORKFLOW_SCHEDULE_MAX_DISPATCH_PER_TICK=0
|
||||||
|
TENANT_ISOLATED_TASK_CONCURRENCY=1
|
||||||
|
ANNOTATION_IMPORT_FILE_SIZE_LIMIT=2
|
||||||
|
ANNOTATION_IMPORT_MAX_RECORDS=10000
|
||||||
|
ANNOTATION_IMPORT_MIN_RECORDS=1
|
||||||
|
ANNOTATION_IMPORT_RATE_LIMIT_PER_MINUTE=5
|
||||||
|
ANNOTATION_IMPORT_RATE_LIMIT_PER_HOUR=20
|
||||||
|
ANNOTATION_IMPORT_MAX_CONCURRENT=5
|
||||||
|
CREATORS_PLATFORM_FEATURES_ENABLED=true
|
||||||
|
CREATORS_PLATFORM_API_URL=https://creators.dify.ai
|
||||||
|
CREATORS_PLATFORM_OAUTH_CLIENT_ID=
|
||||||
|
TIDB_VECTOR_DATABASE=dify
|
||||||
|
ALIBABACLOUD_MYSQL_HOST=127.0.0.1
|
||||||
|
ALIBABACLOUD_MYSQL_PORT=3306
|
||||||
|
ALIBABACLOUD_MYSQL_USER=root
|
||||||
|
ALIBABACLOUD_MYSQL_DATABASE=dify
|
||||||
|
ALIBABACLOUD_MYSQL_MAX_CONNECTION=5
|
||||||
|
ALIBABACLOUD_MYSQL_HNSW_M=6
|
||||||
|
RELYT_DATABASE=postgres
|
||||||
|
TENCENT_VECTOR_DB_DATABASE=dify
|
||||||
|
BAIDU_VECTOR_DB_DATABASE=dify
|
||||||
|
EXPOSE_PLUGIN_DAEMON_PORT=5002
|
||||||
|
GUNICORN_TIMEOUT=360
|
||||||
|
CELERY_WORKER_AMOUNT=
|
||||||
|
CELERY_AUTO_SCALE=false
|
||||||
|
CELERY_MAX_WORKERS=
|
||||||
|
CELERY_MIN_WORKERS=
|
||||||
|
API_TOOL_DEFAULT_CONNECT_TIMEOUT=10
|
||||||
|
API_TOOL_DEFAULT_READ_TIMEOUT=60
|
||||||
|
CELERY_BACKEND=redis
|
||||||
|
CELERY_USE_SENTINEL=false
|
||||||
|
CELERY_SENTINEL_MASTER_NAME=
|
||||||
|
CELERY_SENTINEL_SOCKET_TIMEOUT=0.1
|
||||||
|
WEB_API_CORS_ALLOW_ORIGINS=*
|
||||||
|
CONSOLE_CORS_ALLOW_ORIGINS=*
|
||||||
|
COOKIE_DOMAIN=
|
||||||
|
OPENDAL_SCHEME=fs
|
||||||
|
OPENDAL_FS_ROOT=storage
|
||||||
|
CLICKZETTA_VOLUME_TYPE=user
|
||||||
|
CLICKZETTA_VOLUME_NAME=
|
||||||
|
CLICKZETTA_VOLUME_TABLE_PREFIX=dataset_
|
||||||
|
CLICKZETTA_VOLUME_DIFY_PREFIX=dify_km
|
||||||
|
S3_ENDPOINT=
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
S3_BUCKET_NAME=difyai
|
||||||
|
S3_ADDRESS_STYLE=auto
|
||||||
|
S3_USE_AWS_MANAGED_IAM=false
|
||||||
|
ARCHIVE_STORAGE_ENABLED=false
|
||||||
|
ARCHIVE_STORAGE_ENDPOINT=
|
||||||
|
ARCHIVE_STORAGE_ARCHIVE_BUCKET=
|
||||||
|
ARCHIVE_STORAGE_EXPORT_BUCKET=
|
||||||
|
ARCHIVE_STORAGE_REGION=auto
|
||||||
|
AZURE_BLOB_ACCOUNT_NAME=difyai
|
||||||
|
AZURE_BLOB_CONTAINER_NAME=difyai-container
|
||||||
|
GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name
|
||||||
|
GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=
|
||||||
|
ALIYUN_OSS_BUCKET_NAME=your-bucket-name
|
||||||
|
ALIYUN_OSS_ENDPOINT=https://oss-ap-southeast-1-internal.aliyuncs.com
|
||||||
|
ALIYUN_OSS_REGION=ap-southeast-1
|
||||||
|
ALIYUN_OSS_AUTH_VERSION=v4
|
||||||
|
ALIYUN_OSS_PATH=your-path
|
||||||
|
ALIYUN_CLOUDBOX_ID=your-cloudbox-id
|
||||||
|
TENCENT_COS_BUCKET_NAME=your-bucket-name
|
||||||
|
TENCENT_COS_REGION=your-region
|
||||||
|
TENCENT_COS_SCHEME=your-scheme
|
||||||
|
TENCENT_COS_CUSTOM_DOMAIN=your-custom-domain
|
||||||
|
OCI_ENDPOINT=https://your-object-storage-namespace.compat.objectstorage.us-ashburn-1.oraclecloud.com
|
||||||
|
OCI_BUCKET_NAME=your-bucket-name
|
||||||
|
OCI_REGION=us-ashburn-1
|
||||||
|
HUAWEI_OBS_BUCKET_NAME=your-bucket-name
|
||||||
|
HUAWEI_OBS_SERVER=your-server-url
|
||||||
|
HUAWEI_OBS_PATH_STYLE=false
|
||||||
|
VOLCENGINE_TOS_BUCKET_NAME=your-bucket-name
|
||||||
|
VOLCENGINE_TOS_ENDPOINT=your-server-url
|
||||||
|
VOLCENGINE_TOS_REGION=your-region
|
||||||
|
BAIDU_OBS_BUCKET_NAME=your-bucket-name
|
||||||
|
BAIDU_OBS_ENDPOINT=your-server-url
|
||||||
|
SUPABASE_BUCKET_NAME=your-bucket-name
|
||||||
|
TENCENT_VECTOR_DB_TIMEOUT=30
|
||||||
|
TENCENT_VECTOR_DB_USERNAME=dify
|
||||||
|
TENCENT_VECTOR_DB_SHARD=1
|
||||||
|
TENCENT_VECTOR_DB_REPLICAS=2
|
||||||
|
TENCENT_VECTOR_DB_ENABLE_HYBRID_SEARCH=false
|
||||||
|
BAIDU_VECTOR_DB_ENDPOINT=http://127.0.0.1:5287
|
||||||
|
BAIDU_VECTOR_DB_CONNECTION_TIMEOUT_MS=30000
|
||||||
|
BAIDU_VECTOR_DB_ACCOUNT=root
|
||||||
|
BAIDU_VECTOR_DB_API_KEY=dify
|
||||||
|
BAIDU_VECTOR_DB_SHARD=1
|
||||||
|
BAIDU_VECTOR_DB_REPLICAS=3
|
||||||
|
BAIDU_VECTOR_DB_INVERTED_INDEX_ANALYZER=DEFAULT_ANALYZER
|
||||||
|
BAIDU_VECTOR_DB_INVERTED_INDEX_PARSER_MODE=COARSE_MODE
|
||||||
|
BAIDU_VECTOR_DB_AUTO_BUILD_ROW_COUNT_INCREMENT=500
|
||||||
|
BAIDU_VECTOR_DB_AUTO_BUILD_ROW_COUNT_INCREMENT_RATIO=0.05
|
||||||
|
BAIDU_VECTOR_DB_REBUILD_INDEX_TIMEOUT_IN_SECONDS=300
|
||||||
|
HUAWEI_CLOUD_HOSTS=https://127.0.0.1:9200
|
||||||
|
HUAWEI_CLOUD_USER=admin
|
||||||
|
WORKFLOW_NODE_EXECUTION_STORAGE=rdbms
|
||||||
|
CORE_WORKFLOW_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository
|
||||||
|
CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository
|
||||||
|
API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository
|
||||||
|
API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository
|
||||||
|
ALIYUN_SLS_ENDPOINT=
|
||||||
|
ALIYUN_SLS_REGION=
|
||||||
|
ALIYUN_SLS_PROJECT_NAME=
|
||||||
|
ALIYUN_SLS_LOGSTORE_TTL=365
|
||||||
|
LOGSTORE_DUAL_WRITE_ENABLED=false
|
||||||
|
LOGSTORE_DUAL_READ_ENABLED=true
|
||||||
|
LOGSTORE_ENABLE_PUT_GRAPH_FIELD=true
|
||||||
|
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
||||||
|
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY=True
|
||||||
|
HTTP_REQUEST_MAX_CONNECT_TIMEOUT=10
|
||||||
|
HTTP_REQUEST_MAX_READ_TIMEOUT=600
|
||||||
|
HTTP_REQUEST_MAX_WRITE_TIMEOUT=600
|
||||||
|
PLUGIN_INSTALLED_PATH=plugin
|
||||||
|
PLUGIN_PACKAGE_CACHE_PATH=plugin_packages
|
||||||
|
PLUGIN_MEDIA_CACHE_PATH=assets
|
||||||
|
PLUGIN_S3_USE_AWS=false
|
||||||
|
PLUGIN_S3_USE_AWS_MANAGED_IAM=false
|
||||||
|
PLUGIN_S3_ENDPOINT=
|
||||||
|
PLUGIN_S3_USE_PATH_STYLE=false
|
||||||
|
PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME=
|
||||||
|
PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING=
|
||||||
|
PLUGIN_TENCENT_COS_REGION=
|
||||||
|
PLUGIN_ALIYUN_OSS_REGION=
|
||||||
|
PLUGIN_ALIYUN_OSS_ENDPOINT=
|
||||||
|
PLUGIN_ALIYUN_OSS_AUTH_VERSION=v4
|
||||||
|
PLUGIN_ALIYUN_OSS_PATH=
|
||||||
|
PLUGIN_VOLCENGINE_TOS_ENDPOINT=
|
||||||
|
PLUGIN_VOLCENGINE_TOS_REGION=
|
||||||
|
ENABLE_OTEL=false
|
||||||
|
OTLP_TRACE_ENDPOINT=
|
||||||
|
OTLP_METRIC_ENDPOINT=
|
||||||
|
# Prefix used to create collection name in vector database
|
||||||
|
OTLP_BASE_ENDPOINT=http://localhost:4318
|
||||||
|
WEAVIATE_GRPC_ENDPOINT=grpc://weaviate:50051
|
||||||
|
ANALYTICDB_KEY_ID=your-ak
|
||||||
|
ANALYTICDB_KEY_SECRET=your-sk
|
||||||
|
ANALYTICDB_REGION_ID=cn-hangzhou
|
||||||
|
ANALYTICDB_INSTANCE_ID=gp-ab123456
|
||||||
|
ANALYTICDB_ACCOUNT=testaccount
|
||||||
|
ANALYTICDB_PASSWORD=testpassword
|
||||||
|
ANALYTICDB_NAMESPACE=dify
|
||||||
|
ANALYTICDB_NAMESPACE_PASSWORD=difypassword
|
||||||
|
ANALYTICDB_HOST=gp-test.aliyuncs.com
|
||||||
|
ANALYTICDB_PORT=5432
|
||||||
|
ANALYTICDB_MIN_CONNECTION=1
|
||||||
|
ANALYTICDB_MAX_CONNECTION=5
|
||||||
|
TIDB_VECTOR_HOST=tidb
|
||||||
|
TIDB_VECTOR_PORT=4000
|
||||||
|
TIDB_VECTOR_USER=
|
||||||
|
TIDB_VECTOR_PASSWORD=
|
||||||
|
TIDB_ON_QDRANT_CLIENT_TIMEOUT=20
|
||||||
|
TIDB_ON_QDRANT_GRPC_ENABLED=false
|
||||||
|
TIDB_ON_QDRANT_GRPC_PORT=6334
|
||||||
|
TIDB_PUBLIC_KEY=dify
|
||||||
|
TIDB_PRIVATE_KEY=dify
|
||||||
|
RELYT_HOST=db
|
||||||
|
RELYT_PORT=5432
|
||||||
|
RELYT_USER=postgres
|
||||||
|
VIKINGDB_ACCESS_KEY=your-ak
|
||||||
|
VIKINGDB_SECRET_KEY=your-sk
|
||||||
|
VIKINGDB_REGION=cn-shanghai
|
||||||
|
VIKINGDB_HOST=api-vikingdb.xxx.volces.com
|
||||||
|
VIKINGDB_SCHEME=http
|
||||||
|
VIKINGDB_CONNECTION_TIMEOUT=30
|
||||||
|
VIKINGDB_SOCKET_TIMEOUT=30
|
||||||
|
TABLESTORE_ENDPOINT=https://instance-name.cn-hangzhou.ots.aliyuncs.com
|
||||||
|
TABLESTORE_INSTANCE_NAME=instance-name
|
||||||
|
CLICKZETTA_USERNAME=
|
||||||
|
CLICKZETTA_VECTOR_DISTANCE_FUNCTION=cosine_distance
|
||||||
|
COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql}
|
||||||
|
EXPOSE_NGINX_PORT=80
|
||||||
|
EXPOSE_NGINX_SSL_PORT=443
|
||||||
|
POSITION_TOOL_PINS=
|
||||||
|
POSITION_TOOL_INCLUDES=
|
||||||
|
POSITION_TOOL_EXCLUDES=
|
||||||
|
POSITION_PROVIDER_PINS=
|
||||||
|
POSITION_PROVIDER_INCLUDES=
|
||||||
|
POSITION_PROVIDER_EXCLUDES=
|
||||||
|
CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
||||||
|
MAX_SUBMIT_COUNT=100
|
||||||
|
|
||||||
|
# Vector Store Configuration
|
||||||
|
STORAGE_TYPE=opendal
|
||||||
|
VECTOR_STORE=weaviate
|
||||||
|
VECTOR_INDEX_NAME_PREFIX=Vector_index
|
||||||
|
WEAVIATE_ENDPOINT=http://weaviate:8080
|
||||||
|
WEAVIATE_API_KEY=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih
|
||||||
|
WEAVIATE_TOKENIZATION=word
|
||||||
|
OCEANBASE_VECTOR_HOST=oceanbase
|
||||||
|
OCEANBASE_VECTOR_PORT=2881
|
||||||
|
OCEANBASE_VECTOR_USER=root@test
|
||||||
|
OCEANBASE_VECTOR_PASSWORD=difyai123456
|
||||||
|
OCEANBASE_VECTOR_DATABASE=test
|
||||||
|
OCEANBASE_ENABLE_HYBRID_SEARCH=false
|
||||||
|
OCEANBASE_FULLTEXT_PARSER=ik
|
||||||
|
SEEKDB_MEMORY_LIMIT=2G
|
||||||
|
QDRANT_URL=http://qdrant:6333
|
||||||
|
QDRANT_API_KEY=difyai123456
|
||||||
|
QDRANT_CLIENT_TIMEOUT=20
|
||||||
|
QDRANT_GRPC_ENABLED=false
|
||||||
|
QDRANT_GRPC_PORT=6334
|
||||||
|
QDRANT_REPLICATION_FACTOR=1
|
||||||
|
MILVUS_URI=http://host.docker.internal:19530
|
||||||
|
MILVUS_TOKEN=
|
||||||
|
MILVUS_USER=
|
||||||
|
MILVUS_PASSWORD=
|
||||||
|
MILVUS_ANALYZER_PARAMS=
|
||||||
|
PGVECTOR_HOST=pgvector
|
||||||
|
PGVECTOR_PORT=5432
|
||||||
|
PGVECTOR_USER=postgres
|
||||||
|
PGVECTOR_PASSWORD=difyai123456
|
||||||
|
PGVECTOR_DATABASE=dify
|
||||||
|
PGVECTOR_MIN_CONNECTION=1
|
||||||
|
PGVECTOR_MAX_CONNECTION=5
|
||||||
|
PGVECTOR_PG_BIGM=false
|
||||||
|
PGVECTOR_PG_BIGM_VERSION=1.2-20240606
|
||||||
|
|
||||||
|
# Hologres Configuration
|
||||||
|
HOLOGRES_HOST=
|
||||||
|
HOLOGRES_PORT=80
|
||||||
|
HOLOGRES_DATABASE=
|
||||||
|
HOLOGRES_ACCESS_KEY_ID=
|
||||||
|
HOLOGRES_ACCESS_KEY_SECRET=
|
||||||
|
HOLOGRES_SCHEMA=public
|
||||||
|
HOLOGRES_TOKENIZER=jieba
|
||||||
|
HOLOGRES_DISTANCE_METHOD=Cosine
|
||||||
|
HOLOGRES_BASE_QUANTIZATION_TYPE=rabitq
|
||||||
|
HOLOGRES_MAX_DEGREE=64
|
||||||
|
HOLOGRES_EF_CONSTRUCTION=400
|
||||||
|
|
||||||
|
# Milvus API Configuration
|
||||||
|
MILVUS_DATABASE=
|
||||||
|
MILVUS_ENABLE_HYBRID_SEARCH=False
|
||||||
|
|
||||||
|
# Human Input Task Configuration
|
||||||
|
ENABLE_HUMAN_INPUT_TIMEOUT_TASK=true
|
||||||
|
HUMAN_INPUT_TIMEOUT_TASK_INTERVAL=1
|
||||||
30
docker/envs/core-services/web.env.example
Normal file
30
docker/envs/core-services/web.env.example
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Web Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
CONSOLE_API_URL=
|
||||||
|
APP_API_URL=
|
||||||
|
SENTRY_DSN=
|
||||||
|
NEXT_PUBLIC_SOCKET_URL=ws://localhost
|
||||||
|
EXPERIMENTAL_ENABLE_VINEXT=false
|
||||||
|
LOOP_NODE_MAX_COUNT=100
|
||||||
|
MAX_TOOLS_NUM=10
|
||||||
|
MAX_PARALLEL_LIMIT=10
|
||||||
|
MAX_ITERATIONS_NUM=99
|
||||||
|
TEXT_GENERATION_TIMEOUT_MS=60000
|
||||||
|
ALLOW_INLINE_STYLES=false
|
||||||
|
ALLOW_UNSAFE_DATA_SCHEME=false
|
||||||
|
MAX_TREE_DEPTH=50
|
||||||
|
MARKETPLACE_ENABLED=true
|
||||||
|
MARKETPLACE_API_URL=https://marketplace.dify.ai
|
||||||
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||||
|
ALLOW_EMBED=false
|
||||||
|
AMPLITUDE_API_KEY=
|
||||||
|
ENABLE_WEBSITE_JINAREADER=true
|
||||||
|
ENABLE_WEBSITE_FIRECRAWL=true
|
||||||
|
ENABLE_WEBSITE_WATERCRAWL=true
|
||||||
|
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false
|
||||||
|
NEXT_PUBLIC_COOKIE_DOMAIN=
|
||||||
|
NEXT_PUBLIC_BATCH_CONCURRENCY=5
|
||||||
|
CSP_WHITELIST=
|
||||||
|
TOP_K_MAX_VALUE=10
|
||||||
8
docker/envs/core-services/worker-beat.env.example
Normal file
8
docker/envs/core-services/worker-beat.env.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Worker Beat Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
MODE=beat
|
||||||
|
COMPOSE_WORKER_HEALTHCHECK_DISABLED=true
|
||||||
|
COMPOSE_WORKER_HEALTHCHECK_INTERVAL=30s
|
||||||
|
COMPOSE_WORKER_HEALTHCHECK_TIMEOUT=30s
|
||||||
13
docker/envs/core-services/worker.env.example
Normal file
13
docker/envs/core-services/worker.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Worker Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
MODE=worker
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_TRACES_SAMPLE_RATE=1.0
|
||||||
|
SENTRY_PROFILES_SAMPLE_RATE=1.0
|
||||||
|
PLUGIN_MAX_PACKAGE_SIZE=52428800
|
||||||
|
INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||||
|
COMPOSE_WORKER_HEALTHCHECK_DISABLED=true
|
||||||
|
COMPOSE_WORKER_HEALTHCHECK_INTERVAL=30s
|
||||||
|
COMPOSE_WORKER_HEALTHCHECK_TIMEOUT=30s
|
||||||
9
docker/envs/databases/db-mysql.env.example
Normal file
9
docker/envs/databases/db-mysql.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Db Mysql Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
MYSQL_INNODB_LOG_FILE_SIZE=128M
|
||||||
|
MYSQL_INNODB_FLUSH_LOG_AT_TRX_COMMIT=2
|
||||||
|
MYSQL_MAX_CONNECTIONS=1000
|
||||||
|
MYSQL_INNODB_BUFFER_POOL_SIZE=512M
|
||||||
|
MYSQL_HOST_VOLUME=./volumes/mysql/data
|
||||||
26
docker/envs/databases/db-postgres.env.example
Normal file
26
docker/envs/databases/db-postgres.env.example
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Db Postgres Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
PGDATA=/var/lib/postgresql/data/pgdata
|
||||||
|
DB_TYPE=postgresql
|
||||||
|
DB_USERNAME=postgres
|
||||||
|
DB_PASSWORD=difyai123456
|
||||||
|
DB_HOST=db_postgres
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=dify
|
||||||
|
SQLALCHEMY_POOL_SIZE=30
|
||||||
|
SQLALCHEMY_MAX_OVERFLOW=10
|
||||||
|
SQLALCHEMY_POOL_RECYCLE=3600
|
||||||
|
SQLALCHEMY_ECHO=false
|
||||||
|
SQLALCHEMY_POOL_PRE_PING=false
|
||||||
|
SQLALCHEMY_POOL_USE_LIFO=false
|
||||||
|
SQLALCHEMY_POOL_TIMEOUT=30
|
||||||
|
SQLALCHEMY_POOL_RESET_ON_RETURN=rollback
|
||||||
|
POSTGRES_MAX_CONNECTIONS=100
|
||||||
|
POSTGRES_SHARED_BUFFERS=128MB
|
||||||
|
POSTGRES_WORK_MEM=4MB
|
||||||
|
POSTGRES_MAINTENANCE_WORK_MEM=64MB
|
||||||
|
POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB
|
||||||
|
POSTGRES_STATEMENT_TIMEOUT=0
|
||||||
|
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT=0
|
||||||
35
docker/envs/databases/redis.env.example
Normal file
35
docker/envs/databases/redis.env.example
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Redis Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_USERNAME=
|
||||||
|
REDIS_PASSWORD=difyai123456
|
||||||
|
REDIS_USE_SSL=false
|
||||||
|
REDIS_SSL_CERT_REQS=CERT_NONE
|
||||||
|
REDIS_SSL_CA_CERTS=
|
||||||
|
REDIS_SSL_CERTFILE=
|
||||||
|
REDIS_SSL_KEYFILE=
|
||||||
|
REDIS_DB=0
|
||||||
|
REDIS_KEY_PREFIX=
|
||||||
|
REDIS_MAX_CONNECTIONS=
|
||||||
|
REDIS_USE_SENTINEL=false
|
||||||
|
REDIS_SENTINELS=
|
||||||
|
REDIS_SENTINEL_SERVICE_NAME=
|
||||||
|
REDIS_SENTINEL_USERNAME=
|
||||||
|
REDIS_SENTINEL_PASSWORD=
|
||||||
|
REDIS_SENTINEL_SOCKET_TIMEOUT=0.1
|
||||||
|
REDIS_USE_CLUSTERS=false
|
||||||
|
REDIS_CLUSTERS=
|
||||||
|
REDIS_CLUSTERS_PASSWORD=
|
||||||
|
REDIS_RETRY_RETRIES=3
|
||||||
|
REDIS_RETRY_BACKOFF_BASE=1.0
|
||||||
|
REDIS_RETRY_BACKOFF_CAP=10.0
|
||||||
|
REDIS_SOCKET_TIMEOUT=5.0
|
||||||
|
REDIS_SOCKET_CONNECT_TIMEOUT=5.0
|
||||||
|
REDIS_HEALTH_CHECK_INTERVAL=30
|
||||||
|
EVENT_BUS_REDIS_URL=
|
||||||
|
EVENT_BUS_REDIS_CHANNEL_TYPE=pubsub
|
||||||
|
EVENT_BUS_REDIS_USE_CLUSTERS=false
|
||||||
|
BROKER_USE_SSL=false
|
||||||
7
docker/envs/infrastructure/certbot.env.example
Normal file
7
docker/envs/infrastructure/certbot.env.example
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Certbot Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
CERTBOT_EMAIL=your_email@example.com
|
||||||
|
CERTBOT_DOMAIN=your_domain.com
|
||||||
|
CERTBOT_OPTIONS=
|
||||||
4
docker/envs/infrastructure/etcd.env.example
Normal file
4
docker/envs/infrastructure/etcd.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Etcd Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
4
docker/envs/infrastructure/milvus-standalone.env.example
Normal file
4
docker/envs/infrastructure/milvus-standalone.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Milvus Standalone Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
4
docker/envs/infrastructure/minio.env.example
Normal file
4
docker/envs/infrastructure/minio.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Minio Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
17
docker/envs/infrastructure/nginx.env.example
Normal file
17
docker/envs/infrastructure/nginx.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Nginx Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
NGINX_SERVER_NAME=_
|
||||||
|
NGINX_HTTPS_ENABLED=false
|
||||||
|
NGINX_PORT=80
|
||||||
|
NGINX_SSL_PORT=443
|
||||||
|
NGINX_SSL_CERT_FILENAME=dify.crt
|
||||||
|
NGINX_SSL_CERT_KEY_FILENAME=dify.key
|
||||||
|
NGINX_SSL_PROTOCOLS=TLSv1.2 TLSv1.3
|
||||||
|
NGINX_WORKER_PROCESSES=auto
|
||||||
|
NGINX_CLIENT_MAX_BODY_SIZE=100M
|
||||||
|
NGINX_KEEPALIVE_TIMEOUT=65
|
||||||
|
NGINX_PROXY_READ_TIMEOUT=3600s
|
||||||
|
NGINX_PROXY_SEND_TIMEOUT=3600s
|
||||||
|
NGINX_ENABLE_CERTBOT_CHALLENGE=false
|
||||||
17
docker/envs/infrastructure/ssrf-proxy.env.example
Normal file
17
docker/envs/infrastructure/ssrf-proxy.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Ssrf Proxy Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128
|
||||||
|
SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128
|
||||||
|
SSRF_HTTP_PORT=3128
|
||||||
|
SSRF_COREDUMP_DIR=/var/spool/squid
|
||||||
|
SSRF_REVERSE_PROXY_PORT=8194
|
||||||
|
SSRF_SANDBOX_HOST=sandbox
|
||||||
|
SSRF_DEFAULT_TIME_OUT=5
|
||||||
|
SSRF_DEFAULT_CONNECT_TIME_OUT=5
|
||||||
|
SSRF_DEFAULT_READ_TIME_OUT=5
|
||||||
|
SSRF_DEFAULT_WRITE_TIME_OUT=5
|
||||||
|
SSRF_POOL_MAX_CONNECTIONS=100
|
||||||
|
SSRF_POOL_MAX_KEEPALIVE_CONNECTIONS=20
|
||||||
|
SSRF_POOL_KEEPALIVE_EXPIRY=5.0
|
||||||
40
docker/envs/security.env.example
Normal file
40
docker/envs/security.env.example
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Security Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
TIDB_ON_QDRANT_API_KEY=dify
|
||||||
|
TENCENT_VECTOR_DB_API_KEY=dify
|
||||||
|
ALIBABACLOUD_MYSQL_PASSWORD=difyai123456
|
||||||
|
RELYT_PASSWORD=difyai123456
|
||||||
|
LINDORM_PASSWORD=admin
|
||||||
|
HUAWEI_CLOUD_PASSWORD=admin
|
||||||
|
UPSTASH_VECTOR_TOKEN=dify
|
||||||
|
TABLESTORE_ACCESS_KEY_ID=xxx
|
||||||
|
TABLESTORE_ACCESS_KEY_SECRET=xxx
|
||||||
|
UNSTRUCTURED_API_KEY=
|
||||||
|
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
||||||
|
NOTION_CLIENT_SECRET=
|
||||||
|
NOTION_INTERNAL_SECRET=
|
||||||
|
RESEND_API_KEY=your-resend-api-key
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SENDGRID_API_KEY=
|
||||||
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
CODE_EXECUTION_API_KEY=dify-sandbox
|
||||||
|
ALIYUN_SLS_ACCESS_KEY_ID=
|
||||||
|
ALIYUN_SLS_ACCESS_KEY_SECRET=
|
||||||
|
OTLP_API_KEY=
|
||||||
|
BAIDU_VECTOR_DB_API_KEY=dify
|
||||||
|
ANALYTICDB_KEY_ID=your-ak
|
||||||
|
ANALYTICDB_KEY_SECRET=your-sk
|
||||||
|
ANALYTICDB_PASSWORD=testpassword
|
||||||
|
ANALYTICDB_NAMESPACE_PASSWORD=difypassword
|
||||||
|
TIDB_VECTOR_PASSWORD=
|
||||||
|
TIDB_PUBLIC_KEY=dify
|
||||||
|
TIDB_PRIVATE_KEY=dify
|
||||||
|
VIKINGDB_ACCESS_KEY=your-ak
|
||||||
|
VIKINGDB_SECRET_KEY=your-sk
|
||||||
|
SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
|
||||||
|
INIT_PASSWORD=
|
||||||
13
docker/envs/vectorstores/chroma.env.example
Normal file
13
docker/envs/vectorstores/chroma.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Chroma Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
CHROMA_DATABASE=default_database
|
||||||
|
CHROMA_AUTH_PROVIDER=chromadb.auth.token_authn.TokenAuthClientProvider
|
||||||
|
CHROMA_AUTH_CREDENTIALS=
|
||||||
|
CHROMA_HOST=127.0.0.1
|
||||||
|
CHROMA_PORT=8000
|
||||||
|
CHROMA_TENANT=default_tenant
|
||||||
|
CHROMA_SERVER_AUTHN_CREDENTIALS=difyai123456
|
||||||
|
CHROMA_SERVER_AUTHN_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider
|
||||||
|
CHROMA_IS_PERSISTENT=TRUE
|
||||||
9
docker/envs/vectorstores/couchbase.env.example
Normal file
9
docker/envs/vectorstores/couchbase.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Couchbase Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
COUCHBASE_PASSWORD=password
|
||||||
|
COUCHBASE_BUCKET_NAME=Embeddings
|
||||||
|
COUCHBASE_SCOPE_NAME=_default
|
||||||
|
COUCHBASE_CONNECTION_STRING=couchbase://couchbase-server
|
||||||
|
COUCHBASE_USER=Administrator
|
||||||
17
docker/envs/vectorstores/elasticsearch.env.example
Normal file
17
docker/envs/vectorstores/elasticsearch.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Elasticsearch Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
ELASTICSEARCH_CLOUD_URL=YOUR-ELASTICSEARCH_CLOUD_URL
|
||||||
|
ELASTICSEARCH_PASSWORD=elastic
|
||||||
|
KIBANA_PORT=5601
|
||||||
|
ELASTICSEARCH_USE_CLOUD=false
|
||||||
|
ELASTICSEARCH_API_KEY=YOUR-ELASTICSEARCH_API_KEY
|
||||||
|
ELASTICSEARCH_VERIFY_CERTS=False
|
||||||
|
ELASTICSEARCH_CA_CERTS=
|
||||||
|
ELASTICSEARCH_REQUEST_TIMEOUT=100000
|
||||||
|
ELASTICSEARCH_RETRY_ON_TIMEOUT=True
|
||||||
|
ELASTICSEARCH_MAX_RETRIES=10
|
||||||
|
ELASTICSEARCH_HOST=0.0.0.0
|
||||||
|
ELASTICSEARCH_PORT=9200
|
||||||
|
ELASTICSEARCH_USERNAME=elastic
|
||||||
17
docker/envs/vectorstores/iris.env.example
Normal file
17
docker/envs/vectorstores/iris.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Iris Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
IRIS_CONNECTION_URL=
|
||||||
|
IRIS_MIN_CONNECTION=1
|
||||||
|
IRIS_MAX_CONNECTION=3
|
||||||
|
IRIS_TEXT_INDEX=true
|
||||||
|
IRIS_TEXT_INDEX_LANGUAGE=en
|
||||||
|
IRIS_TIMEZONE=UTC
|
||||||
|
IRIS_PASSWORD=Dify@1234
|
||||||
|
IRIS_DATABASE=USER
|
||||||
|
IRIS_SCHEMA=dify
|
||||||
|
IRIS_HOST=iris
|
||||||
|
IRIS_SUPER_SERVER_PORT=1972
|
||||||
|
IRIS_WEB_SERVER_PORT=52773
|
||||||
|
IRIS_USER=_SYSTEM
|
||||||
9
docker/envs/vectorstores/matrixone.env.example
Normal file
9
docker/envs/vectorstores/matrixone.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Matrixone Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
MATRIXONE_PASSWORD=111
|
||||||
|
MATRIXONE_HOST=matrixone
|
||||||
|
MATRIXONE_PORT=6001
|
||||||
|
MATRIXONE_USER=dump
|
||||||
|
MATRIXONE_DATABASE=dify
|
||||||
13
docker/envs/vectorstores/milvus.env.example
Normal file
13
docker/envs/vectorstores/milvus.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Milvus Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=minioadmin
|
||||||
|
ETCD_ENDPOINTS=etcd:2379
|
||||||
|
MINIO_ADDRESS=minio:9000
|
||||||
|
ETCD_AUTO_COMPACTION_MODE=revision
|
||||||
|
ETCD_AUTO_COMPACTION_RETENTION=1000
|
||||||
|
ETCD_QUOTA_BACKEND_BYTES=4294967296
|
||||||
|
ETCD_SNAPSHOT_COUNT=50000
|
||||||
|
MILVUS_AUTHORIZATION_ENABLED=true
|
||||||
10
docker/envs/vectorstores/myscale.env.example
Normal file
10
docker/envs/vectorstores/myscale.env.example
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Myscale Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
MYSCALE_PASSWORD=
|
||||||
|
MYSCALE_DATABASE=dify
|
||||||
|
MYSCALE_FTS_PARAMS=
|
||||||
|
MYSCALE_HOST=myscale
|
||||||
|
MYSCALE_PORT=8123
|
||||||
|
MYSCALE_USER=default
|
||||||
6
docker/envs/vectorstores/oceanbase.env.example
Normal file
6
docker/envs/vectorstores/oceanbase.env.example
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Oceanbase Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
OCEANBASE_CLUSTER_NAME=difyai
|
||||||
|
OCEANBASE_MEMORY_LIMIT=6G
|
||||||
12
docker/envs/vectorstores/opengauss.env.example
Normal file
12
docker/envs/vectorstores/opengauss.env.example
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Opengauss Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
OPENGAUSS_PASSWORD=Dify@123
|
||||||
|
OPENGAUSS_DATABASE=dify
|
||||||
|
OPENGAUSS_MIN_CONNECTION=1
|
||||||
|
OPENGAUSS_MAX_CONNECTION=5
|
||||||
|
OPENGAUSS_ENABLE_PQ=false
|
||||||
|
OPENGAUSS_HOST=opengauss
|
||||||
|
OPENGAUSS_PORT=6600
|
||||||
|
OPENGAUSS_USER=postgres
|
||||||
22
docker/envs/vectorstores/opensearch.env.example
Normal file
22
docker/envs/vectorstores/opensearch.env.example
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Opensearch Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
OPENSEARCH_PASSWORD=admin
|
||||||
|
OPENSEARCH_AWS_REGION=ap-southeast-1
|
||||||
|
OPENSEARCH_AWS_SERVICE=aoss
|
||||||
|
OPENSEARCH_INITIAL_ADMIN_PASSWORD=Qazwsxedc!@#123
|
||||||
|
OPENSEARCH_MEMLOCK_SOFT=-1
|
||||||
|
OPENSEARCH_MEMLOCK_HARD=-1
|
||||||
|
OPENSEARCH_NOFILE_SOFT=65536
|
||||||
|
OPENSEARCH_NOFILE_HARD=65536
|
||||||
|
OPENSEARCH_HOST=opensearch
|
||||||
|
OPENSEARCH_PORT=9200
|
||||||
|
OPENSEARCH_SECURE=true
|
||||||
|
OPENSEARCH_VERIFY_CERTS=true
|
||||||
|
OPENSEARCH_AUTH_METHOD=basic
|
||||||
|
OPENSEARCH_USER=admin
|
||||||
|
OPENSEARCH_DISCOVERY_TYPE=single-node
|
||||||
|
OPENSEARCH_BOOTSTRAP_MEMORY_LOCK=true
|
||||||
|
OPENSEARCH_JAVA_OPTS_MIN=512m
|
||||||
|
OPENSEARCH_JAVA_OPTS_MAX=1024m
|
||||||
13
docker/envs/vectorstores/oracle.env.example
Normal file
13
docker/envs/vectorstores/oracle.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Oracle Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
ORACLE_PASSWORD=dify
|
||||||
|
ORACLE_DSN=oracle:1521/FREEPDB1
|
||||||
|
ORACLE_CONFIG_DIR=/app/api/storage/wallet
|
||||||
|
ORACLE_WALLET_LOCATION=/app/api/storage/wallet
|
||||||
|
ORACLE_WALLET_PASSWORD=dify
|
||||||
|
ORACLE_IS_AUTONOMOUS=false
|
||||||
|
ORACLE_USER=dify
|
||||||
|
ORACLE_PWD=Dify123456
|
||||||
|
ORACLE_CHARACTERSET=AL32UTF8
|
||||||
9
docker/envs/vectorstores/pgvecto-rs.env.example
Normal file
9
docker/envs/vectorstores/pgvecto-rs.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Pgvecto Rs Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
PGVECTO_RS_HOST=pgvecto-rs
|
||||||
|
PGVECTO_RS_PORT=5432
|
||||||
|
PGVECTO_RS_USER=postgres
|
||||||
|
PGVECTO_RS_PASSWORD=difyai123456
|
||||||
|
PGVECTO_RS_DATABASE=dify
|
||||||
8
docker/envs/vectorstores/pgvector.env.example
Normal file
8
docker/envs/vectorstores/pgvector.env.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Pgvector Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
PGVECTOR_PGUSER=postgres
|
||||||
|
PGVECTOR_POSTGRES_PASSWORD=difyai123456
|
||||||
|
PGVECTOR_POSTGRES_DB=dify
|
||||||
|
PGVECTOR_PGDATA=/var/lib/postgresql/data/pgdata
|
||||||
4
docker/envs/vectorstores/qdrant.env.example
Normal file
4
docker/envs/vectorstores/qdrant.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Qdrant Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
4
docker/envs/vectorstores/seekdb.env.example
Normal file
4
docker/envs/vectorstores/seekdb.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Seekdb Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
11
docker/envs/vectorstores/vastbase.env.example
Normal file
11
docker/envs/vectorstores/vastbase.env.example
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Vastbase Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
VASTBASE_PASSWORD=Difyai123456
|
||||||
|
VASTBASE_DATABASE=dify
|
||||||
|
VASTBASE_MIN_CONNECTION=1
|
||||||
|
VASTBASE_MAX_CONNECTION=5
|
||||||
|
VASTBASE_HOST=vastbase
|
||||||
|
VASTBASE_PORT=5432
|
||||||
|
VASTBASE_USER=dify
|
||||||
18
docker/envs/vectorstores/weaviate.env.example
Normal file
18
docker/envs/vectorstores/weaviate.env.example
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Weaviate Configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
WEAVIATE_PERSISTENCE_DATA_PATH=/var/lib/weaviate
|
||||||
|
WEAVIATE_QUERY_DEFAULTS_LIMIT=25
|
||||||
|
WEAVIATE_AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true
|
||||||
|
WEAVIATE_DEFAULT_VECTORIZER_MODULE=none
|
||||||
|
WEAVIATE_CLUSTER_HOSTNAME=node1
|
||||||
|
WEAVIATE_AUTHENTICATION_APIKEY_ENABLED=true
|
||||||
|
WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih
|
||||||
|
WEAVIATE_AUTHENTICATION_APIKEY_USERS=hello@dify.ai
|
||||||
|
WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED=true
|
||||||
|
WEAVIATE_AUTHORIZATION_ADMINLIST_USERS=hello@dify.ai
|
||||||
|
WEAVIATE_DISABLE_TELEMETRY=false
|
||||||
|
WEAVIATE_ENABLE_TOKENIZER_GSE=false
|
||||||
|
WEAVIATE_ENABLE_TOKENIZER_KAGOME_JA=false
|
||||||
|
WEAVIATE_ENABLE_TOKENIZER_KAGOME_KR=false
|
||||||
@ -64,25 +64,61 @@ def generate_shared_env_block(env_vars, anchor_name="shared-api-worker-env"):
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def insert_shared_env(template_path, output_path, shared_env_block, header_comments):
|
def create_env_files_from_example(env_example_path):
|
||||||
"""
|
"""
|
||||||
Inserts the shared environment variables block and header comments into the template file,
|
Creates actual env files from .env.example by copying the categorized .env.example files.
|
||||||
removing any existing x-shared-env anchors, and generates the final docker-compose.yaml file.
|
This allows docker-compose to use env_file references.
|
||||||
Always writes with LF line endings.
|
Supports per-module structure with subdirectories.
|
||||||
|
"""
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(env_example_path))
|
||||||
|
root_env_file = os.path.join(base_dir, ".env")
|
||||||
|
if not os.path.exists(root_env_file):
|
||||||
|
with open(env_example_path, "r", encoding="utf-8") as src, open(
|
||||||
|
root_env_file, "w", encoding="utf-8", newline="\n"
|
||||||
|
) as dst:
|
||||||
|
dst.write(src.read())
|
||||||
|
print(f"Created {root_env_file}")
|
||||||
|
else:
|
||||||
|
print(f"{root_env_file} already exists, skipping")
|
||||||
|
|
||||||
|
envs_dir = os.path.join(base_dir, "envs")
|
||||||
|
if not os.path.isdir(envs_dir):
|
||||||
|
print(f"No envs directory found at {envs_dir}, skipping split env files")
|
||||||
|
return []
|
||||||
|
|
||||||
|
created_files = []
|
||||||
|
# Walk through all .env.example files in subdirectories
|
||||||
|
for root, dirs, files in os.walk(envs_dir):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('.env.example'):
|
||||||
|
example_file = os.path.join(root, file)
|
||||||
|
env_file = example_file.replace('.env.example', '.env')
|
||||||
|
|
||||||
|
if os.path.exists(env_file):
|
||||||
|
print(f"{env_file} already exists, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Copy .example to actual file
|
||||||
|
with open(example_file, "r", encoding="utf-8") as src, open(
|
||||||
|
env_file, "w", encoding="utf-8", newline="\n"
|
||||||
|
) as dst:
|
||||||
|
dst.write(src.read())
|
||||||
|
created_files.append(env_file)
|
||||||
|
print(f"Created {env_file}")
|
||||||
|
|
||||||
|
return created_files
|
||||||
|
|
||||||
|
|
||||||
|
def insert_shared_env(template_path, output_path, header_comments):
|
||||||
|
"""
|
||||||
|
Copies the template file to output path with header comments.
|
||||||
|
The template now uses env_file references instead of a huge YAML anchor.
|
||||||
"""
|
"""
|
||||||
with open(template_path, "r", encoding="utf-8") as f:
|
with open(template_path, "r", encoding="utf-8") as f:
|
||||||
template_content = f.read()
|
template_content = f.read()
|
||||||
|
|
||||||
# Remove existing x-shared-env: &shared-api-worker-env lines
|
# Prepare the final content with header comments
|
||||||
template_content = re.sub(
|
final_content = f"{header_comments}\n{template_content}"
|
||||||
r"^x-shared-env: &shared-api-worker-env\s*\n?",
|
|
||||||
"",
|
|
||||||
template_content,
|
|
||||||
flags=re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Prepare the final content with header comments and shared env block
|
|
||||||
final_content = f"{header_comments}\n{shared_env_block}\n\n{template_content}"
|
|
||||||
|
|
||||||
with open(output_path, "w", encoding="utf-8", newline="\n") as f:
|
with open(output_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
f.write(final_content)
|
f.write(final_content)
|
||||||
@ -90,10 +126,10 @@ def insert_shared_env(template_path, output_path, shared_env_block, header_comme
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
env_example_path = ".env.example"
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
template_path = "docker-compose-template.yaml"
|
env_example_path = os.path.join(base_dir, ".env.example")
|
||||||
output_path = "docker-compose.yaml"
|
template_path = os.path.join(base_dir, "docker-compose-template.yaml")
|
||||||
anchor_name = "shared-api-worker-env" # Can be modified as needed
|
output_path = os.path.join(base_dir, "docker-compose.yaml")
|
||||||
|
|
||||||
# Define header comments to be added at the top of docker-compose.yaml
|
# Define header comments to be added at the top of docker-compose.yaml
|
||||||
header_comments = (
|
header_comments = (
|
||||||
@ -110,17 +146,14 @@ def main():
|
|||||||
print(f"Error: File {path} does not exist.")
|
print(f"Error: File {path} does not exist.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Parse .env.example file
|
# Create env files from categorized .env.example files
|
||||||
env_vars = parse_env_example(env_example_path)
|
# These files are used by docker-compose's env_file directive
|
||||||
|
# This ensures .env files exist even in CI/CD environments
|
||||||
|
create_env_files_from_example(env_example_path)
|
||||||
|
|
||||||
if not env_vars:
|
# Copy template to output with header comments
|
||||||
print("Warning: No environment variables found in .env.example.")
|
# The template now uses env_file references instead of a huge YAML anchor
|
||||||
|
insert_shared_env(template_path, output_path, header_comments)
|
||||||
# Generate shared environment variables block
|
|
||||||
shared_env_block = generate_shared_env_block(env_vars, anchor_name)
|
|
||||||
|
|
||||||
# Insert shared environment variables block and header comments into the template
|
|
||||||
insert_shared_env(template_path, output_path, shared_env_block, header_comments)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export const webDir = path.join(rootDir, 'web')
|
|||||||
|
|
||||||
export const middlewareComposeFile = path.join(dockerDir, 'docker-compose.middleware.yaml')
|
export const middlewareComposeFile = path.join(dockerDir, 'docker-compose.middleware.yaml')
|
||||||
export const middlewareEnvFile = path.join(dockerDir, 'middleware.env')
|
export const middlewareEnvFile = path.join(dockerDir, 'middleware.env')
|
||||||
export const middlewareEnvExampleFile = path.join(dockerDir, 'middleware.env.example')
|
export const middlewareEnvExampleFile = path.join(dockerDir, 'envs', 'middleware.env.example')
|
||||||
export const webEnvLocalFile = path.join(webDir, '.env.local')
|
export const webEnvLocalFile = path.join(webDir, '.env.local')
|
||||||
export const webEnvExampleFile = path.join(webDir, '.env.example')
|
export const webEnvExampleFile = path.join(webDir, '.env.example')
|
||||||
export const apiEnvExampleFile = path.join(apiDir, 'tests', 'integration_tests', '.env.example')
|
export const apiEnvExampleFile = path.join(apiDir, 'tests', 'integration_tests', '.env.example')
|
||||||
|
|||||||
@ -202,6 +202,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/app/annotation/add-annotation-modal/index.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/app/annotation/batch-add-annotation-modal/index.tsx": {
|
"web/app/components/app/annotation/batch-add-annotation-modal/index.tsx": {
|
||||||
"erasable-syntax-only/enums": {
|
"erasable-syntax-only/enums": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -230,6 +235,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/app/annotation/edit-annotation-modal/index.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/app/annotation/header-opts/index.tsx": {
|
"web/app/components/app/annotation/header-opts/index.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -252,6 +262,9 @@
|
|||||||
"erasable-syntax-only/enums": {
|
"erasable-syntax-only/enums": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"react/set-state-in-effect": {
|
"react/set-state-in-effect": {
|
||||||
"count": 5
|
"count": 5
|
||||||
},
|
},
|
||||||
@ -269,11 +282,6 @@
|
|||||||
"count": 4
|
"count": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/app/app-publisher/index.tsx": {
|
|
||||||
"ts/no-explicit-any": {
|
|
||||||
"count": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/app/app-publisher/version-info-modal.tsx": {
|
"web/app/components/app/app-publisher/version-info-modal.tsx": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -344,6 +352,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx": {
|
"web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"react-hooks/exhaustive-deps": {
|
"react-hooks/exhaustive-deps": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
@ -401,6 +412,16 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/app/configuration/configuration-view.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web/app/components/app/configuration/dataset-config/card-item/index.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/app/configuration/dataset-config/index.tsx": {
|
"web/app/components/app/configuration/dataset-config/index.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -531,6 +552,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/app/log/list.tsx": {
|
"web/app/components/app/log/list.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"react/set-state-in-effect": {
|
"react/set-state-in-effect": {
|
||||||
"count": 6
|
"count": 6
|
||||||
},
|
},
|
||||||
@ -580,6 +604,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/app/workflow-log/list.tsx": {
|
"web/app/components/app/workflow-log/list.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"react/set-state-in-effect": {
|
"react/set-state-in-effect": {
|
||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
@ -904,6 +931,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/base/drawer-plus/index.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/base/emoji-picker/index.tsx": {
|
"web/app/components/base/emoji-picker/index.tsx": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -1029,6 +1061,11 @@
|
|||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/base/float-right-container/index.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/base/form/components/base/base-form.tsx": {
|
"web/app/components/base/form/components/base/base-form.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 6
|
"count": 6
|
||||||
@ -1233,7 +1270,7 @@
|
|||||||
},
|
},
|
||||||
"web/app/components/base/icons/src/vender/line/development/index.ts": {
|
"web/app/components/base/icons/src/vender/line/development/index.ts": {
|
||||||
"no-barrel-files/no-barrel-files": {
|
"no-barrel-files/no-barrel-files": {
|
||||||
"count": 2
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/base/icons/src/vender/line/editor/index.ts": {
|
"web/app/components/base/icons/src/vender/line/editor/index.ts": {
|
||||||
@ -2144,14 +2181,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/datasets/documents/detail/batch-modal/index.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"react/set-state-in-effect": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx": {
|
"web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx": {
|
||||||
"react/set-state-in-effect": {
|
"react/set-state-in-effect": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -2162,11 +2191,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/datasets/documents/detail/completed/components/index.ts": {
|
|
||||||
"no-barrel-files/no-barrel-files": {
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/datasets/documents/detail/completed/components/segment-list-content.tsx": {
|
"web/app/components/datasets/documents/detail/completed/components/segment-list-content.tsx": {
|
||||||
"ts/no-non-null-asserted-optional-chain": {
|
"ts/no-non-null-asserted-optional-chain": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -2231,14 +2255,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/datasets/documents/detail/segment-add/index.tsx": {
|
|
||||||
"erasable-syntax-only/enums": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"react-refresh/only-export-components": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx": {
|
"web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 6
|
"count": 6
|
||||||
@ -2280,6 +2296,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/datasets/hit-testing/index.tsx": {
|
"web/app/components/datasets/hit-testing/index.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"react/unsupported-syntax": {
|
"react/unsupported-syntax": {
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
@ -2319,7 +2338,7 @@
|
|||||||
},
|
},
|
||||||
"web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": {
|
"web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 2
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx": {
|
"web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx": {
|
||||||
@ -2813,10 +2832,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx": {
|
"web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 7
|
"count": 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/plugins/plugin-detail-panel/index.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/plugins/plugin-detail-panel/model-list.tsx": {
|
"web/app/components/plugins/plugin-detail-panel/model-list.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -2838,6 +2865,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx": {
|
"web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
@ -2896,6 +2926,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx": {
|
"web/app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 5
|
"count": 5
|
||||||
}
|
}
|
||||||
@ -2933,16 +2966,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/plugins/readme-panel/index.tsx": {
|
|
||||||
"react/unsupported-syntax": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/plugins/readme-panel/store.ts": {
|
|
||||||
"erasable-syntax-only/enums": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts": {
|
"web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts": {
|
||||||
"erasable-syntax-only/enums": {
|
"erasable-syntax-only/enums": {
|
||||||
"count": 2
|
"count": 2
|
||||||
@ -3170,7 +3193,7 @@
|
|||||||
},
|
},
|
||||||
"web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx": {
|
"web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 1
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/tools/edit-custom-collection-modal/get-schema.tsx": {
|
"web/app/components/tools/edit-custom-collection-modal/get-schema.tsx": {
|
||||||
@ -3179,6 +3202,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/tools/edit-custom-collection-modal/index.tsx": {
|
"web/app/components/tools/edit-custom-collection-modal/index.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"react/set-state-in-effect": {
|
"react/set-state-in-effect": {
|
||||||
"count": 4
|
"count": 4
|
||||||
},
|
},
|
||||||
@ -3187,6 +3213,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/tools/edit-custom-collection-modal/test-api.tsx": {
|
"web/app/components/tools/edit-custom-collection-modal/test-api.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
@ -3196,6 +3225,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/tools/mcp/detail/provider-detail.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/tools/mcp/mcp-server-modal.tsx": {
|
"web/app/components/tools/mcp/mcp-server-modal.tsx": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -3224,12 +3258,20 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/tools/provider/detail.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/tools/provider/empty.tsx": {
|
"web/app/components/tools/provider/empty.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/tools/setting/build-in/config-credentials.tsx": {
|
"web/app/components/tools/setting/build-in/config-credentials.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
@ -4061,6 +4103,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx": {
|
||||||
|
"no-restricted-imports": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx": {
|
"web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "dify",
|
"name": "dify",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@11.0.6",
|
"packageManager": "pnpm@11.0.8",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.22.1"
|
"node": "^22.22.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,6 +28,7 @@ Always import from a **subpath export** — there is no barrel:
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { Dialog, DialogContent, DialogTrigger } from '@langgenius/dify-ui/dialog'
|
import { Dialog, DialogContent, DialogTrigger } from '@langgenius/dify-ui/dialog'
|
||||||
|
import { Drawer, DrawerPopup, DrawerTrigger } from '@langgenius/dify-ui/drawer'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||||
import '@langgenius/dify-ui/styles.css' // once, in the app root
|
import '@langgenius/dify-ui/styles.css' // once, in the app root
|
||||||
```
|
```
|
||||||
@ -36,12 +37,12 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
|
|||||||
|
|
||||||
## Primitives
|
## Primitives
|
||||||
|
|
||||||
| Category | Subpath | Notes |
|
| Category | Subpath | Notes |
|
||||||
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||||
| Overlay | `./alert-dialog`, `./autocomplete`, `./combobox`, `./context-menu`, `./dialog`, `./dropdown-menu`, `./popover`, `./select`, `./toast`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
|
| Overlay | `./alert-dialog`, `./autocomplete`, `./combobox`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./select`, `./toast`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
|
||||||
| Form | `./autocomplete`, `./combobox`, `./number-field`, `./slider`, `./switch` | Controlled / uncontrolled per Base UI defaults. |
|
| Form | `./autocomplete`, `./combobox`, `./number-field`, `./slider`, `./switch` | Controlled / uncontrolled per Base UI defaults. |
|
||||||
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
|
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
|
||||||
| Media | `./avatar`, `./button` | Button exposes `cva` variants. |
|
| Media | `./avatar`, `./button` | Button exposes `cva` variants. |
|
||||||
|
|
||||||
Utilities:
|
Utilities:
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ If a consumer uses Dify UI source files through the workspace, add an explicit s
|
|||||||
|
|
||||||
## Overlay & portal contract
|
## Overlay & portal contract
|
||||||
|
|
||||||
All overlay primitives (`dialog`, `alert-dialog`, `autocomplete`, `combobox`, `popover`, `dropdown-menu`, `context-menu`, `select`, `tooltip`, `toast`) render their content inside a [Base UI Portal] attached to `document.body`. This is the Base UI default — see the upstream [Portals][Base UI Portal] docs for the underlying behavior. Consumers **do not** need to wrap anything in a portal manually.
|
Overlay primitives render their floating surfaces inside a [Base UI Portal] attached to `document.body`. This is the Base UI default — see the upstream [Portals][Base UI Portal] docs for the underlying behavior. Convenience content components such as `DialogContent`, `PopoverContent`, and `SelectContent` own their portal internally; primitives with explicit portal anatomy such as `Drawer` expose the matching `DrawerPortal` part so consumers can compose the full Base UI structure.
|
||||||
|
|
||||||
### Root isolation requirement
|
### Root isolation requirement
|
||||||
|
|
||||||
@ -83,19 +84,19 @@ Equivalent: any root element with `isolation: isolate` in CSS. Without it, overl
|
|||||||
|
|
||||||
Every overlay primitive uses a single, shared z-index. Do **not** override it at call sites.
|
Every overlay primitive uses a single, shared z-index. Do **not** override it at call sites.
|
||||||
|
|
||||||
| Layer | z-index | Where |
|
| Layer | z-index | Where |
|
||||||
| ----------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
|
| ------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
|
||||||
| Overlays (Dialog, AlertDialog, Autocomplete, Combobox, Popover, DropdownMenu, ContextMenu, Select, Tooltip) | `z-1002` | Positioner / Backdrop |
|
| Overlays (Dialog, AlertDialog, Autocomplete, Combobox, Drawer, Popover, DropdownMenu, ContextMenu, Select, Tooltip) | `z-1002` | Positioner / Backdrop |
|
||||||
| Toast viewport | `z-1003` | One layer above overlays so notifications are never hidden under a dialog. |
|
| Toast viewport | `z-1003` | One layer above overlays so notifications are never hidden under a dialog. |
|
||||||
|
|
||||||
Rationale: during Dify's migration from legacy `base/modal` / `base/dialog` overlays to this package, new and old overlays coexist in the DOM. `z-1002` sits above any common legacy layer, eliminating per-call-site z-index hacks. Among themselves, new primitives share the same z-index and **rely on DOM order** for stacking — the portal mounted later wins.
|
Rationale: during Dify's migration from legacy `base/modal` / `base/dialog` / `base/drawer` / `base/drawer-plus` overlays to this package, new and old overlays coexist in the DOM. `z-1002` sits above any common legacy layer, eliminating per-call-site z-index hacks. Among themselves, new primitives share the same z-index and **rely on DOM order** for stacking — the portal mounted later wins.
|
||||||
|
|
||||||
See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for the Dify-web migration history. Once the legacy overlays are gone, the values in this table can drop back to `z-50` / `z-51`.
|
See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for the Dify-web migration history. Once the legacy overlays are gone, the values in this table can drop back to `z-50` / `z-51`.
|
||||||
|
|
||||||
### Rules
|
### Rules
|
||||||
|
|
||||||
- Never add `z-1003` / `z-9999` / etc. overrides on primitives from this package. If something is getting clipped, the **parent** overlay (typically a legacy one) is the problem and should be migrated.
|
- Never add `z-1003` / `z-9999` / etc. overrides on primitives from this package. If something is getting clipped, the **parent** overlay (typically a legacy one) is the problem and should be migrated.
|
||||||
- Never portal an overlay manually on top of our primitives — use `DialogTrigger`, `PopoverTrigger`, etc. Base UI handles focus management, scroll-locking, and dismissal.
|
- Never create an extra manual portal on top of our primitives — use the exported content / portal parts such as `DialogContent`, `PopoverContent`, and `DrawerPortal`. Base UI handles focus management, scroll-locking, and dismissal.
|
||||||
- When a primitive needs additional presentation chrome (e.g. a custom backdrop), add it **inside** the exported component, not at call sites.
|
- When a primitive needs additional presentation chrome (e.g. a custom backdrop), add it **inside** the exported component, not at call sites.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
@ -37,6 +37,10 @@
|
|||||||
"types": "./src/dialog/index.tsx",
|
"types": "./src/dialog/index.tsx",
|
||||||
"import": "./src/dialog/index.tsx"
|
"import": "./src/dialog/index.tsx"
|
||||||
},
|
},
|
||||||
|
"./drawer": {
|
||||||
|
"types": "./src/drawer/index.tsx",
|
||||||
|
"import": "./src/drawer/index.tsx"
|
||||||
|
},
|
||||||
"./dropdown-menu": {
|
"./dropdown-menu": {
|
||||||
"types": "./src/dropdown-menu/index.tsx",
|
"types": "./src/dropdown-menu/index.tsx",
|
||||||
"import": "./src/dropdown-menu/index.tsx"
|
"import": "./src/dropdown-menu/index.tsx"
|
||||||
|
|||||||
61
packages/dify-ui/src/drawer/__tests__/index.spec.tsx
Normal file
61
packages/dify-ui/src/drawer/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { render } from 'vitest-browser-react'
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerBackdrop,
|
||||||
|
DrawerCloseButton,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerPopup,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerViewport,
|
||||||
|
} from '../index'
|
||||||
|
|
||||||
|
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
|
||||||
|
|
||||||
|
describe('Drawer wrapper', () => {
|
||||||
|
describe('User Interactions', () => {
|
||||||
|
it('should open a portalled drawer and close it with the default close button', async () => {
|
||||||
|
const screen = await render(
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger>Open settings</DrawerTrigger>
|
||||||
|
<DrawerPortal>
|
||||||
|
<DrawerBackdrop data-testid="drawer-backdrop" />
|
||||||
|
<DrawerViewport>
|
||||||
|
<DrawerPopup>
|
||||||
|
<DrawerTitle>Settings</DrawerTitle>
|
||||||
|
<DrawerDescription>Configure the current workspace.</DrawerDescription>
|
||||||
|
<DrawerContent>
|
||||||
|
<p>Workspace controls</p>
|
||||||
|
<DrawerCloseButton />
|
||||||
|
</DrawerContent>
|
||||||
|
</DrawerPopup>
|
||||||
|
</DrawerViewport>
|
||||||
|
</DrawerPortal>
|
||||||
|
</Drawer>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(document.body.querySelector('[role="dialog"]')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
asHTMLElement(screen.getByRole('button', { name: 'Open settings' }).element()).click()
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(document.body.querySelector('[role="dialog"]')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialog = asHTMLElement(document.body.querySelector('[role="dialog"]')!)
|
||||||
|
expect(document.body).toContainElement(dialog)
|
||||||
|
expect(screen.container).not.toContainElement(dialog)
|
||||||
|
await expect.element(dialog).toHaveTextContent('Workspace controls')
|
||||||
|
await expect.element(screen.getByText('Configure the current workspace.')).toBeInTheDocument()
|
||||||
|
await expect.element(screen.getByTestId('drawer-backdrop')).toHaveClass('z-1002')
|
||||||
|
|
||||||
|
asHTMLElement(screen.getByRole('button', { name: 'Close drawer' }).element()).click()
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(document.body.querySelector('[role="dialog"]')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
116
packages/dify-ui/src/drawer/index.tsx
Normal file
116
packages/dify-ui/src/drawer/index.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { Drawer as BaseDrawer } from '@base-ui/react/drawer'
|
||||||
|
import { cn } from '../cn'
|
||||||
|
|
||||||
|
export const Drawer = BaseDrawer.Root
|
||||||
|
export const DrawerProvider = BaseDrawer.Provider
|
||||||
|
export const DrawerIndent = BaseDrawer.Indent
|
||||||
|
export const DrawerIndentBackground = BaseDrawer.IndentBackground
|
||||||
|
export const DrawerTrigger = BaseDrawer.Trigger
|
||||||
|
export const DrawerSwipeArea = BaseDrawer.SwipeArea
|
||||||
|
export const DrawerPortal = BaseDrawer.Portal
|
||||||
|
export const DrawerTitle = BaseDrawer.Title
|
||||||
|
export const DrawerDescription = BaseDrawer.Description
|
||||||
|
export const DrawerClose = BaseDrawer.Close
|
||||||
|
export const createDrawerHandle = BaseDrawer.createHandle
|
||||||
|
|
||||||
|
export type DrawerRootProps<Payload = unknown> = BaseDrawer.Root.Props<Payload>
|
||||||
|
export type DrawerRootActions = BaseDrawer.Root.Actions
|
||||||
|
export type DrawerRootChangeEventDetails = BaseDrawer.Root.ChangeEventDetails
|
||||||
|
export type DrawerRootChangeEventReason = BaseDrawer.Root.ChangeEventReason
|
||||||
|
export type DrawerRootSnapPoint = BaseDrawer.Root.SnapPoint
|
||||||
|
export type DrawerRootSnapPointChangeEventDetails = BaseDrawer.Root.SnapPointChangeEventDetails
|
||||||
|
export type DrawerRootSnapPointChangeEventReason = BaseDrawer.Root.SnapPointChangeEventReason
|
||||||
|
export type DrawerTriggerProps<Payload = unknown> = BaseDrawer.Trigger.Props<Payload>
|
||||||
|
|
||||||
|
export function DrawerBackdrop({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BaseDrawer.Backdrop.Props) {
|
||||||
|
return (
|
||||||
|
<BaseDrawer.Backdrop
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 z-1002 bg-background-overlay opacity-[calc(1-var(--drawer-swipe-progress,0))]',
|
||||||
|
'transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:duration-0 motion-reduce:transition-none',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DrawerViewport({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BaseDrawer.Viewport.Props) {
|
||||||
|
return (
|
||||||
|
<BaseDrawer.Viewport
|
||||||
|
className={cn('fixed inset-0 z-1002 touch-none overflow-hidden overscroll-contain outline-hidden', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DrawerPopup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BaseDrawer.Popup.Props) {
|
||||||
|
return (
|
||||||
|
<BaseDrawer.Popup
|
||||||
|
className={cn(
|
||||||
|
'fixed z-1002 flex min-h-0 flex-col overflow-hidden border-[0.5px] border-components-panel-border bg-components-panel-bg text-text-primary shadow-xl outline-hidden touch-none',
|
||||||
|
'transition-[transform,opacity,box-shadow] duration-200 data-swiping:select-none data-swiping:duration-0 motion-reduce:transition-none',
|
||||||
|
'data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:h-dvh data-[swipe-direction=right]:w-120 data-[swipe-direction=right]:max-w-[calc(100vw-2rem)] data-[swipe-direction=right]:rounded-l-2xl data-[swipe-direction=right]:border-r-0 data-[swipe-direction=right]:transform-[translateX(var(--drawer-swipe-movement-x,0px))]',
|
||||||
|
'data-starting-style:data-[swipe-direction=right]:transform-[translateX(calc(100%+2px))] data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(100%+2px))]',
|
||||||
|
'data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:h-dvh data-[swipe-direction=left]:w-120 data-[swipe-direction=left]:max-w-[calc(100vw-2rem)] data-[swipe-direction=left]:rounded-r-2xl data-[swipe-direction=left]:border-l-0 data-[swipe-direction=left]:transform-[translateX(var(--drawer-swipe-movement-x,0px))]',
|
||||||
|
'data-starting-style:data-[swipe-direction=left]:transform-[translateX(calc(-100%-2px))] data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(-100%-2px))]',
|
||||||
|
'data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:max-h-[calc(100dvh-2rem)] data-[swipe-direction=down]:w-full data-[swipe-direction=down]:rounded-t-2xl data-[swipe-direction=down]:border-b-0 data-[swipe-direction=down]:transform-[translateY(calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y,0px)))]',
|
||||||
|
'data-starting-style:data-[swipe-direction=down]:transform-[translateY(calc(100%+2px))] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(100%+2px))]',
|
||||||
|
'data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:max-h-[calc(100dvh-2rem)] data-[swipe-direction=up]:w-full data-[swipe-direction=up]:rounded-b-2xl data-[swipe-direction=up]:border-t-0 data-[swipe-direction=up]:transform-[translateY(var(--drawer-swipe-movement-y,0px))]',
|
||||||
|
'data-starting-style:data-[swipe-direction=up]:transform-[translateY(calc(-100%-2px))] data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(-100%-2px))]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DrawerContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BaseDrawer.Content.Props) {
|
||||||
|
return (
|
||||||
|
<BaseDrawer.Content
|
||||||
|
className={cn('min-h-0 flex-1 overflow-y-auto overscroll-contain p-6 pb-[calc(1.5rem+env(safe-area-inset-bottom,0))]', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DrawerCloseButtonProps = Omit<BaseDrawer.Close.Props, 'children'> & {
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DrawerCloseButton({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
type = 'button',
|
||||||
|
'aria-label': ariaLabel = 'Close drawer',
|
||||||
|
...props
|
||||||
|
}: DrawerCloseButtonProps) {
|
||||||
|
return (
|
||||||
|
<BaseDrawer.Close
|
||||||
|
type={type}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={cn(
|
||||||
|
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <span aria-hidden="true" className="i-ri-close-line h-4 w-4" />}
|
||||||
|
</BaseDrawer.Close>
|
||||||
|
)
|
||||||
|
}
|
||||||
2212
pnpm-lock.yaml
generated
2212
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -55,7 +55,7 @@ overrides:
|
|||||||
yauzl@<3.2.1: 3.2.1
|
yauzl@<3.2.1: 3.2.1
|
||||||
catalog:
|
catalog:
|
||||||
'@amplitude/analytics-browser': 2.42.1
|
'@amplitude/analytics-browser': 2.42.1
|
||||||
'@amplitude/plugin-session-replay-browser': 1.29.0
|
'@amplitude/plugin-session-replay-browser': 1.30.1
|
||||||
'@antfu/eslint-config': 8.2.0
|
'@antfu/eslint-config': 8.2.0
|
||||||
'@base-ui/react': 1.4.1
|
'@base-ui/react': 1.4.1
|
||||||
'@chromatic-com/storybook': 5.1.2
|
'@chromatic-com/storybook': 5.1.2
|
||||||
@ -83,16 +83,16 @@ catalog:
|
|||||||
'@mdx-js/react': 3.1.1
|
'@mdx-js/react': 3.1.1
|
||||||
'@mdx-js/rollup': 3.1.1
|
'@mdx-js/rollup': 3.1.1
|
||||||
'@monaco-editor/react': 4.7.0
|
'@monaco-editor/react': 4.7.0
|
||||||
'@next/eslint-plugin-next': 16.2.4
|
'@next/eslint-plugin-next': 16.2.6
|
||||||
'@next/mdx': 16.2.4
|
'@next/mdx': 16.2.6
|
||||||
'@orpc/client': 1.14.1
|
'@orpc/client': 1.14.2
|
||||||
'@orpc/contract': 1.14.1
|
'@orpc/contract': 1.14.2
|
||||||
'@orpc/openapi-client': 1.14.1
|
'@orpc/openapi-client': 1.14.2
|
||||||
'@orpc/tanstack-query': 1.14.1
|
'@orpc/tanstack-query': 1.14.2
|
||||||
'@playwright/test': 1.59.1
|
'@playwright/test': 1.59.1
|
||||||
'@remixicon/react': 4.9.0
|
'@remixicon/react': 4.9.0
|
||||||
'@rgrove/parse-xml': 4.2.0
|
'@rgrove/parse-xml': 4.2.0
|
||||||
'@sentry/react': 10.51.0
|
'@sentry/react': 10.52.0
|
||||||
'@storybook/addon-docs': 10.3.6
|
'@storybook/addon-docs': 10.3.6
|
||||||
'@storybook/addon-links': 10.3.6
|
'@storybook/addon-links': 10.3.6
|
||||||
'@storybook/addon-onboarding': 10.3.6
|
'@storybook/addon-onboarding': 10.3.6
|
||||||
@ -124,21 +124,21 @@ catalog:
|
|||||||
'@types/js-cookie': 3.0.6
|
'@types/js-cookie': 3.0.6
|
||||||
'@types/js-yaml': 4.0.9
|
'@types/js-yaml': 4.0.9
|
||||||
'@types/negotiator': 0.6.4
|
'@types/negotiator': 0.6.4
|
||||||
'@types/node': 25.6.0
|
'@types/node': 25.6.2
|
||||||
'@types/qs': 6.15.0
|
'@types/qs': 6.15.1
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3
|
'@types/react-dom': 19.2.3
|
||||||
'@types/sortablejs': 1.15.9
|
'@types/sortablejs': 1.15.9
|
||||||
'@typescript-eslint/eslint-plugin': 8.59.2
|
'@typescript-eslint/eslint-plugin': 8.59.2
|
||||||
'@typescript-eslint/parser': 8.59.2
|
'@typescript-eslint/parser': 8.59.2
|
||||||
'@typescript/native-preview': 7.0.0-dev.20260505.1
|
'@typescript/native-preview': 7.0.0-dev.20260507.1
|
||||||
'@vitejs/plugin-react': 6.0.1
|
'@vitejs/plugin-react': 6.0.1
|
||||||
'@vitejs/plugin-rsc': 0.5.25
|
'@vitejs/plugin-rsc': 0.5.26
|
||||||
'@vitest/coverage-v8': 4.1.5
|
'@vitest/coverage-v8': 4.1.5
|
||||||
abcjs: 6.6.3
|
abcjs: 6.6.3
|
||||||
agentation: 3.0.2
|
agentation: 3.0.2
|
||||||
ahooks: 3.9.7
|
ahooks: 3.9.7
|
||||||
c12: 1.10.0
|
c12: 1.11.2
|
||||||
class-variance-authority: 0.7.1
|
class-variance-authority: 0.7.1
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
@ -158,7 +158,7 @@ catalog:
|
|||||||
emoji-mart: 5.6.0
|
emoji-mart: 5.6.0
|
||||||
es-toolkit: 1.46.1
|
es-toolkit: 1.46.1
|
||||||
eslint: 10.3.0
|
eslint: 10.3.0
|
||||||
eslint-markdown: 0.8.0
|
eslint-markdown: 0.9.0
|
||||||
eslint-plugin-better-tailwindcss: 4.5.0
|
eslint-plugin-better-tailwindcss: 4.5.0
|
||||||
eslint-plugin-hyoban: 0.14.1
|
eslint-plugin-hyoban: 0.14.1
|
||||||
eslint-plugin-markdown-preferences: 0.41.1
|
eslint-plugin-markdown-preferences: 0.41.1
|
||||||
@ -167,23 +167,23 @@ catalog:
|
|||||||
eslint-plugin-sonarjs: 4.0.3
|
eslint-plugin-sonarjs: 4.0.3
|
||||||
eslint-plugin-storybook: 10.3.6
|
eslint-plugin-storybook: 10.3.6
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
fuse.js: 7.2.0
|
fuse.js: 7.3.0
|
||||||
happy-dom: 20.9.0
|
happy-dom: 20.9.0
|
||||||
hast-util-to-jsx-runtime: 2.3.6
|
hast-util-to-jsx-runtime: 2.3.6
|
||||||
hono: 4.12.17
|
hono: 4.12.18
|
||||||
html-entities: 2.6.0
|
html-entities: 2.6.0
|
||||||
html-to-image: 1.11.13
|
html-to-image: 1.11.13
|
||||||
i18next: 26.0.8
|
i18next: 26.0.10
|
||||||
i18next-resources-to-backend: 1.2.1
|
i18next-resources-to-backend: 1.2.1
|
||||||
iconify-import-svg: 0.2.0
|
iconify-import-svg: 0.2.0
|
||||||
immer: 11.1.6
|
immer: 11.1.7
|
||||||
jotai: 2.20.0
|
jotai: 2.20.0
|
||||||
js-audio-recorder: 1.0.7
|
js-audio-recorder: 1.0.7
|
||||||
js-cookie: 3.0.5
|
js-cookie: 3.0.5
|
||||||
js-yaml: 4.1.1
|
js-yaml: 4.1.1
|
||||||
jsonschema: 1.5.0
|
jsonschema: 1.5.0
|
||||||
katex: 0.16.45
|
katex: 0.16.45
|
||||||
knip: 6.11.0
|
knip: 6.12.1
|
||||||
ky: 2.0.2
|
ky: 2.0.2
|
||||||
lamejs: 1.2.1
|
lamejs: 1.2.1
|
||||||
lexical: 0.44.0
|
lexical: 0.44.0
|
||||||
@ -192,7 +192,7 @@ catalog:
|
|||||||
mime: 4.1.0
|
mime: 4.1.0
|
||||||
mitt: 3.0.1
|
mitt: 3.0.1
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
next: 16.2.4
|
next: 16.2.6
|
||||||
next-themes: 0.4.6
|
next-themes: 0.4.6
|
||||||
nuqs: 2.8.9
|
nuqs: 2.8.9
|
||||||
pinyin-pro: 3.28.1
|
pinyin-pro: 3.28.1
|
||||||
@ -200,9 +200,9 @@ catalog:
|
|||||||
postcss: 8.5.14
|
postcss: 8.5.14
|
||||||
qrcode.react: 4.2.0
|
qrcode.react: 4.2.0
|
||||||
qs: 6.15.1
|
qs: 6.15.1
|
||||||
react: 19.2.5
|
react: 19.2.6
|
||||||
react-18-input-autosize: 3.0.0
|
react-18-input-autosize: 3.0.0
|
||||||
react-dom: 19.2.5
|
react-dom: 19.2.6
|
||||||
react-easy-crop: 5.5.7
|
react-easy-crop: 5.5.7
|
||||||
react-hotkeys-hook: 5.3.2
|
react-hotkeys-hook: 5.3.2
|
||||||
react-i18next: 16.5.8
|
react-i18next: 16.5.8
|
||||||
@ -233,7 +233,7 @@ catalog:
|
|||||||
unist-util-visit: 5.1.0
|
unist-util-visit: 5.1.0
|
||||||
use-context-selector: 2.0.0
|
use-context-selector: 2.0.0
|
||||||
uuid: 14.0.0
|
uuid: 14.0.0
|
||||||
vinext: 0.0.47
|
vinext: 0.0.49
|
||||||
vite: npm:@voidzero-dev/vite-plus-core@0.1.20
|
vite: npm:@voidzero-dev/vite-plus-core@0.1.20
|
||||||
vite-plugin-inspect: 12.0.0-beta.1
|
vite-plugin-inspect: 12.0.0-beta.1
|
||||||
vite-plus: 0.1.20
|
vite-plus: 0.1.20
|
||||||
|
|||||||
@ -26,15 +26,21 @@ export const SelectTrigger = ({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }) => (
|
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }) => (
|
||||||
<button type="button" {...props}>
|
<button type="button" role="combobox" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const SelectValue = ({ placeholder }: { placeholder?: ReactNode }) => <>{placeholder}</>
|
export const SelectValue = ({ placeholder }: { placeholder?: ReactNode }) => <>{placeholder}</>
|
||||||
|
|
||||||
export const SelectContent = ({ children }: { children?: ReactNode }) => (
|
export const SelectContent = ({
|
||||||
<div data-testid="select-content">{children}</div>
|
children,
|
||||||
|
popupClassName,
|
||||||
|
}: {
|
||||||
|
children?: ReactNode
|
||||||
|
popupClassName?: string
|
||||||
|
}) => (
|
||||||
|
<div data-side="bottom" data-testid="select-content" className={popupClassName}>{children}</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const SelectItem = ({
|
export const SelectItem = ({
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const mockUploadRemoteFileInfo = vi.fn()
|
|||||||
|
|
||||||
vi.mock('@/next/navigation', () => ({
|
vi.mock('@/next/navigation', () => ({
|
||||||
useParams: () => ({}),
|
useParams: () => ({}),
|
||||||
|
usePathname: () => '/',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/service/common', () => ({
|
vi.mock('@/service/common', () => ({
|
||||||
|
|||||||
@ -127,7 +127,7 @@ const createApp = (overrides: Partial<App> = {}): App => ({
|
|||||||
copyright: overrides.copyright ?? '',
|
copyright: overrides.copyright ?? '',
|
||||||
privacy_policy: overrides.privacy_policy ?? null,
|
privacy_policy: overrides.privacy_policy ?? null,
|
||||||
custom_disclaimer: overrides.custom_disclaimer ?? null,
|
custom_disclaimer: overrides.custom_disclaimer ?? null,
|
||||||
category: overrides.category ?? 'Writing',
|
categories: overrides.categories ?? ['Writing'],
|
||||||
position: overrides.position ?? 1,
|
position: overrides.position ?? 1,
|
||||||
is_listed: overrides.is_listed ?? true,
|
is_listed: overrides.is_listed ?? true,
|
||||||
install_count: overrides.install_count ?? 0,
|
install_count: overrides.install_count ?? 0,
|
||||||
@ -165,9 +165,9 @@ describe('Explore App List Flow', () => {
|
|||||||
mockExploreData = {
|
mockExploreData = {
|
||||||
categories: ['Writing', 'Translate', 'Programming'],
|
categories: ['Writing', 'Translate', 'Programming'],
|
||||||
allList: [
|
allList: [
|
||||||
createApp({ app_id: 'app-1', app: { ...createApp().app, name: 'Writer Bot' }, category: 'Writing' }),
|
createApp({ app_id: 'app-1', app: { ...createApp().app, name: 'Writer Bot' }, categories: ['Writing'] }),
|
||||||
createApp({ app_id: 'app-2', app: { ...createApp().app, id: 'app-id-2', name: 'Translator' }, category: 'Translate' }),
|
createApp({ app_id: 'app-2', app: { ...createApp().app, id: 'app-id-2', name: 'Translator' }, categories: ['Translate'] }),
|
||||||
createApp({ app_id: 'app-3', app: { ...createApp().app, id: 'app-id-3', name: 'Code Helper' }, category: 'Programming' }),
|
createApp({ app_id: 'app-3', app: { ...createApp().app, id: 'app-id-3', name: 'Code Helper' }, categories: ['Programming'] }),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -190,6 +190,30 @@ describe('Explore App List Flow', () => {
|
|||||||
expect(screen.queryByText('Code Helper')).not.toBeInTheDocument()
|
expect(screen.queryByText('Code Helper')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should only use categories when filtering by selected category', () => {
|
||||||
|
mockTabValue = 'Writing'
|
||||||
|
mockExploreData = {
|
||||||
|
categories: ['Writing', 'Translate'],
|
||||||
|
allList: [
|
||||||
|
createApp({
|
||||||
|
app_id: 'app-1',
|
||||||
|
app: { ...createApp().app, name: 'Active Writer' },
|
||||||
|
categories: ['Writing'],
|
||||||
|
}),
|
||||||
|
createApp({
|
||||||
|
app_id: 'app-2',
|
||||||
|
app: { ...createApp().app, id: 'app-id-2', name: 'Legacy Writer' },
|
||||||
|
categories: [],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAppList()
|
||||||
|
|
||||||
|
expect(screen.getByText('Active Writer')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Legacy Writer')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('should filter apps by search keyword', async () => {
|
it('should filter apps by search keyword', async () => {
|
||||||
renderAppList()
|
renderAppList()
|
||||||
|
|
||||||
|
|||||||
@ -205,7 +205,7 @@ vi.mock('@/app/components/tools/setting/build-in/config-credentials', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/app/components/tools/workflow-tool', () => ({
|
vi.mock('@/app/components/tools/workflow-tool', () => ({
|
||||||
default: ({ onHide, onSave, onRemove }: { payload: unknown, onHide: () => void, onSave: (d: unknown) => void, onRemove: () => void }) => (
|
WorkflowToolDrawer: ({ onHide, onSave, onRemove }: { payload: unknown, onHide: () => void, onSave: (d: unknown) => void, onRemove: () => void }) => (
|
||||||
<div data-testid="workflow-tool-modal">
|
<div data-testid="workflow-tool-modal">
|
||||||
<button data-testid="wf-modal-hide" onClick={onHide}>Hide</button>
|
<button data-testid="wf-modal-hide" onClick={onHide}>Hide</button>
|
||||||
<button data-testid="wf-modal-save" onClick={() => onSave({ name: 'updated-wf' })}>Save</button>
|
<button data-testid="wf-modal-save" onClick={() => onSave({ name: 'updated-wf' })}>Save</button>
|
||||||
|
|||||||
@ -39,7 +39,9 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const getSigninUrl = useCallback(() => {
|
const getSigninUrl = useCallback(() => {
|
||||||
const params = new URLSearchParams(searchParams)
|
const params = new URLSearchParams(searchParams)
|
||||||
params.delete('message')
|
params.delete('message')
|
||||||
params.set('redirect_url', pathname)
|
const query = params.toString()
|
||||||
|
const fullPath = query ? `${pathname}?${query}` : pathname
|
||||||
|
params.set('redirect_url', fullPath)
|
||||||
return `/webapp-signin?${params.toString()}`
|
return `/webapp-signin?${params.toString()}`
|
||||||
}, [searchParams, pathname])
|
}, [searchParams, pathname])
|
||||||
|
|
||||||
|
|||||||
@ -97,7 +97,7 @@ const AppInfoDetailPanel = ({
|
|||||||
<ContentDialog
|
<ContentDialog
|
||||||
show={show}
|
show={show}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
className="absolute top-2 bottom-2 left-2 flex w-[420px] flex-col rounded-2xl p-0!"
|
className="absolute top-2 bottom-2 left-2 flex w-[452px] max-w-[calc(100vw-1rem)] flex-col rounded-2xl p-0!"
|
||||||
>
|
>
|
||||||
<div className="flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4">
|
<div className="flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4">
|
||||||
<div className="flex items-center gap-3 self-stretch">
|
<div className="flex items-center gap-3 self-stretch">
|
||||||
|
|||||||
@ -20,6 +20,7 @@ const mockOpenAsyncWindow = vi.fn()
|
|||||||
const mockFetchInstalledAppList = vi.fn()
|
const mockFetchInstalledAppList = vi.fn()
|
||||||
const mockFetchAppDetailDirect = vi.fn()
|
const mockFetchAppDetailDirect = vi.fn()
|
||||||
const mockToastError = vi.fn()
|
const mockToastError = vi.fn()
|
||||||
|
const mockWindowOpen = vi.fn()
|
||||||
const mockInvalidateAppWorkflow = vi.fn()
|
const mockInvalidateAppWorkflow = vi.fn()
|
||||||
|
|
||||||
const sectionProps = vi.hoisted(() => ({
|
const sectionProps = vi.hoisted(() => ({
|
||||||
@ -37,6 +38,7 @@ vi.mock('react-i18next', () => ({
|
|||||||
useTranslation: () => ({
|
useTranslation: () => ({
|
||||||
t: (key: string) => key,
|
t: (key: string) => key,
|
||||||
}),
|
}),
|
||||||
|
Trans: ({ i18nKey }: { i18nKey?: string }) => i18nKey ?? null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('ahooks', async () => {
|
vi.mock('ahooks', async () => {
|
||||||
@ -91,6 +93,21 @@ vi.mock('@/service/use-workflow', () => ({
|
|||||||
useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow,
|
useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/service/use-tools', () => ({
|
||||||
|
useWorkflowToolDetailByAppID: () => ({
|
||||||
|
data: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
useInvalidateAllWorkflowTools: () => vi.fn(),
|
||||||
|
useInvalidateWorkflowToolDetailByAppID: () => vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context', () => ({
|
||||||
|
useAppContext: () => ({
|
||||||
|
isCurrentWorkspaceManager: true,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||||
toast: {
|
toast: {
|
||||||
error: (...args: unknown[]) => mockToastError(...args),
|
error: (...args: unknown[]) => mockToastError(...args),
|
||||||
@ -121,6 +138,15 @@ vi.mock('../../app-access-control', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/tools/workflow-tool', () => ({
|
||||||
|
WorkflowToolDrawer: ({ onHide }: { onHide: () => void }) => (
|
||||||
|
<div data-testid="workflow-tool-drawer">
|
||||||
|
workflow tool drawer
|
||||||
|
<button onClick={onHide}>close-workflow-tool-drawer</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||||
|
|
||||||
vi.mock('../sections', () => ({
|
vi.mock('../sections', () => ({
|
||||||
@ -143,6 +169,13 @@ vi.mock('../sections', () => ({
|
|||||||
<div>
|
<div>
|
||||||
<button onClick={props.handleEmbed}>publisher-embed</button>
|
<button onClick={props.handleEmbed}>publisher-embed</button>
|
||||||
<button onClick={() => void props.handleOpenInExplore()}>publisher-open-in-explore</button>
|
<button onClick={() => void props.handleOpenInExplore()}>publisher-open-in-explore</button>
|
||||||
|
{props.handleOpenRunConfig && (
|
||||||
|
<>
|
||||||
|
<button onClick={() => props.handleOpenRunConfig(props.appURL)}>publisher-run-config</button>
|
||||||
|
<button onClick={() => props.handleOpenRunConfig(`${props.appURL}?mode=batch`)}>publisher-batch-run-config</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button onClick={props.onConfigureWorkflowTool}>publisher-workflow-tool</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -175,6 +208,10 @@ describe('AppPublisher', () => {
|
|||||||
mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise<string>) => {
|
mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise<string>) => {
|
||||||
await resolver()
|
await resolver()
|
||||||
})
|
})
|
||||||
|
Object.defineProperty(window, 'open', {
|
||||||
|
writable: true,
|
||||||
|
value: mockWindowOpen,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should open the publish popover and refetch access permission data', async () => {
|
it('should open the publish popover and refetch access permission data', async () => {
|
||||||
@ -231,6 +268,94 @@ describe('AppPublisher', () => {
|
|||||||
expect(screen.getByTestId('embedded-modal'))!.toBeInTheDocument()
|
expect(screen.getByTestId('embedded-modal'))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should collect hidden inputs before opening published run links from config actions', async () => {
|
||||||
|
render(
|
||||||
|
<AppPublisher
|
||||||
|
publishedAt={Date.now()}
|
||||||
|
inputs={[{
|
||||||
|
variable: 'secret',
|
||||||
|
label: 'Secret',
|
||||||
|
type: 'text-input',
|
||||||
|
required: true,
|
||||||
|
hide: true,
|
||||||
|
default: '',
|
||||||
|
} as any]}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.publish'))
|
||||||
|
fireEvent.click(screen.getByText('publisher-run-config'))
|
||||||
|
|
||||||
|
expect(screen.getByText('overview.appInfo.workflowLaunchHiddenInputs.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Secret'), {
|
||||||
|
target: { value: 'top-secret' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'overview.appInfo.launch' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||||
|
`https://example.com${basePath}/chat/token-1?secret=${encodeURIComponent('top-secret')}`,
|
||||||
|
'_blank',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open batch run config links with the configured hidden inputs', async () => {
|
||||||
|
mockAppDetail = {
|
||||||
|
...mockAppDetail,
|
||||||
|
mode: AppModeEnum.WORKFLOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AppPublisher
|
||||||
|
publishedAt={Date.now()}
|
||||||
|
inputs={[{
|
||||||
|
variable: 'batch_secret',
|
||||||
|
label: 'Batch Secret',
|
||||||
|
type: 'text-input',
|
||||||
|
required: true,
|
||||||
|
hide: true,
|
||||||
|
default: '',
|
||||||
|
} as any]}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.publish'))
|
||||||
|
fireEvent.click(screen.getByText('publisher-batch-run-config'))
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Batch Secret'), {
|
||||||
|
target: { value: 'batch-value' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'overview.appInfo.launch' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||||
|
`https://example.com${basePath}/workflow/token-1?mode=batch&batch_secret=${encodeURIComponent('batch-value')}`,
|
||||||
|
'_blank',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep workflow tool drawer mounted after closing the publish popover', () => {
|
||||||
|
mockAppDetail = {
|
||||||
|
...mockAppDetail,
|
||||||
|
mode: AppModeEnum.WORKFLOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AppPublisher
|
||||||
|
publishedAt={Date.now()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.publish'))
|
||||||
|
fireEvent.click(screen.getByText('publisher-workflow-tool'))
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('workflow-tool-drawer')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('should close embedded and access control panels through child callbacks', async () => {
|
it('should close embedded and access control panels through child callbacks', async () => {
|
||||||
render(
|
render(
|
||||||
<AppPublisher
|
<AppPublisher
|
||||||
|
|||||||
@ -18,8 +18,32 @@ vi.mock('../publish-with-multiple-model', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../suggested-action', () => ({
|
vi.mock('../suggested-action', () => ({
|
||||||
default: ({ children, onClick, link, disabled }: { children: ReactNode, onClick?: () => void, link?: string, disabled?: boolean }) => (
|
default: ({
|
||||||
<button type="button" data-link={link} disabled={disabled} onClick={onClick}>{children}</button>
|
children,
|
||||||
|
onClick,
|
||||||
|
link,
|
||||||
|
disabled,
|
||||||
|
actionButton,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
onClick?: () => void
|
||||||
|
link?: string
|
||||||
|
disabled?: boolean
|
||||||
|
actionButton?: { ariaLabel: string, onClick: () => void }
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<button type="button" data-link={link} disabled={disabled} onClick={onClick}>{children}</button>
|
||||||
|
{actionButton && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={actionButton.ariaLabel}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={actionButton.onClick}
|
||||||
|
>
|
||||||
|
{actionButton.ariaLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -170,9 +194,25 @@ describe('app-publisher sections', () => {
|
|||||||
expect(render(<AccessModeDisplay />).container).toBeEmptyDOMElement()
|
expect(render(<AccessModeDisplay />).container).toBeEmptyDOMElement()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should hide access control content when enabled is false', () => {
|
||||||
|
render(
|
||||||
|
<PublisherAccessSection
|
||||||
|
enabled={false}
|
||||||
|
isAppAccessSet
|
||||||
|
isLoading={false}
|
||||||
|
accessMode={AccessMode.PUBLIC}
|
||||||
|
onClick={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByText('publishApp.title')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('accessControlDialog.accessItems.anyone')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('should render workflow actions, batch run links, and workflow tool configuration', () => {
|
it('should render workflow actions, batch run links, and workflow tool configuration', () => {
|
||||||
const handleOpenInExplore = vi.fn()
|
const handleOpenInExplore = vi.fn()
|
||||||
const handleEmbed = vi.fn()
|
const handleEmbed = vi.fn()
|
||||||
|
const handleOpenRunConfig = vi.fn()
|
||||||
|
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<PublisherActionsSection
|
<PublisherActionsSection
|
||||||
@ -190,22 +230,30 @@ describe('app-publisher sections', () => {
|
|||||||
disabledFunctionTooltip="disabled"
|
disabledFunctionTooltip="disabled"
|
||||||
handleEmbed={handleEmbed}
|
handleEmbed={handleEmbed}
|
||||||
handleOpenInExplore={handleOpenInExplore}
|
handleOpenInExplore={handleOpenInExplore}
|
||||||
|
handleOpenRunConfig={handleOpenRunConfig}
|
||||||
handlePublish={vi.fn()}
|
handlePublish={vi.fn()}
|
||||||
hasHumanInputNode={false}
|
hasHumanInputNode={false}
|
||||||
hasTriggerNode={false}
|
hasTriggerNode={false}
|
||||||
inputs={[]}
|
|
||||||
missingStartNode={false}
|
missingStartNode={false}
|
||||||
onRefreshData={vi.fn()}
|
published={false}
|
||||||
outputs={[]}
|
|
||||||
published={true}
|
|
||||||
publishedAt={Date.now()}
|
publishedAt={Date.now()}
|
||||||
|
showBatchRunConfig
|
||||||
|
showRunConfig
|
||||||
toolPublished
|
toolPublished
|
||||||
workflowToolAvailable={false}
|
workflowToolAvailable={false}
|
||||||
|
workflowToolIsLoading={false}
|
||||||
|
workflowToolOutdated={false}
|
||||||
|
workflowToolIsCurrentWorkspaceManager
|
||||||
workflowToolMessage="workflow-disabled"
|
workflowToolMessage="workflow-disabled"
|
||||||
|
onConfigureWorkflowTool={vi.fn()}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(screen.getByText('common.batchRunApp')).toHaveAttribute('data-link', 'https://example.com/app?mode=batch')
|
expect(screen.getByText('common.batchRunApp')).toHaveAttribute('data-link', 'https://example.com/app?mode=batch')
|
||||||
|
fireEvent.click(screen.getAllByRole('button', { name: 'operation.config' })[0]!)
|
||||||
|
expect(handleOpenRunConfig).toHaveBeenCalledWith('https://example.com/app')
|
||||||
|
fireEvent.click(screen.getAllByRole('button', { name: 'operation.config' })[1]!)
|
||||||
|
expect(handleOpenRunConfig).toHaveBeenCalledWith('https://example.com/app?mode=batch')
|
||||||
fireEvent.click(screen.getByText('common.openInExplore'))
|
fireEvent.click(screen.getByText('common.openInExplore'))
|
||||||
expect(handleOpenInExplore).toHaveBeenCalled()
|
expect(handleOpenInExplore).toHaveBeenCalled()
|
||||||
expect(screen.getByText('workflow-tool-configure')).toBeInTheDocument()
|
expect(screen.getByText('workflow-tool-configure')).toBeInTheDocument()
|
||||||
@ -223,17 +271,19 @@ describe('app-publisher sections', () => {
|
|||||||
disabledFunctionTooltip="disabled"
|
disabledFunctionTooltip="disabled"
|
||||||
handleEmbed={handleEmbed}
|
handleEmbed={handleEmbed}
|
||||||
handleOpenInExplore={handleOpenInExplore}
|
handleOpenInExplore={handleOpenInExplore}
|
||||||
|
handleOpenRunConfig={handleOpenRunConfig}
|
||||||
handlePublish={vi.fn()}
|
handlePublish={vi.fn()}
|
||||||
hasHumanInputNode={false}
|
hasHumanInputNode={false}
|
||||||
hasTriggerNode={false}
|
hasTriggerNode={false}
|
||||||
inputs={[]}
|
|
||||||
missingStartNode
|
missingStartNode
|
||||||
onRefreshData={vi.fn()}
|
|
||||||
outputs={[]}
|
|
||||||
published={false}
|
published={false}
|
||||||
publishedAt={Date.now()}
|
publishedAt={Date.now()}
|
||||||
toolPublished={false}
|
toolPublished={false}
|
||||||
workflowToolAvailable
|
workflowToolAvailable
|
||||||
|
workflowToolIsLoading={false}
|
||||||
|
workflowToolOutdated={false}
|
||||||
|
workflowToolIsCurrentWorkspaceManager
|
||||||
|
onConfigureWorkflowTool={vi.fn()}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -248,16 +298,19 @@ describe('app-publisher sections', () => {
|
|||||||
disabledFunctionButton={false}
|
disabledFunctionButton={false}
|
||||||
handleEmbed={handleEmbed}
|
handleEmbed={handleEmbed}
|
||||||
handleOpenInExplore={handleOpenInExplore}
|
handleOpenInExplore={handleOpenInExplore}
|
||||||
|
handleOpenRunConfig={handleOpenRunConfig}
|
||||||
handlePublish={vi.fn()}
|
handlePublish={vi.fn()}
|
||||||
hasHumanInputNode={false}
|
hasHumanInputNode={false}
|
||||||
hasTriggerNode
|
hasTriggerNode
|
||||||
inputs={[]}
|
|
||||||
missingStartNode={false}
|
missingStartNode={false}
|
||||||
outputs={[]}
|
|
||||||
published={false}
|
published={false}
|
||||||
publishedAt={undefined}
|
publishedAt={undefined}
|
||||||
toolPublished={false}
|
toolPublished={false}
|
||||||
workflowToolAvailable
|
workflowToolAvailable
|
||||||
|
workflowToolIsLoading={false}
|
||||||
|
workflowToolOutdated={false}
|
||||||
|
workflowToolIsCurrentWorkspaceManager
|
||||||
|
onConfigureWorkflowTool={vi.fn()}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -46,4 +46,47 @@ describe('SuggestedAction', () => {
|
|||||||
|
|
||||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should render and trigger the trailing action button when configured', () => {
|
||||||
|
const handleActionClick = vi.fn()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<SuggestedAction
|
||||||
|
link="https://example.com/docs"
|
||||||
|
actionButton={{
|
||||||
|
ariaLabel: 'Configure action',
|
||||||
|
icon: <span>config</span>,
|
||||||
|
onClick: handleActionClick,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Configurable action
|
||||||
|
</SuggestedAction>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Configure action' }))
|
||||||
|
|
||||||
|
expect(screen.getByRole('link', { name: 'Configurable action' })).toHaveAttribute('href', 'https://example.com/docs')
|
||||||
|
expect(handleActionClick).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should block action button clicks when disabled', () => {
|
||||||
|
const handleActionClick = vi.fn()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<SuggestedAction
|
||||||
|
link="https://example.com/docs"
|
||||||
|
disabled
|
||||||
|
actionButton={{
|
||||||
|
ariaLabel: 'Configure action',
|
||||||
|
icon: <span>config</span>,
|
||||||
|
onClick: handleActionClick,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Disabled with action
|
||||||
|
</SuggestedAction>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Configure action' }))
|
||||||
|
expect(handleActionClick).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,28 +1,40 @@
|
|||||||
|
import type { FormEvent } from 'react'
|
||||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||||
|
import type { WorkflowHiddenStartVariable, WorkflowLaunchInputValue } from '@/app/components/app/overview/app-card-utils'
|
||||||
import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration'
|
import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration'
|
||||||
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
||||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { RiStoreLine } from '@remixicon/react'
|
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useKeyPress } from 'ahooks'
|
import { useKeyPress } from 'ahooks'
|
||||||
import {
|
import {
|
||||||
|
|
||||||
memo,
|
memo,
|
||||||
|
use,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { WorkflowLaunchDialog } from '@/app/components/app/overview/app-card-sections'
|
||||||
|
import {
|
||||||
|
buildWorkflowLaunchUrl,
|
||||||
|
createWorkflowLaunchInitialValues,
|
||||||
|
isWorkflowLaunchInputSupported,
|
||||||
|
|
||||||
|
} from '@/app/components/app/overview/app-card-utils'
|
||||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { trackEvent } from '@/app/components/base/amplitude'
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
|
import { WorkflowToolDrawer } from '@/app/components/tools/workflow-tool'
|
||||||
|
import { useConfigureButton } from '@/app/components/tools/workflow-tool/hooks/use-configure-button'
|
||||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||||
|
import { appDefaultIconBackground } from '@/config'
|
||||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
@ -57,8 +69,8 @@ export type AppPublisherProps = {
|
|||||||
debugWithMultipleModel?: boolean
|
debugWithMultipleModel?: boolean
|
||||||
multipleModelConfigs?: ModelAndParameter[]
|
multipleModelConfigs?: ModelAndParameter[]
|
||||||
/** modelAndParameter is passed when debugWithMultipleModel is true */
|
/** modelAndParameter is passed when debugWithMultipleModel is true */
|
||||||
onPublish?: (params?: any) => Promise<any> | any
|
onPublish?: AppPublisherPublishHandler
|
||||||
onRestore?: () => Promise<any> | any
|
onRestore?: AppPublisherRestoreHandler
|
||||||
onToggle?: (state: boolean) => void
|
onToggle?: (state: boolean) => void
|
||||||
crossAxisOffset?: number
|
crossAxisOffset?: number
|
||||||
toolPublished?: boolean
|
toolPublished?: boolean
|
||||||
@ -74,6 +86,12 @@ export type AppPublisherProps = {
|
|||||||
|
|
||||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||||
|
|
||||||
|
type AppPublisherPublishHandler
|
||||||
|
= | ((params?: ModelAndParameter | PublishWorkflowParams) => Promise<unknown> | unknown)
|
||||||
|
| ((params?: unknown) => Promise<unknown> | unknown)
|
||||||
|
|
||||||
|
type AppPublisherRestoreHandler = () => Promise<unknown> | unknown
|
||||||
|
|
||||||
const AppPublisher = ({
|
const AppPublisher = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
publishDisabled = false,
|
publishDisabled = false,
|
||||||
@ -100,11 +118,15 @@ const AppPublisher = ({
|
|||||||
const [published, setPublished] = useState(false)
|
const [published, setPublished] = useState(false)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
|
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
|
||||||
|
const [workflowToolDrawerOpen, setWorkflowToolDrawerOpen] = useState(false)
|
||||||
|
|
||||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||||
|
const [workflowLaunchDialogOpen, setWorkflowLaunchDialogOpen] = useState(false)
|
||||||
|
const [workflowLaunchTargetUrl, setWorkflowLaunchTargetUrl] = useState('')
|
||||||
|
const [workflowLaunchValues, setWorkflowLaunchValues] = useState<Record<string, WorkflowLaunchInputValue>>({})
|
||||||
const [publishingToMarketplace, setPublishingToMarketplace] = useState(false)
|
const [publishingToMarketplace, setPublishingToMarketplace] = useState(false)
|
||||||
|
|
||||||
const workflowStore = useContext(WorkflowContext)
|
const workflowStore = use(WorkflowContext)
|
||||||
const appDetail = useAppStore(state => state.appDetail)
|
const appDetail = useAppStore(state => state.appDetail)
|
||||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
@ -113,6 +135,22 @@ const AppPublisher = ({
|
|||||||
|
|
||||||
const appURL = getPublisherAppUrl({ appBaseUrl: appBaseURL, accessToken, mode: appDetail?.mode })
|
const appURL = getPublisherAppUrl({ appBaseUrl: appBaseURL, accessToken, mode: appDetail?.mode })
|
||||||
const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(appDetail?.mode || AppModeEnum.CHAT)
|
const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(appDetail?.mode || AppModeEnum.CHAT)
|
||||||
|
const hiddenLaunchVariables = useMemo<WorkflowHiddenStartVariable[]>(
|
||||||
|
() => (inputs ?? []).filter(input => input.hide === true),
|
||||||
|
[inputs],
|
||||||
|
)
|
||||||
|
const supportedWorkflowLaunchVariables = useMemo(
|
||||||
|
() => hiddenLaunchVariables.filter(isWorkflowLaunchInputSupported),
|
||||||
|
[hiddenLaunchVariables],
|
||||||
|
)
|
||||||
|
const unsupportedWorkflowLaunchVariables = useMemo(
|
||||||
|
() => hiddenLaunchVariables.filter(variable => !isWorkflowLaunchInputSupported(variable)),
|
||||||
|
[hiddenLaunchVariables],
|
||||||
|
)
|
||||||
|
const initialWorkflowLaunchValues = useMemo(
|
||||||
|
() => createWorkflowLaunchInitialValues(supportedWorkflowLaunchVariables),
|
||||||
|
[supportedWorkflowLaunchVariables],
|
||||||
|
)
|
||||||
|
|
||||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
||||||
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||||
@ -222,6 +260,31 @@ const AppPublisher = ({
|
|||||||
}
|
}
|
||||||
}, [appDetail, setAppDetail])
|
}, [appDetail, setAppDetail])
|
||||||
|
|
||||||
|
const handleOpenWorkflowLaunchDialog = useCallback((targetUrl: string) => {
|
||||||
|
setWorkflowLaunchValues(initialWorkflowLaunchValues)
|
||||||
|
setWorkflowLaunchTargetUrl(targetUrl)
|
||||||
|
setWorkflowLaunchDialogOpen(true)
|
||||||
|
}, [initialWorkflowLaunchValues])
|
||||||
|
|
||||||
|
const handleWorkflowLaunchValueChange = useCallback((variable: string, value: WorkflowLaunchInputValue) => {
|
||||||
|
setWorkflowLaunchValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[variable]: value,
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleWorkflowLaunchConfirm = useCallback(async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const targetUrl = await buildWorkflowLaunchUrl({
|
||||||
|
accessibleUrl: workflowLaunchTargetUrl,
|
||||||
|
variables: supportedWorkflowLaunchVariables,
|
||||||
|
values: workflowLaunchValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
window.open(targetUrl, '_blank')
|
||||||
|
setWorkflowLaunchDialogOpen(false)
|
||||||
|
}, [supportedWorkflowLaunchVariables, workflowLaunchTargetUrl, workflowLaunchValues])
|
||||||
const handlePublishToMarketplace = useCallback(async () => {
|
const handlePublishToMarketplace = useCallback(async () => {
|
||||||
if (!appDetail?.id || publishingToMarketplace)
|
if (!appDetail?.id || publishingToMarketplace)
|
||||||
return
|
return
|
||||||
@ -273,6 +336,31 @@ const AppPublisher = ({
|
|||||||
const workflowToolMessage = !hasPublishedVersion || !workflowToolAvailable
|
const workflowToolMessage = !hasPublishedVersion || !workflowToolAvailable
|
||||||
? t('common.workflowAsToolDisabledHint', { ns: 'workflow' })
|
? t('common.workflowAsToolDisabledHint', { ns: 'workflow' })
|
||||||
: undefined
|
: undefined
|
||||||
|
const workflowToolVisible = appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && !hasTriggerNode
|
||||||
|
const workflowToolPublished = !!toolPublished
|
||||||
|
const closeWorkflowToolDrawer = useCallback(() => setWorkflowToolDrawerOpen(false), [])
|
||||||
|
const workflowToolIcon = useMemo(() => ({
|
||||||
|
content: (appDetail?.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
|
||||||
|
background: (appDetail?.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
|
||||||
|
}), [appDetail?.icon, appDetail?.icon_background, appDetail?.icon_type])
|
||||||
|
const workflowTool = useConfigureButton({
|
||||||
|
enabled: workflowToolVisible,
|
||||||
|
published: workflowToolPublished,
|
||||||
|
detailNeedUpdate: workflowToolPublished && published,
|
||||||
|
workflowAppId: appDetail?.id ?? '',
|
||||||
|
icon: workflowToolIcon,
|
||||||
|
name: appDetail?.name ?? '',
|
||||||
|
description: appDetail?.description ?? '',
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
handlePublish,
|
||||||
|
onRefreshData,
|
||||||
|
onConfigured: closeWorkflowToolDrawer,
|
||||||
|
})
|
||||||
|
const openWorkflowToolDrawer = useCallback(() => {
|
||||||
|
handleOpenChange(false)
|
||||||
|
setWorkflowToolDrawerOpen(true)
|
||||||
|
}, [handleOpenChange])
|
||||||
const upgradeHighlightStyle = useMemo(() => ({
|
const upgradeHighlightStyle = useMemo(() => ({
|
||||||
background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)',
|
background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)',
|
||||||
WebkitBackgroundClip: 'text',
|
WebkitBackgroundClip: 'text',
|
||||||
@ -343,23 +431,27 @@ const AppPublisher = ({
|
|||||||
handleOpenChange(false)
|
handleOpenChange(false)
|
||||||
handleOpenInExplore()
|
handleOpenInExplore()
|
||||||
}}
|
}}
|
||||||
|
handleOpenRunConfig={handleOpenWorkflowLaunchDialog}
|
||||||
handlePublish={handlePublish}
|
handlePublish={handlePublish}
|
||||||
hasHumanInputNode={hasHumanInputNode}
|
hasHumanInputNode={hasHumanInputNode}
|
||||||
hasTriggerNode={hasTriggerNode}
|
hasTriggerNode={hasTriggerNode}
|
||||||
inputs={inputs}
|
|
||||||
missingStartNode={missingStartNode}
|
missingStartNode={missingStartNode}
|
||||||
onRefreshData={onRefreshData}
|
|
||||||
outputs={outputs}
|
|
||||||
published={published}
|
published={published}
|
||||||
publishedAt={publishedAt}
|
publishedAt={publishedAt}
|
||||||
|
showBatchRunConfig={hiddenLaunchVariables.length > 0 && (appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION)}
|
||||||
|
showRunConfig={hiddenLaunchVariables.length > 0}
|
||||||
toolPublished={toolPublished}
|
toolPublished={toolPublished}
|
||||||
workflowToolAvailable={workflowToolAvailable}
|
workflowToolAvailable={workflowToolAvailable}
|
||||||
|
workflowToolIsLoading={workflowTool.isLoading}
|
||||||
|
workflowToolOutdated={workflowTool.outdated}
|
||||||
|
workflowToolIsCurrentWorkspaceManager={workflowTool.isCurrentWorkspaceManager}
|
||||||
workflowToolMessage={workflowToolMessage}
|
workflowToolMessage={workflowToolMessage}
|
||||||
|
onConfigureWorkflowTool={openWorkflowToolDrawer}
|
||||||
/>
|
/>
|
||||||
{systemFeatures.enable_creators_platform && (
|
{systemFeatures.enable_creators_platform && (
|
||||||
<div className="border-t border-divider-subtle p-4">
|
<div className="border-t border-divider-subtle p-4">
|
||||||
<SuggestedAction
|
<SuggestedAction
|
||||||
icon={<RiStoreLine className="h-4 w-4" />}
|
icon={<span className="i-ri-store-line h-4 w-4" />}
|
||||||
disabled={!publishedAt || publishingToMarketplace}
|
disabled={!publishedAt || publishingToMarketplace}
|
||||||
onClick={handlePublishToMarketplace}
|
onClick={handlePublishToMarketplace}
|
||||||
>
|
>
|
||||||
@ -377,9 +469,29 @@ const AppPublisher = ({
|
|||||||
onClose={() => setEmbeddingModalOpen(false)}
|
onClose={() => setEmbeddingModalOpen(false)}
|
||||||
appBaseUrl={appBaseURL}
|
appBaseUrl={appBaseURL}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
|
hiddenInputs={hiddenLaunchVariables}
|
||||||
/>
|
/>
|
||||||
{showAppAccessControl && <AccessControl app={appDetail!} onConfirm={handleAccessControlUpdate} onClose={() => { setShowAppAccessControl(false) }} />}
|
{showAppAccessControl && <AccessControl app={appDetail!} onConfirm={handleAccessControlUpdate} onClose={() => { setShowAppAccessControl(false) }} />}
|
||||||
|
<WorkflowLaunchDialog
|
||||||
|
t={t}
|
||||||
|
open={workflowLaunchDialogOpen}
|
||||||
|
hiddenVariables={supportedWorkflowLaunchVariables}
|
||||||
|
unsupportedVariables={unsupportedWorkflowLaunchVariables}
|
||||||
|
values={workflowLaunchValues}
|
||||||
|
onOpenChange={setWorkflowLaunchDialogOpen}
|
||||||
|
onValueChange={handleWorkflowLaunchValueChange}
|
||||||
|
onSubmit={handleWorkflowLaunchConfirm}
|
||||||
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
{workflowToolDrawerOpen && (
|
||||||
|
<WorkflowToolDrawer
|
||||||
|
isAdd={!workflowToolPublished}
|
||||||
|
payload={workflowTool.payload}
|
||||||
|
onHide={closeWorkflowToolDrawer}
|
||||||
|
onCreate={workflowTool.handleCreate}
|
||||||
|
onSave={workflowTool.handleUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,13 +8,12 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@langgenius/dify-ui/tooltip'
|
} from '@langgenius/dify-ui/tooltip'
|
||||||
|
import { RiSettings2Line } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
|
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||||
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
||||||
import { appDefaultIconBackground } from '@/config'
|
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||||
@ -46,11 +45,8 @@ type AccessSectionProps = {
|
|||||||
|
|
||||||
type ActionsSectionProps = Pick<AppPublisherProps, | 'hasHumanInputNode'
|
type ActionsSectionProps = Pick<AppPublisherProps, | 'hasHumanInputNode'
|
||||||
| 'hasTriggerNode'
|
| 'hasTriggerNode'
|
||||||
| 'inputs'
|
|
||||||
| 'missingStartNode'
|
| 'missingStartNode'
|
||||||
| 'onRefreshData'
|
|
||||||
| 'toolPublished'
|
| 'toolPublished'
|
||||||
| 'outputs'
|
|
||||||
| 'publishedAt'
|
| 'publishedAt'
|
||||||
| 'workflowToolAvailable'> & {
|
| 'workflowToolAvailable'> & {
|
||||||
appDetail: {
|
appDetail: {
|
||||||
@ -67,9 +63,16 @@ type ActionsSectionProps = Pick<AppPublisherProps, | 'hasHumanInputNode'
|
|||||||
disabledFunctionTooltip?: string
|
disabledFunctionTooltip?: string
|
||||||
handleEmbed: () => void
|
handleEmbed: () => void
|
||||||
handleOpenInExplore: () => void
|
handleOpenInExplore: () => void
|
||||||
|
handleOpenRunConfig?: (url: string) => void
|
||||||
handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise<void>
|
handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise<void>
|
||||||
published: boolean
|
published: boolean
|
||||||
|
showBatchRunConfig?: boolean
|
||||||
|
showRunConfig?: boolean
|
||||||
|
workflowToolIsLoading: boolean
|
||||||
|
workflowToolOutdated: boolean
|
||||||
|
workflowToolIsCurrentWorkspaceManager: boolean
|
||||||
workflowToolMessage?: string
|
workflowToolMessage?: string
|
||||||
|
onConfigureWorkflowTool: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MAP }) => {
|
export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MAP }) => {
|
||||||
@ -256,18 +259,20 @@ export const PublisherActionsSection = ({
|
|||||||
disabledFunctionTooltip,
|
disabledFunctionTooltip,
|
||||||
handleEmbed,
|
handleEmbed,
|
||||||
handleOpenInExplore,
|
handleOpenInExplore,
|
||||||
handlePublish,
|
handleOpenRunConfig,
|
||||||
hasHumanInputNode = false,
|
hasHumanInputNode = false,
|
||||||
hasTriggerNode = false,
|
hasTriggerNode = false,
|
||||||
inputs,
|
|
||||||
missingStartNode = false,
|
missingStartNode = false,
|
||||||
onRefreshData,
|
|
||||||
outputs,
|
|
||||||
published,
|
|
||||||
publishedAt,
|
publishedAt,
|
||||||
|
showBatchRunConfig = false,
|
||||||
|
showRunConfig = false,
|
||||||
toolPublished,
|
toolPublished,
|
||||||
workflowToolAvailable = true,
|
workflowToolAvailable = true,
|
||||||
|
workflowToolIsLoading,
|
||||||
|
workflowToolOutdated,
|
||||||
|
workflowToolIsCurrentWorkspaceManager,
|
||||||
workflowToolMessage,
|
workflowToolMessage,
|
||||||
|
onConfigureWorkflowTool,
|
||||||
}: ActionsSectionProps) => {
|
}: ActionsSectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -284,6 +289,13 @@ export const PublisherActionsSection = ({
|
|||||||
disabled={disabledFunctionButton}
|
disabled={disabledFunctionButton}
|
||||||
link={appURL}
|
link={appURL}
|
||||||
icon={<span className="i-ri-play-circle-line h-4 w-4" />}
|
icon={<span className="i-ri-play-circle-line h-4 w-4" />}
|
||||||
|
actionButton={showRunConfig
|
||||||
|
? {
|
||||||
|
ariaLabel: t('operation.config', { ns: 'common' }),
|
||||||
|
icon: <RiSettings2Line className="h-4 w-4" />,
|
||||||
|
onClick: () => handleOpenRunConfig?.(appURL),
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
>
|
>
|
||||||
{t('common.runApp', { ns: 'workflow' })}
|
{t('common.runApp', { ns: 'workflow' })}
|
||||||
</SuggestedAction>
|
</SuggestedAction>
|
||||||
@ -296,6 +308,13 @@ export const PublisherActionsSection = ({
|
|||||||
disabled={disabledFunctionButton}
|
disabled={disabledFunctionButton}
|
||||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||||
icon={<span className="i-ri-play-list-2-line h-4 w-4" />}
|
icon={<span className="i-ri-play-list-2-line h-4 w-4" />}
|
||||||
|
actionButton={showBatchRunConfig
|
||||||
|
? {
|
||||||
|
ariaLabel: t('operation.config', { ns: 'common' }),
|
||||||
|
icon: <RiSettings2Line className="h-4 w-4" />,
|
||||||
|
onClick: () => handleOpenRunConfig?.(`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`),
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
>
|
>
|
||||||
{t('common.batchRunApp', { ns: 'workflow' })}
|
{t('common.batchRunApp', { ns: 'workflow' })}
|
||||||
</SuggestedAction>
|
</SuggestedAction>
|
||||||
@ -305,7 +324,7 @@ export const PublisherActionsSection = ({
|
|||||||
<SuggestedAction
|
<SuggestedAction
|
||||||
onClick={handleEmbed}
|
onClick={handleEmbed}
|
||||||
disabled={!publishedAt}
|
disabled={!publishedAt}
|
||||||
icon={<CodeBrowser className="h-4 w-4" />}
|
icon={<span className="i-custom-vender-line-development-code-browser h-4 w-4" />}
|
||||||
>
|
>
|
||||||
{t('common.embedIntoSite', { ns: 'workflow' })}
|
{t('common.embedIntoSite', { ns: 'workflow' })}
|
||||||
</SuggestedAction>
|
</SuggestedAction>
|
||||||
@ -340,18 +359,10 @@ export const PublisherActionsSection = ({
|
|||||||
<WorkflowToolConfigureButton
|
<WorkflowToolConfigureButton
|
||||||
disabled={workflowToolDisabled}
|
disabled={workflowToolDisabled}
|
||||||
published={!!toolPublished}
|
published={!!toolPublished}
|
||||||
detailNeedUpdate={!!toolPublished && published}
|
isLoading={workflowToolIsLoading}
|
||||||
workflowAppId={appDetail?.id ?? ''}
|
outdated={workflowToolOutdated}
|
||||||
icon={{
|
isCurrentWorkspaceManager={workflowToolIsCurrentWorkspaceManager}
|
||||||
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
|
onConfigure={onConfigureWorkflowTool}
|
||||||
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
|
|
||||||
}}
|
|
||||||
name={appDetail?.name ?? ''}
|
|
||||||
description={appDetail?.description ?? ''}
|
|
||||||
inputs={inputs}
|
|
||||||
outputs={outputs}
|
|
||||||
handlePublish={handlePublish}
|
|
||||||
onRefreshData={onRefreshData}
|
|
||||||
disabledReason={workflowToolMessage}
|
disabledReason={workflowToolMessage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user