Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a08d8dd6f | ||
|
|
7b030ff1bc | ||
|
|
60d779152c | ||
|
|
ff97259011 | ||
|
|
4163b4dd81 | ||
|
|
02c9b6e5ce | ||
|
|
83db975374 | ||
|
|
18efc8ed88 | ||
|
|
46e1a5a6db | ||
|
|
a2267276ae | ||
|
|
38af34a742 | ||
|
|
a6084ca3c7 | ||
|
|
d8b847dcf0 | ||
|
|
2509682e07 | ||
|
|
5582b35d56 | ||
|
|
db83df9f9c | ||
|
|
0683c0e7a7 | ||
|
|
42b7bf8152 | ||
|
|
bc33ef1b97 | ||
|
|
0f493a52a1 | ||
|
|
fc4f9db79a | ||
|
|
e9b8a1606e | ||
|
|
453c4c4c5f | ||
|
|
58e25d0534 | ||
|
|
d7b9f2a86b | ||
|
|
18d2423ed1 | ||
|
|
4176e7d146 | ||
|
|
3556611c2b | ||
|
|
e37b4af0e8 | ||
|
|
a0eb2ba0ff | ||
|
|
75f1094459 | ||
|
|
f61f9634f8 | ||
|
|
11f78289d3 | ||
|
|
062341ab26 | ||
|
|
6ea9ba5926 | ||
|
|
7a9054fdea | ||
|
|
508baa782c | ||
|
|
f5ee121d2c | ||
|
|
2bf60a67ad | ||
|
|
f6433097ad | ||
|
|
23b1038b99 | ||
|
|
ed10b82bb1 | ||
|
|
41106ef6c9 | ||
|
|
5ee7bedb56 | ||
|
|
f0bcb77d55 | ||
|
|
44a89bf870 | ||
|
|
60ad023553 | ||
|
|
9c6a7679ac | ||
|
|
11b2ba29c1 | ||
|
|
c52df73117 | ||
|
|
9b8d81c852 | ||
|
|
b28b2892f1 | ||
|
|
7f5349e707 | ||
|
|
0aaa1df1b8 | ||
|
|
bd41c5c3c0 | ||
|
|
95a2eea611 | ||
|
|
19193891cf | ||
|
|
4746dd39d3 | ||
|
|
a5e6f58285 | ||
|
|
4bf0398873 | ||
|
|
e6db98ef64 | ||
|
|
6b694ce829 | ||
|
|
0ab3b5d677 | ||
|
|
346fabda27 | ||
|
|
3da0f12815 | ||
|
|
303cff1353 | ||
|
|
10c3849887 | ||
|
|
29295950ea | ||
|
|
c61a7ce442 | ||
|
|
ab383969a8 | ||
|
|
8a0aab4d81 | ||
|
|
d389284813 | ||
|
|
530e366440 | ||
|
|
e4510a7d8f | ||
|
|
19d7a9b5d9 | ||
|
|
1d063e3fd6 | ||
|
|
850c7e311f | ||
|
|
de0ccbb960 | ||
|
|
407d3e28bb | ||
|
|
8a51b3a296 | ||
|
|
41894ad182 | ||
|
|
95ea709c91 | ||
|
|
3a01b91a45 | ||
|
|
f50abac3f9 | ||
|
|
7f44b2f601 | ||
|
|
c71d2ac460 | ||
|
|
1a7542052e | ||
|
|
6d26f6ea73 | ||
|
|
b0d3347de9 | ||
|
|
bf2a35bff8 | ||
|
|
63d50a61d3 | ||
|
|
101a6fcc53 | ||
|
|
d407c1fbf7 | ||
|
|
fe6ee8aa04 | ||
|
|
25228e3cde | ||
|
|
8049da9331 | ||
|
|
e404195c8c | ||
|
|
b3c7110768 | ||
|
|
7b2b21c348 | ||
|
|
3d3a1f4f90 | ||
|
|
274944f05e | ||
|
|
a0d53b9f07 | ||
|
|
a24d1a0bbc | ||
|
|
fffa89a10e | ||
|
|
422461f360 | ||
|
|
2ce913aaac | ||
|
|
30f0a69fea | ||
|
|
04e8c6127f | ||
|
|
20e37a0457 | ||
|
|
36c7209301 | ||
|
|
ccc9122980 | ||
|
|
9143d44ec6 | ||
|
|
1f856960f0 | ||
|
|
a8218d5809 | ||
|
|
70ec17bc34 | ||
|
|
e4ea9d2e07 | ||
|
|
b72bfe060d | ||
|
|
dfc7f136ef | ||
|
|
15d66814ee | ||
|
|
5f109213a5 | ||
|
|
a53828e826 | ||
|
|
3a2ea826ff | ||
|
|
4581ce2f45 | ||
|
|
3925b2bf4f | ||
|
|
5744148e6d | ||
|
|
6ba42e6d73 | ||
|
|
aff22cb5ed | ||
|
|
58c4a174ba | ||
|
|
c98f65cbcb | ||
|
|
add7c75f18 | ||
|
|
75e74ee8b9 | ||
|
|
40a5236553 | ||
|
|
f820813e9f | ||
|
|
c4c3a2b265 | ||
|
|
99be8b34c8 | ||
|
|
777265d898 | ||
|
|
7024913866 | ||
|
|
3c862c3e98 | ||
|
|
fb497c60dd | ||
|
|
075af9cd44 | ||
|
|
18e07cac9a | ||
|
|
ba06ed5f41 | ||
|
|
3fd9d5eb14 | ||
|
|
15d2714e9d | ||
|
|
f36852646f | ||
|
|
72c3cd0d67 | ||
|
|
fd0cb47a81 | ||
|
|
87382673f3 | ||
|
|
3c716a6eee | ||
|
|
8da34fb60b | ||
|
|
f0efb73fd0 | ||
|
|
e2bd2355e9 | ||
|
|
73b50f5ede | ||
|
|
52cfe62d8d | ||
|
|
f293253a7a | ||
|
|
1ac3ad4c81 | ||
|
|
2d6fd70733 | ||
|
|
639c8d5967 | ||
|
|
74c9b7fddd | ||
|
|
be997384f3 | ||
|
|
acbea6701c | ||
|
|
86ac4dadd6 | ||
|
|
f3e11ec0ee | ||
|
|
c9c4fca7a7 | ||
|
|
d23eddc924 | ||
|
|
d65c7229f2 | ||
|
|
66508326f9 | ||
|
|
310f49229e | ||
|
|
f400be4280 | ||
|
|
2855ab3a15 | ||
|
|
e2fd5421d2 | ||
|
|
ff37ba83b4 | ||
|
|
7b97ec57ef | ||
|
|
19d34b5a93 | ||
|
|
5c315ea7fe | ||
|
|
e525880773 | ||
|
|
d82e30561f | ||
|
|
d2508db11f | ||
|
|
7f76fe68ea | ||
|
|
ac09702d08 | ||
|
|
b14d1e60ec | ||
|
|
b072136ca0 | ||
|
|
c8b6ec5fb0 | ||
|
|
0b5b4271f0 | ||
|
|
fd60339625 | ||
|
|
f56f93f5c2 | ||
|
|
2921d27929 | ||
|
|
98e3bff509 | ||
|
|
6c43fa459e | ||
|
|
d2eae74b5e | ||
|
|
27bdee85fe | ||
|
|
cca8295ad8 | ||
|
|
4d1b3605b5 | ||
|
|
d94be05f68 | ||
|
|
fea7590779 | ||
|
|
71ba903d4c | ||
|
|
44831839d1 | ||
|
|
88c3512471 | ||
|
|
67dee6e07e | ||
|
|
95d7fa997b | ||
|
|
4a0b177eee | ||
|
|
15b2a8fdb7 | ||
|
|
65098a6b4f | ||
|
|
8055f8840c | ||
|
|
f033f91a68 | ||
|
|
0b98319bd3 | ||
|
|
386de25e26 | ||
|
|
d30805353a | ||
|
|
21c5825508 | ||
|
|
2d324add39 | ||
|
|
62beaf493e | ||
|
|
b34e5aa915 | ||
|
|
2ea19c2b1c | ||
|
|
5712f29e8b | ||
|
|
93f7404c6b | ||
|
|
4f631d6f4c | ||
|
|
2b3e15cc83 | ||
|
|
35956247ab | ||
|
|
343531b9dc | ||
|
|
236f389fce | ||
|
|
5a2604265c | ||
|
|
6a8aaa5a36 | ||
|
|
0ad1e8c2d9 | ||
|
|
bf345136eb | ||
|
|
c9ed50c3ae | ||
|
|
c78c603a38 | ||
|
|
48b38446a3 | ||
|
|
8a8bec4bc6 | ||
|
|
95936a8bac | ||
|
|
ac8a1107ca | ||
|
|
0c96426d91 | ||
|
|
67fee14770 | ||
|
|
d94006162d | ||
|
|
3d53cee8a9 | ||
|
|
1acd1b568a | ||
|
|
68f939f3b3 | ||
|
|
1f4b76ba7e | ||
|
|
4d974d8f72 | ||
|
|
1dc12d1661 | ||
|
|
82345977cd | ||
|
|
83c943bc21 | ||
|
|
94a376a5a7 | ||
|
|
33d95ab23a | ||
|
|
7a8a92082b | ||
|
|
ef00f850e4 | ||
|
|
7815228395 | ||
|
|
dcd40b5004 | ||
|
|
c252006644 | ||
|
|
9e5668c233 | ||
|
|
52ce49b3c6 | ||
|
|
e90aa76ba2 | ||
|
|
de9373e1b8 | ||
|
|
58923f38e6 | ||
|
|
8486a5b213 | ||
|
|
28a8be0d5f | ||
|
|
f2d4d5b267 | ||
|
|
f62a59a18a | ||
|
|
b488812714 | ||
|
|
755760b97c | ||
|
|
955c3fb797 | ||
|
|
0c9aa20047 | ||
|
|
065246a9a7 | ||
|
|
0d12b5ab1c | ||
|
|
514dcae189 | ||
|
|
228dd84a91 | ||
|
|
336ddad096 | ||
|
|
92bb9a17b7 | ||
|
|
b8868dab90 | ||
|
|
94225682cd | ||
|
|
18b6568c2a | ||
|
|
a3a9ded29b | ||
|
|
de78a26920 | ||
|
|
c54d029e7c | ||
|
|
ad4b9dc2c3 | ||
|
|
cdec0c69a6 | ||
|
|
53acc3726c | ||
|
|
b1d393f4d9 | ||
|
|
62e9bdd70d | ||
|
|
d36c76c20e | ||
|
|
f525e1a5eb | ||
|
|
e2f779b20d | ||
|
|
e198d6305c | ||
|
|
5e67514265 | ||
|
|
b63896de87 | ||
|
|
e463389f2c | ||
|
|
cda348ca10 | ||
|
|
ca48050666 | ||
|
|
9c0f592f34 | ||
|
|
b70241ad36 | ||
|
|
4abe622b2e | ||
|
|
16c32c82e3 | ||
|
|
46424513d1 | ||
|
|
2c4baa20d8 | ||
|
|
b0ae553f2e | ||
|
|
0266a12ee5 | ||
|
|
9d7765d5fd | ||
|
|
d4ef983f42 | ||
|
|
018f36711d | ||
|
|
dacd333e4a | ||
|
|
b079a26314 | ||
|
|
7e953ebe0b | ||
|
|
b4d28fca54 | ||
|
|
728c6b8201 | ||
|
|
f56e23b5fd | ||
|
|
5600cefa53 | ||
|
|
561eb9cbd2 | ||
|
|
83766ca694 | ||
|
|
678be94d22 | ||
|
|
9e852429be | ||
|
|
d93c5028f1 | ||
|
|
54f189305e | ||
|
|
a610a24507 | ||
|
|
05e8a94bb5 | ||
|
|
b2e2e7b60b | ||
|
|
e7d2e66ff5 | ||
|
|
c51069685c | ||
|
|
28c208f36a | ||
|
|
53a1386b87 | ||
|
|
0e366c7300 | ||
|
|
939bdde373 | ||
|
|
13dfa3aba4 | ||
|
|
2705a7c1db | ||
|
|
258a751b8c | ||
|
|
5a35d3d9cd | ||
|
|
c3fbafae83 | ||
|
|
f727c8f838 | ||
|
|
90af4c39b4 | ||
|
|
f7c3a4e4cb | ||
|
|
be7d043edd | ||
|
|
cef8fe3a4b | ||
|
|
afe0e6c393 | ||
|
|
37309b931e | ||
|
|
6a83c6705c | ||
|
|
3e75d5e443 | ||
|
|
7be8a5b883 | ||
|
|
80dcb344f4 | ||
|
|
b029c9b1cd | ||
|
|
6cb97e9201 | ||
|
|
4ef2e952bd | ||
|
|
cc5545339c | ||
|
|
0a8c46a3a7 | ||
|
|
65770903d1 | ||
|
|
5a6ba2ffb5 | ||
|
|
aa53afe07d | ||
|
|
4740a89f4a | ||
|
|
328db3d67a | ||
|
|
88062fb247 | ||
|
|
045da59220 | ||
|
|
948b0f6bc7 | ||
|
|
14a59f6e44 | ||
|
|
f9f361113e | ||
|
|
eea6f59307 | ||
|
|
718f69dc43 | ||
|
|
82a2ba9264 | ||
|
|
6c8e032fbb | ||
|
|
28c2c3bfd3 | ||
|
|
9d463e1024 | ||
|
|
7f87616625 | ||
|
|
43a04ed0c2 | ||
|
|
5083edd0ce | ||
|
|
8306fa41b9 | ||
|
|
8f33305e90 | ||
|
|
7077a43c1c | ||
|
|
884a43ae0a | ||
|
|
914f89f478 | ||
|
|
163153db18 | ||
|
|
49d890d514 | ||
|
|
0292bc2728 | ||
|
|
5c21120977 |
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: karpathy-guidelines
|
||||
description: Lightweight coding guardrails for making focused, simple, and verifiable changes in this repo. Use for all coding work.
|
||||
---
|
||||
|
||||
# Karpathy Guidelines
|
||||
|
||||
Use this skill whenever you touch code in this repository.
|
||||
|
||||
## Principles
|
||||
|
||||
- Keep the change small and directly tied to the user request.
|
||||
- Prefer the simplest implementation that fits the existing codebase.
|
||||
- Read the nearby code first, then match its patterns.
|
||||
- Avoid unrelated refactors, broad rewrites, or style churn.
|
||||
- Preserve existing behavior unless the user explicitly asked to change it.
|
||||
- Treat regressions as a signal to narrow the change, not to add workaround layers.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the current implementation and tests around the change.
|
||||
2. Make the smallest coherent edit.
|
||||
3. Add or update focused tests when the behavior changes or the risk is non-trivial.
|
||||
4. Run the narrowest relevant verification first.
|
||||
5. Report exactly what was verified and anything left unverified.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- Does this change solve the stated problem without expanding scope?
|
||||
- Did it preserve existing route/component/data-flow semantics?
|
||||
- Are new abstractions justified by real complexity?
|
||||
- Are tests focused on the behavior that could regress?
|
||||
- Are unrelated files and generated artifacts left alone?
|
||||
@@ -11,6 +11,7 @@ from .data_migration import (
|
||||
migration_data_wizard,
|
||||
)
|
||||
from .plugin import (
|
||||
backfill_plugin_auto_upgrade,
|
||||
extract_plugins,
|
||||
extract_unique_plugins,
|
||||
install_plugins,
|
||||
@@ -49,6 +50,7 @@ from .vector import (
|
||||
__all__ = [
|
||||
"add_qdrant_index",
|
||||
"archive_workflow_runs",
|
||||
"backfill_plugin_auto_upgrade",
|
||||
"clean_expired_messages",
|
||||
"clean_workflow_runs",
|
||||
"cleanup_orphaned_draft_variables",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, cast
|
||||
|
||||
import click
|
||||
from pydantic import TypeAdapter
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.engine import CursorResult
|
||||
|
||||
from configs import dify_config
|
||||
@@ -15,11 +16,13 @@ from core.plugin.plugin_service import PluginService
|
||||
from core.tools.utils.system_encryption import encrypt_system_params
|
||||
from extensions.ext_database import db
|
||||
from models import Tenant
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
from models.oauth import DatasourceOauthParamConfig, DatasourceProvider
|
||||
from models.provider_ids import DatasourceProviderID, ToolProviderID
|
||||
from models.source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
|
||||
from models.tools import ToolOAuthSystemClient
|
||||
from services.plugin.data_migration import PluginDataMigration
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
from services.plugin.plugin_migration import PluginMigration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -402,6 +405,110 @@ def migrate_data_for_plugin():
|
||||
click.echo(click.style("Migrate data for plugin completed.", fg="green"))
|
||||
|
||||
|
||||
def _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit: int | None = None):
|
||||
category_count = len(TenantPluginAutoUpgradeStrategy.PluginCategory)
|
||||
stmt = (
|
||||
select(TenantPluginAutoUpgradeStrategy.tenant_id)
|
||||
.group_by(TenantPluginAutoUpgradeStrategy.tenant_id)
|
||||
.having(func.count(func.distinct(TenantPluginAutoUpgradeStrategy.category)) < category_count)
|
||||
.order_by(TenantPluginAutoUpgradeStrategy.tenant_id)
|
||||
)
|
||||
|
||||
if limit is not None:
|
||||
stmt = stmt.limit(limit)
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def _count_auto_upgrade_strategy_tenant_ids(limit: int | None) -> int:
|
||||
candidate_stmt = _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit).subquery()
|
||||
return db.session.scalar(select(func.count()).select_from(candidate_stmt)) or 0
|
||||
|
||||
|
||||
def _iter_auto_upgrade_strategy_tenant_ids(limit: int | None):
|
||||
stmt = _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit).execution_options(yield_per=1000)
|
||||
yield from db.session.scalars(stmt)
|
||||
|
||||
|
||||
@click.command(
|
||||
"backfill-plugin-auto-upgrade",
|
||||
help="Backfill category-scoped plugin auto-upgrade strategies and normalize plugin lists.",
|
||||
)
|
||||
@click.option("--tenant-id", multiple=True, help="Tenant ID to backfill. Can be passed multiple times.")
|
||||
@click.option("--limit", type=int, default=None, help="Maximum number of candidate tenants to process.")
|
||||
@click.option("--batch-size", type=int, default=500, show_default=True, help="Progress reporting batch size.")
|
||||
@click.option("--dry-run", is_flag=True, help="Only print candidate tenant count.")
|
||||
def backfill_plugin_auto_upgrade(
|
||||
tenant_id: tuple[str, ...],
|
||||
limit: int | None,
|
||||
batch_size: int,
|
||||
dry_run: bool,
|
||||
):
|
||||
"""
|
||||
Backfill historical auto-upgrade strategies after the category column exists.
|
||||
|
||||
Missing category rows are created from the tenant's tool/default row. Pure default
|
||||
strategies become latest for model plugins and fix-only for all other categories.
|
||||
Tenants with include/exclude plugin IDs are split
|
||||
by installed plugin category using plugin daemon metadata.
|
||||
"""
|
||||
start_at = time.perf_counter()
|
||||
candidate_count = len(tenant_id) if tenant_id else _count_auto_upgrade_strategy_tenant_ids(limit)
|
||||
click.echo(click.style(f"Found {candidate_count} candidate tenants.", fg="yellow"))
|
||||
|
||||
if dry_run:
|
||||
elapsed = time.perf_counter() - start_at
|
||||
click.echo(click.style(f"Dry run completed. elapsed={elapsed:.2f}s", fg="green"))
|
||||
return
|
||||
|
||||
tenant_ids = list(tenant_id) if tenant_id else _iter_auto_upgrade_strategy_tenant_ids(limit)
|
||||
|
||||
backfilled_count = 0
|
||||
created_count = 0
|
||||
normalized_count = 0
|
||||
skipped_count = 0
|
||||
failed_count = 0
|
||||
for index, current_tenant_id in enumerate(tenant_ids, start=1):
|
||||
try:
|
||||
result = PluginAutoUpgradeService.backfill_strategy_categories(
|
||||
current_tenant_id,
|
||||
)
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
click.echo(click.style(f"Failed tenant {current_tenant_id}: {str(e)}", fg="red"))
|
||||
continue
|
||||
|
||||
if result.created_count > 0:
|
||||
backfilled_count += 1
|
||||
created_count += result.created_count
|
||||
elif not result.normalized:
|
||||
skipped_count += 1
|
||||
if result.normalized:
|
||||
normalized_count += 1
|
||||
|
||||
if batch_size > 0 and index % batch_size == 0:
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Processed {index}/{candidate_count} tenants. "
|
||||
f"backfilled={backfilled_count}, created_rows={created_count}, "
|
||||
f"normalized={normalized_count}, skipped={skipped_count}, failed={failed_count}, "
|
||||
f"elapsed={time.perf_counter() - start_at:.2f}s",
|
||||
fg="yellow",
|
||||
)
|
||||
)
|
||||
|
||||
elapsed = time.perf_counter() - start_at
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Backfill plugin auto-upgrade strategy categories completed. "
|
||||
f"backfilled={backfilled_count}, created_rows={created_count}, "
|
||||
f"normalized={normalized_count}, skipped={skipped_count}, failed={failed_count}, "
|
||||
f"elapsed={elapsed:.2f}s",
|
||||
fg="green",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@click.command("extract-plugins", help="Extract plugins.")
|
||||
@click.option("--output_file", prompt=True, help="The file to store the extracted plugins.", default="plugins.jsonl")
|
||||
@click.option("--workers", prompt=True, help="The number of workers to extract plugins.", default=10)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
@@ -14,7 +15,12 @@ from werkzeug.exceptions import BadRequest
|
||||
|
||||
from controllers.common.fields import RedirectUrlResponse, SimpleResultResponse
|
||||
from controllers.common.helpers import FileInfo
|
||||
from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
register_enum_models,
|
||||
register_response_schema_models,
|
||||
register_schema_models,
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model, with_session
|
||||
from controllers.console.workspace.models import LoadBalancingPayload
|
||||
@@ -36,12 +42,12 @@ from core.trigger.constants import TRIGGER_NODE_TYPES
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from libs.helper import build_icon_url, to_timestamp
|
||||
from libs.helper import build_icon_url, dump_response, to_timestamp
|
||||
from libs.login import login_required
|
||||
from models import Account, App, DatasetPermissionEnum, Workflow
|
||||
from models.model import IconType
|
||||
from services.app_dsl_service import AppDslService
|
||||
from services.app_service import AppListParams, AppService, CreateAppParams
|
||||
from services.app_service import AppListParams, AppListSortBy, AppService, CreateAppParams, StarredAppListParams
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities.dsl_entities import ImportMode, ImportStatus
|
||||
from services.entities.knowledge_entities.knowledge_entities import (
|
||||
@@ -68,10 +74,14 @@ _CREATOR_IDS_BRACKET_PATTERN = re.compile(r"^creator_ids\[(\d+)\]$")
|
||||
AppListMode = Literal["completion", "chat", "advanced-chat", "workflow", "agent-chat", "agent", "channel", "all"]
|
||||
|
||||
|
||||
class AppListQuery(BaseModel):
|
||||
class AppListBaseQuery(BaseModel):
|
||||
page: int = Field(default=1, ge=1, le=99999, description="Page number (1-99999)")
|
||||
limit: int = Field(default=20, ge=1, le=100, description="Page size (1-100)")
|
||||
mode: AppListMode = Field(default=cast(AppListMode, "all"), description="App mode filter")
|
||||
sort_by: AppListSortBy = Field(
|
||||
default="last_modified",
|
||||
description="Sort apps by last modified, recently created, or earliest created",
|
||||
)
|
||||
name: str | None = Field(default=None, description="Filter by app name")
|
||||
tag_ids: list[str] | None = Field(default=None, description="Filter by tag IDs")
|
||||
creator_ids: list[str] | None = Field(default=None, description="Filter by creator account IDs")
|
||||
@@ -114,6 +124,14 @@ class AppListQuery(BaseModel):
|
||||
raise ValueError("Invalid UUID format in creator_ids.") from exc
|
||||
|
||||
|
||||
class AppListQuery(AppListBaseQuery):
|
||||
pass
|
||||
|
||||
|
||||
class StarredAppListQuery(AppListBaseQuery):
|
||||
pass
|
||||
|
||||
|
||||
def _normalize_app_list_query_args(query_args: MultiDict[str, str]) -> dict[str, str | list[str]]:
|
||||
normalized: dict[str, str | list[str]] = {}
|
||||
indexed_tag_ids: list[tuple[int, str]] = []
|
||||
@@ -377,6 +395,7 @@ class AppPartial(ResponseModel):
|
||||
create_user_name: str | None = None
|
||||
author_name: str | None = None
|
||||
has_draft_trigger: bool | None = None
|
||||
is_starred: bool = False
|
||||
|
||||
@computed_field(return_type=str | None) # type: ignore
|
||||
@property
|
||||
@@ -446,12 +465,54 @@ class AppExportResponse(ResponseModel):
|
||||
data: str
|
||||
|
||||
|
||||
def _enrich_app_list_items(session: Session, *, apps: Sequence[App], tenant_id: str) -> None:
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
app_ids = [str(app.id) for app in apps]
|
||||
res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids)
|
||||
if len(res) != len(app_ids):
|
||||
raise BadRequest("Invalid app id in webapp auth")
|
||||
|
||||
for app in apps:
|
||||
if str(app.id) in res:
|
||||
app.access_mode = res[str(app.id)].access_mode
|
||||
|
||||
workflow_capable_app_ids = [str(app.id) for app in apps if app.mode in {"workflow", "advanced-chat"}]
|
||||
draft_trigger_app_ids: set[str] = set()
|
||||
if workflow_capable_app_ids:
|
||||
draft_workflows = (
|
||||
session.execute(
|
||||
select(Workflow).where(
|
||||
Workflow.version == Workflow.VERSION_DRAFT,
|
||||
Workflow.app_id.in_(workflow_capable_app_ids),
|
||||
Workflow.tenant_id == tenant_id,
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
trigger_node_types = TRIGGER_NODE_TYPES
|
||||
for workflow in draft_workflows:
|
||||
node_id = None
|
||||
try:
|
||||
for node_id, node_data in workflow.walk_nodes():
|
||||
if node_data.get("type") in trigger_node_types:
|
||||
draft_trigger_app_ids.add(str(workflow.app_id))
|
||||
break
|
||||
except Exception:
|
||||
_logger.exception("error while walking nodes, workflow_id=%s, node_id=%s", workflow.id, node_id)
|
||||
continue
|
||||
|
||||
for app in apps:
|
||||
app.has_draft_trigger = str(app.id) in draft_trigger_app_ids
|
||||
|
||||
|
||||
register_enum_models(console_ns, RetrievalMethod, WorkflowExecutionStatus, DatasetPermissionEnum)
|
||||
register_response_schema_models(console_ns, RedirectUrlResponse, SimpleResultResponse)
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
AppListQuery,
|
||||
StarredAppListQuery,
|
||||
CreateAppPayload,
|
||||
UpdateAppPayload,
|
||||
CopyAppPayload,
|
||||
@@ -495,7 +556,7 @@ register_schema_models(
|
||||
class AppListApi(Resource):
|
||||
@console_ns.doc("list_apps")
|
||||
@console_ns.doc(description="Get list of applications with pagination and filtering")
|
||||
@console_ns.expect(console_ns.models[AppListQuery.__name__])
|
||||
@console_ns.doc(params=query_params_from_model(AppListQuery))
|
||||
@console_ns.response(200, "Success", console_ns.models[AppPagination.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -511,6 +572,7 @@ class AppListApi(Resource):
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
mode=args.mode,
|
||||
sort_by=args.sort_by,
|
||||
name=args.name,
|
||||
tag_ids=args.tag_ids,
|
||||
creator_ids=args.creator_ids,
|
||||
@@ -524,46 +586,7 @@ class AppListApi(Resource):
|
||||
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
|
||||
return empty.model_dump(mode="json"), 200
|
||||
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
app_ids = [str(app.id) for app in app_pagination.items]
|
||||
res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids)
|
||||
if len(res) != len(app_ids):
|
||||
raise BadRequest("Invalid app id in webapp auth")
|
||||
|
||||
for app in app_pagination.items:
|
||||
if str(app.id) in res:
|
||||
app.access_mode = res[str(app.id)].access_mode
|
||||
|
||||
workflow_capable_app_ids = [
|
||||
str(app.id) for app in app_pagination.items if app.mode in {"workflow", "advanced-chat"}
|
||||
]
|
||||
draft_trigger_app_ids: set[str] = set()
|
||||
if workflow_capable_app_ids:
|
||||
draft_workflows = (
|
||||
session.execute(
|
||||
select(Workflow).where(
|
||||
Workflow.version == Workflow.VERSION_DRAFT,
|
||||
Workflow.app_id.in_(workflow_capable_app_ids),
|
||||
Workflow.tenant_id == current_tenant_id,
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
trigger_node_types = TRIGGER_NODE_TYPES
|
||||
for workflow in draft_workflows:
|
||||
node_id = None
|
||||
try:
|
||||
for node_id, node_data in workflow.walk_nodes():
|
||||
if node_data.get("type") in trigger_node_types:
|
||||
draft_trigger_app_ids.add(str(workflow.app_id))
|
||||
break
|
||||
except Exception:
|
||||
_logger.exception("error while walking nodes, workflow_id=%s, node_id=%s", workflow.id, node_id)
|
||||
continue
|
||||
|
||||
for app in app_pagination.items:
|
||||
app.has_draft_trigger = str(app.id) in draft_trigger_app_ids
|
||||
_enrich_app_list_items(session, apps=app_pagination.items, tenant_id=current_tenant_id)
|
||||
|
||||
pagination_model = AppPagination.model_validate(app_pagination, from_attributes=True)
|
||||
return pagination_model.model_dump(mode="json"), 200
|
||||
@@ -599,6 +622,78 @@ class AppListApi(Resource):
|
||||
return app_detail.model_dump(mode="json"), 201
|
||||
|
||||
|
||||
@console_ns.route("/apps/starred")
|
||||
class StarredAppListApi(Resource):
|
||||
@console_ns.doc("list_starred_apps")
|
||||
@console_ns.doc(description="Get applications starred by the current account")
|
||||
@console_ns.doc(params=query_params_from_model(StarredAppListQuery))
|
||||
@console_ns.response(200, "Success", console_ns.models[AppPagination.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@with_session(write=False)
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, current_user_id: str, session: Session):
|
||||
args = StarredAppListQuery.model_validate(_normalize_app_list_query_args(request.args))
|
||||
params = StarredAppListParams(
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
mode=args.mode,
|
||||
sort_by=args.sort_by,
|
||||
name=args.name,
|
||||
tag_ids=args.tag_ids,
|
||||
creator_ids=args.creator_ids,
|
||||
is_created_by_me=args.is_created_by_me,
|
||||
)
|
||||
|
||||
app_pagination = AppService().get_paginate_starred_apps(current_user_id, current_tenant_id, params)
|
||||
if not app_pagination:
|
||||
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
|
||||
return empty.model_dump(mode="json"), 200
|
||||
|
||||
_enrich_app_list_items(session, apps=app_pagination.items, tenant_id=current_tenant_id)
|
||||
|
||||
pagination_model = AppPagination.model_validate(app_pagination, from_attributes=True)
|
||||
return pagination_model.model_dump(mode="json"), 200
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/star")
|
||||
class AppStarApi(Resource):
|
||||
@console_ns.doc("star_app")
|
||||
@console_ns.doc(description="Star an application for the current account")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(404, "App not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@with_current_user_id
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, current_user_id: str, app_model: App):
|
||||
AppService.star_app(session, app=app_model, account_id=current_user_id)
|
||||
return dump_response(SimpleResultResponse, {"result": "success"})
|
||||
|
||||
@console_ns.doc("unstar_app")
|
||||
@console_ns.doc(description="Remove the current account's star from an application")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(404, "App not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@with_current_user_id
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def delete(self, session: Session, current_user_id: str, app_model: App):
|
||||
AppService.unstar_app(session, app=app_model, account_id=current_user_id)
|
||||
return dump_response(SimpleResultResponse, {"result": "success"})
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>")
|
||||
class AppApi(Resource):
|
||||
@console_ns.doc("get_app_detail")
|
||||
|
||||
@@ -155,19 +155,28 @@ class InstalledAppsListApi(Resource):
|
||||
if current_user.current_tenant is None:
|
||||
raise ValueError("current_user.current_tenant must not be None")
|
||||
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
|
||||
installed_app_list: list[dict[str, Any]] = [
|
||||
{
|
||||
"id": installed_app.id,
|
||||
"app": installed_app.app,
|
||||
"app_owner_tenant_id": installed_app.app_owner_tenant_id,
|
||||
"is_pinned": installed_app.is_pinned,
|
||||
"last_used_at": installed_app.last_used_at,
|
||||
"editable": current_user.role in {"owner", "admin"},
|
||||
"uninstallable": current_tenant_id == installed_app.app_owner_tenant_id,
|
||||
}
|
||||
for installed_app in installed_apps
|
||||
if installed_app.app is not None
|
||||
]
|
||||
|
||||
app_ids = [installed_app.app_id for installed_app in installed_apps]
|
||||
apps = db.session.scalars(select(App).where(App.id.in_(app_ids))).all() if app_ids else []
|
||||
apps_by_id = {app.id: app for app in apps}
|
||||
|
||||
installed_app_list: list[dict[str, Any]] = []
|
||||
for installed_app in installed_apps:
|
||||
app_model = apps_by_id.get(installed_app.app_id)
|
||||
if app_model is None:
|
||||
continue
|
||||
|
||||
installed_app_list.append(
|
||||
{
|
||||
"id": installed_app.id,
|
||||
"app": app_model,
|
||||
"app_owner_tenant_id": installed_app.app_owner_tenant_id,
|
||||
"is_pinned": installed_app.is_pinned,
|
||||
"last_used_at": installed_app.last_used_at,
|
||||
"editable": current_user.role in {"owner", "admin"},
|
||||
"uninstallable": current_tenant_id == installed_app.app_owner_tenant_id,
|
||||
}
|
||||
)
|
||||
|
||||
# filter out apps that user doesn't have access to
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
|
||||
@@ -65,15 +65,28 @@ class RecommendedAppListResponse(ResponseModel):
|
||||
categories: list[str]
|
||||
|
||||
|
||||
class LearnDifyAppListResponse(ResponseModel):
|
||||
recommended_apps: list[RecommendedAppResponse]
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
RecommendedAppsQuery,
|
||||
RecommendedAppInfoResponse,
|
||||
RecommendedAppResponse,
|
||||
RecommendedAppListResponse,
|
||||
LearnDifyAppListResponse,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_language(language: str | None, user: Account) -> str:
|
||||
if language and language in languages:
|
||||
return language
|
||||
if user.interface_language:
|
||||
return user.interface_language
|
||||
return languages[0]
|
||||
|
||||
|
||||
@console_ns.route("/explore/apps")
|
||||
class RecommendedAppListApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(RecommendedAppsQuery))
|
||||
@@ -84,13 +97,7 @@ class RecommendedAppListApi(Resource):
|
||||
def get(self, current_user: Account):
|
||||
# language args
|
||||
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
|
||||
language = args.language
|
||||
if language and language in languages:
|
||||
language_prefix = language
|
||||
elif current_user.interface_language:
|
||||
language_prefix = current_user.interface_language
|
||||
else:
|
||||
language_prefix = languages[0]
|
||||
language_prefix = _resolve_language(args.language, current_user)
|
||||
|
||||
return RecommendedAppListResponse.model_validate(
|
||||
RecommendedAppService.get_recommended_apps_and_categories(language_prefix),
|
||||
@@ -98,6 +105,23 @@ class RecommendedAppListApi(Resource):
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/explore/apps/learn-dify")
|
||||
class LearnDifyAppListApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(RecommendedAppsQuery))
|
||||
@console_ns.response(200, "Success", console_ns.models[LearnDifyAppListResponse.__name__])
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
def get(self, current_user: Account):
|
||||
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
|
||||
language_prefix = _resolve_language(args.language, current_user)
|
||||
|
||||
return LearnDifyAppListResponse.model_validate(
|
||||
RecommendedAppService.get_learn_dify_apps(language_prefix),
|
||||
from_attributes=True,
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/explore/apps/<uuid:app_id>")
|
||||
class RecommendedAppApi(Resource):
|
||||
@login_required
|
||||
|
||||
@@ -1,28 +1,51 @@
|
||||
import io
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Literal
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal, TypedDict
|
||||
|
||||
from flask import request, send_file
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.fields import SuccessResponse
|
||||
from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
register_enum_models,
|
||||
register_response_schema_models,
|
||||
register_schema_models,
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.workspace import plugin_permission_required
|
||||
from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required
|
||||
from core.helper.position_helper import is_filtered
|
||||
from core.plugin.entities.plugin import PluginCategory, PluginDeclaration, PluginInstallationSource
|
||||
from core.plugin.impl.exc import PluginDaemonClientSideError
|
||||
from core.plugin.plugin_service import PluginService
|
||||
from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort
|
||||
from core.tools.entities.common_entities import I18nObject
|
||||
from core.tools.entities.tool_entities import ToolProviderType
|
||||
from core.tools.tool_manager import ToolManager
|
||||
from fields.base import ResponseModel
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from libs.helper import dump_response
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission
|
||||
from models.provider_ids import ToolProviderID
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
from services.plugin.plugin_parameter_service import PluginParameterService
|
||||
from services.plugin.plugin_permission_service import PluginPermissionService
|
||||
from services.tools.tools_transform_service import ToolTransformService
|
||||
|
||||
|
||||
class AutoUpgradeSettingsResponse(TypedDict):
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
|
||||
upgrade_time_of_day: int
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
|
||||
exclude_plugins: list[str]
|
||||
include_plugins: list[str]
|
||||
|
||||
|
||||
class ParserList(BaseModel):
|
||||
@@ -30,6 +53,11 @@ class ParserList(BaseModel):
|
||||
page_size: int = Field(default=256, ge=1, le=256, description="Page size (1-256)")
|
||||
|
||||
|
||||
class PluginCategoryListQuery(BaseModel):
|
||||
page: int = Field(default=1, ge=1, description="Page number")
|
||||
page_size: int = Field(default=256, ge=1, le=256, description="Page size (1-256)")
|
||||
|
||||
|
||||
class ParserLatest(BaseModel):
|
||||
plugin_ids: list[str]
|
||||
|
||||
@@ -88,8 +116,8 @@ class ParserUninstall(BaseModel):
|
||||
|
||||
|
||||
class ParserPermissionChange(BaseModel):
|
||||
install_permission: TenantPluginPermission.InstallPermission
|
||||
debug_permission: TenantPluginPermission.DebugPermission
|
||||
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
|
||||
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
|
||||
|
||||
|
||||
class ParserDynamicOptions(BaseModel):
|
||||
@@ -125,13 +153,40 @@ class PluginAutoUpgradeSettingsPayload(BaseModel):
|
||||
include_plugins: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ParserPreferencesChange(BaseModel):
|
||||
permission: PluginPermissionSettingsPayload
|
||||
class PluginAutoUpgradeChangeResponse(ResponseModel):
|
||||
success: bool
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class PluginAutoUpgradeSettingsResponseModel(ResponseModel):
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
|
||||
upgrade_time_of_day: int
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
|
||||
exclude_plugins: list[str]
|
||||
include_plugins: list[str]
|
||||
|
||||
|
||||
class PluginAutoUpgradeFetchResponse(ResponseModel):
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
auto_upgrade: PluginAutoUpgradeSettingsResponseModel
|
||||
|
||||
|
||||
class ParserAutoUpgradeChange(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
auto_upgrade: PluginAutoUpgradeSettingsPayload
|
||||
|
||||
|
||||
class ParserAutoUpgradeFetch(BaseModel):
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
|
||||
|
||||
class ParserExcludePlugin(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
plugin_id: str
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
|
||||
|
||||
class ParserReadme(BaseModel):
|
||||
@@ -145,9 +200,67 @@ class PluginDebuggingKeyResponse(ResponseModel):
|
||||
port: int
|
||||
|
||||
|
||||
class PluginCategoryInstalledPluginResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
tenant_id: str
|
||||
plugin_id: str
|
||||
plugin_unique_identifier: str
|
||||
endpoints_active: int
|
||||
endpoints_setups: int
|
||||
installation_id: str
|
||||
declaration: PluginDeclaration
|
||||
runtime_type: str
|
||||
version: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
source: PluginInstallationSource
|
||||
checksum: str
|
||||
meta: Mapping[str, Any]
|
||||
|
||||
|
||||
class PluginCategoryBuiltinToolResponse(ResponseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
author: str
|
||||
name: str
|
||||
label: I18nObject
|
||||
description: I18nObject
|
||||
parameters: list[Mapping[str, Any]] | None = None
|
||||
labels: list[str]
|
||||
output_schema: Mapping[str, object]
|
||||
|
||||
|
||||
class PluginCategoryBuiltinToolProviderResponse(ResponseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
id: str
|
||||
author: str
|
||||
name: str
|
||||
plugin_id: str | None
|
||||
plugin_unique_identifier: str | None
|
||||
description: I18nObject
|
||||
icon: str | Mapping[str, str]
|
||||
icon_dark: str | Mapping[str, str] | None
|
||||
label: I18nObject
|
||||
type: ToolProviderType
|
||||
team_credentials: Mapping[str, object]
|
||||
is_team_authorization: bool
|
||||
allow_delete: bool
|
||||
tools: list[PluginCategoryBuiltinToolResponse]
|
||||
labels: list[str]
|
||||
|
||||
|
||||
class PluginCategoryListResponse(ResponseModel):
|
||||
plugins: list[PluginCategoryInstalledPluginResponse]
|
||||
builtin_tools: list[PluginCategoryBuiltinToolProviderResponse]
|
||||
has_more: bool
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
ParserList,
|
||||
PluginCategoryListQuery,
|
||||
PluginAutoUpgradeSettingsPayload,
|
||||
PluginPermissionSettingsPayload,
|
||||
ParserLatest,
|
||||
@@ -164,21 +277,57 @@ register_schema_models(
|
||||
ParserPermissionChange,
|
||||
ParserDynamicOptions,
|
||||
ParserDynamicOptionsWithCredentials,
|
||||
ParserPreferencesChange,
|
||||
ParserAutoUpgradeChange,
|
||||
ParserAutoUpgradeFetch,
|
||||
ParserExcludePlugin,
|
||||
ParserReadme,
|
||||
)
|
||||
register_response_schema_models(console_ns, PluginDebuggingKeyResponse, SuccessResponse)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
PluginAutoUpgradeChangeResponse,
|
||||
PluginAutoUpgradeFetchResponse,
|
||||
PluginAutoUpgradeSettingsResponseModel,
|
||||
PluginDebuggingKeyResponse,
|
||||
PluginCategoryInstalledPluginResponse,
|
||||
PluginCategoryBuiltinToolResponse,
|
||||
PluginCategoryBuiltinToolProviderResponse,
|
||||
PluginCategoryListResponse,
|
||||
SuccessResponse,
|
||||
)
|
||||
|
||||
register_enum_models(
|
||||
console_ns,
|
||||
TenantPluginPermission.DebugPermission,
|
||||
TenantPluginAutoUpgradeStrategy.PluginCategory,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting,
|
||||
TenantPluginPermission.InstallPermission,
|
||||
)
|
||||
|
||||
|
||||
def _default_auto_upgrade_settings(
|
||||
tenant_id: str,
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory,
|
||||
) -> AutoUpgradeSettingsResponse:
|
||||
return {
|
||||
"strategy_setting": PluginAutoUpgradeService.default_strategy_setting_for_category(category),
|
||||
"upgrade_time_of_day": PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id),
|
||||
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
"exclude_plugins": [],
|
||||
"include_plugins": [],
|
||||
}
|
||||
|
||||
|
||||
def _auto_upgrade_settings_to_dict(strategy: TenantPluginAutoUpgradeStrategy) -> AutoUpgradeSettingsResponse:
|
||||
return {
|
||||
"strategy_setting": strategy.strategy_setting,
|
||||
"upgrade_time_of_day": strategy.upgrade_time_of_day,
|
||||
"upgrade_mode": strategy.upgrade_mode,
|
||||
"exclude_plugins": strategy.exclude_plugins,
|
||||
"include_plugins": strategy.include_plugins,
|
||||
}
|
||||
|
||||
|
||||
def _read_upload_content(file: FileStorage, max_size: int) -> bytes:
|
||||
"""
|
||||
Read the uploaded file and validate its actual size before delegating to the plugin service.
|
||||
@@ -193,6 +342,33 @@ def _read_upload_content(file: FileStorage, max_size: int) -> bytes:
|
||||
return content
|
||||
|
||||
|
||||
def _list_hardcoded_builtin_tool_providers(tenant_id: str) -> list[dict[str, Any]]:
|
||||
db_builtin_providers = {
|
||||
str(ToolProviderID(provider.provider)): provider
|
||||
for provider in ToolManager.list_default_builtin_providers(tenant_id)
|
||||
}
|
||||
builtin_providers = []
|
||||
|
||||
for provider in ToolManager.list_hardcoded_providers():
|
||||
if is_filtered(
|
||||
include_set=dify_config.POSITION_TOOL_INCLUDES_SET,
|
||||
exclude_set=dify_config.POSITION_TOOL_EXCLUDES_SET,
|
||||
data=provider,
|
||||
name_func=lambda provider_controller: provider_controller.entity.identity.name,
|
||||
):
|
||||
continue
|
||||
|
||||
user_provider = ToolTransformService.builtin_provider_to_user_provider(
|
||||
provider_controller=provider,
|
||||
db_provider=db_builtin_providers.get(provider.entity.identity.name),
|
||||
decrypt_credentials=False,
|
||||
)
|
||||
ToolTransformService.repack_provider(tenant_id=tenant_id, provider=user_provider)
|
||||
builtin_providers.append(user_provider)
|
||||
|
||||
return [provider.to_dict() for provider in BuiltinToolProviderSort.sort(builtin_providers)]
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/debugging-key")
|
||||
class PluginDebuggingKeyApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[PluginDebuggingKeyResponse.__name__])
|
||||
@@ -230,6 +406,41 @@ class PluginListApi(Resource):
|
||||
return jsonable_encoder({"plugins": plugins_with_total.list, "total": plugins_with_total.total})
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/<string:category>/list")
|
||||
class PluginCategoryListApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(PluginCategoryListQuery))
|
||||
@console_ns.response(200, "Success", console_ns.models[PluginCategoryListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self, category: str):
|
||||
_, tenant_id = current_account_with_tenant()
|
||||
args = PluginCategoryListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
try:
|
||||
plugin_category = PluginCategory(category)
|
||||
except ValueError:
|
||||
return {"code": "invalid_param", "message": "invalid plugin category"}, 400
|
||||
|
||||
try:
|
||||
plugins = PluginService.list_by_category(tenant_id, plugin_category, args.page, args.page_size)
|
||||
except PluginDaemonClientSideError as e:
|
||||
return {"code": "plugin_error", "message": e.description}, 400
|
||||
|
||||
builtin_tools = []
|
||||
if plugin_category == PluginCategory.Tool:
|
||||
builtin_tools = _list_hardcoded_builtin_tool_providers(tenant_id)
|
||||
|
||||
return dump_response(
|
||||
PluginCategoryListResponse,
|
||||
{
|
||||
"plugins": jsonable_encoder(plugins.list),
|
||||
"builtin_tools": builtin_tools,
|
||||
"has_more": plugins.has_more,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/list/latest-versions")
|
||||
class PluginListLatestVersionsApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ParserLatest.__name__])
|
||||
@@ -632,11 +843,13 @@ class PluginChangePermissionApi(Resource):
|
||||
|
||||
tenant_id = current_tenant_id
|
||||
|
||||
return {
|
||||
"success": PluginPermissionService.change_permission(
|
||||
tenant_id, args.install_permission, args.debug_permission
|
||||
)
|
||||
}
|
||||
set_permission_result = PluginPermissionService.change_permission(
|
||||
tenant_id, args.install_permission, args.debug_permission
|
||||
)
|
||||
if not set_permission_result:
|
||||
return jsonable_encoder({"success": False, "message": "Failed to set permission"})
|
||||
|
||||
return jsonable_encoder({"success": True})
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/permission/fetch")
|
||||
@@ -725,9 +938,10 @@ class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource):
|
||||
return jsonable_encoder({"options": options})
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/preferences/change")
|
||||
class PluginChangePreferencesApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ParserPreferencesChange.__name__])
|
||||
@console_ns.route("/workspaces/current/plugin/auto-upgrade/change")
|
||||
class PluginChangeAutoUpgradeApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ParserAutoUpgradeChange.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[PluginAutoUpgradeChangeResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -736,38 +950,17 @@ class PluginChangePreferencesApi(Resource):
|
||||
if not user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
args = ParserPreferencesChange.model_validate(console_ns.payload)
|
||||
|
||||
permission = args.permission
|
||||
|
||||
install_permission = permission.install_permission
|
||||
debug_permission = permission.debug_permission
|
||||
args = ParserAutoUpgradeChange.model_validate(console_ns.payload)
|
||||
|
||||
auto_upgrade = args.auto_upgrade
|
||||
|
||||
strategy_setting = auto_upgrade.strategy_setting
|
||||
upgrade_time_of_day = auto_upgrade.upgrade_time_of_day
|
||||
upgrade_mode = auto_upgrade.upgrade_mode
|
||||
exclude_plugins = auto_upgrade.exclude_plugins
|
||||
include_plugins = auto_upgrade.include_plugins
|
||||
|
||||
# set permission
|
||||
set_permission_result = PluginPermissionService.change_permission(
|
||||
tenant_id,
|
||||
install_permission,
|
||||
debug_permission,
|
||||
)
|
||||
if not set_permission_result:
|
||||
return jsonable_encoder({"success": False, "message": "Failed to set permission"})
|
||||
|
||||
# set auto upgrade strategy
|
||||
set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy(
|
||||
tenant_id,
|
||||
strategy_setting,
|
||||
upgrade_time_of_day,
|
||||
upgrade_mode,
|
||||
exclude_plugins,
|
||||
include_plugins,
|
||||
auto_upgrade.strategy_setting,
|
||||
auto_upgrade.upgrade_time_of_day,
|
||||
auto_upgrade.upgrade_mode,
|
||||
auto_upgrade.exclude_plugins,
|
||||
auto_upgrade.include_plugins,
|
||||
category=args.category,
|
||||
)
|
||||
if not set_auto_upgrade_strategy_result:
|
||||
return jsonable_encoder({"success": False, "message": "Failed to set auto upgrade strategy"})
|
||||
@@ -775,48 +968,36 @@ class PluginChangePreferencesApi(Resource):
|
||||
return jsonable_encoder({"success": True})
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/preferences/fetch")
|
||||
class PluginFetchPreferencesApi(Resource):
|
||||
@console_ns.route("/workspaces/current/plugin/auto-upgrade/fetch")
|
||||
class PluginFetchAutoUpgradeApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(ParserAutoUpgradeFetch))
|
||||
@console_ns.response(200, "Success", console_ns.models[PluginAutoUpgradeFetchResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
_, tenant_id = current_account_with_tenant()
|
||||
|
||||
permission = PluginPermissionService.get_permission(tenant_id)
|
||||
permission_dict = {
|
||||
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
}
|
||||
args = ParserAutoUpgradeFetch.model_validate(request.args.to_dict(flat=True))
|
||||
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id, args.category)
|
||||
auto_upgrade_dict = (
|
||||
_auto_upgrade_settings_to_dict(auto_upgrade)
|
||||
if auto_upgrade
|
||||
else _default_auto_upgrade_settings(tenant_id, args.category)
|
||||
)
|
||||
|
||||
if permission:
|
||||
permission_dict["install_permission"] = permission.install_permission
|
||||
permission_dict["debug_permission"] = permission.debug_permission
|
||||
|
||||
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id)
|
||||
auto_upgrade_dict = {
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
|
||||
"upgrade_time_of_day": 0,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
"exclude_plugins": [],
|
||||
"include_plugins": [],
|
||||
}
|
||||
|
||||
if auto_upgrade:
|
||||
auto_upgrade_dict = {
|
||||
"strategy_setting": auto_upgrade.strategy_setting,
|
||||
"upgrade_time_of_day": auto_upgrade.upgrade_time_of_day,
|
||||
"upgrade_mode": auto_upgrade.upgrade_mode,
|
||||
"exclude_plugins": auto_upgrade.exclude_plugins,
|
||||
"include_plugins": auto_upgrade.include_plugins,
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"category": args.category,
|
||||
"auto_upgrade": auto_upgrade_dict,
|
||||
}
|
||||
|
||||
return jsonable_encoder({"permission": permission_dict, "auto_upgrade": auto_upgrade_dict})
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/preferences/autoupgrade/exclude")
|
||||
@console_ns.route("/workspaces/current/plugin/auto-upgrade/exclude")
|
||||
class PluginAutoUpgradeExcludePluginApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ParserExcludePlugin.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SuccessResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -826,7 +1007,9 @@ class PluginAutoUpgradeExcludePluginApi(Resource):
|
||||
|
||||
args = ParserExcludePlugin.model_validate(console_ns.payload)
|
||||
|
||||
return jsonable_encoder({"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args.plugin_id)})
|
||||
return jsonable_encoder(
|
||||
{"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args.plugin_id, args.category)}
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/readme")
|
||||
|
||||
@@ -31,9 +31,9 @@ from controllers.console.wraps import (
|
||||
from enums.cloud_plan import CloudPlan
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import TimestampField, dump_response, to_timestamp
|
||||
from libs.helper import OptionalTimestampField, TimestampField, dump_response, to_timestamp
|
||||
from libs.login import login_required
|
||||
from models.account import Account, Tenant, TenantCustomConfigDict, TenantStatus
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantCustomConfigDict, TenantStatus
|
||||
from services.account_service import TenantService
|
||||
from services.billing_service import BillingService, SubscriptionPlan
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
@@ -144,6 +144,7 @@ tenants_fields = {
|
||||
"plan": fields.String,
|
||||
"status": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"last_opened_at": OptionalTimestampField,
|
||||
"current": fields.Boolean,
|
||||
}
|
||||
|
||||
@@ -158,7 +159,12 @@ class TenantListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, current_user: Account):
|
||||
tenants = TenantService.get_join_tenants(current_user)
|
||||
tenant_rows: list[tuple[Tenant, TenantAccountJoin]] = [
|
||||
(tenant, membership)
|
||||
for tenant, membership in TenantService.get_workspaces_for_account(db.session, current_user.id)
|
||||
if tenant.status == TenantStatus.NORMAL
|
||||
]
|
||||
tenants = [tenant for tenant, _ in tenant_rows]
|
||||
tenant_dicts = []
|
||||
is_enterprise_only = dify_config.ENTERPRISE_ENABLED and not dify_config.BILLING_ENABLED
|
||||
is_saas = dify_config.EDITION == "CLOUD" and dify_config.BILLING_ENABLED
|
||||
@@ -171,7 +177,7 @@ class TenantListApi(Resource):
|
||||
if not tenant_plans:
|
||||
logger.warning("get_plan_bulk returned empty result, falling back to legacy feature path")
|
||||
|
||||
for tenant in tenants:
|
||||
for tenant, membership in tenant_rows:
|
||||
plan: str = CloudPlan.SANDBOX
|
||||
if is_saas:
|
||||
tenant_plan = tenant_plans.get(tenant.id)
|
||||
@@ -190,6 +196,7 @@ class TenantListApi(Resource):
|
||||
"name": tenant.name,
|
||||
"status": tenant.status,
|
||||
"created_at": tenant.created_at,
|
||||
"last_opened_at": membership.last_opened_at,
|
||||
"plan": plan,
|
||||
"current": tenant.id == current_tenant_id if current_tenant_id else False,
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ class PluginInstallTask(BasePluginEntity):
|
||||
class PluginInstallTaskStartResponse(BaseModel):
|
||||
all_installed: bool = Field(description="Whether all plugins are installed.")
|
||||
task_id: str = Field(description="The ID of the install task.")
|
||||
task: PluginInstallTask | None = Field(default=None, description="The install task.")
|
||||
|
||||
|
||||
class PluginVerification(BaseModel):
|
||||
@@ -206,6 +207,11 @@ class PluginListResponse(BaseModel):
|
||||
total: int
|
||||
|
||||
|
||||
class PluginListWithoutTotalResponse(BaseModel):
|
||||
list: list[PluginEntity]
|
||||
has_more: bool
|
||||
|
||||
|
||||
class PluginDynamicSelectOptionsResponse(BaseModel):
|
||||
options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.")
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from requests import HTTPError
|
||||
from core.plugin.entities.bundle import PluginBundleDependency
|
||||
from core.plugin.entities.plugin import (
|
||||
MissingPluginDependency,
|
||||
PluginCategory,
|
||||
PluginDeclaration,
|
||||
PluginEntity,
|
||||
PluginInstallation,
|
||||
@@ -16,6 +17,7 @@ from core.plugin.entities.plugin_daemon import (
|
||||
PluginInstallTask,
|
||||
PluginInstallTaskStartResponse,
|
||||
PluginListResponse,
|
||||
PluginListWithoutTotalResponse,
|
||||
PluginReadmeResponse,
|
||||
)
|
||||
from core.plugin.impl.base import BasePluginClient
|
||||
@@ -74,6 +76,16 @@ class PluginInstaller(BasePluginClient):
|
||||
params={"page": page, "page_size": page_size, "response_type": "paged"},
|
||||
)
|
||||
|
||||
def list_plugins_by_category(
|
||||
self, tenant_id: str, category: PluginCategory, page: int, page_size: int
|
||||
) -> PluginListWithoutTotalResponse:
|
||||
return self._request_with_plugin_daemon_response(
|
||||
"GET",
|
||||
f"plugin/{tenant_id}/management/{category.value}/list",
|
||||
PluginListWithoutTotalResponse,
|
||||
params={"page": page, "page_size": page_size, "response_type": "paged"},
|
||||
)
|
||||
|
||||
def upload_pkg(
|
||||
self,
|
||||
tenant_id: str,
|
||||
|
||||
@@ -23,6 +23,7 @@ from core.helper.marketplace import download_plugin_pkg
|
||||
from core.helper.model_provider_cache import ProviderCredentialsCache, ProviderCredentialsCacheType
|
||||
from core.plugin.entities.bundle import PluginBundleDependency
|
||||
from core.plugin.entities.plugin import (
|
||||
PluginCategory,
|
||||
PluginDeclaration,
|
||||
PluginEntity,
|
||||
PluginInstallation,
|
||||
@@ -33,6 +34,7 @@ from core.plugin.entities.plugin_daemon import (
|
||||
PluginInstallTask,
|
||||
PluginInstallTaskStatus,
|
||||
PluginListResponse,
|
||||
PluginListWithoutTotalResponse,
|
||||
PluginModelProviderEntity,
|
||||
PluginVerification,
|
||||
)
|
||||
@@ -295,6 +297,19 @@ class PluginService:
|
||||
plugins = manager.list_plugins_with_total(tenant_id, page, page_size)
|
||||
return plugins
|
||||
|
||||
@staticmethod
|
||||
def list_by_category(
|
||||
tenant_id: str, category: PluginCategory, page: int, page_size: int
|
||||
) -> PluginListWithoutTotalResponse:
|
||||
"""
|
||||
List plugins in one category with a has-more cursor signal and without calculating total.
|
||||
|
||||
The daemon scans tenant installations in the existing list order and stops once it finds one extra match.
|
||||
This keeps pagination usable before category is persisted on installation rows.
|
||||
"""
|
||||
manager = PluginInstaller()
|
||||
return manager.list_plugins_by_category(tenant_id, category, page, page_size)
|
||||
|
||||
@staticmethod
|
||||
def list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]:
|
||||
"""
|
||||
|
||||
@@ -5,6 +5,7 @@ def init_app(app: DifyApp):
|
||||
from commands import (
|
||||
add_qdrant_index,
|
||||
archive_workflow_runs,
|
||||
backfill_plugin_auto_upgrade,
|
||||
clean_expired_messages,
|
||||
clean_workflow_runs,
|
||||
cleanup_orphaned_draft_variables,
|
||||
@@ -53,6 +54,7 @@ def init_app(app: DifyApp):
|
||||
upgrade_db,
|
||||
fix_app_site_missing,
|
||||
migrate_data_for_plugin,
|
||||
backfill_plugin_auto_upgrade,
|
||||
extract_plugins,
|
||||
extract_unique_plugins,
|
||||
install_plugins,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""add plugin auto upgrade category
|
||||
|
||||
Revision ID: f6a7b8c9d012
|
||||
Revises: 8d4c2a1b9f03
|
||||
Create Date: 2026-05-15 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f6a7b8c9d012"
|
||||
down_revision = "8d4c2a1b9f03"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
LEGACY_CATEGORY = "tool"
|
||||
UNIQUE_CONSTRAINT_NAME = "unique_tenant_plugin_auto_upgrade_strategy"
|
||||
UPGRADE_TIME_INDEX_NAME = "idx_tenant_plugin_auto_upgrade_strategy_time"
|
||||
STRATEGY_TABLE_NAME = "tenant_plugin_auto_upgrade_strategies"
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table(STRATEGY_TABLE_NAME, schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("category", sa.String(length=32), server_default=LEGACY_CATEGORY, nullable=False)
|
||||
)
|
||||
batch_op.drop_constraint(UNIQUE_CONSTRAINT_NAME, type_="unique")
|
||||
batch_op.create_unique_constraint(UNIQUE_CONSTRAINT_NAME, ["tenant_id", "category"])
|
||||
batch_op.create_index(UPGRADE_TIME_INDEX_NAME, ["upgrade_time_of_day"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute(sa.text(f"DELETE FROM {STRATEGY_TABLE_NAME} WHERE category != '{LEGACY_CATEGORY}'"))
|
||||
|
||||
with op.batch_alter_table(STRATEGY_TABLE_NAME, schema=None) as batch_op:
|
||||
batch_op.drop_index(UPGRADE_TIME_INDEX_NAME)
|
||||
batch_op.drop_constraint(UNIQUE_CONSTRAINT_NAME, type_="unique")
|
||||
batch_op.drop_column("category")
|
||||
batch_op.create_unique_constraint(UNIQUE_CONSTRAINT_NAME, ["tenant_id"])
|
||||
@@ -0,0 +1,26 @@
|
||||
"""add learn dify flag to recommended apps
|
||||
|
||||
Revision ID: f5e8a9c0d2b3
|
||||
Revises: f6a7b8c9d012
|
||||
Create Date: 2026-05-18 15:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f5e8a9c0d2b3"
|
||||
down_revision = "f6a7b8c9d012"
|
||||
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("is_learn_dify", sa.Boolean(), server_default=sa.text("false"), nullable=False))
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
|
||||
batch_op.drop_column("is_learn_dify")
|
||||
@@ -1,7 +1,7 @@
|
||||
"""add customized snippets
|
||||
|
||||
Revision ID: 2b3c4d5e6f70
|
||||
Revises: 8d4c2a1b9f03
|
||||
Revises: f5e8a9c0d2b3
|
||||
Create Date: 2026-06-03 11:00:00.000000
|
||||
|
||||
"""
|
||||
@@ -13,7 +13,7 @@ import models.types
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2b3c4d5e6f70"
|
||||
down_revision = "8d4c2a1b9f03"
|
||||
down_revision = "f5e8a9c0d2b3"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""add tenant account join last opened at
|
||||
|
||||
Revision ID: b7c2d9e8a1f4
|
||||
Revises: 2b3c4d5e6f70
|
||||
Create Date: 2026-06-05 11:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "b7c2d9e8a1f4"
|
||||
down_revision = "2b3c4d5e6f70"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table("tenant_account_joins", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("last_opened_at", sa.DateTime(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("tenant_account_joins", schema=None) as batch_op:
|
||||
batch_op.drop_column("last_opened_at")
|
||||
@@ -0,0 +1,38 @@
|
||||
"""add app stars
|
||||
|
||||
Revision ID: c4d5e6f7a8b9
|
||||
Revises: b7c2d9e8a1f4
|
||||
Create Date: 2026-06-08 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import models.types
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c4d5e6f7a8b9"
|
||||
down_revision = "b7c2d9e8a1f4"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"app_stars",
|
||||
sa.Column("id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("tenant_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("app_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("account_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id", name="app_star_pkey"),
|
||||
sa.UniqueConstraint("tenant_id", "account_id", "app_id", name="app_star_tenant_account_app_unique"),
|
||||
)
|
||||
with op.batch_alter_table("app_stars", schema=None) as batch_op:
|
||||
batch_op.create_index("app_star_tenant_account_idx", ["tenant_id", "account_id"], unique=False)
|
||||
batch_op.create_index("app_star_app_idx", ["app_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("app_stars")
|
||||
@@ -71,6 +71,7 @@ from .model import (
|
||||
AppMCPServer,
|
||||
AppMode,
|
||||
AppModelConfig,
|
||||
AppStar,
|
||||
Conversation,
|
||||
DatasetRetrieverResource,
|
||||
DifySetup,
|
||||
@@ -171,6 +172,7 @@ __all__ = [
|
||||
"AppMCPServer",
|
||||
"AppMode",
|
||||
"AppModelConfig",
|
||||
"AppStar",
|
||||
"AppTrigger",
|
||||
"AppTriggerStatus",
|
||||
"AppTriggerType",
|
||||
|
||||
@@ -301,6 +301,7 @@ class TenantAccountJoin(TypeBase):
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.current_timestamp(), nullable=False, init=False, onupdate=func.current_timestamp()
|
||||
)
|
||||
last_opened_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None)
|
||||
|
||||
|
||||
class AccountIntegrate(TypeBase):
|
||||
@@ -389,6 +390,14 @@ class TenantPluginPermission(TypeBase):
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeStrategy(TypeBase):
|
||||
class PluginCategory(enum.StrEnum):
|
||||
TOOL = "tool"
|
||||
MODEL = "model"
|
||||
EXTENSION = "extension"
|
||||
AGENT_STRATEGY = "agent-strategy"
|
||||
DATASOURCE = "datasource"
|
||||
TRIGGER = "trigger"
|
||||
|
||||
class StrategySetting(enum.StrEnum):
|
||||
DISABLED = "disabled"
|
||||
FIX_ONLY = "fix_only"
|
||||
@@ -402,13 +411,20 @@ class TenantPluginAutoUpgradeStrategy(TypeBase):
|
||||
__tablename__ = "tenant_plugin_auto_upgrade_strategies"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"),
|
||||
sa.UniqueConstraint("tenant_id", name="unique_tenant_plugin_auto_upgrade_strategy"),
|
||||
sa.UniqueConstraint("tenant_id", "category", name="unique_tenant_plugin_auto_upgrade_strategy"),
|
||||
sa.Index("idx_tenant_plugin_auto_upgrade_strategy_time", "upgrade_time_of_day"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
|
||||
)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
category: Mapped[PluginCategory] = mapped_column(
|
||||
EnumText(PluginCategory, length=32),
|
||||
nullable=False,
|
||||
server_default="tool",
|
||||
default=PluginCategory.TOOL,
|
||||
)
|
||||
strategy_setting: Mapped[StrategySetting] = mapped_column(
|
||||
EnumText(StrategySetting, length=16),
|
||||
nullable=False,
|
||||
|
||||
@@ -654,6 +654,29 @@ class App(Base):
|
||||
return None
|
||||
|
||||
|
||||
class AppStar(Base):
|
||||
"""Account-scoped star marker for apps in a workspace."""
|
||||
|
||||
__tablename__ = "app_stars"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="app_star_pkey"),
|
||||
sa.UniqueConstraint("tenant_id", "account_id", "app_id", name="app_star_tenant_account_app_unique"),
|
||||
sa.Index("app_star_tenant_account_idx", "tenant_id", "account_id"),
|
||||
sa.Index("app_star_app_idx", "app_id"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuidv7()))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
sa.DateTime, nullable=False, server_default=func.current_timestamp()
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AppStar app_id={self.app_id} account_id={self.account_id}>"
|
||||
|
||||
|
||||
class AppModelConfig(TypeBase):
|
||||
__tablename__ = "app_model_configs"
|
||||
__table_args__ = (sa.PrimaryKeyConstraint("id", name="app_model_config_pkey"), sa.Index("app_app_id_idx", "app_id"))
|
||||
@@ -907,6 +930,9 @@ class RecommendedApp(TypeBase):
|
||||
custom_disclaimer: Mapped[str] = mapped_column(LongText, default="")
|
||||
position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
|
||||
is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
|
||||
is_learn_dify: Mapped[bool] = mapped_column(
|
||||
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
|
||||
)
|
||||
install_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
|
||||
language: Mapped[str] = mapped_column(
|
||||
String(255),
|
||||
|
||||
@@ -6043,6 +6043,21 @@ Delete an API key for a dataset
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | [RecommendedAppListResponse](#recommendedapplistresponse) |
|
||||
|
||||
### /explore/apps/learn-dify
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| language | query | Language code for recommended app localization | No | string |
|
||||
|
||||
##### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | [LearnDifyAppListResponse](#learndifyapplistresponse) |
|
||||
|
||||
### /explore/apps/{app_id}
|
||||
|
||||
#### GET
|
||||
@@ -10180,6 +10195,51 @@ Returns permission flags that control workspace features like member invitations
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /workspaces/current/plugin/auto-upgrade/change
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| payload | body | | Yes | [ParserAutoUpgradeChange](#parserautoupgradechange) |
|
||||
|
||||
##### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /workspaces/current/plugin/auto-upgrade/exclude
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| payload | body | | Yes | [ParserExcludePlugin](#parserexcludeplugin) |
|
||||
|
||||
##### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /workspaces/current/plugin/auto-upgrade/fetch
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| category | query | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /workspaces/current/plugin/debugging-key
|
||||
|
||||
#### GET
|
||||
@@ -10382,45 +10442,6 @@ Fetch dynamic options using credentials directly (for edit mode)
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /workspaces/current/plugin/preferences/autoupgrade/exclude
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| payload | body | | Yes | [ParserExcludePlugin](#parserexcludeplugin) |
|
||||
|
||||
##### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /workspaces/current/plugin/preferences/change
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| payload | body | | Yes | [ParserPreferencesChange](#parserpreferenceschange) |
|
||||
|
||||
##### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /workspaces/current/plugin/preferences/fetch
|
||||
|
||||
#### GET
|
||||
##### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /workspaces/current/plugin/readme
|
||||
|
||||
#### GET
|
||||
@@ -15137,6 +15158,12 @@ Enum class for large language model mode.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| LLMMode | string | Enum class for large language model mode. | |
|
||||
|
||||
#### LearnDifyAppListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| recommended_apps | [ [RecommendedAppResponse](#recommendedappresponse) ] | | Yes |
|
||||
|
||||
#### LegacyEndpointUpdatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -15648,6 +15675,19 @@ Form input definition.
|
||||
| file_name | string | | Yes |
|
||||
| plugin_unique_identifier | string | | Yes |
|
||||
|
||||
#### ParserAutoUpgradeChange
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
|
||||
#### ParserAutoUpgradeFetch
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
|
||||
#### ParserCreateCredential
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -15744,6 +15784,7 @@ Form input definition.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
|
||||
#### ParserGetCredentials
|
||||
@@ -15831,8 +15872,8 @@ Form input definition.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| debug_permission | [DebugPermission](#debugpermission) | | Yes |
|
||||
| install_permission | [InstallPermission](#installpermission) | | Yes |
|
||||
| debug_permission | [DebugPermission](#debugpermission) | | No |
|
||||
| install_permission | [InstallPermission](#installpermission) | | No |
|
||||
|
||||
#### ParserPluginIdentifierQuery
|
||||
|
||||
@@ -15862,13 +15903,6 @@ Form input definition.
|
||||
| model | string | | Yes |
|
||||
| model_type | [ModelType](#modeltype) | | Yes |
|
||||
|
||||
#### ParserPreferencesChange
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes |
|
||||
| permission | [PluginPermissionSettingsPayload](#pluginpermissionsettingspayload) | | Yes |
|
||||
|
||||
#### ParserPreferredProviderType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -16023,6 +16057,12 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| upgrade_mode | [UpgradeMode](#upgrademode) | | No |
|
||||
| upgrade_time_of_day | integer | | No |
|
||||
|
||||
#### PluginCategory
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| PluginCategory | string | | |
|
||||
|
||||
#### PluginDebuggingKeyResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@@ -73,6 +73,7 @@ def check_upgradable_plugin_task():
|
||||
strategy.upgrade_mode,
|
||||
strategy.exclude_plugins,
|
||||
strategy.include_plugins,
|
||||
strategy.category,
|
||||
)
|
||||
|
||||
# Only sleep if batch_interval_time > 0.0001 AND current batch is not the last one
|
||||
|
||||
@@ -70,6 +70,7 @@ from services.errors.account import (
|
||||
)
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
from tasks.delete_account_task import delete_account_task
|
||||
from tasks.mail_account_deletion_task import send_account_deletion_verification_code
|
||||
from tasks.mail_change_mail_task import (
|
||||
@@ -263,6 +264,7 @@ class AccountService:
|
||||
|
||||
account.set_tenant_id(available_ta.tenant_id)
|
||||
available_ta.current = True
|
||||
available_ta.last_opened_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
|
||||
AccountService._refresh_account_last_active(account)
|
||||
@@ -1167,15 +1169,17 @@ class TenantService:
|
||||
db.session.add(tenant)
|
||||
db.session.commit()
|
||||
|
||||
plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant.id,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
)
|
||||
db.session.add(plugin_upgrade_strategy)
|
||||
for category in TenantPluginAutoUpgradeStrategy.PluginCategory:
|
||||
plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant.id,
|
||||
category=category,
|
||||
strategy_setting=PluginAutoUpgradeService.default_strategy_setting_for_category(category),
|
||||
upgrade_time_of_day=PluginAutoUpgradeService.default_upgrade_time_of_day(tenant.id),
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
)
|
||||
db.session.add(plugin_upgrade_strategy)
|
||||
db.session.commit()
|
||||
|
||||
tenant.encrypt_public_key = generate_key_pair(tenant.id)
|
||||
@@ -1447,6 +1451,7 @@ class TenantService:
|
||||
.values(current=False)
|
||||
)
|
||||
tenant_account_join.current = True
|
||||
tenant_account_join.last_opened_at = naive_utc_now()
|
||||
# Set the current tenant for the account
|
||||
account.set_tenant_id(tenant_account_join.tenant_id)
|
||||
db.session.commit()
|
||||
|
||||
@@ -22,7 +22,7 @@ from graphon.model_runtime.entities.model_entities import ModelPropertyKey, Mode
|
||||
from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.login import current_user
|
||||
from models import Account
|
||||
from models import Account, AppStar
|
||||
from models.agent import AgentIconType
|
||||
from models.model import App, AppMode, AppModelConfig, IconType, Site
|
||||
from models.tools import ApiToolProvider
|
||||
@@ -35,19 +35,29 @@ from tasks.remove_app_and_related_data_task import remove_app_and_related_data_t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AppListSortBy = Literal["last_modified", "recently_created", "earliest_created"]
|
||||
|
||||
class AppListParams(BaseModel):
|
||||
|
||||
class AppListBaseParams(BaseModel):
|
||||
page: int = Field(default=1, ge=1)
|
||||
limit: int = Field(default=20, ge=1, le=100)
|
||||
mode: Literal["completion", "chat", "advanced-chat", "workflow", "agent-chat", "agent", "channel", "all"] = "all"
|
||||
sort_by: AppListSortBy = "last_modified"
|
||||
name: str | None = None
|
||||
tag_ids: list[str] | None = None
|
||||
creator_ids: list[str] | None = None
|
||||
is_created_by_me: bool | None = None
|
||||
|
||||
|
||||
class AppListParams(AppListBaseParams):
|
||||
status: str | None = None
|
||||
openapi_visible: bool = False
|
||||
|
||||
|
||||
class StarredAppListParams(AppListBaseParams):
|
||||
pass
|
||||
|
||||
|
||||
class CreateAppParams(BaseModel):
|
||||
name: str = Field(min_length=1)
|
||||
description: str | None = None
|
||||
@@ -61,6 +71,83 @@ class CreateAppParams(BaseModel):
|
||||
|
||||
|
||||
class AppService:
|
||||
@staticmethod
|
||||
def _build_app_list_filters(
|
||||
user_id: str, tenant_id: str, params: AppListBaseParams
|
||||
) -> list[sa.ColumnElement[bool]]:
|
||||
filters = [App.tenant_id == tenant_id, App.is_universal == False]
|
||||
|
||||
if params.mode == "workflow":
|
||||
filters.append(App.mode == AppMode.WORKFLOW)
|
||||
elif params.mode == "completion":
|
||||
filters.append(App.mode == AppMode.COMPLETION)
|
||||
elif params.mode == "chat":
|
||||
filters.append(App.mode == AppMode.CHAT)
|
||||
elif params.mode == "advanced-chat":
|
||||
filters.append(App.mode == AppMode.ADVANCED_CHAT)
|
||||
elif params.mode == "agent-chat":
|
||||
filters.append(App.mode == AppMode.AGENT_CHAT)
|
||||
elif params.mode == "agent":
|
||||
filters.append(App.mode == AppMode.AGENT)
|
||||
|
||||
if isinstance(params, AppListParams):
|
||||
if params.status:
|
||||
filters.append(App.status == params.status)
|
||||
# OpenAPI surface visibility gate. Pushed into the query so
|
||||
# `pagination.total` reflects only apps the openapi caller can
|
||||
# actually reach; post-filtering by enable_api after the page
|
||||
# arrives would make `total` page-dependent.
|
||||
if params.openapi_visible:
|
||||
filters.append(App.enable_api.is_(True))
|
||||
|
||||
if params.is_created_by_me:
|
||||
filters.append(App.created_by == user_id)
|
||||
if params.creator_ids:
|
||||
filters.append(App.created_by.in_(params.creator_ids))
|
||||
if params.name:
|
||||
from libs.helper import escape_like_pattern
|
||||
|
||||
name = params.name[:30]
|
||||
escaped_name = escape_like_pattern(name)
|
||||
filters.append(App.name.ilike(f"%{escaped_name}%", escape="\\"))
|
||||
if params.tag_ids and len(params.tag_ids) > 0:
|
||||
target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, params.tag_ids)
|
||||
if target_ids and len(target_ids) > 0:
|
||||
filters.append(App.id.in_(target_ids))
|
||||
else:
|
||||
return []
|
||||
|
||||
return filters
|
||||
|
||||
@staticmethod
|
||||
def _build_app_list_order_by(sort_by: AppListSortBy) -> sa.ColumnElement[Any]:
|
||||
return {
|
||||
"last_modified": App.updated_at.desc(),
|
||||
"recently_created": App.created_at.desc(),
|
||||
"earliest_created": App.created_at.asc(),
|
||||
}[sort_by]
|
||||
|
||||
@staticmethod
|
||||
def get_starred_app_ids(
|
||||
session: Session | scoped_session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
app_ids: Sequence[str],
|
||||
) -> set[str]:
|
||||
"""Return app IDs starred by this account within the tenant."""
|
||||
if not app_ids:
|
||||
return set()
|
||||
|
||||
starred_app_ids = session.scalars(
|
||||
select(AppStar.app_id).where(
|
||||
AppStar.tenant_id == tenant_id,
|
||||
AppStar.account_id == account_id,
|
||||
AppStar.app_id.in_(list(app_ids)),
|
||||
)
|
||||
).all()
|
||||
return set(starred_app_ids)
|
||||
|
||||
@staticmethod
|
||||
def get_app_by_id(
|
||||
session: Session | scoped_session,
|
||||
@@ -108,61 +195,104 @@ class AppService:
|
||||
|
||||
def get_paginate_apps(self, user_id: str, tenant_id: str, params: AppListParams) -> Pagination | None:
|
||||
"""
|
||||
Get app list with pagination
|
||||
Get app list with pagination, filters, and explicit sort order.
|
||||
:param user_id: user id
|
||||
:param tenant_id: tenant id
|
||||
:param params: query parameters
|
||||
:return:
|
||||
"""
|
||||
filters = [App.tenant_id == tenant_id, App.is_universal == False]
|
||||
filters = self._build_app_list_filters(user_id, tenant_id, params)
|
||||
if not filters:
|
||||
return None
|
||||
|
||||
if params.mode == "workflow":
|
||||
filters.append(App.mode == AppMode.WORKFLOW)
|
||||
elif params.mode == "completion":
|
||||
filters.append(App.mode == AppMode.COMPLETION)
|
||||
elif params.mode == "chat":
|
||||
filters.append(App.mode == AppMode.CHAT)
|
||||
elif params.mode == "advanced-chat":
|
||||
filters.append(App.mode == AppMode.ADVANCED_CHAT)
|
||||
elif params.mode == "agent-chat":
|
||||
filters.append(App.mode == AppMode.AGENT_CHAT)
|
||||
elif params.mode == "agent":
|
||||
filters.append(App.mode == AppMode.AGENT)
|
||||
|
||||
if params.status:
|
||||
filters.append(App.status == params.status)
|
||||
# OpenAPI surface visibility gate. Pushed into the query so
|
||||
# `pagination.total` reflects only apps the openapi caller can
|
||||
# actually reach — post-filtering by enable_api after the page
|
||||
# arrives would make `total` page-dependent.
|
||||
if params.openapi_visible:
|
||||
filters.append(App.enable_api.is_(True))
|
||||
if params.is_created_by_me:
|
||||
filters.append(App.created_by == user_id)
|
||||
if params.creator_ids:
|
||||
filters.append(App.created_by.in_(params.creator_ids))
|
||||
if params.name:
|
||||
from libs.helper import escape_like_pattern
|
||||
|
||||
name = params.name[:30]
|
||||
escaped_name = escape_like_pattern(name)
|
||||
filters.append(App.name.ilike(f"%{escaped_name}%", escape="\\"))
|
||||
if params.tag_ids and len(params.tag_ids) > 0:
|
||||
target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, params.tag_ids)
|
||||
if target_ids and len(target_ids) > 0:
|
||||
filters.append(App.id.in_(target_ids))
|
||||
else:
|
||||
return None
|
||||
order_by = self._build_app_list_order_by(params.sort_by)
|
||||
|
||||
app_models = db.paginate(
|
||||
sa.select(App).where(*filters).order_by(App.created_at.desc()),
|
||||
sa.select(App).where(*filters).order_by(order_by),
|
||||
page=params.page,
|
||||
per_page=params.limit,
|
||||
error_out=False,
|
||||
)
|
||||
|
||||
app_ids = [str(app.id) for app in app_models.items]
|
||||
starred_app_ids = self.get_starred_app_ids(
|
||||
db.session,
|
||||
tenant_id=tenant_id,
|
||||
account_id=user_id,
|
||||
app_ids=app_ids,
|
||||
)
|
||||
for app in app_models.items:
|
||||
app.is_starred = str(app.id) in starred_app_ids # type: ignore[attr-defined]
|
||||
|
||||
return app_models
|
||||
|
||||
def get_paginate_starred_apps(
|
||||
self, user_id: str, tenant_id: str, params: StarredAppListParams
|
||||
) -> Pagination | None:
|
||||
"""
|
||||
Get apps starred by the current account with pagination, filters, and explicit sort order.
|
||||
"""
|
||||
filters = self._build_app_list_filters(user_id, tenant_id, params)
|
||||
if not filters:
|
||||
return None
|
||||
|
||||
order_by = self._build_app_list_order_by(params.sort_by)
|
||||
app_models = db.paginate(
|
||||
sa.select(App)
|
||||
.join(
|
||||
AppStar,
|
||||
sa.and_(
|
||||
AppStar.tenant_id == App.tenant_id,
|
||||
AppStar.app_id == App.id,
|
||||
AppStar.account_id == user_id,
|
||||
),
|
||||
)
|
||||
.where(AppStar.tenant_id == tenant_id, *filters)
|
||||
.order_by(order_by),
|
||||
page=params.page,
|
||||
per_page=params.limit,
|
||||
error_out=False,
|
||||
)
|
||||
|
||||
for app in app_models.items:
|
||||
app.is_starred = True # type: ignore[attr-defined]
|
||||
|
||||
return app_models
|
||||
|
||||
@staticmethod
|
||||
def star_app(session: Session, *, app: App, account_id: str) -> None:
|
||||
"""Create the account's app star if it does not already exist."""
|
||||
existing_star = session.scalar(
|
||||
select(AppStar)
|
||||
.where(
|
||||
AppStar.tenant_id == app.tenant_id,
|
||||
AppStar.app_id == app.id,
|
||||
AppStar.account_id == account_id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if existing_star:
|
||||
return
|
||||
|
||||
session.add(AppStar(tenant_id=app.tenant_id, app_id=app.id, account_id=account_id))
|
||||
|
||||
@staticmethod
|
||||
def unstar_app(session: Session, *, app: App, account_id: str) -> None:
|
||||
"""Remove the account's app star if present."""
|
||||
existing_star = session.scalar(
|
||||
select(AppStar)
|
||||
.where(
|
||||
AppStar.tenant_id == app.tenant_id,
|
||||
AppStar.app_id == app.id,
|
||||
AppStar.account_id == account_id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if not existing_star:
|
||||
return
|
||||
|
||||
session.delete(existing_star)
|
||||
|
||||
def create_app(self, tenant_id: str, params: CreateAppParams, account: Account) -> App:
|
||||
"""
|
||||
Create app
|
||||
@@ -493,6 +623,7 @@ class AppService:
|
||||
"""
|
||||
app_was_deleted.send(app)
|
||||
|
||||
db.session.execute(sa.delete(AppStar).where(AppStar.tenant_id == app.tenant_id, AppStar.app_id == app.id))
|
||||
db.session.delete(app)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -1,18 +1,295 @@
|
||||
"""Manage tenant plugin auto-upgrade strategies.
|
||||
|
||||
The storage is category-scoped: each tenant can have one strategy per plugin
|
||||
category. Public mutation helpers require an explicit category so callers do
|
||||
not accidentally overwrite every plugin type with one workspace-level policy.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha256
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
PLUGIN_CATEGORIES = tuple(PluginCategory)
|
||||
SECONDS_PER_DAY = 24 * 60 * 60
|
||||
AUTO_UPGRADE_CHECK_SLOT_SECONDS = 15 * 60
|
||||
AUTO_UPGRADE_CHECK_SLOT_COUNT = SECONDS_PER_DAY // AUTO_UPGRADE_CHECK_SLOT_SECONDS
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PluginAutoUpgradeBackfillResult:
|
||||
created_count: int
|
||||
normalized: bool
|
||||
|
||||
|
||||
class PluginAutoUpgradeService:
|
||||
@staticmethod
|
||||
def get_strategy(tenant_id: str) -> TenantPluginAutoUpgradeStrategy | None:
|
||||
with session_factory.create_session() as session:
|
||||
return session.scalar(
|
||||
select(TenantPluginAutoUpgradeStrategy)
|
||||
.where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
def default_strategy_setting_for_category(
|
||||
category: PluginCategory,
|
||||
) -> TenantPluginAutoUpgradeStrategy.StrategySetting:
|
||||
if category == PluginCategory.MODEL:
|
||||
return TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
return TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
|
||||
|
||||
@staticmethod
|
||||
def default_upgrade_time_of_day(tenant_id: str) -> int:
|
||||
"""Spread default checks across 15-minute aligned slots by tenant."""
|
||||
hash_input = tenant_id.encode()
|
||||
slot = int.from_bytes(sha256(hash_input).digest()[:8], "big") % AUTO_UPGRADE_CHECK_SLOT_COUNT
|
||||
return slot * AUTO_UPGRADE_CHECK_SLOT_SECONDS
|
||||
|
||||
@staticmethod
|
||||
def _coerce_category(category: object) -> PluginCategory | None:
|
||||
"""Accept daemon enum/string categories and ignore unknown values."""
|
||||
category_value = getattr(category, "value", category)
|
||||
if category_value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return PluginCategory(str(category_value))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_installed_plugin_categories(tenant_id: str) -> dict[str, PluginCategory]:
|
||||
"""Build a plugin_id -> category map for splitting legacy include/exclude lists."""
|
||||
installed_plugins = PluginInstaller().list_plugins(tenant_id)
|
||||
plugin_categories: dict[str, PluginCategory] = {}
|
||||
|
||||
for plugin in installed_plugins:
|
||||
plugin_category = PluginAutoUpgradeService._coerce_category(plugin.declaration.category)
|
||||
if plugin_category is not None:
|
||||
plugin_categories[plugin.plugin_id] = plugin_category
|
||||
|
||||
return plugin_categories
|
||||
|
||||
@staticmethod
|
||||
def _filter_plugin_ids_for_category(
|
||||
plugin_ids: list[str],
|
||||
category: PluginCategory,
|
||||
plugin_categories: dict[str, PluginCategory],
|
||||
) -> list[str]:
|
||||
return [plugin_id for plugin_id in plugin_ids if plugin_categories.get(plugin_id) == category]
|
||||
|
||||
@staticmethod
|
||||
def _log_unknown_plugin_ids(
|
||||
tenant_id: str,
|
||||
field_name: str,
|
||||
plugin_ids: list[str],
|
||||
plugin_categories: dict[str, PluginCategory],
|
||||
) -> None:
|
||||
unknown_plugin_ids = [plugin_id for plugin_id in plugin_ids if plugin_id not in plugin_categories]
|
||||
if not unknown_plugin_ids:
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
"Skipped unknown plugin IDs while backfilling plugin auto-upgrade strategies: "
|
||||
"tenant_id=%s, field=%s, plugin_ids=%s",
|
||||
tenant_id,
|
||||
field_name,
|
||||
unknown_plugin_ids,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _has_default_strategy(strategy: TenantPluginAutoUpgradeStrategy) -> bool:
|
||||
return (
|
||||
strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
|
||||
and strategy.upgrade_time_of_day == 0
|
||||
and strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
and not strategy.exclude_plugins
|
||||
and not strategy.include_plugins
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _strategy_setting_for_category(
|
||||
source_strategy: TenantPluginAutoUpgradeStrategy,
|
||||
category: PluginCategory,
|
||||
source_has_default_strategy: bool,
|
||||
) -> TenantPluginAutoUpgradeStrategy.StrategySetting:
|
||||
# Only pure legacy defaults adopt the new model=latest default. User-edited
|
||||
# strategies keep their original setting across all categories.
|
||||
if source_has_default_strategy:
|
||||
return PluginAutoUpgradeService.default_strategy_setting_for_category(category)
|
||||
return source_strategy.strategy_setting
|
||||
|
||||
@staticmethod
|
||||
def _upgrade_time_of_day_for_category(
|
||||
tenant_id: str,
|
||||
source_strategy: TenantPluginAutoUpgradeStrategy,
|
||||
source_has_default_strategy: bool,
|
||||
) -> int:
|
||||
# Pure legacy defaults are spread by tenant so all default rows do not
|
||||
# concentrate in the same scheduler window. User-edited schedules keep their time.
|
||||
if source_has_default_strategy:
|
||||
return PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id)
|
||||
return source_strategy.upgrade_time_of_day
|
||||
|
||||
@staticmethod
|
||||
def backfill_strategy_categories(
|
||||
tenant_id: str,
|
||||
) -> PluginAutoUpgradeBackfillResult:
|
||||
"""Create missing category strategies and split include/exclude lists when needed.
|
||||
|
||||
The historical row is treated as the workspace-level source strategy.
|
||||
New category rows copy it first, then plugin lists are narrowed by real
|
||||
plugin category when the source strategy contains include/exclude IDs.
|
||||
"""
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
strategies = list(
|
||||
session.scalars(
|
||||
select(TenantPluginAutoUpgradeStrategy).where(
|
||||
TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id
|
||||
)
|
||||
).all()
|
||||
)
|
||||
if not strategies:
|
||||
return PluginAutoUpgradeBackfillResult(created_count=0, normalized=False)
|
||||
|
||||
# Schema migration marks the historical workspace-level row as tool.
|
||||
source_strategy = next(
|
||||
(strategy for strategy in strategies if strategy.category == PluginCategory.TOOL),
|
||||
strategies[0],
|
||||
)
|
||||
source_has_default_strategy = PluginAutoUpgradeService._has_default_strategy(source_strategy)
|
||||
strategies_by_category = {strategy.category: strategy for strategy in strategies}
|
||||
exclude_plugins = source_strategy.exclude_plugins
|
||||
include_plugins = source_strategy.include_plugins
|
||||
should_split_plugin_lists = bool(exclude_plugins or include_plugins)
|
||||
# Query daemon only for tenants that actually customized plugin lists.
|
||||
plugin_categories = (
|
||||
PluginAutoUpgradeService._get_installed_plugin_categories(tenant_id)
|
||||
if should_split_plugin_lists
|
||||
else {}
|
||||
)
|
||||
if should_split_plugin_lists:
|
||||
PluginAutoUpgradeService._log_unknown_plugin_ids(
|
||||
tenant_id,
|
||||
"exclude_plugins",
|
||||
exclude_plugins,
|
||||
plugin_categories,
|
||||
)
|
||||
PluginAutoUpgradeService._log_unknown_plugin_ids(
|
||||
tenant_id,
|
||||
"include_plugins",
|
||||
include_plugins,
|
||||
plugin_categories,
|
||||
)
|
||||
|
||||
created_count = 0
|
||||
for category in PLUGIN_CATEGORIES:
|
||||
strategy = strategies_by_category.get(category)
|
||||
if strategy is None:
|
||||
# Start from the legacy workspace-level behavior before narrowing lists.
|
||||
strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant_id,
|
||||
category=category,
|
||||
strategy_setting=PluginAutoUpgradeService._strategy_setting_for_category(
|
||||
source_strategy, category, source_has_default_strategy
|
||||
),
|
||||
upgrade_time_of_day=PluginAutoUpgradeService._upgrade_time_of_day_for_category(
|
||||
tenant_id, source_strategy, source_has_default_strategy
|
||||
),
|
||||
upgrade_mode=source_strategy.upgrade_mode,
|
||||
exclude_plugins=source_strategy.exclude_plugins.copy(),
|
||||
include_plugins=source_strategy.include_plugins.copy(),
|
||||
)
|
||||
session.add(strategy)
|
||||
created_count += 1
|
||||
elif source_has_default_strategy:
|
||||
strategy.strategy_setting = PluginAutoUpgradeService.default_strategy_setting_for_category(
|
||||
strategy.category
|
||||
)
|
||||
strategy.upgrade_time_of_day = PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id)
|
||||
|
||||
if not should_split_plugin_lists:
|
||||
continue
|
||||
|
||||
# Narrow include/exclude lists to the current category after all rows exist.
|
||||
strategy.exclude_plugins = PluginAutoUpgradeService._filter_plugin_ids_for_category(
|
||||
exclude_plugins,
|
||||
strategy.category,
|
||||
plugin_categories,
|
||||
)
|
||||
strategy.include_plugins = PluginAutoUpgradeService._filter_plugin_ids_for_category(
|
||||
include_plugins,
|
||||
strategy.category,
|
||||
plugin_categories,
|
||||
)
|
||||
|
||||
return PluginAutoUpgradeBackfillResult(created_count=created_count, normalized=should_split_plugin_lists)
|
||||
|
||||
@staticmethod
|
||||
def _get_strategy(
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
) -> TenantPluginAutoUpgradeStrategy | None:
|
||||
return session.scalar(
|
||||
select(TenantPluginAutoUpgradeStrategy)
|
||||
.where(
|
||||
TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id,
|
||||
TenantPluginAutoUpgradeStrategy.category == category,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_strategy(
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
) -> TenantPluginAutoUpgradeStrategy | None:
|
||||
with session_factory.create_session() as session:
|
||||
return PluginAutoUpgradeService._get_strategy(session, tenant_id, category)
|
||||
|
||||
@staticmethod
|
||||
def get_strategies(tenant_id: str) -> list[TenantPluginAutoUpgradeStrategy]:
|
||||
with session_factory.create_session() as session:
|
||||
return list(
|
||||
session.scalars(
|
||||
select(TenantPluginAutoUpgradeStrategy).where(
|
||||
TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id
|
||||
)
|
||||
).all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _change_strategy(
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
|
||||
upgrade_time_of_day: int,
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
exclude_plugins: list[str],
|
||||
include_plugins: list[str],
|
||||
) -> None:
|
||||
exist_strategy = PluginAutoUpgradeService._get_strategy(session, tenant_id, category)
|
||||
if not exist_strategy:
|
||||
strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant_id,
|
||||
category=category,
|
||||
strategy_setting=strategy_setting,
|
||||
upgrade_time_of_day=upgrade_time_of_day,
|
||||
upgrade_mode=upgrade_mode,
|
||||
exclude_plugins=exclude_plugins,
|
||||
include_plugins=include_plugins,
|
||||
)
|
||||
session.add(strategy)
|
||||
else:
|
||||
exist_strategy.strategy_setting = strategy_setting
|
||||
exist_strategy.upgrade_time_of_day = upgrade_time_of_day
|
||||
exist_strategy.upgrade_mode = upgrade_mode
|
||||
exist_strategy.exclude_plugins = exclude_plugins
|
||||
exist_strategy.include_plugins = include_plugins
|
||||
|
||||
@staticmethod
|
||||
def change_strategy(
|
||||
@@ -22,64 +299,72 @@ class PluginAutoUpgradeService:
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
exclude_plugins: list[str],
|
||||
include_plugins: list[str],
|
||||
category: PluginCategory,
|
||||
) -> bool:
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
exist_strategy = session.scalar(
|
||||
select(TenantPluginAutoUpgradeStrategy)
|
||||
.where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
PluginAutoUpgradeService._change_strategy(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
category=category,
|
||||
strategy_setting=strategy_setting,
|
||||
upgrade_time_of_day=upgrade_time_of_day,
|
||||
upgrade_mode=upgrade_mode,
|
||||
exclude_plugins=exclude_plugins,
|
||||
include_plugins=include_plugins,
|
||||
)
|
||||
if not exist_strategy:
|
||||
strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant_id,
|
||||
strategy_setting=strategy_setting,
|
||||
upgrade_time_of_day=upgrade_time_of_day,
|
||||
upgrade_mode=upgrade_mode,
|
||||
exclude_plugins=exclude_plugins,
|
||||
include_plugins=include_plugins,
|
||||
)
|
||||
session.add(strategy)
|
||||
else:
|
||||
exist_strategy.strategy_setting = strategy_setting
|
||||
exist_strategy.upgrade_time_of_day = upgrade_time_of_day
|
||||
exist_strategy.upgrade_mode = upgrade_mode
|
||||
exist_strategy.exclude_plugins = exclude_plugins
|
||||
exist_strategy.include_plugins = include_plugins
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def exclude_plugin(tenant_id: str, plugin_id: str) -> bool:
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
exist_strategy = session.scalar(
|
||||
select(TenantPluginAutoUpgradeStrategy)
|
||||
.where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
def _exclude_plugin(
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
plugin_id: str,
|
||||
) -> None:
|
||||
"""Remove one plugin from automatic updates for a single category strategy."""
|
||||
exist_strategy = PluginAutoUpgradeService._get_strategy(session, tenant_id, category)
|
||||
if not exist_strategy:
|
||||
PluginAutoUpgradeService._change_strategy(
|
||||
session,
|
||||
tenant_id,
|
||||
category,
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
0,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
[plugin_id],
|
||||
[],
|
||||
)
|
||||
if not exist_strategy:
|
||||
# create for this tenant
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant_id,
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
0,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
[plugin_id],
|
||||
[],
|
||||
)
|
||||
return True
|
||||
else:
|
||||
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
|
||||
if plugin_id not in exist_strategy.exclude_plugins:
|
||||
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
|
||||
new_exclude_plugins.append(plugin_id)
|
||||
exist_strategy.exclude_plugins = new_exclude_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
|
||||
if plugin_id in exist_strategy.include_plugins:
|
||||
new_include_plugins = exist_strategy.include_plugins.copy()
|
||||
new_include_plugins.remove(plugin_id)
|
||||
exist_strategy.include_plugins = new_include_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
exist_strategy.exclude_plugins = [plugin_id]
|
||||
else:
|
||||
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
|
||||
# In exclude mode, disabling one plugin means adding it to exclude_plugins.
|
||||
if plugin_id not in exist_strategy.exclude_plugins:
|
||||
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
|
||||
new_exclude_plugins.append(plugin_id)
|
||||
exist_strategy.exclude_plugins = new_exclude_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
|
||||
# In partial mode, disabling one plugin means removing it from include_plugins.
|
||||
if plugin_id in exist_strategy.include_plugins:
|
||||
new_include_plugins = exist_strategy.include_plugins.copy()
|
||||
new_include_plugins.remove(plugin_id)
|
||||
exist_strategy.include_plugins = new_include_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||
# In all mode, switch to exclude mode so only this plugin is skipped.
|
||||
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
exist_strategy.exclude_plugins = [plugin_id]
|
||||
|
||||
return True
|
||||
@staticmethod
|
||||
def exclude_plugin(
|
||||
tenant_id: str,
|
||||
plugin_id: str,
|
||||
category: PluginCategory,
|
||||
) -> bool:
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
PluginAutoUpgradeService._exclude_plugin(
|
||||
session,
|
||||
tenant_id,
|
||||
category,
|
||||
plugin_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, TypedDict, override
|
||||
from typing import Any, NotRequired, TypedDict, override
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -22,6 +22,7 @@ class RecommendedAppItemDict(TypedDict):
|
||||
categories: list[str]
|
||||
position: int
|
||||
is_listed: bool
|
||||
can_trial: NotRequired[bool]
|
||||
|
||||
|
||||
class RecommendedAppsResultDict(TypedDict):
|
||||
@@ -64,14 +65,47 @@ class DatabaseRecommendAppRetrieval(RecommendAppRetrievalBase):
|
||||
:param language: language
|
||||
:return:
|
||||
"""
|
||||
recommended_apps = db.session.scalars(
|
||||
select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.language == language)
|
||||
).all()
|
||||
recommended_apps = cls._fetch_listed_recommended_apps(language)
|
||||
|
||||
if len(recommended_apps) == 0:
|
||||
recommended_apps = db.session.scalars(
|
||||
select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.language == languages[0])
|
||||
).all()
|
||||
recommended_apps = cls._fetch_listed_recommended_apps(languages[0])
|
||||
|
||||
return cls._format_recommended_apps(recommended_apps, language)
|
||||
|
||||
@classmethod
|
||||
def fetch_learn_dify_apps_from_db(cls, language: str) -> RecommendedAppsResultDict:
|
||||
"""
|
||||
Fetch listed recommended apps explicitly marked for the Learn Dify section.
|
||||
:param language: language
|
||||
:return:
|
||||
"""
|
||||
recommended_apps = cls._fetch_listed_recommended_apps(language, is_learn_dify=True)
|
||||
|
||||
if len(recommended_apps) == 0 and language != languages[0]:
|
||||
recommended_apps = cls._fetch_listed_recommended_apps(languages[0], is_learn_dify=True)
|
||||
|
||||
return cls._format_recommended_apps(recommended_apps, language)
|
||||
|
||||
@classmethod
|
||||
def _fetch_listed_recommended_apps(
|
||||
cls, language: str, *, is_learn_dify: bool | None = None
|
||||
) -> list[RecommendedApp]:
|
||||
filters = [RecommendedApp.is_listed.is_(True), RecommendedApp.language == language]
|
||||
if is_learn_dify is not None:
|
||||
filters.append(RecommendedApp.is_learn_dify.is_(is_learn_dify))
|
||||
|
||||
return db.session.scalars(select(RecommendedApp).where(*filters)).all()
|
||||
|
||||
@classmethod
|
||||
def _format_recommended_apps(
|
||||
cls, recommended_apps: list[RecommendedApp], language: str
|
||||
) -> RecommendedAppsResultDict:
|
||||
"""
|
||||
Serialize DB recommended app rows into the Explore list response shape.
|
||||
:param recommended_apps: recommended app rows
|
||||
:param language: language used for category ordering
|
||||
:return:
|
||||
"""
|
||||
|
||||
categories = set()
|
||||
recommended_apps_result: list[RecommendedAppItemDict] = []
|
||||
|
||||
@@ -6,6 +6,7 @@ from configs import dify_config
|
||||
from extensions.ext_database import db
|
||||
from models.model import AccountTrialAppRecord, TrialApp
|
||||
from services.feature_service import FeatureService
|
||||
from services.recommend_app.database.database_retrieval import DatabaseRecommendAppRetrieval
|
||||
from services.recommend_app.recommend_app_factory import RecommendAppRetrievalFactory
|
||||
|
||||
|
||||
@@ -31,13 +32,24 @@ class RecommendedAppService:
|
||||
apps = result["recommended_apps"]
|
||||
for app in apps:
|
||||
app_id = app["app_id"]
|
||||
trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
|
||||
if trial_app_model:
|
||||
app["can_trial"] = True
|
||||
else:
|
||||
app["can_trial"] = False
|
||||
app["can_trial"] = cls._can_trial_app(app_id)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_learn_dify_apps(cls, language: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get database-backed recommended apps marked as Learn Dify.
|
||||
:param language: language
|
||||
:return:
|
||||
"""
|
||||
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db(language)
|
||||
|
||||
if FeatureService.get_system_features().enable_trial_app:
|
||||
for app in result["recommended_apps"]:
|
||||
app["can_trial"] = cls._can_trial_app(app["app_id"])
|
||||
|
||||
return {"recommended_apps": result["recommended_apps"]}
|
||||
|
||||
@classmethod
|
||||
def get_recommend_app_detail(cls, app_id: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
@@ -52,11 +64,7 @@ class RecommendedAppService:
|
||||
return None
|
||||
if FeatureService.get_system_features().enable_trial_app:
|
||||
app_id = result["id"]
|
||||
trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
|
||||
if trial_app_model:
|
||||
result["can_trial"] = True
|
||||
else:
|
||||
result["can_trial"] = False
|
||||
result["can_trial"] = cls._can_trial_app(app_id)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@@ -77,3 +85,8 @@ class RecommendedAppService:
|
||||
else:
|
||||
db.session.add(AccountTrialAppRecord(app_id=app_id, count=1, account_id=account_id))
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def _can_trial_app(app_id: str) -> bool:
|
||||
trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
|
||||
return trial_app_model is not None
|
||||
|
||||
@@ -7,7 +7,7 @@ import click
|
||||
from celery import shared_task
|
||||
|
||||
from core.plugin.entities.marketplace import MarketplacePluginSnapshot
|
||||
from core.plugin.entities.plugin import PluginInstallationSource
|
||||
from core.plugin.entities.plugin import PluginInstallation, PluginInstallationSource
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
from core.plugin.plugin_service import PluginService
|
||||
from extensions.ext_redis import redis_client
|
||||
@@ -15,6 +15,7 @@ from models.account import TenantPluginAutoUpgradeStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3
|
||||
CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_snapshot:"
|
||||
CACHE_REDIS_TTL = 60 * 60 # 1 hour
|
||||
@@ -72,6 +73,25 @@ def marketplace_batch_fetch_plugin_manifests(
|
||||
return result
|
||||
|
||||
|
||||
def _normalize_category(category: PluginCategory | str | None) -> str | None:
|
||||
if category is None:
|
||||
return None
|
||||
if isinstance(category, PluginCategory):
|
||||
return category.value
|
||||
return str(category)
|
||||
|
||||
|
||||
def _plugin_matches_category(plugin: PluginInstallation, category: str | None) -> bool:
|
||||
"""Return whether an installed plugin should be checked by a category strategy."""
|
||||
if category is None:
|
||||
return True
|
||||
|
||||
declaration = getattr(plugin, "declaration", None)
|
||||
plugin_category = getattr(declaration, "category", None)
|
||||
plugin_category_value = getattr(plugin_category, "value", plugin_category)
|
||||
return plugin_category_value == category
|
||||
|
||||
|
||||
@shared_task(queue="plugin")
|
||||
def process_tenant_plugin_autoupgrade_check_task(
|
||||
tenant_id: str,
|
||||
@@ -80,13 +100,15 @@ def process_tenant_plugin_autoupgrade_check_task(
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
exclude_plugins: list[str],
|
||||
include_plugins: list[str],
|
||||
category: PluginCategory | str | None = None,
|
||||
):
|
||||
try:
|
||||
manager = PluginInstaller()
|
||||
category_value = _normalize_category(category)
|
||||
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Checking upgradable plugin for tenant: {tenant_id}",
|
||||
f"Checking upgradable plugin for tenant: {tenant_id}, category: {category_value or 'all'}",
|
||||
fg="green",
|
||||
)
|
||||
)
|
||||
@@ -102,7 +124,11 @@ def process_tenant_plugin_autoupgrade_check_task(
|
||||
all_plugins = manager.list_plugins(tenant_id)
|
||||
|
||||
for plugin in all_plugins:
|
||||
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id in include_plugins:
|
||||
if (
|
||||
plugin.source == PluginInstallationSource.Marketplace
|
||||
and plugin.plugin_id in include_plugins
|
||||
and _plugin_matches_category(plugin, category_value)
|
||||
):
|
||||
plugin_ids.append(
|
||||
(
|
||||
plugin.plugin_id,
|
||||
@@ -117,7 +143,9 @@ def process_tenant_plugin_autoupgrade_check_task(
|
||||
plugin_ids = [
|
||||
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
|
||||
for plugin in all_plugins
|
||||
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id not in exclude_plugins
|
||||
if plugin.source == PluginInstallationSource.Marketplace
|
||||
and plugin.plugin_id not in exclude_plugins
|
||||
and _plugin_matches_category(plugin, category_value)
|
||||
]
|
||||
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||
all_plugins = manager.list_plugins(tenant_id)
|
||||
@@ -125,6 +153,7 @@ def process_tenant_plugin_autoupgrade_check_task(
|
||||
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
|
||||
for plugin in all_plugins
|
||||
if plugin.source == PluginInstallationSource.Marketplace
|
||||
and _plugin_matches_category(plugin, category_value)
|
||||
]
|
||||
|
||||
if not plugin_ids:
|
||||
|
||||
@@ -7,6 +7,8 @@ from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermissi
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
from services.plugin.plugin_permission_service import PluginPermissionService
|
||||
|
||||
PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant(flask_req_ctx):
|
||||
@@ -71,7 +73,7 @@ class TestPluginPermissionLifecycle:
|
||||
|
||||
class TestPluginAutoUpgradeLifecycle:
|
||||
def test_get_returns_none_for_new_tenant(self, tenant):
|
||||
assert PluginAutoUpgradeService.get_strategy(tenant) is None
|
||||
assert PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) is None
|
||||
|
||||
def test_change_creates_row(self, tenant):
|
||||
result = PluginAutoUpgradeService.change_strategy(
|
||||
@@ -81,10 +83,11 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
assert result is True
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant)
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
assert strategy.upgrade_time_of_day == 3
|
||||
@@ -97,6 +100,7 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant,
|
||||
@@ -105,9 +109,10 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=["plugin-a"],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant)
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
assert strategy.upgrade_time_of_day == 12
|
||||
@@ -115,9 +120,9 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
assert strategy.include_plugins == ["plugin-a"]
|
||||
|
||||
def test_exclude_plugin_creates_strategy_when_none_exists(self, tenant):
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "my-plugin")
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "my-plugin", PLUGIN_CATEGORY)
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant)
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
assert "my-plugin" in strategy.exclude_plugins
|
||||
@@ -130,10 +135,11 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=["existing"],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "new-plugin")
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "new-plugin", PLUGIN_CATEGORY)
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant)
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert "existing" in strategy.exclude_plugins
|
||||
assert "new-plugin" in strategy.exclude_plugins
|
||||
@@ -146,10 +152,11 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=["same-plugin"],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "same-plugin")
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "same-plugin", PLUGIN_CATEGORY)
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant)
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.exclude_plugins.count("same-plugin") == 1
|
||||
|
||||
@@ -161,10 +168,11 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=["p1", "p2"],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "p1")
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "p1", PLUGIN_CATEGORY)
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant)
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert "p1" not in strategy.include_plugins
|
||||
assert "p2" in strategy.include_plugins
|
||||
@@ -177,10 +185,11 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "excluded-plugin")
|
||||
PluginAutoUpgradeService.exclude_plugin(tenant, "excluded-plugin", PLUGIN_CATEGORY)
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant)
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
assert "excluded-plugin" in strategy.exclude_plugins
|
||||
|
||||
@@ -51,6 +51,7 @@ def _create_recommended_app(
|
||||
categories: list[str] | None = None,
|
||||
language: str = "en-US",
|
||||
is_listed: bool = True,
|
||||
is_learn_dify: bool = False,
|
||||
position: int = 1,
|
||||
) -> RecommendedApp:
|
||||
rec = RecommendedApp(
|
||||
@@ -62,6 +63,7 @@ def _create_recommended_app(
|
||||
categories=[category] if categories is None else categories,
|
||||
language=language,
|
||||
is_listed=is_listed,
|
||||
is_learn_dify=is_learn_dify,
|
||||
position=position,
|
||||
)
|
||||
rec.id = str(uuid4())
|
||||
@@ -205,6 +207,65 @@ class TestFetchRecommendedAppsFromDb:
|
||||
app_ids = {r["app_id"] for r in result["recommended_apps"]}
|
||||
assert app1.id not in app_ids
|
||||
|
||||
def test_fetch_learn_dify_apps_uses_flag_not_categories(
|
||||
self,
|
||||
flask_app_with_containers,
|
||||
db_session_with_containers: Session,
|
||||
):
|
||||
tenant_id = str(uuid4())
|
||||
learn_dify_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
|
||||
_create_site(db_session_with_containers, app_id=learn_dify_app.id)
|
||||
_create_recommended_app(
|
||||
db_session_with_containers,
|
||||
app_id=learn_dify_app.id,
|
||||
category="workflow",
|
||||
categories=["Workflow"],
|
||||
is_learn_dify=True,
|
||||
)
|
||||
|
||||
category_only_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
|
||||
_create_site(db_session_with_containers, app_id=category_only_app.id)
|
||||
_create_recommended_app(
|
||||
db_session_with_containers,
|
||||
app_id=category_only_app.id,
|
||||
category="Learn Dify",
|
||||
categories=["Learn Dify"],
|
||||
is_learn_dify=False,
|
||||
)
|
||||
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db("en-US")
|
||||
|
||||
app_ids = {r["app_id"] for r in result["recommended_apps"]}
|
||||
assert learn_dify_app.id in app_ids
|
||||
assert category_only_app.id not in app_ids
|
||||
recommended_app = next(item for item in result["recommended_apps"] if item["app_id"] == learn_dify_app.id)
|
||||
assert recommended_app["categories"] == ["Workflow"]
|
||||
|
||||
def test_fetch_learn_dify_apps_falls_back_to_default_language(
|
||||
self,
|
||||
flask_app_with_containers,
|
||||
db_session_with_containers: Session,
|
||||
):
|
||||
tenant_id = str(uuid4())
|
||||
learn_dify_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
|
||||
_create_site(db_session_with_containers, app_id=learn_dify_app.id)
|
||||
_create_recommended_app(
|
||||
db_session_with_containers,
|
||||
app_id=learn_dify_app.id,
|
||||
categories=["Workflow"],
|
||||
is_learn_dify=True,
|
||||
language="en-US",
|
||||
)
|
||||
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db("fr-FR")
|
||||
|
||||
app_ids = {r["app_id"] for r in result["recommended_apps"]}
|
||||
assert learn_dify_app.id in app_ids
|
||||
|
||||
|
||||
class TestFetchRecommendedAppDetailFromDb:
|
||||
def test_returns_none_when_not_listed(self, flask_app_with_containers: Flask, db_session_with_containers: Session):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import create_autospec, patch
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
from faker import Faker
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -245,6 +247,236 @@ class TestAppService:
|
||||
assert app.tenant_id == tenant.id
|
||||
assert app.mode == "chat"
|
||||
|
||||
def test_get_paginate_apps_sorts_by_modified_and_created_times(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test app list sort options for modified time and creation time.
|
||||
"""
|
||||
fake = Faker()
|
||||
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
password=generate_valid_password(fake),
|
||||
)
|
||||
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
|
||||
tenant = account.current_tenant
|
||||
|
||||
from services.app_service import AppListParams, AppService, CreateAppParams
|
||||
|
||||
app_service = AppService()
|
||||
oldest_created = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Oldest Created", mode="chat", icon_type="emoji", icon="1"),
|
||||
account,
|
||||
)
|
||||
newest_modified = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Newest Modified", mode="chat", icon_type="emoji", icon="2"),
|
||||
account,
|
||||
)
|
||||
newest_created = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Newest Created", mode="chat", icon_type="emoji", icon="3"),
|
||||
account,
|
||||
)
|
||||
|
||||
timestamp_by_app_id = {
|
||||
oldest_created.id: (datetime(2026, 1, 1, 10, 0, 0), datetime(2026, 1, 1, 10, 0, 0)),
|
||||
newest_modified.id: (datetime(2026, 1, 2, 10, 0, 0), datetime(2026, 1, 4, 10, 0, 0)),
|
||||
newest_created.id: (datetime(2026, 1, 3, 10, 0, 0), datetime(2026, 1, 3, 10, 0, 0)),
|
||||
}
|
||||
for app_id, (created_at, updated_at) in timestamp_by_app_id.items():
|
||||
db_session_with_containers.execute(
|
||||
sa.update(App).where(App.id == app_id).values(created_at=created_at, updated_at=updated_at)
|
||||
)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
last_modified_apps = app_service.get_paginate_apps(
|
||||
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat")
|
||||
)
|
||||
assert last_modified_apps is not None
|
||||
assert [app.name for app in last_modified_apps.items] == [
|
||||
"Newest Modified",
|
||||
"Newest Created",
|
||||
"Oldest Created",
|
||||
]
|
||||
|
||||
recently_created_apps = app_service.get_paginate_apps(
|
||||
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", sort_by="recently_created")
|
||||
)
|
||||
assert recently_created_apps is not None
|
||||
assert [app.name for app in recently_created_apps.items] == [
|
||||
"Newest Created",
|
||||
"Newest Modified",
|
||||
"Oldest Created",
|
||||
]
|
||||
|
||||
earliest_created_apps = app_service.get_paginate_apps(
|
||||
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created")
|
||||
)
|
||||
assert earliest_created_apps is not None
|
||||
assert [app.name for app in earliest_created_apps.items] == [
|
||||
"Oldest Created",
|
||||
"Newest Modified",
|
||||
"Newest Created",
|
||||
]
|
||||
|
||||
def test_get_paginate_apps_marks_starred_apps(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test app list marks apps starred by the current account.
|
||||
"""
|
||||
fake = Faker()
|
||||
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
password=generate_valid_password(fake),
|
||||
)
|
||||
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
|
||||
tenant = account.current_tenant
|
||||
|
||||
from models import AppStar
|
||||
from services.app_service import AppListParams, AppService, CreateAppParams
|
||||
|
||||
app_service = AppService()
|
||||
starred_app = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Starred App", mode="chat", icon_type="emoji", icon="1"),
|
||||
account,
|
||||
)
|
||||
unstarred_app = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Unstarred App", mode="chat", icon_type="emoji", icon="2"),
|
||||
account,
|
||||
)
|
||||
|
||||
app_service.star_app(db_session_with_containers, app=starred_app, account_id=account.id)
|
||||
app_service.star_app(db_session_with_containers, app=starred_app, account_id=account.id)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
star_count = db_session_with_containers.scalar(
|
||||
sa.select(sa.func.count()).select_from(AppStar).where(AppStar.app_id == starred_app.id)
|
||||
)
|
||||
assert star_count == 1
|
||||
|
||||
paginated_apps = app_service.get_paginate_apps(
|
||||
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat")
|
||||
)
|
||||
assert paginated_apps is not None
|
||||
starred_by_app_id = {app.id: app.is_starred for app in paginated_apps.items}
|
||||
assert starred_by_app_id[starred_app.id] is True
|
||||
assert starred_by_app_id[unstarred_app.id] is False
|
||||
|
||||
app_service.unstar_app(db_session_with_containers, app=starred_app, account_id=account.id)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
paginated_apps = app_service.get_paginate_apps(
|
||||
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat")
|
||||
)
|
||||
assert paginated_apps is not None
|
||||
starred_by_app_id = {app.id: app.is_starred for app in paginated_apps.items}
|
||||
assert starred_by_app_id[starred_app.id] is False
|
||||
assert starred_by_app_id[unstarred_app.id] is False
|
||||
|
||||
def test_get_paginate_starred_apps_returns_only_starred_apps_with_requested_sort(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test starred app list returns only starred apps ordered by requested app sort.
|
||||
"""
|
||||
fake = Faker()
|
||||
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
password=generate_valid_password(fake),
|
||||
)
|
||||
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
|
||||
tenant = account.current_tenant
|
||||
|
||||
from services.app_service import AppService, CreateAppParams, StarredAppListParams
|
||||
|
||||
app_service = AppService()
|
||||
oldest_created_app = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Oldest Created Starred App", mode="chat", icon_type="emoji", icon="1"),
|
||||
account,
|
||||
)
|
||||
newest_modified_app = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Newest Modified Starred App", mode="chat", icon_type="emoji", icon="2"),
|
||||
account,
|
||||
)
|
||||
newest_created_app = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Newest Created Starred App", mode="chat", icon_type="emoji", icon="3"),
|
||||
account,
|
||||
)
|
||||
unstarred_app = app_service.create_app(
|
||||
tenant.id,
|
||||
CreateAppParams(name="Unstarred App", mode="chat", icon_type="emoji", icon="4"),
|
||||
account,
|
||||
)
|
||||
|
||||
app_service.star_app(db_session_with_containers, app=oldest_created_app, account_id=account.id)
|
||||
app_service.star_app(db_session_with_containers, app=newest_modified_app, account_id=account.id)
|
||||
app_service.star_app(db_session_with_containers, app=newest_created_app, account_id=account.id)
|
||||
|
||||
timestamp_by_app_id = {
|
||||
oldest_created_app.id: (datetime(2026, 1, 1, 10, 0, 0), datetime(2026, 1, 1, 10, 0, 0)),
|
||||
newest_modified_app.id: (datetime(2026, 1, 2, 10, 0, 0), datetime(2026, 1, 4, 10, 0, 0)),
|
||||
newest_created_app.id: (datetime(2026, 1, 3, 10, 0, 0), datetime(2026, 1, 3, 10, 0, 0)),
|
||||
unstarred_app.id: (datetime(2026, 1, 5, 10, 0, 0), datetime(2026, 1, 5, 10, 0, 0)),
|
||||
}
|
||||
for app_id, (created_at, updated_at) in timestamp_by_app_id.items():
|
||||
db_session_with_containers.execute(
|
||||
sa.update(App).where(App.id == app_id).values(created_at=created_at, updated_at=updated_at)
|
||||
)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
last_modified_apps = app_service.get_paginate_starred_apps(
|
||||
account.id, tenant.id, StarredAppListParams(page=1, limit=10, mode="chat")
|
||||
)
|
||||
assert last_modified_apps is not None
|
||||
assert [app.name for app in last_modified_apps.items] == [
|
||||
"Newest Modified Starred App",
|
||||
"Newest Created Starred App",
|
||||
"Oldest Created Starred App",
|
||||
]
|
||||
assert all(app.is_starred for app in last_modified_apps.items)
|
||||
assert unstarred_app.id not in {app.id for app in last_modified_apps.items}
|
||||
|
||||
recently_created_apps = app_service.get_paginate_starred_apps(
|
||||
account.id,
|
||||
tenant.id,
|
||||
StarredAppListParams(page=1, limit=10, mode="chat", sort_by="recently_created"),
|
||||
)
|
||||
assert recently_created_apps is not None
|
||||
assert [app.name for app in recently_created_apps.items] == [
|
||||
"Newest Created Starred App",
|
||||
"Newest Modified Starred App",
|
||||
"Oldest Created Starred App",
|
||||
]
|
||||
|
||||
earliest_created_apps = app_service.get_paginate_starred_apps(
|
||||
account.id,
|
||||
tenant.id,
|
||||
StarredAppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created"),
|
||||
)
|
||||
assert earliest_created_apps is not None
|
||||
assert [app.name for app in earliest_created_apps.items] == [
|
||||
"Oldest Created Starred App",
|
||||
"Newest Modified Starred App",
|
||||
"Newest Created Starred App",
|
||||
]
|
||||
|
||||
def test_get_paginate_apps_with_filters(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
|
||||
@@ -36,6 +36,7 @@ def current_user(tenant_id: str) -> MagicMock:
|
||||
def installed_app() -> MagicMock:
|
||||
app = MagicMock()
|
||||
app.id = "ia1"
|
||||
app.app_id = "a1"
|
||||
app.app = MagicMock(id="a1")
|
||||
app.app_owner_tenant_id = "t2"
|
||||
app.is_pinned = False
|
||||
@@ -43,6 +44,22 @@ def installed_app() -> MagicMock:
|
||||
return app
|
||||
|
||||
|
||||
def make_scalars_result(items: list[MagicMock]) -> MagicMock:
|
||||
result = MagicMock()
|
||||
result.all.return_value = items
|
||||
return result
|
||||
|
||||
|
||||
def make_installed_apps_session(installed_apps: list[MagicMock]) -> MagicMock:
|
||||
session = MagicMock()
|
||||
app_models = [installed_app.app for installed_app in installed_apps if installed_app.app is not None]
|
||||
session.scalars.side_effect = [
|
||||
make_scalars_result(installed_apps),
|
||||
make_scalars_result(app_models),
|
||||
]
|
||||
return session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def payload_patch() -> PayloadPatch:
|
||||
def _patch(payload: Payload) -> AbstractContextManager[object]:
|
||||
@@ -63,8 +80,7 @@ class TestInstalledAppsListApi:
|
||||
api = module.InstalledAppsListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [installed_app]
|
||||
session = make_installed_apps_session([installed_app])
|
||||
|
||||
with (
|
||||
app.test_request_context("/"),
|
||||
@@ -86,8 +102,7 @@ class TestInstalledAppsListApi:
|
||||
api = module.InstalledAppsListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = []
|
||||
session = make_installed_apps_session([])
|
||||
|
||||
with (
|
||||
app.test_request_context("/?app_id=a1"),
|
||||
@@ -110,8 +125,7 @@ class TestInstalledAppsListApi:
|
||||
api = module.InstalledAppsListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [installed_app]
|
||||
session = make_installed_apps_session([installed_app])
|
||||
|
||||
mock_webapp_setting = MagicMock()
|
||||
mock_webapp_setting.access_mode = "restricted"
|
||||
@@ -147,8 +161,7 @@ class TestInstalledAppsListApi:
|
||||
api = module.InstalledAppsListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [installed_app]
|
||||
session = make_installed_apps_session([installed_app])
|
||||
|
||||
mock_webapp_setting = MagicMock()
|
||||
mock_webapp_setting.access_mode = "restricted"
|
||||
@@ -184,8 +197,7 @@ class TestInstalledAppsListApi:
|
||||
api = module.InstalledAppsListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [installed_app]
|
||||
session = make_installed_apps_session([installed_app])
|
||||
|
||||
mock_webapp_setting = MagicMock()
|
||||
mock_webapp_setting.access_mode = "sso_verified"
|
||||
@@ -215,10 +227,10 @@ class TestInstalledAppsListApi:
|
||||
method = unwrap(api.get)
|
||||
|
||||
installed_app_with_null = MagicMock()
|
||||
installed_app_with_null.app_id = "a1"
|
||||
installed_app_with_null.app = None
|
||||
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [installed_app_with_null]
|
||||
session = make_installed_apps_session([installed_app_with_null])
|
||||
|
||||
with (
|
||||
app.test_request_context("/"),
|
||||
@@ -242,8 +254,7 @@ class TestInstalledAppsListApi:
|
||||
current_user = MagicMock()
|
||||
current_user.current_tenant = None
|
||||
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [installed_app]
|
||||
session = make_installed_apps_session([installed_app])
|
||||
|
||||
with (
|
||||
app.test_request_context("/"),
|
||||
|
||||
@@ -79,6 +79,48 @@ class TestRecommendedAppListApi:
|
||||
assert result == result_data
|
||||
|
||||
|
||||
class TestLearnDifyAppListApi:
|
||||
def test_get_with_language_param(self, app: Flask):
|
||||
api = module.LearnDifyAppListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
result_data = {"recommended_apps": []}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", query_string={"language": "en-US"}),
|
||||
patch.object(module, "current_user", MagicMock(interface_language="fr-FR")),
|
||||
patch.object(
|
||||
module.RecommendedAppService,
|
||||
"get_learn_dify_apps",
|
||||
return_value=result_data,
|
||||
) as service_mock,
|
||||
):
|
||||
result = method(api)
|
||||
|
||||
service_mock.assert_called_once_with("en-US")
|
||||
assert result == result_data
|
||||
|
||||
def test_get_fallback_to_user_language(self, app: Flask):
|
||||
api = module.LearnDifyAppListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
result_data = {"recommended_apps": []}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", query_string={"language": "invalid"}),
|
||||
patch.object(module, "current_user", MagicMock(interface_language="fr-FR")),
|
||||
patch.object(
|
||||
module.RecommendedAppService,
|
||||
"get_learn_dify_apps",
|
||||
return_value=result_data,
|
||||
) as service_mock,
|
||||
):
|
||||
result = method(api)
|
||||
|
||||
service_mock.assert_called_once_with("fr-FR")
|
||||
assert result == result_data
|
||||
|
||||
|
||||
class TestRecommendedAppApi:
|
||||
def test_get_success(self, app: Flask):
|
||||
api = module.RecommendedAppApi()
|
||||
@@ -144,3 +186,29 @@ class TestRecommendedAppResponseModels:
|
||||
assert response["recommended_apps"][0]["app_id"] == "app-1"
|
||||
assert response["recommended_apps"][0]["categories"] == ["cat", "other"]
|
||||
assert response["categories"] == ["cat"]
|
||||
|
||||
def test_learn_dify_app_list_response_serialization(self):
|
||||
response = module.LearnDifyAppListResponse.model_validate(
|
||||
{
|
||||
"recommended_apps": [
|
||||
{
|
||||
"app": {
|
||||
"id": "app-1",
|
||||
"name": "App",
|
||||
"mode": "chat",
|
||||
"icon": "icon.png",
|
||||
"icon_type": "emoji",
|
||||
"icon_background": "#fff",
|
||||
},
|
||||
"app_id": "app-1",
|
||||
"description": "desc",
|
||||
"categories": ["Workflow"],
|
||||
"position": 1,
|
||||
"is_listed": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
).model_dump(mode="json")
|
||||
|
||||
assert response["recommended_apps"][0]["app_id"] == "app-1"
|
||||
assert response["recommended_apps"][0]["categories"] == ["Workflow"]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import io
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,12 +11,14 @@ from werkzeug.exceptions import Forbidden
|
||||
from controllers.console.workspace.plugin import (
|
||||
PluginAssetApi,
|
||||
PluginAutoUpgradeExcludePluginApi,
|
||||
PluginCategoryListApi,
|
||||
PluginChangeAutoUpgradeApi,
|
||||
PluginChangePermissionApi,
|
||||
PluginChangePreferencesApi,
|
||||
PluginDebuggingKeyApi,
|
||||
PluginDeleteAllInstallTaskItemsApi,
|
||||
PluginDeleteInstallTaskApi,
|
||||
PluginDeleteInstallTaskItemApi,
|
||||
PluginFetchAutoUpgradeApi,
|
||||
PluginFetchDynamicSelectOptionsApi,
|
||||
PluginFetchDynamicSelectOptionsWithCredentialsApi,
|
||||
PluginFetchInstallTaskApi,
|
||||
@@ -22,7 +26,6 @@ from controllers.console.workspace.plugin import (
|
||||
PluginFetchManifestApi,
|
||||
PluginFetchMarketplacePkgApi,
|
||||
PluginFetchPermissionApi,
|
||||
PluginFetchPreferencesApi,
|
||||
PluginIconApi,
|
||||
PluginInstallFromGithubApi,
|
||||
PluginInstallFromMarketplaceApi,
|
||||
@@ -42,6 +45,68 @@ from core.plugin.impl.exc import PluginDaemonClientSideError
|
||||
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission
|
||||
|
||||
|
||||
def _plugin_category_list_item() -> dict[str, Any]:
|
||||
now = datetime(2023, 1, 1, 0, 0, 0)
|
||||
return {
|
||||
"id": "entity-1",
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"tenant_id": "t1",
|
||||
"endpoints_setups": 0,
|
||||
"endpoints_active": 0,
|
||||
"runtime_type": "remote",
|
||||
"source": "marketplace",
|
||||
"meta": {},
|
||||
"plugin_id": "test-author/test-plugin",
|
||||
"plugin_unique_identifier": "test-author/test-plugin:1.0.0@checksum",
|
||||
"version": "1.0.0",
|
||||
"checksum": "checksum",
|
||||
"name": "test-plugin",
|
||||
"installation_id": "entity-1",
|
||||
"declaration": {
|
||||
"version": "1.0.0",
|
||||
"author": "test-author",
|
||||
"name": "test-plugin",
|
||||
"description": {"en_US": "Test plugin"},
|
||||
"icon": "icon.svg",
|
||||
"label": {"en_US": "Test Plugin"},
|
||||
"created_at": now,
|
||||
"resource": {"memory": 268435456, "permission": None},
|
||||
"plugins": {"tools": ["provider/test.yaml"]},
|
||||
"meta": {"version": "1.0.0"},
|
||||
"tool": {
|
||||
"identity": {
|
||||
"author": "test-author",
|
||||
"name": "test-plugin",
|
||||
"description": {"en_US": "Test plugin"},
|
||||
"icon": "icon.svg",
|
||||
"label": {"en_US": "Test Plugin"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _builtin_tool_provider_item() -> dict[str, Any]:
|
||||
return {
|
||||
"id": "builtin",
|
||||
"author": "dify",
|
||||
"name": "builtin",
|
||||
"plugin_id": "",
|
||||
"plugin_unique_identifier": "",
|
||||
"description": {"en_US": "Builtin tool provider"},
|
||||
"icon": "icon.svg",
|
||||
"icon_dark": "",
|
||||
"label": {"en_US": "Builtin"},
|
||||
"type": "builtin",
|
||||
"team_credentials": {},
|
||||
"is_team_authorization": False,
|
||||
"allow_delete": True,
|
||||
"tools": [],
|
||||
"labels": [],
|
||||
}
|
||||
|
||||
|
||||
def unwrap(func):
|
||||
while hasattr(func, "__wrapped__"):
|
||||
func = func.__wrapped__
|
||||
@@ -142,6 +207,87 @@ class TestPluginListApi:
|
||||
assert result["total"] == 1
|
||||
|
||||
|
||||
class TestPluginCategoryListApi:
|
||||
def test_plugin_category_list(self, app: Flask):
|
||||
api = PluginCategoryListApi()
|
||||
method = unwrap(api.get)
|
||||
plugin_item = _plugin_category_list_item()
|
||||
builtin_item = _builtin_tool_provider_item()
|
||||
mock_list = MagicMock(list=[plugin_item], has_more=True)
|
||||
|
||||
with (
|
||||
app.test_request_context("/?page=2&page_size=10"),
|
||||
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(None, "t1")),
|
||||
patch(
|
||||
"controllers.console.workspace.plugin.PluginService.list_by_category", return_value=mock_list
|
||||
) as list_mock,
|
||||
patch(
|
||||
"controllers.console.workspace.plugin._list_hardcoded_builtin_tool_providers",
|
||||
return_value=[builtin_item],
|
||||
) as builtin_mock,
|
||||
):
|
||||
result = method(api, "tool")
|
||||
|
||||
list_mock.assert_called_once()
|
||||
assert list_mock.call_args.args[0] == "t1"
|
||||
assert list_mock.call_args.args[1] == "tool"
|
||||
assert list_mock.call_args.args[2] == 2
|
||||
assert list_mock.call_args.args[3] == 10
|
||||
assert result["plugins"][0]["id"] == "entity-1"
|
||||
assert result["plugins"][0]["plugin_unique_identifier"] == "test-author/test-plugin:1.0.0@checksum"
|
||||
assert result["builtin_tools"][0]["id"] == "builtin"
|
||||
assert result["builtin_tools"][0]["type"] == "builtin"
|
||||
assert result["has_more"] is True
|
||||
assert "total" not in result
|
||||
builtin_mock.assert_called_once_with("t1")
|
||||
|
||||
def test_non_tool_category_does_not_include_builtin_tools(self, app: Flask):
|
||||
api = PluginCategoryListApi()
|
||||
method = unwrap(api.get)
|
||||
mock_list = MagicMock(list=[_plugin_category_list_item()], has_more=False)
|
||||
|
||||
with (
|
||||
app.test_request_context("/?page=1&page_size=10"),
|
||||
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(None, "t1")),
|
||||
patch("controllers.console.workspace.plugin.PluginService.list_by_category", return_value=mock_list),
|
||||
patch("controllers.console.workspace.plugin._list_hardcoded_builtin_tool_providers") as builtin_mock,
|
||||
):
|
||||
result = method(api, "datasource")
|
||||
|
||||
assert result["plugins"][0]["id"] == "entity-1"
|
||||
assert result["builtin_tools"] == []
|
||||
assert result["has_more"] is False
|
||||
builtin_mock.assert_not_called()
|
||||
|
||||
def test_invalid_category(self, app: Flask):
|
||||
api = PluginCategoryListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
with (
|
||||
app.test_request_context("/?page=1&page_size=10"),
|
||||
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(None, "t1")),
|
||||
):
|
||||
result = method(api, "unknown")
|
||||
|
||||
assert result == ({"code": "invalid_param", "message": "invalid plugin category"}, 400)
|
||||
|
||||
def test_daemon_error(self, app: Flask):
|
||||
api = PluginCategoryListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
with (
|
||||
app.test_request_context("/?page=1&page_size=10"),
|
||||
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(None, "t1")),
|
||||
patch(
|
||||
"controllers.console.workspace.plugin.PluginService.list_by_category",
|
||||
side_effect=PluginDaemonClientSideError("error"),
|
||||
),
|
||||
):
|
||||
result = method(api, "tool")
|
||||
|
||||
assert result == ({"code": "plugin_error", "message": "error"}, 400)
|
||||
|
||||
|
||||
class TestPluginIconApi:
|
||||
def test_plugin_icon(self, app: Flask):
|
||||
api = PluginIconApi()
|
||||
@@ -901,18 +1047,15 @@ class TestPluginFetchDynamicSelectOptionsWithCredentialsApi:
|
||||
assert result == ({"code": "plugin_error", "message": "error"}, 400)
|
||||
|
||||
|
||||
class TestPluginChangePreferencesApi:
|
||||
class TestPluginChangeAutoUpgradeApi:
|
||||
def test_success(self, app: Flask):
|
||||
api = PluginChangePreferencesApi()
|
||||
api = PluginChangeAutoUpgradeApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
user = MagicMock(is_admin_or_owner=True)
|
||||
|
||||
payload = {
|
||||
"permission": {
|
||||
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
},
|
||||
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value,
|
||||
"auto_upgrade": {
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
"upgrade_time_of_day": 0,
|
||||
@@ -925,24 +1068,53 @@ class TestPluginChangePreferencesApi:
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(user, "t1")),
|
||||
patch("controllers.console.workspace.plugin.PluginPermissionService.change_permission", return_value=True),
|
||||
patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True),
|
||||
patch(
|
||||
"controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True
|
||||
) as change,
|
||||
):
|
||||
result = method(api)
|
||||
|
||||
assert result["success"] is True
|
||||
change.assert_called_once()
|
||||
|
||||
def test_permission_fail(self, app: Flask):
|
||||
api = PluginChangePreferencesApi()
|
||||
def test_success_with_model_category_auto_upgrade(self, app: Flask):
|
||||
api = PluginChangeAutoUpgradeApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
user = MagicMock(is_admin_or_owner=True)
|
||||
|
||||
payload = {
|
||||
"permission": {
|
||||
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL.value,
|
||||
"auto_upgrade": {
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
|
||||
"upgrade_time_of_day": 3600,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
"exclude_plugins": [],
|
||||
"include_plugins": [],
|
||||
},
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(user, "t1")),
|
||||
patch(
|
||||
"controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True
|
||||
) as change,
|
||||
):
|
||||
result = method(api)
|
||||
|
||||
assert result["success"] is True
|
||||
change.assert_called_once()
|
||||
assert change.call_args.kwargs["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL
|
||||
|
||||
def test_auto_upgrade_fail(self, app: Flask):
|
||||
api = PluginChangeAutoUpgradeApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
user = MagicMock(is_admin_or_owner=True)
|
||||
|
||||
payload = {
|
||||
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value,
|
||||
"auto_upgrade": {
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
"upgrade_time_of_day": 0,
|
||||
@@ -955,24 +1127,20 @@ class TestPluginChangePreferencesApi:
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(user, "t1")),
|
||||
patch("controllers.console.workspace.plugin.PluginPermissionService.change_permission", return_value=False),
|
||||
patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=False),
|
||||
):
|
||||
result = method(api)
|
||||
|
||||
assert result["success"] is False
|
||||
|
||||
|
||||
class TestPluginFetchPreferencesApi:
|
||||
class TestPluginFetchAutoUpgradeApi:
|
||||
def test_success(self, app: Flask):
|
||||
api = PluginFetchPreferencesApi()
|
||||
api = PluginFetchAutoUpgradeApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
permission = MagicMock(
|
||||
install_permission=TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
debug_permission=TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
)
|
||||
|
||||
auto_upgrade = MagicMock(
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=1,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
@@ -981,19 +1149,17 @@ class TestPluginFetchPreferencesApi:
|
||||
)
|
||||
|
||||
with (
|
||||
app.test_request_context("/"),
|
||||
app.test_request_context(f"/?category={TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}"),
|
||||
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(None, "t1")),
|
||||
patch(
|
||||
"controllers.console.workspace.plugin.PluginPermissionService.get_permission", return_value=permission
|
||||
),
|
||||
patch(
|
||||
"controllers.console.workspace.plugin.PluginAutoUpgradeService.get_strategy", return_value=auto_upgrade
|
||||
"controllers.console.workspace.plugin.PluginAutoUpgradeService.get_strategy",
|
||||
return_value=auto_upgrade,
|
||||
),
|
||||
):
|
||||
result = method(api)
|
||||
|
||||
assert "permission" in result
|
||||
assert "auto_upgrade" in result
|
||||
assert result["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
|
||||
assert result["auto_upgrade"]["upgrade_time_of_day"] == 1
|
||||
|
||||
|
||||
class TestPluginAutoUpgradeExcludePluginApi:
|
||||
@@ -1001,7 +1167,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
|
||||
api = PluginAutoUpgradeExcludePluginApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
payload = {"plugin_id": "p"}
|
||||
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -1016,7 +1182,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
|
||||
api = PluginAutoUpgradeExcludePluginApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
payload = {"plugin_id": "p"}
|
||||
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
|
||||
@@ -53,6 +53,12 @@ def make_tenant(
|
||||
return tenant
|
||||
|
||||
|
||||
def make_membership(*, last_opened_at=None) -> MagicMock:
|
||||
membership = MagicMock()
|
||||
membership.last_opened_at = last_opened_at
|
||||
return membership
|
||||
|
||||
|
||||
def make_account_with_tenant(tenant: Tenant) -> Account:
|
||||
account = make_account()
|
||||
account._current_tenant = tenant
|
||||
@@ -66,13 +72,17 @@ class TestTenantListApi:
|
||||
|
||||
tenant1 = make_tenant("t1", name="Tenant 1")
|
||||
tenant2 = make_tenant("t2", name="Tenant 2")
|
||||
last_opened_at = naive_utc_now()
|
||||
user = make_account()
|
||||
|
||||
with (
|
||||
app.test_request_context("/workspaces"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
|
||||
return_value=[tenant1, tenant2],
|
||||
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
|
||||
return_value=[
|
||||
(tenant1, make_membership(last_opened_at=last_opened_at)),
|
||||
(tenant2, make_membership()),
|
||||
],
|
||||
),
|
||||
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True),
|
||||
@@ -92,7 +102,9 @@ class TestTenantListApi:
|
||||
assert len(result["workspaces"]) == 2
|
||||
assert result["workspaces"][0]["current"] is True
|
||||
assert result["workspaces"][0]["plan"] == CloudPlan.TEAM
|
||||
assert result["workspaces"][0]["last_opened_at"] == int(last_opened_at.timestamp())
|
||||
assert result["workspaces"][1]["plan"] == CloudPlan.PROFESSIONAL
|
||||
assert result["workspaces"][1]["last_opened_at"] is None
|
||||
get_plan_bulk_mock.assert_called_once_with(["t1", "t2"])
|
||||
get_features_mock.assert_not_called()
|
||||
|
||||
@@ -116,8 +128,8 @@ class TestTenantListApi:
|
||||
with (
|
||||
app.test_request_context("/workspaces"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
|
||||
return_value=[tenant1, tenant2],
|
||||
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
|
||||
return_value=[(tenant1, make_membership()), (tenant2, make_membership())],
|
||||
),
|
||||
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True),
|
||||
@@ -159,8 +171,8 @@ class TestTenantListApi:
|
||||
with (
|
||||
app.test_request_context("/workspaces"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
|
||||
return_value=[tenant1, tenant2],
|
||||
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
|
||||
return_value=[(tenant1, make_membership()), (tenant2, make_membership())],
|
||||
),
|
||||
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True),
|
||||
@@ -198,8 +210,8 @@ class TestTenantListApi:
|
||||
with (
|
||||
app.test_request_context("/workspaces"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
|
||||
return_value=[tenant],
|
||||
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
|
||||
return_value=[(tenant, make_membership())],
|
||||
),
|
||||
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", False),
|
||||
@@ -226,8 +238,8 @@ class TestTenantListApi:
|
||||
with (
|
||||
app.test_request_context("/workspaces"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
|
||||
return_value=[tenant1, tenant2],
|
||||
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
|
||||
return_value=[(tenant1, make_membership()), (tenant2, make_membership())],
|
||||
),
|
||||
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", True),
|
||||
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", False),
|
||||
@@ -251,7 +263,7 @@ class TestTenantListApi:
|
||||
with (
|
||||
app.test_request_context("/workspaces"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
|
||||
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
|
||||
return_value=[],
|
||||
),
|
||||
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", True),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Swagger JSON rendering tests for Flask-RESTX API blueprints."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
@@ -160,3 +162,36 @@ def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest
|
||||
assert "payload" not in params
|
||||
assert params["avatar"]["in"] == "query"
|
||||
assert params["avatar"]["required"] is True
|
||||
|
||||
|
||||
def test_console_plugin_category_list_exported_schema_uses_typed_items(tmp_path):
|
||||
from dev.generate_swagger_specs import generate_specs
|
||||
|
||||
written_paths = generate_specs(tmp_path)
|
||||
console_swagger_path = next(path for path in written_paths if path.name == "console-swagger.json")
|
||||
payload = json.loads(console_swagger_path.read_text(encoding="utf-8"))
|
||||
operation = payload["paths"]["/workspaces/current/plugin/{category}/list"]["get"]
|
||||
response_ref = operation["responses"]["200"]["schema"]["$ref"].removeprefix("#/definitions/")
|
||||
response_schema = payload["definitions"][response_ref]
|
||||
|
||||
assert response_schema["properties"]["plugins"]["items"]["$ref"] == (
|
||||
"#/definitions/PluginCategoryInstalledPluginResponse"
|
||||
)
|
||||
assert response_schema["properties"]["builtin_tools"]["items"]["$ref"] == (
|
||||
"#/definitions/PluginCategoryBuiltinToolProviderResponse"
|
||||
)
|
||||
|
||||
installed_plugin_schema = payload["definitions"]["PluginCategoryInstalledPluginResponse"]
|
||||
for field in (
|
||||
"plugin_unique_identifier",
|
||||
"source",
|
||||
"version",
|
||||
"declaration",
|
||||
"endpoints_active",
|
||||
"endpoints_setups",
|
||||
):
|
||||
assert field in installed_plugin_schema["properties"]
|
||||
|
||||
builtin_tool_schema = payload["definitions"]["PluginCategoryBuiltinToolProviderResponse"]
|
||||
for field in ("plugin_unique_identifier", "team_credentials", "type", "tools"):
|
||||
assert field in builtin_tool_schema["properties"]
|
||||
|
||||
@@ -33,6 +33,7 @@ from core.plugin.entities.plugin_daemon import (
|
||||
PluginInstallTaskStartResponse,
|
||||
PluginInstallTaskStatus,
|
||||
PluginListResponse,
|
||||
PluginListWithoutTotalResponse,
|
||||
PluginReadmeResponse,
|
||||
PluginVerification,
|
||||
)
|
||||
@@ -123,6 +124,26 @@ class TestPluginDiscovery:
|
||||
assert call_args[1]["params"]["page_size"] == 5
|
||||
assert result.total == 10
|
||||
|
||||
def test_list_plugins_by_category(self, plugin_installer, mock_plugin_entity):
|
||||
"""Test category plugin listing without total."""
|
||||
mock_response = PluginListWithoutTotalResponse(list=[mock_plugin_entity], has_more=True)
|
||||
|
||||
with patch.object(
|
||||
plugin_installer, "_request_with_plugin_daemon_response", return_value=mock_response
|
||||
) as mock_request:
|
||||
result = plugin_installer.list_plugins_by_category(
|
||||
"test-tenant", category=PluginCategory.Tool, page=2, page_size=10
|
||||
)
|
||||
|
||||
mock_request.assert_called_once()
|
||||
call_args = mock_request.call_args
|
||||
assert call_args.args[1] == "plugin/test-tenant/management/tool/list"
|
||||
assert call_args.args[2] is PluginListWithoutTotalResponse
|
||||
assert call_args.kwargs["params"]["page"] == 2
|
||||
assert call_args.kwargs["params"]["page_size"] == 10
|
||||
assert result.list == [mock_plugin_entity]
|
||||
assert result.has_more is True
|
||||
|
||||
def test_list_plugins_empty_result(self, plugin_installer):
|
||||
"""Test plugin listing when no plugins are installed."""
|
||||
# Arrange: Mock empty response
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
|
||||
MODULE = "services.plugin.plugin_auto_upgrade_service"
|
||||
PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
|
||||
|
||||
|
||||
def _patched_session():
|
||||
@@ -25,7 +27,7 @@ class TestGetStrategy:
|
||||
with p1:
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.get_strategy("t1")
|
||||
result = PluginAutoUpgradeService.get_strategy("t1", PLUGIN_CATEGORY)
|
||||
|
||||
assert result is strategy
|
||||
|
||||
@@ -36,7 +38,7 @@ class TestGetStrategy:
|
||||
with p1:
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.get_strategy("t1")
|
||||
result = PluginAutoUpgradeService.get_strategy("t1", PLUGIN_CATEGORY)
|
||||
|
||||
assert result is None
|
||||
|
||||
@@ -57,6 +59,7 @@ class TestChangeStrategy:
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
[],
|
||||
[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
|
||||
assert result is True
|
||||
@@ -77,6 +80,7 @@ class TestChangeStrategy:
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
|
||||
["p1"],
|
||||
["p2"],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
|
||||
assert result is True
|
||||
@@ -96,17 +100,19 @@ class TestExcludePlugin:
|
||||
p1,
|
||||
patch(f"{MODULE}.select"),
|
||||
patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls,
|
||||
patch(f"{MODULE}.PluginAutoUpgradeService.change_strategy") as cs,
|
||||
):
|
||||
strat_cls.StrategySetting.FIX_ONLY = "fix_only"
|
||||
strat_cls.UpgradeMode.EXCLUDE = "exclude"
|
||||
cs.return_value = True
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "plugin-1")
|
||||
result = PluginAutoUpgradeService.exclude_plugin(
|
||||
"t1",
|
||||
"plugin-1",
|
||||
PLUGIN_CATEGORY,
|
||||
)
|
||||
|
||||
assert result is True
|
||||
cs.assert_called_once()
|
||||
session.add.assert_called_once()
|
||||
|
||||
def test_appends_to_exclude_list_in_exclude_mode(self):
|
||||
p1, session = _patched_session()
|
||||
@@ -121,7 +127,7 @@ class TestExcludePlugin:
|
||||
strat_cls.UpgradeMode.ALL = "all"
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new")
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new", PLUGIN_CATEGORY)
|
||||
|
||||
assert result is True
|
||||
assert existing.exclude_plugins == ["p-existing", "p-new"]
|
||||
@@ -139,7 +145,7 @@ class TestExcludePlugin:
|
||||
strat_cls.UpgradeMode.ALL = "all"
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1")
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
|
||||
|
||||
assert result is True
|
||||
assert existing.include_plugins == ["p2"]
|
||||
@@ -156,7 +162,7 @@ class TestExcludePlugin:
|
||||
strat_cls.UpgradeMode.ALL = "all"
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1")
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
|
||||
|
||||
assert result is True
|
||||
assert existing.upgrade_mode == "exclude"
|
||||
@@ -175,6 +181,101 @@ class TestExcludePlugin:
|
||||
strat_cls.UpgradeMode.ALL = "all"
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
PluginAutoUpgradeService.exclude_plugin("t1", "p1")
|
||||
PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
|
||||
|
||||
assert existing.exclude_plugins == ["p1"]
|
||||
|
||||
|
||||
class TestBackfillStrategyCategories:
|
||||
def test_creates_default_missing_categories_without_fetching_daemon(self):
|
||||
p1, session = _patched_session()
|
||||
tool_strategy = SimpleNamespace(
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
)
|
||||
session.scalars.return_value.all.return_value = [tool_strategy]
|
||||
installer = MagicMock()
|
||||
|
||||
with p1, patch(f"{MODULE}.PluginInstaller", return_value=installer):
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.backfill_strategy_categories("t1")
|
||||
expected_time = PluginAutoUpgradeService.default_upgrade_time_of_day("t1")
|
||||
|
||||
assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 1
|
||||
assert result.normalized is False
|
||||
installer.list_plugins.assert_not_called()
|
||||
assert tool_strategy.upgrade_time_of_day == expected_time
|
||||
created_strategies = [call.args[0] for call in session.add.call_args_list]
|
||||
model_strategy = next(
|
||||
strategy
|
||||
for strategy in created_strategies
|
||||
if strategy.category == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL
|
||||
)
|
||||
assert model_strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
assert model_strategy.upgrade_time_of_day == expected_time
|
||||
|
||||
def test_default_upgrade_time_is_aligned_to_fifteen_minutes(self):
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
default_time = PluginAutoUpgradeService.default_upgrade_time_of_day("t1")
|
||||
|
||||
assert default_time % (15 * 60) == 0
|
||||
assert 0 <= default_time < 24 * 60 * 60
|
||||
|
||||
def test_creates_missing_categories_and_splits_known_plugins(self):
|
||||
p1, session = _patched_session()
|
||||
tool_strategy = SimpleNamespace(
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"],
|
||||
include_plugins=["model-plugin", "tool-plugin"],
|
||||
)
|
||||
model_strategy = SimpleNamespace(
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"],
|
||||
include_plugins=["model-plugin", "tool-plugin"],
|
||||
)
|
||||
session.scalars.return_value.all.return_value = [tool_strategy, model_strategy]
|
||||
|
||||
installed_plugins = [
|
||||
SimpleNamespace(
|
||||
plugin_id="tool-plugin",
|
||||
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL),
|
||||
),
|
||||
SimpleNamespace(
|
||||
plugin_id="model-plugin",
|
||||
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL),
|
||||
),
|
||||
]
|
||||
installer = MagicMock()
|
||||
installer.list_plugins.return_value = installed_plugins
|
||||
|
||||
with p1, patch(f"{MODULE}.PluginInstaller", return_value=installer), patch(f"{MODULE}.logger") as logger:
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.backfill_strategy_categories("t1")
|
||||
|
||||
assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2
|
||||
assert result.normalized is True
|
||||
assert session.add.call_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2
|
||||
assert tool_strategy.exclude_plugins == ["tool-plugin"]
|
||||
assert tool_strategy.include_plugins == ["tool-plugin"]
|
||||
assert model_strategy.exclude_plugins == ["model-plugin"]
|
||||
assert model_strategy.include_plugins == ["model-plugin"]
|
||||
logger.warning.assert_called_once_with(
|
||||
"Skipped unknown plugin IDs while backfilling plugin auto-upgrade strategies: "
|
||||
"tenant_id=%s, field=%s, plugin_ids=%s",
|
||||
"t1",
|
||||
"exclude_plugins",
|
||||
["unknown-plugin"],
|
||||
)
|
||||
|
||||
@@ -65,6 +65,7 @@ class TestAccountAssociatedDataFactory:
|
||||
tenant_join.account_id = account_id
|
||||
tenant_join.current = current
|
||||
tenant_join.role = role
|
||||
tenant_join.last_opened_at = kwargs.pop("last_opened_at", None)
|
||||
for key, value in kwargs.items():
|
||||
setattr(tenant_join, key, value)
|
||||
return tenant_join
|
||||
@@ -489,11 +490,13 @@ class TestAccountService:
|
||||
# Mock datetime
|
||||
with (
|
||||
patch("services.account_service.datetime") as mock_datetime,
|
||||
patch("services.account_service.naive_utc_now") as mock_naive_utc_now,
|
||||
patch.object(AccountService, "_refresh_account_last_active") as mock_refresh_last_active,
|
||||
):
|
||||
mock_now = datetime.now()
|
||||
mock_datetime.now.return_value = mock_now
|
||||
mock_datetime.UTC = "UTC"
|
||||
mock_naive_utc_now.return_value = mock_now
|
||||
|
||||
# Execute test
|
||||
result = AccountService.load_user("user-123")
|
||||
@@ -501,6 +504,7 @@ class TestAccountService:
|
||||
# Verify results
|
||||
assert result == mock_account
|
||||
assert mock_available_tenant.current is True
|
||||
assert mock_available_tenant.last_opened_at == mock_now
|
||||
self._assert_database_operations_called(mock_db_dependencies["db"])
|
||||
mock_refresh_last_active.assert_called_once_with(mock_account)
|
||||
|
||||
@@ -922,11 +926,16 @@ class TestTenantService:
|
||||
# Mock scalar for the join query
|
||||
mock_db.session.scalar.return_value = mock_tenant_join
|
||||
|
||||
# Execute test
|
||||
TenantService.switch_tenant(mock_account, "tenant-456")
|
||||
with patch("services.account_service.naive_utc_now") as mock_naive_utc_now:
|
||||
mock_now = datetime(2026, 6, 5, 11, 0, 0)
|
||||
mock_naive_utc_now.return_value = mock_now
|
||||
|
||||
# Execute test
|
||||
TenantService.switch_tenant(mock_account, "tenant-456")
|
||||
|
||||
# Verify tenant was switched
|
||||
assert mock_tenant_join.current is True
|
||||
assert mock_tenant_join.last_opened_at == mock_now
|
||||
self._assert_database_operations_called(mock_db)
|
||||
|
||||
def test_switch_tenant_no_tenant_id(self):
|
||||
|
||||
@@ -7,6 +7,7 @@ returns None causes a TypeError / KeyError in self-hosted mode.
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from services import recommended_app_service as service_module
|
||||
from services.recommended_app_service import RecommendedAppService
|
||||
|
||||
|
||||
@@ -44,3 +45,40 @@ class TestGetRecommendAppDetailNullCheck:
|
||||
|
||||
assert result is None
|
||||
mock_instance.get_recommend_app_detail.assert_called_once_with("nonexistent")
|
||||
|
||||
|
||||
class TestGetLearnDifyApps:
|
||||
@patch("services.recommended_app_service.FeatureService", autospec=True)
|
||||
@patch("services.recommended_app_service.DatabaseRecommendAppRetrieval", autospec=True)
|
||||
def test_returns_database_learn_dify_apps_without_remote_factory(
|
||||
self, mock_database_retrieval, mock_feature_service
|
||||
):
|
||||
expected_app = {"app_id": "app-1", "categories": ["Workflow"]}
|
||||
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = {
|
||||
"recommended_apps": [expected_app],
|
||||
"categories": ["Workflow"],
|
||||
}
|
||||
mock_feature_service.get_system_features.return_value = SimpleNamespace(enable_trial_app=False)
|
||||
|
||||
with patch.object(service_module.RecommendAppRetrievalFactory, "get_recommend_app_factory") as factory_mock:
|
||||
result = RecommendedAppService.get_learn_dify_apps("en-US")
|
||||
|
||||
assert result == {"recommended_apps": [expected_app]}
|
||||
mock_database_retrieval.fetch_learn_dify_apps_from_db.assert_called_once_with("en-US")
|
||||
factory_mock.assert_not_called()
|
||||
|
||||
@patch("services.recommended_app_service.FeatureService", autospec=True)
|
||||
@patch("services.recommended_app_service.DatabaseRecommendAppRetrieval", autospec=True)
|
||||
def test_sets_can_trial_when_trial_feature_enabled(self, mock_database_retrieval, mock_feature_service):
|
||||
app = {"app_id": "app-1", "categories": ["Workflow"]}
|
||||
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = {
|
||||
"recommended_apps": [app],
|
||||
"categories": ["Workflow"],
|
||||
}
|
||||
mock_feature_service.get_system_features.return_value = SimpleNamespace(enable_trial_app=True)
|
||||
|
||||
with patch.object(RecommendedAppService, "_can_trial_app", return_value=True) as can_trial_mock:
|
||||
result = RecommendedAppService.get_learn_dify_apps("en-US")
|
||||
|
||||
assert result["recommended_apps"][0]["can_trial"] is True
|
||||
can_trial_mock.assert_called_once_with("app-1")
|
||||
|
||||
@@ -4,19 +4,25 @@ from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from core.plugin.entities.marketplace import MarketplacePluginSnapshot
|
||||
from core.plugin.entities.plugin import PluginInstallationSource
|
||||
from core.plugin.entities.plugin import PluginCategory, PluginInstallationSource
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
|
||||
MODULE = "tasks.process_tenant_plugin_autoupgrade_check_task"
|
||||
|
||||
|
||||
def _make_plugin(plugin_id: str, version: str, source=PluginInstallationSource.Marketplace):
|
||||
def _make_plugin(
|
||||
plugin_id: str,
|
||||
version: str,
|
||||
source=PluginInstallationSource.Marketplace,
|
||||
category: PluginCategory = PluginCategory.Tool,
|
||||
):
|
||||
"""Build a minimal stand-in for a PluginInstallation entry returned by manager.list_plugins."""
|
||||
return SimpleNamespace(
|
||||
plugin_id=plugin_id,
|
||||
version=version,
|
||||
plugin_unique_identifier=f"{plugin_id}:{version}@deadbeef",
|
||||
source=source,
|
||||
declaration=SimpleNamespace(category=category),
|
||||
)
|
||||
|
||||
|
||||
@@ -39,6 +45,7 @@ def _run_task(
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
exclude_plugins=None,
|
||||
include_plugins=None,
|
||||
category=None,
|
||||
):
|
||||
"""
|
||||
Execute the celery task synchronously with mocks for the plugin manager,
|
||||
@@ -72,6 +79,7 @@ def _run_task(
|
||||
upgrade_mode,
|
||||
exclude_plugins or [],
|
||||
include_plugins or [],
|
||||
category,
|
||||
)
|
||||
|
||||
return upgrade_mock, upgrade_calls
|
||||
@@ -246,6 +254,26 @@ class TestUpgradeMode:
|
||||
assert upgrade_mock.call_count == 1
|
||||
assert calls[0][1] == plugins[0].plugin_unique_identifier
|
||||
|
||||
def test_category_strategy_only_upgrades_matching_category(self):
|
||||
plugins = [
|
||||
_make_plugin("acme/model-provider", "1.0.0", category=PluginCategory.Model),
|
||||
_make_plugin("acme/tool-provider", "1.0.0", category=PluginCategory.Tool),
|
||||
]
|
||||
manifests = [
|
||||
_make_manifest("acme/model-provider", "1.0.1"),
|
||||
_make_manifest("acme/tool-provider", "1.0.1"),
|
||||
]
|
||||
|
||||
upgrade_mock, calls = _run_task(
|
||||
plugins=plugins,
|
||||
manifests=manifests,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL,
|
||||
)
|
||||
|
||||
upgrade_mock.assert_called_once()
|
||||
assert calls[0][1] == plugins[0].plugin_unique_identifier
|
||||
|
||||
|
||||
class TestErrorIsolation:
|
||||
def test_one_plugin_failure_does_not_block_others(self):
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Integrations Folder Structure Follow-ups
|
||||
|
||||
Context: the onboarding UI rewrite moved Integrations into a first-class `/integrations/...` route family, but the implementation still intentionally reuses several legacy Account Settings, Tools, and Plugins components. This document records the recommended cleanup order after this branch is merged into `main`.
|
||||
|
||||
## Current Structure
|
||||
|
||||
| Area | Current location | Status |
|
||||
| --- | --- | --- |
|
||||
| MainNav shell | `web/app/components/main-nav` | Healthy. Keep the shallow `components/` layout. |
|
||||
| Integrations route adapter | `web/app/(commonLayout)/integrations/[[...slug]]/page.tsx` | Canonical route entry. |
|
||||
| Integrations route contract | `web/app/components/tools/integration-routes.ts` | Works, but ownership belongs to Integrations long-term. |
|
||||
| Integrations shell and sidebar | `web/app/components/tools/integrations-page.tsx` | Works, but ownership belongs to Integrations long-term. |
|
||||
| Integrations section renderer | `web/app/components/tools/integration-section-renderer.tsx` | Reuse-first adapter for existing pages. |
|
||||
| Model Provider page | `web/app/components/header/account-setting/model-provider-page` | Still active and widely imported. Do not move first. |
|
||||
| Data Source page | `web/app/components/header/account-setting/data-source-page-new` | Still active and reused by Integrations. Do not move first. |
|
||||
| API Extension page | `web/app/components/header/account-setting/api-based-extension-page` | Still active and reused by Integrations. Do not move first. |
|
||||
| Plugin management primitives | `web/app/components/plugins/plugin-page` | Still active for `/plugins` and reused by Integrations. Do not delete. |
|
||||
|
||||
## Recommended Timing
|
||||
|
||||
Do not do a broad folder move in the current onboarding UI branch. This branch already carries UI, route, i18n, and permission behavior changes; adding large path churn would increase merge conflict risk with `main` and make review harder.
|
||||
|
||||
After this branch is merged into `main`, do the structure cleanup in small PRs.
|
||||
|
||||
## Step 1: Establish Integrations Ownership
|
||||
|
||||
When: first cleanup PR after the onboarding UI branch lands on `main`.
|
||||
|
||||
Goal: make the new feature ownership visible without moving legacy implementation internals.
|
||||
|
||||
Recommended target:
|
||||
|
||||
```text
|
||||
web/app/components/integrations/
|
||||
routes.ts
|
||||
integrations-page.tsx
|
||||
integration-section-renderer.tsx
|
||||
sidebar/
|
||||
sections/
|
||||
hooks/
|
||||
```
|
||||
|
||||
Move or wrap only Integrations-owned shell files first:
|
||||
|
||||
| Current file | Target |
|
||||
| --- | --- |
|
||||
| `web/app/components/tools/integration-routes.ts` | `web/app/components/integrations/routes.ts` |
|
||||
| `web/app/components/tools/integrations-page.tsx` | `web/app/components/integrations/integrations-page.tsx` |
|
||||
| `web/app/components/tools/integration-section-renderer.tsx` | `web/app/components/integrations/integration-section-renderer.tsx` |
|
||||
| `web/app/components/tools/integration-page-header.tsx` | `web/app/components/integrations/integration-page-header.tsx` |
|
||||
| `web/app/components/tools/integration-sidebar-nav-item.tsx` | `web/app/components/integrations/sidebar/nav-item.tsx` |
|
||||
| `web/app/components/tools/integration-sidebar-nav-item-styles.ts` | `web/app/components/integrations/sidebar/nav-item-styles.ts` |
|
||||
| `web/app/components/tools/permission-quick-panel.tsx` | `web/app/components/integrations/sidebar/permission-quick-panel.tsx` |
|
||||
| `web/app/components/tools/hooks/use-integration-*` | `web/app/components/integrations/hooks/*` |
|
||||
|
||||
Keep compatibility re-exports from the old `components/tools` paths during this PR if the import churn becomes large.
|
||||
|
||||
## Step 2: Add Section Adapters
|
||||
|
||||
When: same PR as Step 1 if the diff stays small, otherwise a second cleanup PR.
|
||||
|
||||
Goal: avoid importing legacy Account Settings components directly from the shared renderer.
|
||||
|
||||
Add thin adapters:
|
||||
|
||||
```text
|
||||
web/app/components/integrations/sections/model-provider-section.tsx
|
||||
web/app/components/integrations/sections/data-source-section.tsx
|
||||
web/app/components/integrations/sections/api-extension-section.tsx
|
||||
web/app/components/integrations/sections/tools-section.tsx
|
||||
web/app/components/integrations/sections/plugin-category-section.tsx
|
||||
```
|
||||
|
||||
These adapters should keep importing the existing implementation from its current location. For example, `model-provider-section.tsx` can wrap `header/account-setting/model-provider-page` and pass Integrations-specific props such as `stickyToolbar`, `fixedWarningAlignment`, and `hideSystemModelSelectorProviderSettingsFooter`.
|
||||
|
||||
Do not duplicate page logic in the adapters.
|
||||
|
||||
## Step 3: Remove Confirmed Dead Account Settings Pages
|
||||
|
||||
When: after Step 1 and Step 2 are merged and the app still passes route/modal smoke tests.
|
||||
|
||||
Likely removal candidates:
|
||||
|
||||
| Candidate | Reason |
|
||||
| --- | --- |
|
||||
| `web/app/components/header/account-setting/Integrations-page` | Superseded by the new Integrations shell. No production reference should remain. |
|
||||
| `web/app/components/header/account-setting/plugin-page` | Superseded by `web/app/components/plugins/plugin-page`. No production reference should remain. |
|
||||
|
||||
Before deleting, run a fresh reference check:
|
||||
|
||||
```bash
|
||||
rg "Integrations-page|header/account-setting/plugin-page" web/app web/context web/service
|
||||
```
|
||||
|
||||
Delete only if the remaining hits are tests for the files being removed.
|
||||
|
||||
## Step 4: Consider Deeper Page Moves Later
|
||||
|
||||
When: only after the Integrations shell ownership is stable and `main` has absorbed the onboarding rewrite.
|
||||
|
||||
Do not start by moving these directories:
|
||||
|
||||
| Directory | Why |
|
||||
| --- | --- |
|
||||
| `header/account-setting/model-provider-page` | Its types, hooks, model selector, model auth, and modals are imported by workflows, datasets, app debug, services, and global modal context. |
|
||||
| `header/account-setting/data-source-page-new` | Its types and credential flows are used by dataset creation, Notion selectors, and Integrations. |
|
||||
| `header/account-setting/api-based-extension-page` | It is still reused by feature settings and Integrations. |
|
||||
| `plugins/plugin-page` | `/plugins`, Integrations install controls, plugin category pages, plugin task status, and plugin detail flows still depend on it. |
|
||||
|
||||
If these are moved later, split the work by domain:
|
||||
|
||||
1. Extract shared types/hooks into stable shared modules.
|
||||
2. Move only one page family per PR.
|
||||
3. Keep temporary re-export files at old paths if external imports are still broad.
|
||||
4. Remove re-exports only after the branch has settled on `main`.
|
||||
|
||||
## Merge Conflict Strategy
|
||||
|
||||
Prefer additive changes first:
|
||||
|
||||
- New adapter files under `components/integrations`.
|
||||
- Small import updates in the route adapter and renderer.
|
||||
- Temporary re-exports from old paths when needed.
|
||||
|
||||
Avoid early broad changes:
|
||||
|
||||
- Large `git mv` batches.
|
||||
- Renaming model-provider/data-source imports across workflow and dataset code.
|
||||
- Deleting reused plugin primitives.
|
||||
- Combining structure cleanup with visual changes.
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Run at least:
|
||||
|
||||
```bash
|
||||
pnpm test app/components/tools/__tests__/integrations-page.spec.tsx
|
||||
pnpm test app/components/tools/__tests__/integration-routes.spec.ts
|
||||
pnpm test app/components/main-nav/__tests__/index.spec.tsx
|
||||
pnpm eslint --cache --quiet app/components/tools app/components/main-nav
|
||||
```
|
||||
|
||||
For deeper moves, also run targeted tests for the moved page family, such as model-provider, data-source, API extension, plugin-page, and plugin detail panel tests.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Integrations Route Contract
|
||||
|
||||
This document records the current canonical routes for the Integrations navigation, the legacy routes that redirect to them, and the remaining migration gaps. The first migration only moves existing pages onto the new routes; UI redesign work is out of scope.
|
||||
|
||||
## Current Status
|
||||
|
||||
Completed:
|
||||
|
||||
| Area | Status |
|
||||
| --- | --- |
|
||||
| Canonical `/integrations/...` route adapter | Implemented in `web/app/(commonLayout)/integrations/[[...slug]]/page.tsx`. |
|
||||
| Route contract utility | Implemented in `web/app/components/tools/integration-routes.ts`. |
|
||||
| Existing page reuse | Implemented through `IntegrationSectionRenderer`; no duplicated UI copy. |
|
||||
| Legacy `/tools?...` redirects | Implemented through `web/app/(commonLayout)/tools/page.tsx`. |
|
||||
| Legacy `/plugins` installed redirects | Implemented through `web/app/(commonLayout)/plugins/page.tsx`. |
|
||||
| Tools tab navigation under new URLs | Implemented; scoped tool tabs push canonical `/integrations/tools/...` URLs. |
|
||||
| Singular-only canonical URLs | Implemented; plural and misplaced aliases are intentionally unsupported. |
|
||||
|
||||
Not completed:
|
||||
|
||||
| Area | Remaining work |
|
||||
| --- | --- |
|
||||
| Integrations overview page | Not introduced; `/integrations` currently redirects to `/integrations/model-provider`. |
|
||||
| Tools overview page | Not introduced; `/integrations/tools` currently redirects to `/integrations/tools/built-in`. |
|
||||
| Plugin route migration | `/plugins` still owns the old plugin management and marketplace surface; no `/integrations/plugin` route will be introduced. Non-marketplace plugin URLs should redirect to `/integrations`. |
|
||||
| Marketplace route migration | `/marketplace/...` routes below are future recommendations only; they are not implemented here. |
|
||||
| New onboarding UI redesign | Not started in this route migration; current pages intentionally reuse existing UI. |
|
||||
| Marketplace plugin redirects | Not implemented; marketplace plugin URLs intentionally keep rendering the legacy plugin marketplace surface for now. |
|
||||
|
||||
## Navigation Labels
|
||||
|
||||
| Navigation item | Canonical label |
|
||||
| --- | --- |
|
||||
| Model Provider | Model Provider |
|
||||
| Built-in tools | Built-in |
|
||||
| Custom Tool | Swagger API as Tool |
|
||||
| Workflow | Workflow as Tool |
|
||||
| MCP | MCP |
|
||||
| Data Source | Data Source |
|
||||
| API Extension | API Extension |
|
||||
| Plugins | Plugins |
|
||||
| Marketplace | Marketplace |
|
||||
|
||||
## Canonical Integrations Routes
|
||||
|
||||
| Route | Destination |
|
||||
| --- | --- |
|
||||
| `/integrations` | Redirect to `/integrations/model-provider` unless an overview page is introduced. |
|
||||
| `/integrations/model-provider` | Existing model provider management page. |
|
||||
| `/integrations/tools` | Redirect to `/integrations/tools/built-in` unless a tools overview page is introduced. |
|
||||
| `/integrations/tools/built-in` | Existing built-in tools list. |
|
||||
| `/integrations/tool/api` | Existing custom API tool list, relabeled as Swagger API as Tool. |
|
||||
| `/integrations/tools/workflow` | Existing Workflow as Tool management page. |
|
||||
| `/integrations/tools/mcp` | Existing MCP tools management page. |
|
||||
| `/integrations/trigger` | Existing plugin trigger list filtered from plugin management. |
|
||||
| `/integrations/agent-strategy` | Existing agent strategy plugin list filtered from plugin management. |
|
||||
| `/integrations/extension` | Existing extension plugin list filtered from plugin management. |
|
||||
| `/integrations/data-source` | Existing data source page. |
|
||||
| `/integrations/tools/api-extension` | Existing API extension page under the Tools group. |
|
||||
|
||||
## Integration Plugin Category Routes
|
||||
|
||||
These navigation items use plugin categories from the existing plugin management surface:
|
||||
|
||||
| Navigation item | Plugin category | Route |
|
||||
| --- | --- | --- |
|
||||
| Trigger | `trigger` | `/integrations/trigger` |
|
||||
| Agent Strategy | `agent-strategy` | `/integrations/agent-strategy` |
|
||||
| Extension | `extension` | `/integrations/extension` |
|
||||
|
||||
The install and filter controls in the Integrations sidebar are disabled actions, not route destinations.
|
||||
|
||||
These routes reuse the installed plugin management list with an initial category filter. They are not marketplace category pages.
|
||||
|
||||
Do not treat every plugin category as an Integrations navigation item automatically. `trigger`, `agent-strategy`, and `extension` are currently exposed under Integrations because they are explicit navigation items. Other plugin categories have different product meanings:
|
||||
|
||||
| Plugin category | Integrations relationship |
|
||||
| --- | --- |
|
||||
| `tool` | Not equal to `/integrations/tools/...`; tool plugins can expose tool providers that appear in Tools, but the Tools page is provider-based. |
|
||||
| `model` | Not equal to `/integrations/model-provider`; model providers are managed through the model provider page. |
|
||||
| `datasource` | Not equal to the full Data Source page; data source integrations have their own existing page. |
|
||||
| `trigger` | Reused as `/integrations/trigger`, installed plugins filtered by category. |
|
||||
| `agent-strategy` | Reused as `/integrations/agent-strategy`, installed plugins filtered by category. |
|
||||
| `extension` | Reused as `/integrations/extension`, installed plugins filtered by category. |
|
||||
|
||||
## Legacy Tools Redirects
|
||||
|
||||
| Legacy route | New route |
|
||||
| --- | --- |
|
||||
| `/tools` | `/integrations/tools/built-in` |
|
||||
| `/tools?section=provider` | `/integrations/model-provider` |
|
||||
| `/tools?section=builtin` | `/integrations/tools/built-in` |
|
||||
| `/tools?section=builtin&category=builtin` | `/integrations/tools/built-in` |
|
||||
| `/tools?category=builtin` | `/integrations/tools/built-in` |
|
||||
| `/tools?section=custom-tool` | `/integrations/tool/api` |
|
||||
| `/tools?section=custom-tool&category=api` | `/integrations/tool/api` |
|
||||
| `/tools?category=api` | `/integrations/tool/api` |
|
||||
| `/tools?section=workflow-tool` | `/integrations/tools/workflow` |
|
||||
| `/tools?section=workflow-tool&category=workflow` | `/integrations/tools/workflow` |
|
||||
| `/tools?category=workflow` | `/integrations/tools/workflow` |
|
||||
| `/tools?section=mcp` | `/integrations/tools/mcp` |
|
||||
| `/tools?section=mcp&category=mcp` | `/integrations/tools/mcp` |
|
||||
| `/tools?category=mcp` | `/integrations/tools/mcp` |
|
||||
| `/tools?section=data-source` | `/integrations/data-source` |
|
||||
| `/tools?section=api-based-extension` | `/integrations/tools/api-extension` |
|
||||
| `/tools?section=trigger` | `/integrations/trigger` |
|
||||
| `/tools?section=agent-strategy` | `/integrations/agent-strategy` |
|
||||
| `/tools?section=extension` | `/integrations/extension` |
|
||||
|
||||
Preserve non-routing query parameters such as `q`, `tags`, and `sort`, but drop legacy routing parameters such as `section` and `category` during redirects.
|
||||
|
||||
## Non-Canonical Integrations Routes
|
||||
|
||||
Do not add plural or misplaced alias redirects for new Integrations URLs. Only the singular canonical routes above should resolve. For example, `/integrations/model-providers`, `/integrations/data-sources`, `/integrations/api-extensions`, `/integrations/tools/trigger`, `/integrations/tools/agent-strategy`, and `/integrations/tools/extension` should not be treated as supported URLs unless they are later confirmed to have shipped externally.
|
||||
|
||||
## Legacy Plugin Redirects
|
||||
|
||||
Plugins have two different product meanings today: installed plugin management and marketplace discovery. Only the non-marketplace plugin URLs should redirect into Integrations. There is no `/integrations/plugin` route.
|
||||
|
||||
| Old Plugin URL | Recommended redirect | Reason |
|
||||
| --- | --- | --- |
|
||||
| `/plugins` | `/integrations` | Installed plugin management entry should move into the Integrations main entry. |
|
||||
| `/plugins?tab=plugins` | `/integrations` | Explicit installed plugins tab; non-marketplace semantics. |
|
||||
| `/plugins?tab=discover` | Do not redirect to Integrations | Marketplace discovery. |
|
||||
| `/plugins?tab=all` | Do not redirect to Integrations | Marketplace category: all. |
|
||||
| `/plugins?tab=tool` | Do not redirect to Integrations | Marketplace tool category, not installed tools management. |
|
||||
| `/plugins?tab=model` | Do not redirect to Integrations | Marketplace model category. |
|
||||
| `/plugins?tab=trigger` | Do not redirect to Integrations | Marketplace trigger category. |
|
||||
| `/plugins?tab=agent-strategy` | Do not redirect to Integrations | Marketplace agent strategy category. |
|
||||
| `/plugins?tab=extension` | Do not redirect to Integrations | Marketplace extension category. |
|
||||
| `/plugins?tab=datasource` | Do not redirect to Integrations | Marketplace datasource category. |
|
||||
| `/plugins?tab=bundle` | Do not redirect to Integrations | Marketplace bundle category. |
|
||||
|
||||
## Migration Order
|
||||
|
||||
1. Add the canonical route map and route tests.
|
||||
2. Mount the existing pages under the new Integrations routes without UI redesign.
|
||||
3. Update internal links to generate canonical URLs.
|
||||
4. Add legacy redirects for `/tools` and `/plugins`.
|
||||
5. Keep compatibility tests for each legacy route until old links can be removed.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Main Nav Gating Follow-ups
|
||||
|
||||
Context: the desktop MainNav rewrite moved several workspace, account, tools, and marketplace entry points out of the old header/account-setting layout. These notes track product-contract questions that should be resolved before treating the rewrite as behavior-complete.
|
||||
|
||||
Current status:
|
||||
|
||||
- Open: account-setting modal navigation API naming, Marketplace/Integrations install task status parity, account language/timezone access parity.
|
||||
- Partially resolved: Apps/Datasets quick-switch/create parity.
|
||||
|
||||
## 1. Account-setting modal naming and moved destinations
|
||||
|
||||
Status: Open.
|
||||
|
||||
Current branch behavior:
|
||||
|
||||
- `setShowAccountSettingModal(PROVIDER)` routes to `/integrations/model-provider`.
|
||||
- `setShowAccountSettingModal(DATA_SOURCE)` routes to `/integrations/data-source`.
|
||||
- `setShowAccountSettingModal(API_BASED_EXTENSION)` routes to `/integrations/tools/api-extension`.
|
||||
- Document Settings no longer directly renders the old Account Settings modal for Provider; it uses the Integrations destination helper.
|
||||
|
||||
Old behavior: these calls opened the account-setting modal and switched to the matching tab.
|
||||
|
||||
Question:
|
||||
|
||||
- Since Provider, Data Source, and API-based Extension are no longer inside Account Settings in the new design, should this API still be named `setShowAccountSettingModal` for those destinations?
|
||||
|
||||
Follow-up decision needed:
|
||||
|
||||
- Either keep the compatibility shim but document that these payloads are now route destinations, or introduce a clearer navigation API for integration destinations and update call sites intentionally.
|
||||
- Re-check call sites launched from workflows, datasets, and app configuration when new entry points are added. The known Document Settings provider entry has been migrated.
|
||||
|
||||
## 2. Plugin and marketplace status parity
|
||||
|
||||
Status: Open.
|
||||
|
||||
Old header behavior:
|
||||
|
||||
- `PluginsNav` showed plugin install progress and error state through the installing icon and red indicator.
|
||||
- Installing tasks showed the downloading icon.
|
||||
- Failed or erroring install tasks showed the red status indicator.
|
||||
|
||||
Current MainNav behavior:
|
||||
|
||||
- MainNav has a Marketplace link, but it does not surface plugin installing/error state.
|
||||
- Integrations has install entry points, but it does not surface the old `PluginTasks` install-task status entry near the Integrations install action.
|
||||
- Marketplace is the product discovery surface for uninstalled integrations; `PluginTasks` is only the transient install-task status inbox for running/succeeded/failed installs.
|
||||
|
||||
Follow-up decision needed:
|
||||
|
||||
- Decide whether MainNav Marketplace should preserve the old plugin task status indicator.
|
||||
- Decide whether Integrations should expose `PluginTasks` near the install action so users can inspect failed/running install tasks without returning to the old `/plugins` shell.
|
||||
- If yes, reuse the existing `usePluginTaskStatus` behavior instead of creating a parallel status source.
|
||||
|
||||
## 3. Account language and timezone access
|
||||
|
||||
Status: Open.
|
||||
|
||||
Current branch behavior:
|
||||
|
||||
- Desktop MainNav account menu includes Language and Timezone submenus.
|
||||
- The main app layout now uses MainNav across breakpoints.
|
||||
- The default account dropdown does not expose a direct Language/Timezone settings entry.
|
||||
- The default account dropdown still exists in non-MainNav account/header surfaces such as the account layout.
|
||||
- Language and Timezone still belong to Account Settings, not Integrations.
|
||||
- `UpdateSettingPopover` still links the timezone hint to `ACCOUNT_SETTING_TAB.LANGUAGE`.
|
||||
- The legacy `ReferenceSettingModal` auto-update timezone hint also still links to `ACCOUNT_SETTING_TAB.LANGUAGE`.
|
||||
|
||||
Decision:
|
||||
|
||||
- Preserve the old language-access contract across breakpoints.
|
||||
- The desktop MainNav path is acceptable; the remaining question is default account-dropdown parity wherever that path remains active, plus whether hidden Account Settings Language entry points should remain acceptable.
|
||||
|
||||
Follow-up decision needed:
|
||||
|
||||
- Add an equivalent language/timezone entry to the default account path, or otherwise ensure users in those surfaces can still reach language settings.
|
||||
- Decide whether the Update Setting timezone hint should keep opening the hidden Account Settings Language page, or whether Account Settings should surface Language in its visible menu.
|
||||
- Keep this as gate-contract parity, not a visual requirement to recreate the old Account Settings sidebar.
|
||||
|
||||
## 4. Apps and Datasets quick-switch/create parity
|
||||
|
||||
Status: Partially resolved.
|
||||
|
||||
Old header behavior:
|
||||
|
||||
- `AppNav` could show the current app, list more apps, load more results, and launch create-app flows from the header nav.
|
||||
- `DatasetNav` could show the current dataset, list more datasets, load more results, and launch dataset creation from the header nav.
|
||||
|
||||
Current MainNav behavior:
|
||||
|
||||
- Apps is no longer only a static navigation link: MainNav includes a Web Apps section with installed web app search, pin, delete, and navigation behavior.
|
||||
- Apps still does not preserve the old `AppNav` current-app switcher, load-more behavior, or create-app flows.
|
||||
- Datasets is still a navigation link and does not preserve the old `DatasetNav` current-dataset switcher, load-more behavior, or dataset creation entry.
|
||||
|
||||
Follow-up decision needed:
|
||||
|
||||
- Decide whether the new design intentionally removes these quick-switch/create affordances.
|
||||
- If not, add equivalent behavior in the MainNav flow without copying the old header UI directly.
|
||||
@@ -108,17 +108,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx": {
|
||||
"no-console": {
|
||||
"count": 19
|
||||
@@ -249,17 +238,9 @@
|
||||
},
|
||||
"web/app/components/app-sidebar/index.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app-sidebar/nav-link/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
@@ -611,11 +592,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/settings/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/trigger-card.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -662,17 +638,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/apps/new-app-card.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/action-button/index.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
@@ -2264,11 +2229,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/list/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -2348,19 +2308,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/app-list/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/banner/banner-item.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/banner/indicator-button.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
@@ -2382,34 +2329,16 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/actions/commands/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/actions/commands/registry.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/actions/commands/slash.tsx": {
|
||||
"react-refresh/only-export-components": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/actions/commands/types.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/actions/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/actions/plugin.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@@ -2425,11 +2354,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/command-selector.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/components/footer.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
@@ -2453,21 +2377,11 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/hooks/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/hooks/use-goto-anything-results.ts": {
|
||||
"@tanstack/query/exhaustive-deps": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/data-source-page-new/card.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/data-source-page-new/hooks/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
@@ -2483,14 +2397,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/data-source-page-new/item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/data-source-page-new/types.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -2636,11 +2542,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/card/index.tsx": {
|
||||
"ts/no-non-null-asserted-optional-chain": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/install-plugin/hooks.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
@@ -2869,11 +2770,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/empty/index.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/filter-management/category-filter.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@@ -2884,11 +2780,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/filter-management/tag-filter.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
@@ -2902,11 +2793,6 @@
|
||||
"count": 25
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/update-plugin/from-market-place.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/hidden-fields.spec.tsx": {
|
||||
"react/static-components": {
|
||||
"count": 4
|
||||
@@ -3187,54 +3073,16 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/create-card.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/headers-input.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/mcp-server-param-item.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/provider-card.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/sections/authentication-section.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/sections/configurations-section.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/provider-list.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/provider/empty.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/setting/build-in/config-credentials.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
@@ -3389,11 +3237,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/header/__tests__/index.spec.tsx": {
|
||||
"react/static-components": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/header/test-run-menu.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
@@ -3463,16 +3306,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/hooks/use-workflow-canvas-maximize.ts": {
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/hooks/use-workflow-interactions.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/hooks/use-workflow-run-event/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 19
|
||||
@@ -4739,11 +4572,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/store/workflow/layout-slice.ts": {
|
||||
"no-restricted-properties": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/store/workflow/workflow-draft-slice.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -4990,11 +4818,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/context/provider-context-provider.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/context/web-app-context.tsx": {
|
||||
"react-refresh/only-export-components": {
|
||||
"count": 1
|
||||
@@ -5043,46 +4866,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/i18n/de-DE/billing.json": {
|
||||
"no-irregular-whitespace": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/i18n/en-US/app-debug.json": {
|
||||
"no-irregular-whitespace": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/i18n/fr-FR/app-debug.json": {
|
||||
"no-irregular-whitespace": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/i18n/fr-FR/plugin-trigger.json": {
|
||||
"no-irregular-whitespace": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/i18n/fr-FR/tools.json": {
|
||||
"no-irregular-whitespace": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/i18n/pt-BR/common.json": {
|
||||
"no-irregular-whitespace": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/i18n/ru-RU/common.json": {
|
||||
"no-irregular-whitespace": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/i18n/uk-UA/app-debug.json": {
|
||||
"no-irregular-whitespace": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/models/access-control.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
@@ -5093,14 +4876,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/models/common.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/models/datasets.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 7
|
||||
@@ -5215,7 +4990,7 @@
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 29
|
||||
"count": 27
|
||||
}
|
||||
},
|
||||
"web/service/datasets.ts": {
|
||||
@@ -5411,20 +5186,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/service/use-plugins.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"regexp/no-unused-capturing-group": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/service/use-snippet-workflows.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@@ -936,6 +936,7 @@ export type AppPartial = {
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
id: string
|
||||
is_starred?: boolean
|
||||
max_active_requests?: number | null
|
||||
mode_compatible_with_agent: string
|
||||
name: string
|
||||
@@ -2029,8 +2030,8 @@ export type GetAppsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
creator_ids?: Array<string> | null
|
||||
is_created_by_me?: boolean | null
|
||||
creator_ids?: Array<string>
|
||||
is_created_by_me?: boolean
|
||||
limit?: number
|
||||
mode?:
|
||||
| 'advanced-chat'
|
||||
@@ -2041,9 +2042,10 @@ export type GetAppsData = {
|
||||
| 'chat'
|
||||
| 'completion'
|
||||
| 'workflow'
|
||||
name?: string | null
|
||||
name?: string
|
||||
page?: number
|
||||
tag_ids?: Array<string> | null
|
||||
sort_by?: 'earliest_created' | 'last_modified' | 'recently_created'
|
||||
tag_ids?: Array<string>
|
||||
}
|
||||
url: '/apps'
|
||||
}
|
||||
@@ -2137,6 +2139,36 @@ export type PostAppsImportsByImportIdConfirmResponses = {
|
||||
export type PostAppsImportsByImportIdConfirmResponse
|
||||
= PostAppsImportsByImportIdConfirmResponses[keyof PostAppsImportsByImportIdConfirmResponses]
|
||||
|
||||
export type GetAppsStarredData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
creator_ids?: Array<string>
|
||||
is_created_by_me?: boolean
|
||||
limit?: number
|
||||
mode?:
|
||||
| 'advanced-chat'
|
||||
| 'agent'
|
||||
| 'agent-chat'
|
||||
| 'all'
|
||||
| 'channel'
|
||||
| 'chat'
|
||||
| 'completion'
|
||||
| 'workflow'
|
||||
name?: string
|
||||
page?: number
|
||||
sort_by?: 'earliest_created' | 'last_modified' | 'recently_created'
|
||||
tag_ids?: Array<string>
|
||||
}
|
||||
url: '/apps/starred'
|
||||
}
|
||||
|
||||
export type GetAppsStarredResponses = {
|
||||
200: AppPagination
|
||||
}
|
||||
|
||||
export type GetAppsStarredResponse = GetAppsStarredResponses[keyof GetAppsStarredResponses]
|
||||
|
||||
export type PostAppsWorkflowsOnlineUsersData = {
|
||||
body: WorkflowOnlineUsersPayload
|
||||
path?: never
|
||||
@@ -3750,6 +3782,55 @@ export type PostAppsByAppIdSiteAccessTokenResetResponses = {
|
||||
export type PostAppsByAppIdSiteAccessTokenResetResponse
|
||||
= PostAppsByAppIdSiteAccessTokenResetResponses[keyof PostAppsByAppIdSiteAccessTokenResetResponses]
|
||||
|
||||
export type DeleteAppsByAppIdStarData = {
|
||||
body?: never
|
||||
path: {
|
||||
app_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/apps/{app_id}/star'
|
||||
}
|
||||
|
||||
export type DeleteAppsByAppIdStarErrors = {
|
||||
404: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type DeleteAppsByAppIdStarError
|
||||
= DeleteAppsByAppIdStarErrors[keyof DeleteAppsByAppIdStarErrors]
|
||||
|
||||
export type DeleteAppsByAppIdStarResponses = {
|
||||
200: SimpleResultResponse
|
||||
}
|
||||
|
||||
export type DeleteAppsByAppIdStarResponse
|
||||
= DeleteAppsByAppIdStarResponses[keyof DeleteAppsByAppIdStarResponses]
|
||||
|
||||
export type PostAppsByAppIdStarData = {
|
||||
body?: never
|
||||
path: {
|
||||
app_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/apps/{app_id}/star'
|
||||
}
|
||||
|
||||
export type PostAppsByAppIdStarErrors = {
|
||||
404: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type PostAppsByAppIdStarError = PostAppsByAppIdStarErrors[keyof PostAppsByAppIdStarErrors]
|
||||
|
||||
export type PostAppsByAppIdStarResponses = {
|
||||
200: SimpleResultResponse
|
||||
}
|
||||
|
||||
export type PostAppsByAppIdStarResponse
|
||||
= PostAppsByAppIdStarResponses[keyof PostAppsByAppIdStarResponses]
|
||||
|
||||
export type GetAppsByAppIdStatisticsAverageResponseTimeData = {
|
||||
body?: never
|
||||
path: {
|
||||
|
||||
@@ -1401,6 +1401,7 @@ export const zAppPartial = z.object({
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
id: z.string(),
|
||||
is_starred: z.boolean().optional().default(false),
|
||||
max_active_requests: z.int().nullish(),
|
||||
mode_compatible_with_agent: z.string(),
|
||||
name: z.string(),
|
||||
@@ -2711,8 +2712,8 @@ export const zWorkflowCommentDetailWritable = z.object({
|
||||
})
|
||||
|
||||
export const zGetAppsQuery = z.object({
|
||||
creator_ids: z.array(z.string()).nullish(),
|
||||
is_created_by_me: z.boolean().nullish(),
|
||||
creator_ids: z.array(z.string()).optional(),
|
||||
is_created_by_me: z.boolean().optional(),
|
||||
limit: z.int().gte(1).lte(100).optional().default(20),
|
||||
mode: z
|
||||
.enum([
|
||||
@@ -2727,9 +2728,13 @@ export const zGetAppsQuery = z.object({
|
||||
])
|
||||
.optional()
|
||||
.default('all'),
|
||||
name: z.string().nullish(),
|
||||
name: z.string().optional(),
|
||||
page: z.int().gte(1).lte(99999).optional().default(1),
|
||||
tag_ids: z.array(z.string()).nullish(),
|
||||
sort_by: z
|
||||
.enum(['earliest_created', 'last_modified', 'recently_created'])
|
||||
.optional()
|
||||
.default('last_modified'),
|
||||
tag_ids: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -2769,6 +2774,37 @@ export const zPostAppsImportsByImportIdConfirmPath = z.object({
|
||||
*/
|
||||
export const zPostAppsImportsByImportIdConfirmResponse = zImport
|
||||
|
||||
export const zGetAppsStarredQuery = z.object({
|
||||
creator_ids: z.array(z.string()).optional(),
|
||||
is_created_by_me: z.boolean().optional(),
|
||||
limit: z.int().gte(1).lte(100).optional().default(20),
|
||||
mode: z
|
||||
.enum([
|
||||
'advanced-chat',
|
||||
'agent',
|
||||
'agent-chat',
|
||||
'all',
|
||||
'channel',
|
||||
'chat',
|
||||
'completion',
|
||||
'workflow',
|
||||
])
|
||||
.optional()
|
||||
.default('all'),
|
||||
name: z.string().optional(),
|
||||
page: z.int().gte(1).lte(99999).optional().default(1),
|
||||
sort_by: z
|
||||
.enum(['earliest_created', 'last_modified', 'recently_created'])
|
||||
.optional()
|
||||
.default('last_modified'),
|
||||
tag_ids: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zGetAppsStarredResponse = zAppPagination
|
||||
|
||||
export const zPostAppsWorkflowsOnlineUsersBody = zWorkflowOnlineUsersPayload
|
||||
|
||||
/**
|
||||
@@ -3549,6 +3585,24 @@ export const zPostAppsByAppIdSiteAccessTokenResetPath = z.object({
|
||||
*/
|
||||
export const zPostAppsByAppIdSiteAccessTokenResetResponse = zAppSiteResponse
|
||||
|
||||
export const zDeleteAppsByAppIdStarPath = z.object({
|
||||
app_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zDeleteAppsByAppIdStarResponse = zSimpleResultResponse
|
||||
|
||||
export const zPostAppsByAppIdStarPath = z.object({
|
||||
app_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostAppsByAppIdStarResponse = zSimpleResultResponse
|
||||
|
||||
export const zGetAppsByAppIdStatisticsAverageResponseTimePath = z.object({
|
||||
app_id: z.string(),
|
||||
})
|
||||
|
||||
@@ -6,17 +6,34 @@ import * as z from 'zod'
|
||||
import {
|
||||
zGetExploreAppsByAppIdPath,
|
||||
zGetExploreAppsByAppIdResponse,
|
||||
zGetExploreAppsLearnDifyQuery,
|
||||
zGetExploreAppsLearnDifyResponse,
|
||||
zGetExploreAppsQuery,
|
||||
zGetExploreAppsResponse,
|
||||
zGetExploreBannersResponse,
|
||||
} from './zod.gen'
|
||||
|
||||
export const get = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
operationId: 'getExploreAppsLearnDify',
|
||||
path: '/explore/apps/learn-dify',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetExploreAppsLearnDifyQuery.optional() }))
|
||||
.output(zGetExploreAppsLearnDifyResponse)
|
||||
|
||||
export const learnDify = {
|
||||
get,
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const get = oc
|
||||
export const get2 = oc
|
||||
.route({
|
||||
deprecated: true,
|
||||
description:
|
||||
@@ -31,10 +48,10 @@ export const get = oc
|
||||
.output(zGetExploreAppsByAppIdResponse)
|
||||
|
||||
export const byAppId = {
|
||||
get,
|
||||
get: get2,
|
||||
}
|
||||
|
||||
export const get2 = oc
|
||||
export const get3 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -46,7 +63,8 @@ export const get2 = oc
|
||||
.output(zGetExploreAppsResponse)
|
||||
|
||||
export const apps = {
|
||||
get: get2,
|
||||
get: get3,
|
||||
learnDify,
|
||||
byAppId,
|
||||
}
|
||||
|
||||
@@ -57,7 +75,7 @@ export const apps = {
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const get3 = oc
|
||||
export const get4 = oc
|
||||
.route({
|
||||
deprecated: true,
|
||||
description:
|
||||
@@ -72,7 +90,7 @@ export const get3 = oc
|
||||
.output(zGetExploreBannersResponse)
|
||||
|
||||
export const banners = {
|
||||
get: get3,
|
||||
get: get4,
|
||||
}
|
||||
|
||||
export const explore = {
|
||||
|
||||
@@ -9,6 +9,10 @@ export type RecommendedAppListResponse = {
|
||||
recommended_apps: Array<RecommendedAppResponse>
|
||||
}
|
||||
|
||||
export type LearnDifyAppListResponse = {
|
||||
recommended_apps: Array<RecommendedAppResponse>
|
||||
}
|
||||
|
||||
export type RecommendedAppResponse = {
|
||||
app?: RecommendedAppInfoResponse
|
||||
app_id: string
|
||||
@@ -46,6 +50,22 @@ export type GetExploreAppsResponses = {
|
||||
|
||||
export type GetExploreAppsResponse = GetExploreAppsResponses[keyof GetExploreAppsResponses]
|
||||
|
||||
export type GetExploreAppsLearnDifyData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
language?: string
|
||||
}
|
||||
url: '/explore/apps/learn-dify'
|
||||
}
|
||||
|
||||
export type GetExploreAppsLearnDifyResponses = {
|
||||
200: LearnDifyAppListResponse
|
||||
}
|
||||
|
||||
export type GetExploreAppsLearnDifyResponse
|
||||
= GetExploreAppsLearnDifyResponses[keyof GetExploreAppsLearnDifyResponses]
|
||||
|
||||
export type GetExploreAppsByAppIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
|
||||
@@ -38,6 +38,13 @@ export const zRecommendedAppListResponse = z.object({
|
||||
recommended_apps: z.array(zRecommendedAppResponse),
|
||||
})
|
||||
|
||||
/**
|
||||
* LearnDifyAppListResponse
|
||||
*/
|
||||
export const zLearnDifyAppListResponse = z.object({
|
||||
recommended_apps: z.array(zRecommendedAppResponse),
|
||||
})
|
||||
|
||||
export const zGetExploreAppsQuery = z.object({
|
||||
language: z.string().optional(),
|
||||
})
|
||||
@@ -47,6 +54,15 @@ export const zGetExploreAppsQuery = z.object({
|
||||
*/
|
||||
export const zGetExploreAppsResponse = zRecommendedAppListResponse
|
||||
|
||||
export const zGetExploreAppsLearnDifyQuery = z.object({
|
||||
language: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zGetExploreAppsLearnDifyResponse = zLearnDifyAppListResponse
|
||||
|
||||
export const zGetExploreAppsByAppIdPath = z.object({
|
||||
app_id: z.string(),
|
||||
})
|
||||
|
||||
@@ -285,6 +285,30 @@ export type WorkspacePermissionResponse = {
|
||||
workspace_id: string
|
||||
}
|
||||
|
||||
export type ParserAutoUpgradeChange = {
|
||||
auto_upgrade: PluginAutoUpgradeSettingsPayload
|
||||
category: PluginCategory
|
||||
}
|
||||
|
||||
export type PluginAutoUpgradeChangeResponse = {
|
||||
message?: string | null
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export type ParserExcludePlugin = {
|
||||
category: PluginCategory
|
||||
plugin_id: string
|
||||
}
|
||||
|
||||
export type SuccessResponse = {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export type PluginAutoUpgradeFetchResponse = {
|
||||
auto_upgrade: PluginAutoUpgradeSettingsResponseModel
|
||||
category: PluginCategory
|
||||
}
|
||||
|
||||
export type PluginDebuggingKeyResponse = {
|
||||
host: string
|
||||
key: string
|
||||
@@ -318,21 +342,8 @@ export type ParserDynamicOptionsWithCredentials = {
|
||||
}
|
||||
|
||||
export type ParserPermissionChange = {
|
||||
debug_permission: DebugPermission
|
||||
install_permission: InstallPermission
|
||||
}
|
||||
|
||||
export type SuccessResponse = {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export type ParserExcludePlugin = {
|
||||
plugin_id: string
|
||||
}
|
||||
|
||||
export type ParserPreferencesChange = {
|
||||
auto_upgrade: PluginAutoUpgradeSettingsPayload
|
||||
permission: PluginPermissionSettingsPayload
|
||||
debug_permission?: DebugPermission
|
||||
install_permission?: InstallPermission
|
||||
}
|
||||
|
||||
export type ParserUninstall = {
|
||||
@@ -358,6 +369,12 @@ export type ParserGithubUpload = {
|
||||
version: string
|
||||
}
|
||||
|
||||
export type PluginCategoryListResponse = {
|
||||
builtin_tools: Array<PluginCategoryBuiltinToolProviderResponse>
|
||||
has_more: boolean
|
||||
plugins: Array<PluginCategoryInstalledPluginResponse>
|
||||
}
|
||||
|
||||
export type ApiToolProviderAddPayload = {
|
||||
credentials: {
|
||||
[key: string]: unknown
|
||||
@@ -650,10 +667,6 @@ export type LoadBalancingPayload = {
|
||||
enabled?: boolean | null
|
||||
}
|
||||
|
||||
export type DebugPermission = 'admins' | 'everyone' | 'noone'
|
||||
|
||||
export type InstallPermission = 'admins' | 'everyone' | 'noone'
|
||||
|
||||
export type PluginAutoUpgradeSettingsPayload = {
|
||||
exclude_plugins?: Array<string>
|
||||
include_plugins?: Array<string>
|
||||
@@ -662,9 +675,66 @@ export type PluginAutoUpgradeSettingsPayload = {
|
||||
upgrade_time_of_day?: number
|
||||
}
|
||||
|
||||
export type PluginPermissionSettingsPayload = {
|
||||
debug_permission?: DebugPermission
|
||||
install_permission?: InstallPermission
|
||||
export type PluginCategory
|
||||
= | 'agent-strategy'
|
||||
| 'datasource'
|
||||
| 'extension'
|
||||
| 'model'
|
||||
| 'tool'
|
||||
| 'trigger'
|
||||
|
||||
export type PluginAutoUpgradeSettingsResponseModel = {
|
||||
exclude_plugins: Array<string>
|
||||
include_plugins: Array<string>
|
||||
strategy_setting: StrategySetting
|
||||
upgrade_mode: UpgradeMode
|
||||
upgrade_time_of_day: number
|
||||
}
|
||||
|
||||
export type DebugPermission = 'admins' | 'everyone' | 'noone'
|
||||
|
||||
export type InstallPermission = 'admins' | 'everyone' | 'noone'
|
||||
|
||||
export type PluginCategoryBuiltinToolProviderResponse = {
|
||||
allow_delete: boolean
|
||||
author: string
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
icon: unknown
|
||||
icon_dark: unknown
|
||||
id: string
|
||||
is_team_authorization: boolean
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
labels: Array<string>
|
||||
name: string
|
||||
plugin_id: string | null
|
||||
plugin_unique_identifier: string | null
|
||||
team_credentials: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
tools: Array<PluginCategoryBuiltinToolResponse>
|
||||
type: ToolProviderType
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type PluginCategoryInstalledPluginResponse = {
|
||||
checksum: string
|
||||
created_at: string
|
||||
declaration: PluginDeclaration
|
||||
endpoints_active: number
|
||||
endpoints_setups: number
|
||||
id: string
|
||||
installation_id: string
|
||||
meta: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
name: string
|
||||
plugin_id: string
|
||||
plugin_unique_identifier: string
|
||||
runtime_type: string
|
||||
source: PluginInstallationSource
|
||||
tenant_id: string
|
||||
updated_at: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export type ApiProviderSchemaType = 'openai_actions' | 'openai_plugin' | 'openapi' | 'swagger'
|
||||
@@ -683,8 +753,467 @@ export type StrategySetting = 'disabled' | 'fix_only' | 'latest'
|
||||
|
||||
export type UpgradeMode = 'all' | 'exclude' | 'partial'
|
||||
|
||||
export type CoreToolsEntitiesCommonEntitiesI18nObject = {
|
||||
en_US: string
|
||||
ja_JP?: string | null
|
||||
pt_BR?: string | null
|
||||
zh_Hans?: string | null
|
||||
}
|
||||
|
||||
export type PluginCategoryBuiltinToolResponse = {
|
||||
author: string
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
labels: Array<string>
|
||||
name: string
|
||||
output_schema: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
parameters?: Array<{
|
||||
[key: string]: unknown
|
||||
}> | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ToolProviderType
|
||||
= | 'api'
|
||||
| 'app'
|
||||
| 'builtin'
|
||||
| 'dataset-retrieval'
|
||||
| 'mcp'
|
||||
| 'plugin'
|
||||
| 'workflow'
|
||||
|
||||
export type PluginDeclaration = {
|
||||
agent_strategy?: AgentStrategyProviderEntity
|
||||
author: string | null
|
||||
category: PluginCategory
|
||||
created_at: string
|
||||
datasource?: DatasourceProviderEntity
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
endpoint?: EndpointProviderDeclaration
|
||||
icon: string
|
||||
icon_dark?: string | null
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
meta: Meta
|
||||
model?: ProviderEntity
|
||||
name: string
|
||||
plugins: Plugins
|
||||
repo?: string | null
|
||||
resource: PluginResourceRequirements
|
||||
tags?: Array<string>
|
||||
tool?: ToolProviderEntity
|
||||
trigger?: TriggerProviderEntity
|
||||
verified?: boolean
|
||||
version: string
|
||||
}
|
||||
|
||||
export type PluginInstallationSource = 'github' | 'marketplace' | 'package' | 'remote'
|
||||
|
||||
export type ToolParameterForm = 'form' | 'llm' | 'schema'
|
||||
|
||||
export type AgentStrategyProviderEntity = {
|
||||
identity: AgentStrategyProviderIdentity
|
||||
plugin_id?: string | null
|
||||
}
|
||||
|
||||
export type DatasourceProviderEntity = {
|
||||
credentials_schema?: Array<ProviderConfig>
|
||||
identity: DatasourceProviderIdentity
|
||||
oauth_schema?: OAuthSchema
|
||||
provider_type: DatasourceProviderType
|
||||
}
|
||||
|
||||
export type EndpointProviderDeclaration = {
|
||||
endpoints?: Array<EndpointDeclaration> | null
|
||||
settings?: Array<ProviderConfig>
|
||||
}
|
||||
|
||||
export type Meta = {
|
||||
minimum_dify_version?: string | null
|
||||
version?: string | null
|
||||
}
|
||||
|
||||
export type ProviderEntity = {
|
||||
background?: string | null
|
||||
configurate_methods: Array<ConfigurateMethod>
|
||||
description?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
help?: ProviderHelpEntity
|
||||
icon_small?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
icon_small_dark?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
model_credential_schema?: ModelCredentialSchema
|
||||
models?: Array<AiModelEntity>
|
||||
position?: {
|
||||
[key: string]: Array<string>
|
||||
} | null
|
||||
provider: string
|
||||
provider_credential_schema?: ProviderCredentialSchema
|
||||
provider_name?: string
|
||||
supported_model_types: Array<ModelType>
|
||||
}
|
||||
|
||||
export type Plugins = {
|
||||
datasources?: Array<string> | null
|
||||
endpoints?: Array<string> | null
|
||||
models?: Array<string> | null
|
||||
tools?: Array<string> | null
|
||||
triggers?: Array<string> | null
|
||||
}
|
||||
|
||||
export type PluginResourceRequirements = {
|
||||
memory: number
|
||||
permission?: Permission
|
||||
}
|
||||
|
||||
export type ToolProviderEntity = {
|
||||
credentials_schema?: Array<ProviderConfig>
|
||||
identity: ToolProviderIdentity
|
||||
oauth_schema?: OAuthSchema
|
||||
plugin_id?: string | null
|
||||
}
|
||||
|
||||
export type TriggerProviderEntity = {
|
||||
events?: Array<EventEntity>
|
||||
identity: TriggerProviderIdentity
|
||||
subscription_constructor?: SubscriptionConstructor
|
||||
subscription_schema?: Array<ProviderConfig>
|
||||
}
|
||||
|
||||
export type AgentStrategyProviderIdentity = {
|
||||
author: string
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
icon: string
|
||||
icon_dark?: string | null
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
name: string
|
||||
tags?: Array<ToolLabelEnum> | null
|
||||
}
|
||||
|
||||
export type ProviderConfig = {
|
||||
default?: unknown
|
||||
help?: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
label?: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
multiple?: boolean | null
|
||||
name: string
|
||||
options?: Array<Option> | null
|
||||
placeholder?: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
required?: boolean
|
||||
scope?: unknown
|
||||
type: CoreEntitiesProviderEntitiesBasicProviderConfigType
|
||||
url?: string | null
|
||||
}
|
||||
|
||||
export type DatasourceProviderIdentity = {
|
||||
author: string
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
icon: string
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
name: string
|
||||
tags?: Array<ToolLabelEnum> | null
|
||||
}
|
||||
|
||||
export type OAuthSchema = {
|
||||
client_schema?: Array<ProviderConfig>
|
||||
credentials_schema?: Array<ProviderConfig>
|
||||
}
|
||||
|
||||
export type DatasourceProviderType
|
||||
= | 'local_file'
|
||||
| 'online_document'
|
||||
| 'online_drive'
|
||||
| 'website_crawl'
|
||||
|
||||
export type EndpointDeclaration = {
|
||||
hidden?: boolean
|
||||
method: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export type ConfigurateMethod = 'customizable-model' | 'predefined-model'
|
||||
|
||||
export type GraphonModelRuntimeEntitiesCommonEntitiesI18nObject = {
|
||||
en_US: string
|
||||
zh_Hans?: string | null
|
||||
}
|
||||
|
||||
export type ProviderHelpEntity = {
|
||||
title: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
url: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
}
|
||||
|
||||
export type ModelCredentialSchema = {
|
||||
credential_form_schemas: Array<CredentialFormSchema>
|
||||
model: FieldModelSchema
|
||||
}
|
||||
|
||||
export type AiModelEntity = {
|
||||
deprecated?: boolean
|
||||
features?: Array<ModelFeature> | null
|
||||
fetch_from: FetchFrom
|
||||
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
model: string
|
||||
model_properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
model_type: ModelType
|
||||
parameter_rules?: Array<ParameterRule>
|
||||
pricing?: PriceConfig
|
||||
}
|
||||
|
||||
export type ProviderCredentialSchema = {
|
||||
credential_form_schemas: Array<CredentialFormSchema>
|
||||
}
|
||||
|
||||
export type Permission = {
|
||||
endpoint?: Endpoint
|
||||
model?: Model
|
||||
node?: Node
|
||||
storage?: Storage
|
||||
tool?: Tool
|
||||
}
|
||||
|
||||
export type ToolProviderIdentity = {
|
||||
author: string
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
icon: string
|
||||
icon_dark?: string | null
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
name: string
|
||||
tags?: Array<ToolLabelEnum> | null
|
||||
}
|
||||
|
||||
export type EventEntity = {
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
identity: EventIdentity
|
||||
output_schema?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
parameters?: Array<EventParameter>
|
||||
}
|
||||
|
||||
export type TriggerProviderIdentity = {
|
||||
author: string
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
icon?: string | null
|
||||
icon_dark?: string | null
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
name: string
|
||||
tags?: Array<string>
|
||||
}
|
||||
|
||||
export type SubscriptionConstructor = {
|
||||
credentials_schema?: Array<ProviderConfig>
|
||||
oauth_schema?: OAuthSchema
|
||||
parameters?: Array<EventParameter>
|
||||
}
|
||||
|
||||
export type ToolLabelEnum
|
||||
= | 'business'
|
||||
| 'design'
|
||||
| 'education'
|
||||
| 'entertainment'
|
||||
| 'finance'
|
||||
| 'image'
|
||||
| 'medical'
|
||||
| 'news'
|
||||
| 'other'
|
||||
| 'productivity'
|
||||
| 'rag'
|
||||
| 'search'
|
||||
| 'social'
|
||||
| 'travel'
|
||||
| 'utilities'
|
||||
| 'videos'
|
||||
| 'weather'
|
||||
|
||||
export type Option = {
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
value: string
|
||||
}
|
||||
|
||||
export type AppSelectorScope = 'all' | 'chat' | 'completion' | 'workflow'
|
||||
|
||||
export type ModelSelectorScope
|
||||
= | 'llm'
|
||||
| 'moderation'
|
||||
| 'rerank'
|
||||
| 'speech2text'
|
||||
| 'text-embedding'
|
||||
| 'tts'
|
||||
| 'vision'
|
||||
|
||||
export type ToolSelectorScope = 'all' | 'builtin' | 'custom' | 'workflow'
|
||||
|
||||
export type CoreEntitiesProviderEntitiesBasicProviderConfigType
|
||||
= | 'app-selector'
|
||||
| 'array[tools]'
|
||||
| 'boolean'
|
||||
| 'model-selector'
|
||||
| 'secret-input'
|
||||
| 'select'
|
||||
| 'text-input'
|
||||
|
||||
export type CredentialFormSchema = {
|
||||
default?: string | null
|
||||
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
max_length?: number
|
||||
options?: Array<FormOption> | null
|
||||
placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
required?: boolean
|
||||
show_on?: Array<FormShowOnObject>
|
||||
type: FormType
|
||||
variable: string
|
||||
}
|
||||
|
||||
export type FieldModelSchema = {
|
||||
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
}
|
||||
|
||||
export type ModelFeature
|
||||
= | 'agent-thought'
|
||||
| 'audio'
|
||||
| 'document'
|
||||
| 'multi-tool-call'
|
||||
| 'stream-tool-call'
|
||||
| 'structured-output'
|
||||
| 'tool-call'
|
||||
| 'video'
|
||||
| 'vision'
|
||||
|
||||
export type FetchFrom = 'customizable-model' | 'predefined-model'
|
||||
|
||||
export type ModelPropertyKey
|
||||
= | 'audio_type'
|
||||
| 'context_size'
|
||||
| 'default_voice'
|
||||
| 'file_upload_limit'
|
||||
| 'max_characters_per_chunk'
|
||||
| 'max_chunks'
|
||||
| 'max_workers'
|
||||
| 'mode'
|
||||
| 'supported_file_extensions'
|
||||
| 'voices'
|
||||
| 'word_limit'
|
||||
|
||||
export type ParameterRule = {
|
||||
default?: unknown
|
||||
help?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
max?: number | null
|
||||
min?: number | null
|
||||
name: string
|
||||
options?: Array<string>
|
||||
precision?: number | null
|
||||
required?: boolean
|
||||
type: ParameterType
|
||||
use_template?: string | null
|
||||
}
|
||||
|
||||
export type PriceConfig = {
|
||||
currency: string
|
||||
input: string
|
||||
output?: string | null
|
||||
unit: string
|
||||
}
|
||||
|
||||
export type Endpoint = {
|
||||
enabled?: boolean | null
|
||||
}
|
||||
|
||||
export type Model = {
|
||||
enabled?: boolean | null
|
||||
llm?: boolean | null
|
||||
moderation?: boolean | null
|
||||
rerank?: boolean | null
|
||||
speech2text?: boolean | null
|
||||
text_embedding?: boolean | null
|
||||
tts?: boolean | null
|
||||
}
|
||||
|
||||
export type Node = {
|
||||
enabled?: boolean | null
|
||||
}
|
||||
|
||||
export type Storage = {
|
||||
enabled?: boolean | null
|
||||
size?: number
|
||||
}
|
||||
|
||||
export type Tool = {
|
||||
enabled?: boolean | null
|
||||
}
|
||||
|
||||
export type EventIdentity = {
|
||||
author: string
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
name: string
|
||||
provider?: string | null
|
||||
}
|
||||
|
||||
export type EventParameter = {
|
||||
auto_generate?: PluginParameterAutoGenerate
|
||||
default?: unknown
|
||||
description?: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
max?: unknown
|
||||
min?: unknown
|
||||
multiple?: boolean | null
|
||||
name: string
|
||||
options?: Array<PluginParameterOption> | null
|
||||
precision?: number | null
|
||||
required?: boolean | null
|
||||
scope?: string | null
|
||||
template?: PluginParameterTemplate
|
||||
type: EventParameterType
|
||||
}
|
||||
|
||||
export type FormOption = {
|
||||
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
|
||||
show_on?: Array<FormShowOnObject>
|
||||
value: string
|
||||
}
|
||||
|
||||
export type FormShowOnObject = {
|
||||
value: string
|
||||
variable: string
|
||||
}
|
||||
|
||||
export type FormType = 'radio' | 'secret-input' | 'select' | 'switch' | 'text-input'
|
||||
|
||||
export type ParameterType = 'boolean' | 'float' | 'int' | 'string' | 'text'
|
||||
|
||||
export type PluginParameterAutoGenerate = {
|
||||
type: CorePluginEntitiesParametersPluginParameterAutoGenerateType
|
||||
}
|
||||
|
||||
export type PluginParameterOption = {
|
||||
icon?: string | null
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
value: string
|
||||
}
|
||||
|
||||
export type PluginParameterTemplate = {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export type EventParameterType
|
||||
= | 'app-selector'
|
||||
| 'array'
|
||||
| 'boolean'
|
||||
| 'checkbox'
|
||||
| 'dynamic-select'
|
||||
| 'file'
|
||||
| 'files'
|
||||
| 'model-selector'
|
||||
| 'number'
|
||||
| 'object'
|
||||
| 'select'
|
||||
| 'string'
|
||||
|
||||
export type CorePluginEntitiesParametersPluginParameterAutoGenerateType = 'prompt_instruction'
|
||||
|
||||
export type GetWorkspacesData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -1858,6 +2387,50 @@ export type GetWorkspacesCurrentPluginAssetResponses = {
|
||||
export type GetWorkspacesCurrentPluginAssetResponse
|
||||
= GetWorkspacesCurrentPluginAssetResponses[keyof GetWorkspacesCurrentPluginAssetResponses]
|
||||
|
||||
export type PostWorkspacesCurrentPluginAutoUpgradeChangeData = {
|
||||
body: ParserAutoUpgradeChange
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/plugin/auto-upgrade/change'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentPluginAutoUpgradeChangeResponses = {
|
||||
200: PluginAutoUpgradeChangeResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentPluginAutoUpgradeChangeResponse
|
||||
= PostWorkspacesCurrentPluginAutoUpgradeChangeResponses[keyof PostWorkspacesCurrentPluginAutoUpgradeChangeResponses]
|
||||
|
||||
export type PostWorkspacesCurrentPluginAutoUpgradeExcludeData = {
|
||||
body: ParserExcludePlugin
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/plugin/auto-upgrade/exclude'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses = {
|
||||
200: SuccessResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentPluginAutoUpgradeExcludeResponse
|
||||
= PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses[keyof PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses]
|
||||
|
||||
export type GetWorkspacesCurrentPluginAutoUpgradeFetchData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query: {
|
||||
category: string
|
||||
}
|
||||
url: '/workspaces/current/plugin/auto-upgrade/fetch'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentPluginAutoUpgradeFetchResponses = {
|
||||
200: PluginAutoUpgradeFetchResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentPluginAutoUpgradeFetchResponse
|
||||
= GetWorkspacesCurrentPluginAutoUpgradeFetchResponses[keyof GetWorkspacesCurrentPluginAutoUpgradeFetchResponses]
|
||||
|
||||
export type GetWorkspacesCurrentPluginDebuggingKeyData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -2095,54 +2668,6 @@ export type GetWorkspacesCurrentPluginPermissionFetchResponses = {
|
||||
export type GetWorkspacesCurrentPluginPermissionFetchResponse
|
||||
= GetWorkspacesCurrentPluginPermissionFetchResponses[keyof GetWorkspacesCurrentPluginPermissionFetchResponses]
|
||||
|
||||
export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeData = {
|
||||
body: ParserExcludePlugin
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/plugin/preferences/autoupgrade/exclude'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses = {
|
||||
200: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse
|
||||
= PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses[keyof PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses]
|
||||
|
||||
export type PostWorkspacesCurrentPluginPreferencesChangeData = {
|
||||
body: ParserPreferencesChange
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/plugin/preferences/change'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentPluginPreferencesChangeResponses = {
|
||||
200: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentPluginPreferencesChangeResponse
|
||||
= PostWorkspacesCurrentPluginPreferencesChangeResponses[keyof PostWorkspacesCurrentPluginPreferencesChangeResponses]
|
||||
|
||||
export type GetWorkspacesCurrentPluginPreferencesFetchData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/plugin/preferences/fetch'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentPluginPreferencesFetchResponses = {
|
||||
200: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentPluginPreferencesFetchResponse
|
||||
= GetWorkspacesCurrentPluginPreferencesFetchResponses[keyof GetWorkspacesCurrentPluginPreferencesFetchResponses]
|
||||
|
||||
export type GetWorkspacesCurrentPluginReadmeData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -2340,6 +2865,25 @@ export type PostWorkspacesCurrentPluginUploadPkgResponses = {
|
||||
export type PostWorkspacesCurrentPluginUploadPkgResponse
|
||||
= PostWorkspacesCurrentPluginUploadPkgResponses[keyof PostWorkspacesCurrentPluginUploadPkgResponses]
|
||||
|
||||
export type GetWorkspacesCurrentPluginByCategoryListData = {
|
||||
body?: never
|
||||
path: {
|
||||
category: string
|
||||
}
|
||||
query?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
url: '/workspaces/current/plugin/{category}/list'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentPluginByCategoryListResponses = {
|
||||
200: PluginCategoryListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentPluginByCategoryListResponse
|
||||
= GetWorkspacesCurrentPluginByCategoryListResponses[keyof GetWorkspacesCurrentPluginByCategoryListResponses]
|
||||
|
||||
export type GetWorkspacesCurrentToolLabelsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
@@ -205,6 +205,21 @@ export const zWorkspacePermissionResponse = z.object({
|
||||
workspace_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginAutoUpgradeChangeResponse
|
||||
*/
|
||||
export const zPluginAutoUpgradeChangeResponse = z.object({
|
||||
message: z.string().nullish(),
|
||||
success: z.boolean(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SuccessResponse
|
||||
*/
|
||||
export const zSuccessResponse = z.object({
|
||||
success: z.boolean(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginDebuggingKeyResponse
|
||||
*/
|
||||
@@ -250,20 +265,6 @@ export const zParserDynamicOptionsWithCredentials = z.object({
|
||||
provider: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SuccessResponse
|
||||
*/
|
||||
export const zSuccessResponse = z.object({
|
||||
success: z.boolean(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ParserExcludePlugin
|
||||
*/
|
||||
export const zParserExcludePlugin = z.object({
|
||||
plugin_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ParserUninstall
|
||||
*/
|
||||
@@ -704,6 +705,26 @@ export const zParserPostModels = z.object({
|
||||
model_type: zModelType,
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginCategory
|
||||
*/
|
||||
export const zPluginCategory = z.enum([
|
||||
'agent-strategy',
|
||||
'datasource',
|
||||
'extension',
|
||||
'model',
|
||||
'tool',
|
||||
'trigger',
|
||||
])
|
||||
|
||||
/**
|
||||
* ParserExcludePlugin
|
||||
*/
|
||||
export const zParserExcludePlugin = z.object({
|
||||
category: zPluginCategory,
|
||||
plugin_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* DebugPermission
|
||||
*/
|
||||
@@ -718,14 +739,6 @@ export const zInstallPermission = z.enum(['admins', 'everyone', 'noone'])
|
||||
* ParserPermissionChange
|
||||
*/
|
||||
export const zParserPermissionChange = z.object({
|
||||
debug_permission: zDebugPermission,
|
||||
install_permission: zInstallPermission,
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginPermissionSettingsPayload
|
||||
*/
|
||||
export const zPluginPermissionSettingsPayload = z.object({
|
||||
debug_permission: zDebugPermission.optional(),
|
||||
install_permission: zInstallPermission.optional(),
|
||||
})
|
||||
@@ -860,13 +873,98 @@ export const zPluginAutoUpgradeSettingsPayload = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* ParserPreferencesChange
|
||||
* ParserAutoUpgradeChange
|
||||
*/
|
||||
export const zParserPreferencesChange = z.object({
|
||||
export const zParserAutoUpgradeChange = z.object({
|
||||
auto_upgrade: zPluginAutoUpgradeSettingsPayload,
|
||||
permission: zPluginPermissionSettingsPayload,
|
||||
category: zPluginCategory,
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginAutoUpgradeSettingsResponseModel
|
||||
*/
|
||||
export const zPluginAutoUpgradeSettingsResponseModel = z.object({
|
||||
exclude_plugins: z.array(z.string()),
|
||||
include_plugins: z.array(z.string()),
|
||||
strategy_setting: zStrategySetting,
|
||||
upgrade_mode: zUpgradeMode,
|
||||
upgrade_time_of_day: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginAutoUpgradeFetchResponse
|
||||
*/
|
||||
export const zPluginAutoUpgradeFetchResponse = z.object({
|
||||
auto_upgrade: zPluginAutoUpgradeSettingsResponseModel,
|
||||
category: zPluginCategory,
|
||||
})
|
||||
|
||||
/**
|
||||
* I18nObject
|
||||
*
|
||||
* Model class for i18n object.
|
||||
*/
|
||||
export const zCoreToolsEntitiesCommonEntitiesI18nObject = z.object({
|
||||
en_US: z.string(),
|
||||
ja_JP: z.string().nullish(),
|
||||
pt_BR: z.string().nullish(),
|
||||
zh_Hans: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginCategoryBuiltinToolResponse
|
||||
*/
|
||||
export const zPluginCategoryBuiltinToolResponse = z.object({
|
||||
author: z.string(),
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
labels: z.array(z.string()),
|
||||
name: z.string(),
|
||||
output_schema: z.record(z.string(), z.unknown()),
|
||||
parameters: z.array(z.record(z.string(), z.unknown())).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ToolProviderType
|
||||
*
|
||||
* Enum class for tool provider
|
||||
*/
|
||||
export const zToolProviderType = z.enum([
|
||||
'api',
|
||||
'app',
|
||||
'builtin',
|
||||
'dataset-retrieval',
|
||||
'mcp',
|
||||
'plugin',
|
||||
'workflow',
|
||||
])
|
||||
|
||||
/**
|
||||
* PluginCategoryBuiltinToolProviderResponse
|
||||
*/
|
||||
export const zPluginCategoryBuiltinToolProviderResponse = z.object({
|
||||
allow_delete: z.boolean(),
|
||||
author: z.string(),
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
icon: z.unknown(),
|
||||
icon_dark: z.unknown(),
|
||||
id: z.string(),
|
||||
is_team_authorization: z.boolean(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
labels: z.array(z.string()),
|
||||
name: z.string(),
|
||||
plugin_id: z.string().nullable(),
|
||||
plugin_unique_identifier: z.string().nullable(),
|
||||
team_credentials: z.record(z.string(), z.unknown()),
|
||||
tools: z.array(zPluginCategoryBuiltinToolResponse),
|
||||
type: zToolProviderType,
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginInstallationSource
|
||||
*/
|
||||
export const zPluginInstallationSource = z.enum(['github', 'marketplace', 'package', 'remote'])
|
||||
|
||||
/**
|
||||
* ToolParameterForm
|
||||
*/
|
||||
@@ -911,6 +1009,706 @@ export const zWorkflowToolUpdatePayload = z.object({
|
||||
workflow_tool_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Meta
|
||||
*/
|
||||
export const zMeta = z.object({
|
||||
minimum_dify_version: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Plugins
|
||||
*/
|
||||
export const zPlugins = z.object({
|
||||
datasources: z.array(z.string()).nullish(),
|
||||
endpoints: z.array(z.string()).nullish(),
|
||||
models: z.array(z.string()).nullish(),
|
||||
tools: z.array(z.string()).nullish(),
|
||||
triggers: z.array(z.string()).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* DatasourceProviderType
|
||||
*
|
||||
* Enum class for datasource provider
|
||||
*/
|
||||
export const zDatasourceProviderType = z.enum([
|
||||
'local_file',
|
||||
'online_document',
|
||||
'online_drive',
|
||||
'website_crawl',
|
||||
])
|
||||
|
||||
/**
|
||||
* EndpointDeclaration
|
||||
*
|
||||
* declaration of an endpoint
|
||||
*/
|
||||
export const zEndpointDeclaration = z.object({
|
||||
hidden: z.boolean().optional().default(false),
|
||||
method: z.string(),
|
||||
path: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ConfigurateMethod
|
||||
*
|
||||
* Enum class for configurate method of provider model.
|
||||
*/
|
||||
export const zConfigurateMethod = z.enum(['customizable-model', 'predefined-model'])
|
||||
|
||||
/**
|
||||
* I18nObject
|
||||
*
|
||||
* Model class for i18n object.
|
||||
*/
|
||||
export const zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject = z.object({
|
||||
en_US: z.string(),
|
||||
zh_Hans: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ProviderHelpEntity
|
||||
*
|
||||
* Model class for provider help.
|
||||
*/
|
||||
export const zProviderHelpEntity = z.object({
|
||||
title: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
|
||||
url: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
|
||||
})
|
||||
|
||||
/**
|
||||
* TriggerProviderIdentity
|
||||
*
|
||||
* The identity of the trigger provider
|
||||
*/
|
||||
export const zTriggerProviderIdentity = z.object({
|
||||
author: z.string(),
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
icon: z.string().nullish(),
|
||||
icon_dark: z.string().nullish(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
name: z.string(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ToolLabelEnum
|
||||
*/
|
||||
export const zToolLabelEnum = z.enum([
|
||||
'business',
|
||||
'design',
|
||||
'education',
|
||||
'entertainment',
|
||||
'finance',
|
||||
'image',
|
||||
'medical',
|
||||
'news',
|
||||
'other',
|
||||
'productivity',
|
||||
'rag',
|
||||
'search',
|
||||
'social',
|
||||
'travel',
|
||||
'utilities',
|
||||
'videos',
|
||||
'weather',
|
||||
])
|
||||
|
||||
/**
|
||||
* AgentStrategyProviderIdentity
|
||||
*
|
||||
* Inherits from ToolProviderIdentity, without any additional fields.
|
||||
*/
|
||||
export const zAgentStrategyProviderIdentity = z.object({
|
||||
author: z.string(),
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
icon: z.string(),
|
||||
icon_dark: z.string().nullish(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
name: z.string(),
|
||||
tags: z.array(zToolLabelEnum).nullish().default([]),
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentStrategyProviderEntity
|
||||
*/
|
||||
export const zAgentStrategyProviderEntity = z.object({
|
||||
identity: zAgentStrategyProviderIdentity,
|
||||
plugin_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* DatasourceProviderIdentity
|
||||
*/
|
||||
export const zDatasourceProviderIdentity = z.object({
|
||||
author: z.string(),
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
icon: z.string(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
name: z.string(),
|
||||
tags: z.array(zToolLabelEnum).nullish().default([]),
|
||||
})
|
||||
|
||||
/**
|
||||
* ToolProviderIdentity
|
||||
*/
|
||||
export const zToolProviderIdentity = z.object({
|
||||
author: z.string(),
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
icon: z.string(),
|
||||
icon_dark: z.string().nullish(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
name: z.string(),
|
||||
tags: z.array(zToolLabelEnum).nullish().default([]),
|
||||
})
|
||||
|
||||
/**
|
||||
* Option
|
||||
*/
|
||||
export const zOption = z.object({
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
value: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AppSelectorScope
|
||||
*/
|
||||
export const zAppSelectorScope = z.enum(['all', 'chat', 'completion', 'workflow'])
|
||||
|
||||
/**
|
||||
* ModelSelectorScope
|
||||
*/
|
||||
export const zModelSelectorScope = z.enum([
|
||||
'llm',
|
||||
'moderation',
|
||||
'rerank',
|
||||
'speech2text',
|
||||
'text-embedding',
|
||||
'tts',
|
||||
'vision',
|
||||
])
|
||||
|
||||
/**
|
||||
* ToolSelectorScope
|
||||
*/
|
||||
export const zToolSelectorScope = z.enum(['all', 'builtin', 'custom', 'workflow'])
|
||||
|
||||
/**
|
||||
* Type
|
||||
*/
|
||||
export const zCoreEntitiesProviderEntitiesBasicProviderConfigType = z.enum([
|
||||
'app-selector',
|
||||
'array[tools]',
|
||||
'boolean',
|
||||
'model-selector',
|
||||
'secret-input',
|
||||
'select',
|
||||
'text-input',
|
||||
])
|
||||
|
||||
/**
|
||||
* ProviderConfig
|
||||
*
|
||||
* Model class for common provider settings like credentials
|
||||
*/
|
||||
export const zProviderConfig = z.object({
|
||||
default: z.unknown().optional(),
|
||||
help: zCoreToolsEntitiesCommonEntitiesI18nObject.optional(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject.optional(),
|
||||
multiple: z.boolean().nullish().default(false),
|
||||
name: z.string(),
|
||||
options: z.array(zOption).nullish(),
|
||||
placeholder: zCoreToolsEntitiesCommonEntitiesI18nObject.optional(),
|
||||
required: z.boolean().optional().default(false),
|
||||
scope: z.unknown().optional(),
|
||||
type: zCoreEntitiesProviderEntitiesBasicProviderConfigType,
|
||||
url: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* EndpointProviderDeclaration
|
||||
*
|
||||
* declaration of an endpoint group
|
||||
*/
|
||||
export const zEndpointProviderDeclaration = z.object({
|
||||
endpoints: z.array(zEndpointDeclaration).nullish(),
|
||||
settings: z.array(zProviderConfig).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* OAuthSchema
|
||||
*
|
||||
* OAuth schema
|
||||
*/
|
||||
export const zOAuthSchema = z.object({
|
||||
client_schema: z.array(zProviderConfig).optional(),
|
||||
credentials_schema: z.array(zProviderConfig).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* DatasourceProviderEntity
|
||||
*
|
||||
* Datasource provider entity
|
||||
*/
|
||||
export const zDatasourceProviderEntity = z.object({
|
||||
credentials_schema: z.array(zProviderConfig).optional(),
|
||||
identity: zDatasourceProviderIdentity,
|
||||
oauth_schema: zOAuthSchema.optional(),
|
||||
provider_type: zDatasourceProviderType,
|
||||
})
|
||||
|
||||
/**
|
||||
* ToolProviderEntity
|
||||
*/
|
||||
export const zToolProviderEntity = z.object({
|
||||
credentials_schema: z.array(zProviderConfig).optional(),
|
||||
identity: zToolProviderIdentity,
|
||||
oauth_schema: zOAuthSchema.optional(),
|
||||
plugin_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* FieldModelSchema
|
||||
*/
|
||||
export const zFieldModelSchema = z.object({
|
||||
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
|
||||
placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ModelFeature
|
||||
*
|
||||
* Enum class for llm feature.
|
||||
*/
|
||||
export const zModelFeature = z.enum([
|
||||
'agent-thought',
|
||||
'audio',
|
||||
'document',
|
||||
'multi-tool-call',
|
||||
'stream-tool-call',
|
||||
'structured-output',
|
||||
'tool-call',
|
||||
'video',
|
||||
'vision',
|
||||
])
|
||||
|
||||
/**
|
||||
* FetchFrom
|
||||
*
|
||||
* Enum class for fetch from.
|
||||
*/
|
||||
export const zFetchFrom = z.enum(['customizable-model', 'predefined-model'])
|
||||
|
||||
/**
|
||||
* ModelPropertyKey
|
||||
*
|
||||
* Enum class for model property key.
|
||||
*/
|
||||
export const zModelPropertyKey = z.enum([
|
||||
'audio_type',
|
||||
'context_size',
|
||||
'default_voice',
|
||||
'file_upload_limit',
|
||||
'max_characters_per_chunk',
|
||||
'max_chunks',
|
||||
'max_workers',
|
||||
'mode',
|
||||
'supported_file_extensions',
|
||||
'voices',
|
||||
'word_limit',
|
||||
])
|
||||
|
||||
/**
|
||||
* PriceConfig
|
||||
*
|
||||
* Model class for pricing info.
|
||||
*/
|
||||
export const zPriceConfig = z.object({
|
||||
currency: z.string(),
|
||||
input: z.string().regex(/^(?![-+.]*$)[+-]?\d*(?:\.\d*)?$/),
|
||||
output: z
|
||||
.string()
|
||||
.regex(/^(?![-+.]*$)[+-]?\d*(?:\.\d*)?$/)
|
||||
.nullish(),
|
||||
unit: z.string().regex(/^(?![-+.]*$)[+-]?\d*(?:\.\d*)?$/),
|
||||
})
|
||||
|
||||
/**
|
||||
* Endpoint
|
||||
*/
|
||||
export const zEndpoint = z.object({
|
||||
enabled: z.boolean().nullish().default(false),
|
||||
})
|
||||
|
||||
/**
|
||||
* Model
|
||||
*/
|
||||
export const zModel = z.object({
|
||||
enabled: z.boolean().nullish().default(false),
|
||||
llm: z.boolean().nullish().default(false),
|
||||
moderation: z.boolean().nullish().default(false),
|
||||
rerank: z.boolean().nullish().default(false),
|
||||
speech2text: z.boolean().nullish().default(false),
|
||||
text_embedding: z.boolean().nullish().default(false),
|
||||
tts: z.boolean().nullish().default(false),
|
||||
})
|
||||
|
||||
/**
|
||||
* Node
|
||||
*/
|
||||
export const zNode = z.object({
|
||||
enabled: z.boolean().nullish().default(false),
|
||||
})
|
||||
|
||||
/**
|
||||
* Storage
|
||||
*/
|
||||
export const zStorage = z.object({
|
||||
enabled: z.boolean().nullish().default(false),
|
||||
size: z.int().gte(1024).lte(1073741824).optional().default(1048576),
|
||||
})
|
||||
|
||||
/**
|
||||
* Tool
|
||||
*/
|
||||
export const zTool = z.object({
|
||||
enabled: z.boolean().nullish().default(false),
|
||||
})
|
||||
|
||||
/**
|
||||
* Permission
|
||||
*/
|
||||
export const zPermission = z.object({
|
||||
endpoint: zEndpoint.optional(),
|
||||
model: zModel.optional(),
|
||||
node: zNode.optional(),
|
||||
storage: zStorage.optional(),
|
||||
tool: zTool.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginResourceRequirements
|
||||
*/
|
||||
export const zPluginResourceRequirements = z.object({
|
||||
memory: z.int(),
|
||||
permission: zPermission.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* EventIdentity
|
||||
*
|
||||
* The identity of the event
|
||||
*/
|
||||
export const zEventIdentity = z.object({
|
||||
author: z.string(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
name: z.string(),
|
||||
provider: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* FormShowOnObject
|
||||
*
|
||||
* Model class for form show on.
|
||||
*/
|
||||
export const zFormShowOnObject = z.object({
|
||||
value: z.string(),
|
||||
variable: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* FormOption
|
||||
*
|
||||
* Model class for form option.
|
||||
*/
|
||||
export const zFormOption = z.object({
|
||||
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
|
||||
show_on: z.array(zFormShowOnObject).optional().default([]),
|
||||
value: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* FormType
|
||||
*
|
||||
* Enum class for form type.
|
||||
*/
|
||||
export const zFormType = z.enum(['radio', 'secret-input', 'select', 'switch', 'text-input'])
|
||||
|
||||
/**
|
||||
* CredentialFormSchema
|
||||
*
|
||||
* Model class for credential form schema.
|
||||
*/
|
||||
export const zCredentialFormSchema = z.object({
|
||||
default: z.string().nullish(),
|
||||
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
|
||||
max_length: z.int().optional().default(0),
|
||||
options: z.array(zFormOption).nullish(),
|
||||
placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
|
||||
required: z.boolean().optional().default(true),
|
||||
show_on: z.array(zFormShowOnObject).optional().default([]),
|
||||
type: zFormType,
|
||||
variable: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ModelCredentialSchema
|
||||
*
|
||||
* Model class for model credential schema.
|
||||
*/
|
||||
export const zModelCredentialSchema = z.object({
|
||||
credential_form_schemas: z.array(zCredentialFormSchema),
|
||||
model: zFieldModelSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* ProviderCredentialSchema
|
||||
*
|
||||
* Model class for provider credential schema.
|
||||
*/
|
||||
export const zProviderCredentialSchema = z.object({
|
||||
credential_form_schemas: z.array(zCredentialFormSchema),
|
||||
})
|
||||
|
||||
/**
|
||||
* ParameterType
|
||||
*
|
||||
* Enum class for parameter type.
|
||||
*/
|
||||
export const zParameterType = z.enum(['boolean', 'float', 'int', 'string', 'text'])
|
||||
|
||||
/**
|
||||
* ParameterRule
|
||||
*
|
||||
* Model class for parameter rule.
|
||||
*/
|
||||
export const zParameterRule = z.object({
|
||||
default: z.unknown().optional(),
|
||||
help: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
|
||||
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
|
||||
max: z.number().nullish(),
|
||||
min: z.number().nullish(),
|
||||
name: z.string(),
|
||||
options: z.array(z.string()).optional().default([]),
|
||||
precision: z.int().nullish(),
|
||||
required: z.boolean().optional().default(false),
|
||||
type: zParameterType,
|
||||
use_template: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AIModelEntity
|
||||
*
|
||||
* Model class for AI model.
|
||||
*/
|
||||
export const zAiModelEntity = z.object({
|
||||
deprecated: z.boolean().optional().default(false),
|
||||
features: z.array(zModelFeature).nullish(),
|
||||
fetch_from: zFetchFrom,
|
||||
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
|
||||
model: z.string(),
|
||||
model_properties: z.record(z.string(), z.unknown()),
|
||||
model_type: zModelType,
|
||||
parameter_rules: z.array(zParameterRule).optional().default([]),
|
||||
pricing: zPriceConfig.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ProviderEntity
|
||||
*
|
||||
* Runtime-native provider schema.
|
||||
*
|
||||
* `provider` is the canonical runtime identifier. `provider_name` is a
|
||||
* compatibility alias for callers that still resolve providers by short name and
|
||||
* is empty when no alias exists.
|
||||
*/
|
||||
export const zProviderEntity = z.object({
|
||||
background: z.string().nullish(),
|
||||
configurate_methods: z.array(zConfigurateMethod),
|
||||
description: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
|
||||
help: zProviderHelpEntity.optional(),
|
||||
icon_small: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
|
||||
icon_small_dark: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
|
||||
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
|
||||
model_credential_schema: zModelCredentialSchema.optional(),
|
||||
models: z.array(zAiModelEntity).optional(),
|
||||
position: z.record(z.string(), z.array(z.string())).nullish().default({}),
|
||||
provider: z.string(),
|
||||
provider_credential_schema: zProviderCredentialSchema.optional(),
|
||||
provider_name: z.string().optional().default(''),
|
||||
supported_model_types: z.array(zModelType),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginParameterOption
|
||||
*/
|
||||
export const zPluginParameterOption = z.object({
|
||||
icon: z.string().nullish(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
value: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginParameterTemplate
|
||||
*/
|
||||
export const zPluginParameterTemplate = z.object({
|
||||
enabled: z.boolean().optional().default(false),
|
||||
})
|
||||
|
||||
/**
|
||||
* EventParameterType
|
||||
*
|
||||
* The type of the parameter
|
||||
*/
|
||||
export const zEventParameterType = z.enum([
|
||||
'app-selector',
|
||||
'array',
|
||||
'boolean',
|
||||
'checkbox',
|
||||
'dynamic-select',
|
||||
'file',
|
||||
'files',
|
||||
'model-selector',
|
||||
'number',
|
||||
'object',
|
||||
'select',
|
||||
'string',
|
||||
])
|
||||
|
||||
/**
|
||||
* Type
|
||||
*/
|
||||
export const zCorePluginEntitiesParametersPluginParameterAutoGenerateType = z.enum([
|
||||
'prompt_instruction',
|
||||
])
|
||||
|
||||
/**
|
||||
* PluginParameterAutoGenerate
|
||||
*/
|
||||
export const zPluginParameterAutoGenerate = z.object({
|
||||
type: zCorePluginEntitiesParametersPluginParameterAutoGenerateType,
|
||||
})
|
||||
|
||||
/**
|
||||
* EventParameter
|
||||
*
|
||||
* The parameter of the event
|
||||
*/
|
||||
export const zEventParameter = z.object({
|
||||
auto_generate: zPluginParameterAutoGenerate.optional(),
|
||||
default: z.unknown().optional(),
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject.optional(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
max: z.unknown().optional(),
|
||||
min: z.unknown().optional(),
|
||||
multiple: z.boolean().nullish().default(false),
|
||||
name: z.string(),
|
||||
options: z.array(zPluginParameterOption).nullish(),
|
||||
precision: z.int().nullish(),
|
||||
required: z.boolean().nullish().default(false),
|
||||
scope: z.string().nullish(),
|
||||
template: zPluginParameterTemplate.optional(),
|
||||
type: zEventParameterType,
|
||||
})
|
||||
|
||||
/**
|
||||
* EventEntity
|
||||
*
|
||||
* The configuration of an event
|
||||
*/
|
||||
export const zEventEntity = z.object({
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
identity: zEventIdentity,
|
||||
output_schema: z.record(z.string(), z.unknown()).nullish(),
|
||||
parameters: z.array(zEventParameter).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SubscriptionConstructor
|
||||
*
|
||||
* The subscription constructor of the trigger provider
|
||||
*/
|
||||
export const zSubscriptionConstructor = z.object({
|
||||
credentials_schema: z.array(zProviderConfig).optional(),
|
||||
oauth_schema: zOAuthSchema.optional(),
|
||||
parameters: z.array(zEventParameter).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* TriggerProviderEntity
|
||||
*
|
||||
* The configuration of a trigger provider
|
||||
*/
|
||||
export const zTriggerProviderEntity = z.object({
|
||||
events: z.array(zEventEntity).optional(),
|
||||
identity: zTriggerProviderIdentity,
|
||||
subscription_constructor: zSubscriptionConstructor.optional(),
|
||||
subscription_schema: z.array(zProviderConfig).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginDeclaration
|
||||
*/
|
||||
export const zPluginDeclaration = z.object({
|
||||
agent_strategy: zAgentStrategyProviderEntity.optional(),
|
||||
author: z
|
||||
.string()
|
||||
.regex(/^[\w-]{1,64}$/)
|
||||
.nullable(),
|
||||
category: zPluginCategory,
|
||||
created_at: z.iso.datetime(),
|
||||
datasource: zDatasourceProviderEntity.optional(),
|
||||
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
endpoint: zEndpointProviderDeclaration.optional(),
|
||||
icon: z.string(),
|
||||
icon_dark: z.string().nullish(),
|
||||
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
|
||||
meta: zMeta,
|
||||
model: zProviderEntity.optional(),
|
||||
name: z.string().regex(/^[a-z0-9_-]{1,128}$/),
|
||||
plugins: zPlugins,
|
||||
repo: z.string().nullish(),
|
||||
resource: zPluginResourceRequirements,
|
||||
tags: z.array(z.string()).optional(),
|
||||
tool: zToolProviderEntity.optional(),
|
||||
trigger: zTriggerProviderEntity.optional(),
|
||||
verified: z.boolean().optional().default(false),
|
||||
version: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginCategoryInstalledPluginResponse
|
||||
*/
|
||||
export const zPluginCategoryInstalledPluginResponse = z.object({
|
||||
checksum: z.string(),
|
||||
created_at: z.iso.datetime(),
|
||||
declaration: zPluginDeclaration,
|
||||
endpoints_active: z.int(),
|
||||
endpoints_setups: z.int(),
|
||||
id: z.string(),
|
||||
installation_id: z.string(),
|
||||
meta: z.record(z.string(), z.unknown()),
|
||||
name: z.string(),
|
||||
plugin_id: z.string(),
|
||||
plugin_unique_identifier: z.string(),
|
||||
runtime_type: z.string(),
|
||||
source: zPluginInstallationSource,
|
||||
tenant_id: z.string(),
|
||||
updated_at: z.iso.datetime(),
|
||||
version: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginCategoryListResponse
|
||||
*/
|
||||
export const zPluginCategoryListResponse = z.object({
|
||||
builtin_tools: z.array(zPluginCategoryBuiltinToolProviderResponse),
|
||||
has_more: z.boolean(),
|
||||
plugins: z.array(zPluginCategoryInstalledPluginResponse),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
@@ -1564,6 +2362,30 @@ export const zGetWorkspacesCurrentPluginAssetQuery = z.object({
|
||||
*/
|
||||
export const zGetWorkspacesCurrentPluginAssetResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
export const zPostWorkspacesCurrentPluginAutoUpgradeChangeBody = zParserAutoUpgradeChange
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostWorkspacesCurrentPluginAutoUpgradeChangeResponse
|
||||
= zPluginAutoUpgradeChangeResponse
|
||||
|
||||
export const zPostWorkspacesCurrentPluginAutoUpgradeExcludeBody = zParserExcludePlugin
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostWorkspacesCurrentPluginAutoUpgradeExcludeResponse = zSuccessResponse
|
||||
|
||||
export const zGetWorkspacesCurrentPluginAutoUpgradeFetchQuery = z.object({
|
||||
category: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse = zPluginAutoUpgradeFetchResponse
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
@@ -1691,31 +2513,6 @@ export const zPostWorkspacesCurrentPluginPermissionChangeResponse = zSuccessResp
|
||||
*/
|
||||
export const zGetWorkspacesCurrentPluginPermissionFetchResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
export const zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeBody = zParserExcludePlugin
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse = z.record(
|
||||
z.string(),
|
||||
z.unknown(),
|
||||
)
|
||||
|
||||
export const zPostWorkspacesCurrentPluginPreferencesChangeBody = zParserPreferencesChange
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostWorkspacesCurrentPluginPreferencesChangeResponse = z.record(
|
||||
z.string(),
|
||||
z.unknown(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zGetWorkspacesCurrentPluginPreferencesFetchResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
export const zGetWorkspacesCurrentPluginReadmeQuery = z.object({
|
||||
language: z.string().optional().default('en-US'),
|
||||
plugin_unique_identifier: z.string(),
|
||||
@@ -1810,6 +2607,20 @@ export const zPostWorkspacesCurrentPluginUploadGithubResponse = z.record(z.strin
|
||||
*/
|
||||
export const zPostWorkspacesCurrentPluginUploadPkgResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
export const zGetWorkspacesCurrentPluginByCategoryListPath = z.object({
|
||||
category: z.string(),
|
||||
})
|
||||
|
||||
export const zGetWorkspacesCurrentPluginByCategoryListQuery = z.object({
|
||||
page: z.int().gte(1).optional().default(1),
|
||||
page_size: z.int().gte(1).lte(256).optional().default(256),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zGetWorkspacesCurrentPluginByCategoryListResponse = zPluginCategoryListResponse
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
|
||||
@@ -398,6 +398,24 @@ const removeNullDefaults = (value: unknown, visited = new WeakSet<object>()) =>
|
||||
Object.values(schema).forEach(item => removeNullDefaults(item, visited))
|
||||
}
|
||||
|
||||
const normalizeRegexPatterns = (value: unknown, visited = new WeakSet<object>()) => {
|
||||
if (!value || typeof value !== 'object' || visited.has(value))
|
||||
return
|
||||
|
||||
visited.add(value)
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => normalizeRegexPatterns(item, visited))
|
||||
return
|
||||
}
|
||||
|
||||
const schema = value as SwaggerSchema
|
||||
if (typeof schema.pattern === 'string')
|
||||
schema.pattern = schema.pattern.replace(/^\^\(\?!\^/, '^(?!')
|
||||
|
||||
Object.values(schema).forEach(item => normalizeRegexPatterns(item, visited))
|
||||
}
|
||||
|
||||
const isNullSchema = (schema: SwaggerSchema) => {
|
||||
return schema.type === 'null'
|
||||
}
|
||||
@@ -791,6 +809,7 @@ const normalizeApiSwagger = (document: SwaggerDocument, surface: string) => {
|
||||
ensureReferencedDefinitions(document)
|
||||
normalizeNullableAnyOf(document)
|
||||
removeNullDefaults(document)
|
||||
normalizeRegexPatterns(document)
|
||||
normalizeOperations(document, surface)
|
||||
|
||||
return document
|
||||
|
||||
@@ -265,6 +265,12 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@utility title-5xl-semi-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@utility title-5xl-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -156,6 +156,19 @@ html[data-theme="dark"] {
|
||||
--color-components-main-nav-nav-button-bg-active: rgb(200 206 218 / 0.14);
|
||||
--color-components-main-nav-nav-button-border: rgb(255 255 255 / 0.08);
|
||||
--color-components-main-nav-nav-button-bg-hover: rgb(200 206 218 / 0.04);
|
||||
--color-components-main-nav-glass-text-glow: #3146ff2e;
|
||||
--color-components-main-nav-glass-surface-first: #0033ff14;
|
||||
--color-components-main-nav-glass-surface-middle-1: #0033ff1f;
|
||||
--color-components-main-nav-glass-surface-middle-2: #0033ff1a;
|
||||
--color-components-main-nav-glass-surface-end: #0033ff14;
|
||||
--color-components-main-nav-glass-edge-highlight-first: #fffffffa;
|
||||
--color-components-main-nav-glass-edge-highlight-end: #ffffff6b;
|
||||
--color-components-main-nav-glass-edge-reflection-first: #0033ff00;
|
||||
--color-components-main-nav-glass-edge-reflection-middle: #0033ff99;
|
||||
--color-components-main-nav-glass-edge-reflection-end: #0033ff00;
|
||||
--color-components-main-nav-glass-inner-glow: #ffffff4d;
|
||||
--color-components-main-nav-glass-shadow-reflection: #0033ff0a;
|
||||
--color-components-main-nav-glass-shadow-reflection-glow: #ffffff00;
|
||||
|
||||
--color-components-main-nav-nav-user-border: rgb(255 255 255 / 0.05);
|
||||
|
||||
|
||||
@@ -156,6 +156,19 @@ html[data-theme="light"] {
|
||||
--color-components-main-nav-nav-button-bg-active: #fcfcfd;
|
||||
--color-components-main-nav-nav-button-border: rgb(255 255 255 / 0.95);
|
||||
--color-components-main-nav-nav-button-bg-hover: rgb(16 24 40 / 0.04);
|
||||
--color-components-main-nav-glass-text-glow: #3146ff2e;
|
||||
--color-components-main-nav-glass-surface-first: #0033ff14;
|
||||
--color-components-main-nav-glass-surface-middle-1: #0033ff1f;
|
||||
--color-components-main-nav-glass-surface-middle-2: #0033ff1a;
|
||||
--color-components-main-nav-glass-surface-end: #0033ff14;
|
||||
--color-components-main-nav-glass-edge-highlight-first: #fffffffa;
|
||||
--color-components-main-nav-glass-edge-highlight-end: #ffffff6b;
|
||||
--color-components-main-nav-glass-edge-reflection-first: #0033ff00;
|
||||
--color-components-main-nav-glass-edge-reflection-middle: #0033ff99;
|
||||
--color-components-main-nav-glass-edge-reflection-end: #0033ff00;
|
||||
--color-components-main-nav-glass-inner-glow: #ffffff4d;
|
||||
--color-components-main-nav-glass-shadow-reflection: #0033ff0a;
|
||||
--color-components-main-nav-glass-shadow-reflection-glow: #ffffff00;
|
||||
|
||||
--color-components-main-nav-nav-user-border: #ffffff;
|
||||
|
||||
|
||||
@@ -163,6 +163,19 @@
|
||||
--color-components-main-nav-nav-button-bg-active: var(--color-components-main-nav-nav-button-bg-active);
|
||||
--color-components-main-nav-nav-button-border: var(--color-components-main-nav-nav-button-border);
|
||||
--color-components-main-nav-nav-button-bg-hover: var(--color-components-main-nav-nav-button-bg-hover);
|
||||
--color-components-main-nav-glass-text-glow: var(--color-components-main-nav-glass-text-glow);
|
||||
--color-components-main-nav-glass-surface-first: var(--color-components-main-nav-glass-surface-first);
|
||||
--color-components-main-nav-glass-surface-middle-1: var(--color-components-main-nav-glass-surface-middle-1);
|
||||
--color-components-main-nav-glass-surface-middle-2: var(--color-components-main-nav-glass-surface-middle-2);
|
||||
--color-components-main-nav-glass-surface-end: var(--color-components-main-nav-glass-surface-end);
|
||||
--color-components-main-nav-glass-edge-highlight-first: var(--color-components-main-nav-glass-edge-highlight-first);
|
||||
--color-components-main-nav-glass-edge-highlight-end: var(--color-components-main-nav-glass-edge-highlight-end);
|
||||
--color-components-main-nav-glass-edge-reflection-first: var(--color-components-main-nav-glass-edge-reflection-first);
|
||||
--color-components-main-nav-glass-edge-reflection-middle: var(--color-components-main-nav-glass-edge-reflection-middle);
|
||||
--color-components-main-nav-glass-edge-reflection-end: var(--color-components-main-nav-glass-edge-reflection-end);
|
||||
--color-components-main-nav-glass-inner-glow: var(--color-components-main-nav-glass-inner-glow);
|
||||
--color-components-main-nav-glass-shadow-reflection: var(--color-components-main-nav-glass-shadow-reflection);
|
||||
--color-components-main-nav-glass-shadow-reflection-glow: var(--color-components-main-nav-glass-shadow-reflection-glow);
|
||||
|
||||
--color-components-main-nav-nav-user-border: var(--color-components-main-nav-nav-user-border);
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# @dify/iconify-collections
|
||||
|
||||
Pre-generated Iconify collections for Dify custom SVG icons. The web app imports these collections from this package so Tailwind does not need to scan and build custom SVG icon data from the old `web/app/components/base/icons/src` tree during dev startup.
|
||||
|
||||
## Adding Custom SVG Icons
|
||||
|
||||
Add new SVG source files under one of these directories:
|
||||
|
||||
- `assets/public/...` for multi-color or public brand-like icons.
|
||||
- `assets/vender/...` for UI vendor icons that should render with `currentColor`.
|
||||
|
||||
After adding or changing SVG files, regenerate the packaged collections:
|
||||
|
||||
```bash
|
||||
pnpm --filter @dify/iconify-collections generate
|
||||
```
|
||||
|
||||
Then run the dimension guard:
|
||||
|
||||
```bash
|
||||
pnpm --filter @dify/iconify-collections check:dimensions
|
||||
```
|
||||
|
||||
This protects existing icon groups with layout-sensitive intrinsic sizes, such as the `main-nav-*` icons that must remain `20x20` after collection flattening.
|
||||
|
||||
Commit both the SVG source files and the generated package files under `custom-public/` or `custom-vender/`.
|
||||
Restart the web dev server after regenerating icons. Tailwind loads this plugin collection at startup, so an already-running dev server may not render newly-added `i-custom-*` classes until it restarts.
|
||||
|
||||
Use the generated icons through Tailwind icon classes in frontend code. For example:
|
||||
|
||||
```text
|
||||
assets/vender/integrations/mcp.svg
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```tsx
|
||||
<span aria-hidden className="i-custom-vender-integrations-mcp size-4" />
|
||||
```
|
||||
|
||||
Do not add new generated React icon components or JSON files under `web/app/components/base/icons/src/...` for new custom SVG icons. That path is legacy; new custom icons should flow through this package and be consumed as `i-custom-*` classes.
|
||||
|
||||
When reviewing generated `icons.json` diffs, check that unrelated existing icon groups did not lose or change their intrinsic `width` and `height`. If a group is layout-sensitive, add it to `scripts/check-icon-dimensions.ts`.
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M13.1423 4.75207L12.9779 5.12919C12.8576 5.40528 12.4757 5.40528 12.3554 5.12919L12.1911 4.75207C11.8981 4.07965 11.3703 3.54427 10.7118 3.25139L10.2053 3.02615C9.93153 2.90435 9.93153 2.50587 10.2053 2.38408L10.6835 2.17143C11.3589 1.87101 11.8961 1.31582 12.1841 0.620552L12.3529 0.213023C12.4705 -0.0710075 12.8628 -0.0710075 12.9804 0.213023L13.1492 0.620552C13.4372 1.31582 13.9744 1.87101 14.6499 2.17143L15.1279 2.38408C15.4018 2.50587 15.4018 2.90435 15.1279 3.02615L14.6215 3.25139C13.963 3.54427 13.4353 4.07965 13.1423 4.75207ZM5.33333 1.33333C8.045 1.33333 10.284 3.35708 10.6225 5.97663L12.1228 8.3358C12.2216 8.49113 12.2017 8.72313 11.9729 8.82113L10.6667 9.38067V11.3333C10.6667 12.0697 10.0697 12.6667 9.33333 12.6667H8.00067L8 14.6667H2L2.00017 12.2041C2.00022 11.4168 1.70901 10.6725 1.17033 10.0007C0.438047 9.08753 0 7.92827 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333ZM13.4357 12.0683L12.3262 11.3286C12.9624 10.3761 13.3333 9.2314 13.3333 8.00007C13.3333 7.65933 13.3049 7.3252 13.2504 7L14.5457 6.66667C14.6252 7.09907 14.6667 7.54467 14.6667 8.00007C14.6667 9.50507 14.2133 10.9041 13.4357 12.0683Z" fill="var(--fill-0, #18222F)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M13.1423 4.75207L12.9779 5.12919C12.8576 5.40528 12.4757 5.40528 12.3554 5.12919L12.1911 4.75207C11.8981 4.07965 11.3703 3.54427 10.7118 3.25139L10.2053 3.02615C9.93153 2.90435 9.93153 2.50587 10.2053 2.38408L10.6835 2.17143C11.3589 1.87101 11.8961 1.31582 12.1841 0.620552L12.3529 0.213023C12.4705 -0.0710075 12.8628 -0.0710075 12.9804 0.213023L13.1492 0.620552C13.4372 1.31582 13.9744 1.87101 14.6499 2.17143L15.1279 2.38408C15.4018 2.50587 15.4018 2.90435 15.1279 3.02615L14.6215 3.25139C13.963 3.54427 13.4353 4.07965 13.1423 4.75207ZM5.33333 1.33333C8.045 1.33333 10.284 3.35708 10.6225 5.97663L12.1228 8.3358C12.2216 8.49113 12.2017 8.72313 11.9729 8.82113L10.6667 9.38067V11.3333C10.6667 12.0697 10.0697 12.6667 9.33333 12.6667H8.00067L8 14.6667H2L2.00017 12.2041C2.00022 11.4168 1.70901 10.6725 1.17033 10.0007C0.438047 9.08753 0 7.92827 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333ZM5.33333 2.66667C3.12419 2.66667 1.33333 4.45753 1.33333 6.66667C1.33333 7.58993 1.64545 8.46193 2.21052 9.1666C2.93977 10.076 3.33357 11.1115 3.3335 12.2042L3.33342 13.3333H6.66713L6.6678 11.3333H9.33333V8.50127L10.3665 8.05873L9.33813 6.44175L9.30007 6.14745C9.04433 4.16761 7.34953 2.66667 5.33333 2.66667ZM12.3262 11.3286L13.4357 12.0683C14.2133 10.9041 14.6667 9.50507 14.6667 8.00007C14.6667 7.54467 14.6252 7.09907 14.5457 6.66667L13.2504 7C13.3049 7.3252 13.3333 7.65933 13.3333 8.00007C13.3333 9.2314 12.9624 10.3761 12.3262 11.3286Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<g id="Vector">
|
||||
<path d="M5.92578 11.0095C5.92578 10.0175 5.12163 9.21267 4.12956 9.21267C3.13752 9.21271 2.33333 10.0175 2.33333 11.0095C2.33349 12.0015 3.13762 12.8057 4.12956 12.8058C5.12153 12.8058 5.92562 12.0015 5.92578 11.0095ZM13.6667 11.0095C13.6667 10.0175 12.8625 9.21271 11.8704 9.21267C10.8784 9.21267 10.0742 10.0175 10.0742 11.0095C10.0744 12.0015 10.8785 12.8058 11.8704 12.8058C12.8624 12.8057 13.6665 12.0015 13.6667 11.0095ZM9.79622 4.324C9.79619 3.33197 8.99205 2.52778 8 2.52778C7.00795 2.52778 6.20382 3.33197 6.20378 4.324C6.20378 5.31607 7.00793 6.12023 8 6.12023C8.99207 6.12023 9.79622 5.31607 9.79622 4.324ZM11.1296 4.324C11.1296 5.82362 10.0748 7.07639 8.66667 7.38194V7.9197L9.74284 8.71398C10.3012 8.19618 11.0489 7.87934 11.8704 7.87934C13.5989 7.87938 15 9.28112 15 11.0095C14.9998 12.7378 13.5988 14.1391 11.8704 14.1391C10.1421 14.1391 8.74104 12.7379 8.74089 11.0095C8.74089 10.5838 8.82585 10.1777 8.97982 9.80772L8 9.08377L7.01953 9.80772C7.17356 10.1778 7.25911 10.5837 7.25911 11.0095C7.25896 12.7379 5.85791 14.1391 4.12956 14.1391C2.40124 14.1391 1.00016 12.7378 1 11.0095C1 9.28112 2.40114 7.87938 4.12956 7.87934C4.95094 7.87934 5.69819 8.19637 6.25651 8.71398L7.33333 7.9197V7.38194C5.92523 7.07639 4.87044 5.82362 4.87044 4.324C4.87048 2.59559 6.27158 1.19444 8 1.19444C9.72842 1.19444 11.1295 2.59559 11.1296 4.324Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M9.79622 4.324C9.79619 3.33197 8.99205 2.52778 8 2.52778C7.00795 2.52778 6.20382 3.33197 6.20378 4.324C6.20378 5.31607 7.00793 6.12023 8 6.12023C8.99207 6.12023 9.79622 5.31607 9.79622 4.324Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M5.92578 11.0095C5.92578 10.0175 5.12163 9.21267 4.12956 9.21267C3.13752 9.21271 2.33333 10.0175 2.33333 11.0095C2.33349 12.0015 3.13762 12.8057 4.12956 12.8058C5.12153 12.8058 5.92562 12.0015 5.92578 11.0095Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M13.6667 11.0095C13.6667 10.0175 12.8625 9.21271 11.8704 9.21267C10.8784 9.21267 10.0742 10.0175 10.0742 11.0095C10.0744 12.0015 10.8785 12.8058 11.8704 12.8058C12.8624 12.8057 13.6665 12.0015 13.6667 11.0095Z" fill="var(--fill-0, #18222F)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14 12.9447" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M4.92578 9.8151C4.92578 8.82303 4.12163 8.01823 3.12956 8.01823C2.13752 8.01827 1.33333 8.82306 1.33333 9.8151C1.33349 10.807 2.13762 11.6113 3.12956 11.6113C4.12153 11.6113 4.92563 10.807 4.92578 9.8151ZM12.6667 9.8151C12.6667 8.82306 11.8625 8.01827 10.8704 8.01823C9.87837 8.01823 9.07422 8.82303 9.07422 9.8151C9.07438 10.807 9.87847 11.6113 10.8704 11.6113C11.8624 11.6113 12.6665 10.807 12.6667 9.8151ZM8.79622 3.12956C8.79619 2.13752 7.99205 1.33333 7 1.33333C6.00795 1.33333 5.20382 2.13752 5.20378 3.12956C5.20378 4.12163 6.00793 4.92578 7 4.92578C7.99207 4.92578 8.79622 4.12163 8.79622 3.12956ZM10.1296 3.12956C10.1296 4.62918 9.07477 5.88194 7.66667 6.1875V6.72526L8.74284 7.51953C9.3012 7.00174 10.0489 6.6849 10.8704 6.6849C12.5989 6.68493 14 8.08668 14 9.8151C13.9998 11.5434 12.5988 12.9446 10.8704 12.9447C9.14209 12.9447 7.74104 11.5434 7.74089 9.8151C7.74089 9.38937 7.82585 8.98325 7.97982 8.61328L7 7.88932L6.01953 8.61328C6.17356 8.98332 6.25911 9.38929 6.25911 9.8151C6.25896 11.5434 4.85791 12.9447 3.12956 12.9447C1.40124 12.9446 0.000156326 11.5434 0 9.8151C0 8.08668 1.40114 6.68493 3.12956 6.6849C3.95094 6.6849 4.69819 7.00193 5.25651 7.51953L6.33333 6.72526V6.1875C4.92523 5.88194 3.87044 4.62918 3.87044 3.12956C3.87048 1.40114 5.27158 0 7 0C8.72843 0 10.1295 1.40114 10.1296 3.12956Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 14.2807" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M12.0014 3.2815L6.33333 0L0.665287 3.2815L6.33333 6.563L12.0014 3.2815ZM0 4.437V11L5.66667 14.2807V7.71767L0 4.437ZM7 14.2807L12.6667 11V4.437L7 7.71767V14.2807Z" fill="var(--fill-0, #18222F)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 403 B |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M6.33333 0L12.6667 3.66667V11L6.33333 14.6667L0 11V3.66667L6.33333 0ZM1.99592 4.0518L6.3334 6.56293L10.6708 4.05183L6.33333 1.54067L1.99592 4.0518ZM1.33333 5.20886V10.2313L5.66673 12.7401V7.71767L1.33333 5.20886ZM7.00007 12.74L11.3333 10.2313V5.20891L7.00007 7.71767V12.74Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 515 B |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M3.33333 2C3.33333 0.895433 4.22877 0 5.33333 0C6.43793 0 7.33333 0.895433 7.33333 2C7.33333 2.23376 7.2932 2.45815 7.21953 2.66667H11.3333C11.7015 2.66667 12 2.96515 12 3.33333V5.41735C12 5.62345 11.9047 5.81796 11.7418 5.94423C11.5789 6.07053 11.3667 6.11433 11.1671 6.063C11.0079 6.022 10.8403 6 10.6667 6C9.56207 6 8.66667 6.8954 8.66667 8C8.66667 9.1046 9.56207 10 10.6667 10C10.8403 10 11.0079 9.978 11.1671 9.937C11.3667 9.88567 11.5789 9.92947 11.7418 10.0557C11.9047 10.1821 12 10.3765 12 10.5827V12.6667C12 13.0349 11.7015 13.3333 11.3333 13.3333H0.666667C0.29848 13.3333 0 13.0349 0 12.6667V3.33333C0 2.96515 0.29848 2.66667 0.666667 2.66667H3.44714C3.37343 2.45815 3.33333 2.23376 3.33333 2Z" fill="var(--fill-0, #18222F)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 940 B |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M2.66667 2.66667C2.66667 1.19391 3.86057 0 5.33333 0C6.80607 0 8 1.19391 8 2.66667H11.3333C11.7015 2.66667 12 2.96515 12 3.33333V6.1138C12 6.3302 11.8949 6.53313 11.7183 6.65813C11.5415 6.78307 11.3152 6.81447 11.1112 6.74233C10.973 6.69353 10.8237 6.66667 10.6667 6.66667C9.93027 6.66667 9.33333 7.2636 9.33333 8C9.33333 8.7364 9.93027 9.33333 10.6667 9.33333C10.8237 9.33333 10.973 9.30647 11.1112 9.25767C11.3152 9.18553 11.5415 9.21693 11.7183 9.34187C11.8949 9.46687 12 9.6698 12 9.8862V12.6667C12 13.0349 11.7015 13.3333 11.3333 13.3333H0.666667C0.29848 13.3333 0 13.0349 0 12.6667V3.33333C0 2.96515 0.29848 2.66667 0.666667 2.66667H2.66667ZM5.33333 1.33333C4.59695 1.33333 4 1.93029 4 2.66667C4 2.82369 4.02687 2.97301 4.0757 3.11117C4.14781 3.31521 4.11641 3.54157 3.99145 3.71826C3.86649 3.89495 3.66355 4 3.44714 4H1.33333V12H10.6667V10.6667C9.19393 10.6667 8 9.47273 8 8C8 6.52727 9.19393 5.33333 10.6667 5.33333V4H7.21953C7.00313 4 6.8002 3.89495 6.6752 3.71826C6.55027 3.54157 6.51887 3.31521 6.591 3.11117C6.6398 2.97301 6.66667 2.8237 6.66667 2.66667C6.66667 1.93029 6.06973 1.33333 5.33333 1.33333Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="13.3333" height="13.3333" viewBox="0 0 13.3333 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6709 1.83301C12.754 1.83301 12.833 1.90708 12.833 2.00195V9.125L9.125 12.8311L2.00098 12.832C1.90424 12.8318 1.83301 12.7562 1.83301 12.6709V7.16699H2.16699V12.5H8.5V8.66699C8.5 8.57647 8.57647 8.5 8.66699 8.5L12 8.49902H12.5V2.16699H7.16699V1.83301H12.6709ZM11.4473 8.83301H8.83301V12.6533L9.68652 11.7998L11.8008 9.68652L12.6553 8.83203L11.4473 8.83301ZM2.83301 0.5V2.5H4.83301V2.83301H2.83301V4.83301H2.5V2.83301H0.5V2.5H2.5V0.5H2.83301Z" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 602 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="11.6416" height="13.086" viewBox="0 0 11.6416 13.086" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0996 0.512695C10.1337 0.518171 10.156 0.524407 10.168 0.52832C10.2148 0.543681 10.2522 0.579318 10.2705 0.625C10.5581 1.34398 10.5694 1.95842 10.4502 2.44824L10.3955 2.67578L10.5342 2.86328C10.9293 3.39873 11.1416 4.03345 11.1416 4.76172C11.1416 5.94177 10.8789 6.76774 10.4385 7.34668C9.99983 7.92308 9.33952 8.3137 8.42773 8.53711L7.91602 8.66309L8.06738 9.16699C8.1351 9.39201 8.17383 9.65236 8.17383 9.94336L8.16895 11.2803C8.16815 11.4068 8.16748 11.5418 8.16602 11.75L8.16309 12.1553L8.55762 12.2422C8.62114 12.2562 8.67222 12.3062 8.68555 12.3721C8.7036 12.4623 8.64494 12.5503 8.55469 12.5684C8.30873 12.6174 8.13226 12.5563 8.02148 12.4668C7.90812 12.3752 7.83118 12.2287 7.83105 12.043C7.83105 11.9847 7.83103 11.8999 7.83203 11.748C7.83356 11.5396 7.83507 11.4046 7.83594 11.2783C7.83901 10.8059 7.84082 10.3843 7.84082 9.94336C7.84082 9.41961 7.70678 8.93648 7.38281 8.65723C7.27271 8.56225 7.32913 8.38146 7.47363 8.36523C8.5007 8.24979 9.36433 7.98413 9.96094 7.37695C10.5669 6.76005 10.8086 5.884 10.8086 4.76172C10.8086 4.00303 10.5554 3.35601 10.0693 2.82227C10.0264 2.77514 10.0144 2.70773 10.0381 2.64844C10.1874 2.27509 10.24 1.81114 10.126 1.28125L10.0137 0.759766L9.50098 0.905273L9.49414 0.907227C9.09648 1.01979 8.63458 1.25105 8.11035 1.60742C8.06963 1.63503 8.01887 1.64314 7.97168 1.62988C7.37871 1.46312 6.74527 1.37892 6.1084 1.37891C5.4715 1.37891 4.83803 1.46315 4.24512 1.62988C4.19802 1.64313 4.14702 1.63474 4.10645 1.60742C3.57968 1.25283 3.11619 1.02297 2.71777 0.910156L2.20703 0.765625L2.09277 1.28418C1.97674 1.813 2.02979 2.27614 2.17871 2.64844C2.20225 2.70762 2.19033 2.77513 2.14746 2.82227C1.66395 3.35317 1.4082 4.00895 1.4082 4.76172C1.40822 5.88234 1.64983 6.75778 2.25391 7.375C2.84913 7.98307 3.71022 8.25023 4.7334 8.36523C4.87762 8.38143 4.93358 8.56104 4.82422 8.65625C4.66044 8.79875 4.55302 9.0215 4.48828 9.20898C4.41614 9.41793 4.36621 9.67286 4.36621 9.94336V12.043C4.36598 12.3722 4.10732 12.6499 3.64648 12.5693C3.55582 12.5535 3.49488 12.4666 3.51074 12.376C3.52276 12.3083 3.57458 12.2564 3.63965 12.2422L4.0332 12.1562V10.5586L3.49902 10.5947C2.9627 10.6308 2.58298 10.5389 2.30859 10.3555C2.16409 10.2588 2.0263 10.1279 1.83984 9.90527C1.80759 9.86673 1.71954 9.75814 1.64258 9.66211C1.60519 9.61545 1.57219 9.5735 1.55176 9.54785C1.54666 9.54146 1.54226 9.53528 1.53906 9.53125L1.53711 9.5293C1.53789 9.53032 1.54048 9.53427 1.54395 9.53906C1.54454 9.53989 1.55212 9.55023 1.56055 9.56348C1.56664 9.57332 1.58653 9.60864 1.59863 9.63477C1.62166 9.72473 1.52906 9.94237 1.35645 10.1016L1.14648 9.84082L1.53613 9.52734C1.53304 9.52351 1.52837 9.51827 1.52539 9.51465C1.52488 9.51403 1.52438 9.51285 1.52344 9.51172C1.52279 9.51094 1.51991 9.50772 1.5166 9.50391C1.51604 9.50326 1.51446 9.50199 1.5127 9.5C1.21539 9.13323 0.948869 8.85872 0.610352 8.7373C0.523946 8.70626 0.479023 8.61085 0.509766 8.52441C0.540776 8.43794 0.636154 8.39219 0.722656 8.42285C1.0883 8.55402 1.35448 8.77291 1.7793 9.29785C1.78078 9.29986 1.78248 9.30178 1.7832 9.30273C1.78551 9.30578 1.78769 9.30808 1.78809 9.30859C1.79204 9.31367 1.79973 9.32481 1.80859 9.33594C1.82821 9.36058 1.8612 9.401 1.89746 9.44629L2.0957 9.69141C2.2267 9.84782 2.35754 9.98732 2.49316 10.0781C2.78339 10.2725 3.19313 10.2925 3.58887 10.2529L4.01172 10.2109L4.03809 9.78711C4.05126 9.57333 4.09056 9.36582 4.15039 9.17578L4.31055 8.66699L3.79199 8.54004C2.88088 8.31734 2.21997 7.92594 1.78027 7.34863C1.33864 6.76857 1.07521 5.94109 1.0752 4.76172C1.0752 4.0396 1.28837 3.39917 1.68262 2.86328L1.82129 2.67578L1.76562 2.44824C1.64647 1.95845 1.65776 1.34391 1.94531 0.625C1.96354 0.57943 2.00137 0.544774 2.04785 0.529297C2.07893 0.520398 2.08893 0.517862 2.11621 0.513672C2.47591 0.45859 3.10496 0.581722 4.05176 1.1748L4.22852 1.28516L4.43164 1.2373C4.97053 1.11008 5.53743 1.04492 6.1084 1.04492C6.67867 1.04494 7.24479 1.11034 7.7832 1.2373L7.9873 1.28516L8.16504 1.17285C9.10977 0.576084 9.73863 0.454651 10.0996 0.512695ZM2.39551 9.2666C2.28256 9.36997 2.13716 9.45037 1.96484 9.45117C1.91043 9.42066 1.8462 9.37172 1.83301 9.35937C1.82701 9.35346 1.81779 9.34341 1.81445 9.33984C1.81233 9.33754 1.80911 9.3337 1.80762 9.33203C1.80466 9.3287 1.80177 9.32635 1.80078 9.3252L1.79687 9.32031L1.80078 9.32422L2.19043 9.01172C2.22028 9.0493 2.31937 9.17218 2.39551 9.2666Z" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="13.3333" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.332 0.5C11.426 0.500094 11.5 0.580915 11.5 0.661133V12.6719C11.5 12.7601 11.4282 12.8329 11.3379 12.833H0.662109C0.578569 12.8329 0.5 12.7632 0.5 12.6621V4.20703L4.20898 0.5H11.332ZM4.83301 4.66699C4.83283 4.75878 4.75878 4.83283 4.66699 4.83301H0.833008V12.5H11.167V0.833008H4.83301V4.66699ZM3.64648 1.5332L1.53223 3.64648L0.678711 4.5H4.5V0.680664L3.64648 1.5332Z" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="14.6667" height="13.3333" viewBox="0 0 14.6667 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.13867 6.72949C8.67785 7.21017 8.03241 7.5 7.33301 7.5C6.63364 7.49991 5.9881 7.21021 5.52734 6.72949L5.16699 6.35254L4.80566 6.72949C4.34483 7.21027 3.69949 7.5 3 7.5C2.90569 7.5 2.81278 7.49447 2.72168 7.48438L2.16699 7.42285V12.5H12.5V7.42285L11.9453 7.48438C11.8543 7.49446 11.7612 7.49999 11.667 7.5C10.9676 7.5 10.3221 7.21017 9.86133 6.72949L9.5 6.35254L9.13867 6.72949ZM2.75977 1.08301L1.14258 3.88379C0.941179 4.21836 0.833008 4.60162 0.833008 5C0.833008 6.19661 1.80338 7.16699 3 7.16699C3.89354 7.16699 4.68499 6.62068 5.01172 5.80566C5.06765 5.66613 5.26532 5.66617 5.32129 5.80566C5.64794 6.62063 6.43956 7.16686 7.33301 7.16699C8.22659 7.16699 9.019 6.62072 9.3457 5.80566C9.4017 5.66633 9.5983 5.66633 9.6543 5.80566C9.981 6.62072 10.7734 7.16699 11.667 7.16699C12.8635 7.16682 13.833 6.1965 13.833 5C13.833 4.59984 13.725 4.21734 13.5186 3.87402H13.5176L11.9072 1.08301L11.7627 0.833008H2.90332L2.75977 1.08301ZM1.83301 7.22754L1.61133 7.0791C0.94019 6.62981 0.5 5.86624 0.5 5C0.5 4.53874 0.625339 4.09665 0.850586 3.72266L0.855469 3.71484L2.66309 0.583008C2.69288 0.531554 2.74815 0.5 2.80762 0.5H11.8594C11.9188 0.500086 11.9742 0.531575 12.0039 0.583008L13.8057 3.7041L13.8096 3.71191C14.0416 4.09734 14.167 4.53978 14.167 5C14.167 5.86614 13.7267 6.62979 13.0557 7.0791L12.833 7.22754V12.5H13.5V12.833H1.16699V12.5H1.83301V7.22754Z" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.4445 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path d="M7.873 1.01976C8.28503 1.01976 8.68078 1.18055 8.97605 1.46791C9.12039 1.60841 9.23516 1.77638 9.31357 1.96193C9.39199 2.14747 9.43248 2.34683 9.43265 2.54827C9.43282 2.7497 9.39267 2.94913 9.31457 3.13481C9.23647 3.32048 9.12199 3.48865 8.97788 3.62939L4.53331 7.98841C4.48512 8.03528 4.44681 8.09133 4.42065 8.15326C4.39449 8.21519 4.38102 8.28173 4.38102 8.34896C4.38102 8.41619 4.39449 8.48273 4.42065 8.54466C4.44681 8.60659 4.48512 8.66264 4.53331 8.70951C4.63178 8.80538 4.76378 8.85902 4.9012 8.85902C5.03862 8.85902 5.17062 8.80538 5.26908 8.70951L5.32897 8.65024L5.33019 8.64901L9.71304 4.3505C10.0085 4.06393 10.404 3.90381 10.8155 3.90415C11.2271 3.90449 11.6224 4.06527 11.9173 4.35233L11.9479 4.38228C12.0924 4.52293 12.2072 4.69111 12.2856 4.87689C12.3641 5.06267 12.4045 5.26228 12.4045 5.46393C12.4045 5.66559 12.3641 5.8652 12.2856 6.05098C12.2072 6.23676 12.0924 6.40494 11.9479 6.54559L6.62757 11.7632C6.51503 11.8726 6.42557 12.0034 6.36448 12.1479C6.30339 12.2925 6.27191 12.4478 6.27191 12.6047C6.27191 12.7616 6.30339 12.9169 6.36448 13.0615C6.42557 13.206 6.51503 13.3368 6.62757 13.4462L7.72023 14.5175C7.81867 14.6131 7.95053 14.6667 8.08781 14.6667C8.22508 14.6667 8.35695 14.6131 8.45539 14.5175C8.50358 14.4706 8.54189 14.4145 8.56805 14.3526C8.59421 14.2907 8.60769 14.2241 8.60769 14.1569C8.60769 14.0897 8.59421 14.0231 8.56805 13.9612C8.54189 13.8993 8.50358 13.8432 8.45539 13.7964L7.36273 12.7245C7.34667 12.7089 7.33391 12.6902 7.32519 12.6696C7.31647 12.649 7.31198 12.6268 7.31198 12.6044C7.31198 12.582 7.31647 12.5598 7.32519 12.5392C7.33391 12.5186 7.34667 12.4999 7.36273 12.4843L12.683 7.2673C12.924 7.03296 13.1155 6.75268 13.2463 6.44304C13.3771 6.1334 13.4445 5.80068 13.4445 5.46454C13.4445 5.12841 13.3771 4.79569 13.2463 4.48605C13.1155 4.17641 12.924 3.89613 12.683 3.66178L12.6525 3.63123C12.3646 3.35023 12.016 3.13908 11.6336 3.01405C11.2512 2.88903 10.8453 2.85347 10.447 2.91012C10.5038 2.51705 10.4668 2.1161 10.3389 1.74009C10.211 1.36408 9.99593 1.0237 9.71121 0.746808C9.21914 0.267942 8.55962 0 7.873 0C7.18639 0 6.52687 0.267942 6.0348 0.746808L0.152297 6.51564C0.104103 6.56251 0.0657945 6.61857 0.039636 6.6805C0.0134776 6.74243 0 6.80897 0 6.8762C0 6.94342 0.0134776 7.00997 0.039636 7.0719C0.0657945 7.13382 0.104103 7.18988 0.152297 7.23675C0.250735 7.33243 0.382601 7.38595 0.519877 7.38595C0.657153 7.38595 0.789019 7.33243 0.887457 7.23675L6.76996 1.46791C7.06523 1.18055 7.46098 1.01976 7.873 1.01976Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M8.35362 2.74526C8.32747 2.80719 8.28916 2.86324 8.24096 2.91011L3.88989 7.17685C3.74539 7.3175 3.63053 7.48568 3.5521 7.67146C3.47367 7.85724 3.43327 8.05685 3.43327 8.25851C3.43327 8.46016 3.47367 8.65977 3.5521 8.84555C3.63053 9.03133 3.74539 9.19951 3.88989 9.34017C4.18516 9.62753 4.58092 9.78832 4.99294 9.78832C5.40496 9.78832 5.80072 9.62753 6.09598 9.34017L10.4464 5.07343C10.5449 4.97756 10.6769 4.92392 10.8143 4.92392C10.9518 4.92392 11.0837 4.97756 11.1822 5.07343C11.2304 5.1203 11.2687 5.17635 11.2949 5.23828C11.321 5.30021 11.3345 5.36675 11.3345 5.43398C11.3345 5.5012 11.321 5.56775 11.2949 5.62968C11.2687 5.69161 11.2304 5.74766 11.1822 5.79453L6.83114 10.0613C6.339 10.54 5.67951 10.8078 4.99294 10.8078C4.30636 10.8078 3.64688 10.54 3.15473 10.0613C2.91376 9.82692 2.72222 9.54664 2.59143 9.237C2.46064 8.92736 2.39325 8.59464 2.39325 8.25851C2.39325 7.92238 2.46064 7.58966 2.59143 7.28001C2.72222 6.97037 2.91376 6.69009 3.15473 6.45575L7.50519 2.18901C7.60366 2.09315 7.73566 2.03951 7.87308 2.03951C8.0105 2.03951 8.1425 2.09315 8.24096 2.18901C8.28916 2.23588 8.32747 2.29193 8.35362 2.35386C8.37978 2.41579 8.39326 2.48233 8.39326 2.54956C8.39326 2.61679 8.37978 2.68333 8.35362 2.74526Z" fill="var(--fill-0, #495464)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14.5 14.5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M5.08333 0.75V13.75M2.19444 0.75H12.3056C13.1033 0.75 13.75 1.3967 13.75 2.19444V12.3056C13.75 13.1033 13.1033 13.75 12.3056 13.75H2.19444C1.3967 13.75 0.75 13.1033 0.75 12.3056V2.19444C0.75 1.3967 1.3967 0.75 2.19444 0.75Z" stroke="var(--stroke-0, black)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.3333 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M9.66667 4V0H11.6667C12.0349 0 12.3333 0.29848 12.3333 0.666667V3.33333C12.3333 3.70152 12.0349 4 11.6667 4H9.66667ZM8.33333 13.3333C8.33333 13.7015 8.03487 14 7.66667 14H5C4.63181 14 4.33333 13.7015 4.33333 13.3333V4H0V2.71625C0 2.47913 0.12594 2.25987 0.330753 2.14039L4 0H8.33333V13.3333Z" fill="var(--fill-0, #18222F)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 528 B |
@@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.3333 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M11.6667 0C12.0349 0 12.3333 0.29848 12.3333 0.666667V4C12.3333 4.36819 12.0349 4.66667 11.6667 4.66667H8.33333V13.3333C8.33333 13.7015 8.03487 14 7.66667 14H5C4.63181 14 4.33333 13.7015 4.33333 13.3333V4.66667H0.666667C0.29848 4.66667 0 4.36819 0 4V2.41202C0 2.15951 0.142667 1.92867 0.368527 1.81574L4 0H11.6667ZM8.33333 1.33333H4.31476L1.33333 2.82405V3.33333H5.66667V12.6667H7V3.33333H8.33333V1.33333ZM11 1.33333H9.66667V3.33333H11V1.33333Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
@@ -0,0 +1,10 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.325 13.325" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81641 5.01888L5.91797 5.04883L12.8913 7.70573L12.9837 7.7487C13.3889 7.97463 13.4445 8.54581 13.0905 8.8457L13.0085 8.9056L10.4837 10.4837L8.9056 13.0085C8.62921 13.4507 7.99075 13.4178 7.7487 12.9837L7.70573 12.8913L5.04883 5.91797C4.85479 5.40863 5.31088 4.90871 5.81641 5.01888Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M3.87891 9.06445L2.22917 10.7142L1.28646 9.77148L2.9362 8.12175L3.87891 9.06445Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M2.33333 6.66667H0V5.33333H2.33333V6.66667Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M3.87891 2.93555L2.9362 3.87826L1.28646 2.22852L2.22917 1.28581L3.87891 2.93555Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M10.7142 2.22852L9.06445 3.87826L8.12175 2.93555L9.77148 1.28581L10.7142 2.22852Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M6.66667 2.33333H5.33333V0H6.66667V2.33333Z" fill="var(--fill-0, #18222F)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.325 13.325" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81641 5.01888L5.91797 5.04883L12.8913 7.70573L12.9837 7.7487C13.3889 7.97463 13.4445 8.54581 13.0905 8.8457L13.0085 8.9056L10.4837 10.4837L8.9056 13.0085C8.62921 13.4507 7.99075 13.4178 7.7487 12.9837L7.70573 12.8913L5.04883 5.91797C4.85479 5.40863 5.31088 4.90871 5.81641 5.01888ZM8.47852 11.1751L9.43359 9.64779L9.47786 9.58529C9.52536 9.52564 9.5828 9.47422 9.64779 9.43359L11.1751 8.47852L6.81901 6.81901L8.47852 11.1751Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M3.87891 9.06445L2.22917 10.7142L1.28646 9.77148L2.9362 8.12175L3.87891 9.06445Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M2.33333 6.66667H0V5.33333H2.33333V6.66667Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M3.87891 2.93555L2.9362 3.87826L1.28646 2.22852L2.22917 1.28581L3.87891 2.93555Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M10.7142 2.22852L9.06445 3.87826L8.12175 2.93555L9.77148 1.28581L10.7142 2.22852Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M6.66667 2.33333H5.33333V0H6.66667V2.33333Z" fill="var(--fill-0, #495464)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.1 11.4333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path d="M1.71667 0C0.768578 0 0 0.768578 0 1.71667C0 2.66476 0.768578 3.43333 1.71667 3.43333H3.71667C4.66475 3.43333 5.43333 2.66476 5.43333 1.71667C5.43333 0.768578 4.66476 0 3.71667 0H1.71667Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M8.38333 4C7.43524 4 6.66667 4.76858 6.66667 5.71667C6.66667 6.66476 7.43524 7.43333 8.38333 7.43333H10.3833C11.3314 7.43333 12.1 6.66476 12.1 5.71667C12.1 4.76858 11.3314 4 10.3833 4H8.38333Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M0 9.71667C0 8.76858 0.768578 8 1.71667 8H3.71667C4.66476 8 5.43333 8.76858 5.43333 9.71667C5.43333 10.6648 4.66475 11.4333 3.71667 11.4333H1.71667C0.768578 11.4333 0 10.6648 0 9.71667Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M7.05001 2.38334H6.38334V1.05001H7.05001C8.15458 1.05001 9.05001 1.94544 9.05001 3.05001H7.71667C7.71667 2.68182 7.4182 2.38334 7.05001 2.38334Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M9.05001 8.38334C9.05001 9.48791 8.15458 10.3833 7.05001 10.3833H6.38334V9.05001H7.05001C7.4182 9.05001 7.71667 8.75153 7.71667 8.38334H9.05001Z" fill="var(--fill-0, #18222F)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 2C13.7015 2 14 2.29848 14 2.66667V6C14 6.36819 13.7015 6.66667 13.3333 6.66667H10C9.63181 6.66667 9.33333 6.36819 9.33333 6V5H6.63086C5.94959 5.00026 5.70734 5.90184 6.29688 6.24349L10.3717 8.60221C12.1411 9.62681 11.4138 12.3331 9.36914 12.3333H6.66667V13.3333C6.66667 13.7015 6.36819 14 6 14H2.66667C2.29848 14 2 13.7015 2 13.3333V10C2 9.63181 2.29848 9.33333 2.66667 9.33333H6C6.36819 9.33333 6.66667 9.63181 6.66667 10V11H9.36914C10.0504 10.9997 10.2929 10.0981 9.70378 9.75651L5.62891 7.39779C3.85933 6.37322 4.58611 3.66693 6.63086 3.66667H9.33333V2.66667C9.33333 2.29848 9.63181 2 10 2H13.3333ZM3.33333 12.6667H5.33333V10.6667H3.33333V12.6667ZM10.6667 5.33333H12.6667V3.33333H10.6667V5.33333Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.66667 6.17435C2.66667 5.98785 2.66667 5.89459 2.69021 5.80837C2.71107 5.73198 2.74537 5.65992 2.7915 5.59556C2.84357 5.52292 2.91595 5.46411 3.0607 5.3465L7.3274 1.87983C7.56713 1.68502 7.687 1.58762 7.82027 1.55031C7.93787 1.51741 8.06213 1.51741 8.17973 1.55031C8.313 1.58762 8.43287 1.68502 8.6726 1.87983L12.9393 5.3465C13.0841 5.46411 13.1564 5.52292 13.2085 5.59556C13.2547 5.65992 13.2889 5.73198 13.3098 5.80837C13.3333 5.89459 13.3333 5.98785 13.3333 6.17435V12.2667C13.3333 12.64 13.3333 12.8267 13.2607 12.9693C13.1967 13.0947 13.0948 13.1967 12.9693 13.2607C12.8267 13.3333 12.6401 13.3333 12.2667 13.3333H3.73333C3.35997 13.3333 3.17328 13.3333 3.03067 13.2607C2.90523 13.1967 2.80325 13.0947 2.73933 12.9693C2.66667 12.8267 2.66667 12.64 2.66667 12.2667V6.17435Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M10 13.3333V9.66667C10 9.11438 9.55228 8.66667 9 8.66667H7C6.44772 8.66667 6 9.11438 6 9.66667V13.3333" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(3 3) scale(1.5)">
|
||||
<path d="M7 10.5C8.933 10.5 10.5 8.4853 10.5 6C10.5 3.51472 8.933 1.5 7 1.5M7 10.5C5.067 10.5 3.5 8.4853 3.5 6C3.5 3.51472 5.067 1.5 7 1.5M7 10.5H5C3.06701 10.5 1.5 8.4853 1.5 6C1.5 3.51472 3.06701 1.5 5 1.5H7" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 410 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15.25C12.4142 15.25 12.75 15.5858 12.75 16V16.0098C12.75 16.424 12.4142 16.7598 12 16.7598C11.5858 16.7598 11.25 16.424 11.25 16.0098V16C11.25 15.5858 11.5858 15.25 12 15.25Z" fill="currentColor"/>
|
||||
<path d="M14 7.25C14.4142 7.25 14.75 7.58579 14.75 8V10.5C14.75 10.7359 14.6389 10.958 14.4502 11.0996L12.75 12.374V13C12.75 13.4142 12.4142 13.75 12 13.75C11.5858 13.75 11.25 13.4142 11.25 13V12C11.25 11.7641 11.3611 11.542 11.5498 11.4004L13.25 10.125V8.75H10.75V9C10.75 9.41421 10.4142 9.75 10 9.75C9.58579 9.75 9.25 9.41421 9.25 9V8C9.25 7.58579 9.58579 7.25 10 7.25H14Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-5.5 -6.95)">
|
||||
<path d="M14.449 8.37314C15.0613 7.87562 15.9387 7.87562 16.551 8.37314L22.3843 13.1127C22.7738 13.4292 23 13.9044 23 14.4063V22.7596C23 23.6801 22.2538 24.4263 21.3333 24.4263H18.8333V17.7599C18.8333 17.2997 18.4602 16.9266 18 16.9266H13C12.5398 16.9266 12.1667 17.2997 12.1667 17.7599V24.4263H9.66667C8.74619 24.4263 8 23.6801 8 22.7596V14.4063C8 13.9044 8.22616 13.4292 8.61568 13.1127L14.449 8.37314Z" fill="currentColor"/>
|
||||
<path d="M13.833 24.4263H17.1663V18.5933H13.833V24.4263Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 666 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33301 7.71788C3.33301 7.48475 3.33301 7.36818 3.36243 7.2604C3.38851 7.16492 3.43138 7.07484 3.48905 6.99439C3.55414 6.90359 3.64461 6.83008 3.82555 6.68307L9.15892 2.34973C9.45859 2.10622 9.60842 1.98447 9.77501 1.93783C9.92201 1.8967 10.0773 1.8967 10.2243 1.93783C10.3909 1.98447 10.5408 2.10622 10.8404 2.34973L16.1738 6.68307C16.3548 6.83008 16.4452 6.90359 16.5103 6.99439C16.568 7.07484 16.6108 7.16492 16.6369 7.2604C16.6663 7.36818 16.6663 7.48475 16.6663 7.71788V15.3333C16.6663 15.7999 16.6663 16.0334 16.5755 16.2116C16.4956 16.3684 16.3682 16.4959 16.2113 16.5758C16.0331 16.6666 15.7998 16.6666 15.333 16.6666H4.66634C4.19963 16.6666 3.96627 16.6666 3.78802 16.5758C3.63122 16.4959 3.50373 16.3684 3.42383 16.2116C3.33301 16.0334 3.33301 15.7999 3.33301 15.3333V7.71788Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M12.5 16.6666V11.8333C12.5 11.281 12.0523 10.8333 11.5 10.8333H8.5C7.94772 10.8333 7.5 11.281 7.5 11.8333V16.6666" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-6.33 -5.55)">
|
||||
<path d="M9.66602 8.83333C9.66602 8.3731 10.0391 8 10.4993 8H14.666C15.1263 8 15.4993 8.3731 15.4993 8.83333V10.5H9.66602V8.83333Z" fill="currentColor"/>
|
||||
<path d="M17.166 8.83333C17.166 8.3731 17.5391 8 17.9993 8H22.166C22.6263 8 22.9993 8.3731 22.9993 8.83333V10.5H17.166V8.83333Z" fill="currentColor"/>
|
||||
<path d="M8 13.0001C8 12.5398 8.3731 12.1667 8.83333 12.1667H23.8333C24.2936 12.1667 24.6667 12.5398 24.6667 13.0001V21.3334C24.6667 21.7937 24.2936 22.1667 23.8333 22.1667H8.83333C8.3731 22.1667 8 21.7937 8 21.3334V13.0001Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 7.50008C2.5 7.03984 2.8731 6.66675 3.33333 6.66675H16.6667C17.1269 6.66675 17.5 7.03985 17.5 7.50008V15.0001C17.5 15.4603 17.1269 15.8334 16.6667 15.8334H3.33333C2.8731 15.8334 2.5 15.4603 2.5 15.0001V7.50008Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.16699 6.66659V4.58325C4.16699 3.89289 4.72663 3.33325 5.41699 3.33325H7.08366C7.77402 3.33325 8.33366 3.89289 8.33366 4.58325V6.66659" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.667 6.66659V4.16659C11.667 3.70635 12.0401 3.33325 12.5003 3.33325H15.0003C15.4606 3.33325 15.8337 3.70635 15.8337 4.16659V6.66659" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 896 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-4.667 -6.333)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 8C9.11929 8 8 9.11929 8 10.5V22.1667C8 23.5474 9.11929 24.6667 10.5 24.6667H20.5C20.9602 24.6667 21.3333 24.2936 21.3333 23.8333V8.83333C21.3333 8.3731 20.9602 8 20.5 8H10.5ZM9.66667 22.1667C9.66667 22.6269 10.0398 23 10.5 23H19.6667V21.3333H10.5C10.0398 21.3333 9.66667 21.7064 9.66667 22.1667ZM12.1667 11.3333C11.7064 11.3333 11.3333 11.7064 11.3333 12.1667C11.3333 12.6269 11.7064 13 12.1667 13H17.1667C17.6269 13 18 12.6269 18 12.1667C18 11.7064 17.6269 11.3333 17.1667 11.3333H12.1667ZM11.3333 15.5C11.3333 15.0397 11.7064 14.6667 12.1667 14.6667H14.6667C15.1269 14.6667 15.5 15.0397 15.5 15.5C15.5 15.9602 15.1269 16.3333 14.6667 16.3333H12.1667C11.7064 16.3333 11.3333 15.9602 11.3333 15.5Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 933 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 9.16675H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 5.83325H12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.16699 4.16667C4.16699 3.24619 4.91318 2.5 5.83366 2.5H15.417C15.6471 2.5 15.8337 2.68655 15.8337 2.91667V17.5H5.83366C4.91318 17.5 4.16699 16.7538 4.16699 15.8333V4.16667Z" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M4.16699 15.8334C4.16699 14.9129 4.91318 14.1667 5.83366 14.1667H15.8337V17.5001H5.83366C4.91318 17.5001 4.16699 16.7539 4.16699 15.8334Z" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 764 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-6.3 -5.5)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.25369 8.58477C9.3624 8.23688 9.6846 8 10.0491 8H22.5491C22.9136 8 23.2357 8.23688 23.3445 8.58477L24.4481 12.1162C24.8042 13.256 24.5023 14.4059 23.7991 15.2164V22.1667C23.7991 22.6269 23.426 23 22.9657 23H9.63242C9.17219 23 8.79909 22.6269 8.79909 22.1667V15.2164C8.09588 14.4059 7.79393 13.256 8.15013 12.1162L9.25369 8.58477ZM18.0271 12.7092L17.6467 9.66667H14.9514L14.5711 12.7092C14.4412 13.7486 15.2516 14.6667 16.2991 14.6667C17.3465 14.6667 18.1568 13.7485 18.0271 12.7092ZM13.2718 9.66667H10.6617L9.74093 12.6133C9.42266 13.6317 10.1835 14.6667 11.2505 14.6667C12.0482 14.6667 12.721 14.0728 12.82 13.2812L13.2718 9.66667ZM19.3264 9.66667L19.6809 12.5025L19.7782 13.2812C19.8772 14.0728 20.55 14.6667 21.3477 14.6667C22.4147 14.6667 23.1755 13.6317 22.8572 12.6133L21.9364 9.66667H19.3264Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.667 9.99992V16.6666H3.33366V9.99992M7.91699 3.33325H12.0837M7.91699 3.33325L7.44543 7.10578C7.25334 8.6425 8.45158 9.99992 10.0003 9.99992C11.5491 9.99992 12.7473 8.6425 12.5552 7.10578L12.0837 3.33325M7.91699 3.33325H3.75033L2.64677 6.86465C2.16081 8.41975 3.32257 9.99992 4.95179 9.99992C6.1697 9.99992 7.19703 9.093 7.34809 7.88451L7.91699 3.33325ZM12.0837 3.33325H16.2503L17.3539 6.86465C17.8398 8.41975 16.6781 9.99992 15.0489 9.99992C13.831 9.99992 12.8037 9.093 12.6526 7.88451L12.0837 3.33325Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 712 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.0004 1.875C17.0398 1.87509 21.1246 5.9602 21.1249 10.9995C21.1249 13.1138 20.4037 15.0582 19.1972 16.6055L21.7958 19.2041C22.235 19.6433 22.2348 20.3556 21.7958 20.7949C21.3565 21.2343 20.6443 21.2343 20.205 20.7949L17.6064 18.1963C16.3417 19.1831 14.8123 19.8466 13.14 20.0552C12.5235 20.132 11.9616 19.6932 11.8847 19.0767C11.8081 18.4603 12.2454 17.8981 12.8617 17.8213C16.2516 17.3983 18.8749 14.5044 18.8749 10.9995C18.8746 7.20283 15.7971 4.12509 12.0004 4.125C8.4954 4.12505 5.60139 6.74948 5.17862 10.1396C5.10154 10.7559 4.53955 11.1934 3.92325 11.1167C3.30688 11.0398 2.86963 10.4777 2.9462 9.86133C3.50765 5.35896 7.34631 1.87505 12.0004 1.875Z" fill="currentColor"/>
|
||||
<path d="M3.70727 16.1747L7.91781 11.2624C8.24038 10.8861 8.85505 11.158 8.79357 11.6498L8.49979 14.0001H10.9127C11.3399 14.0001 11.5703 14.5012 11.2923 14.8255L7.08177 19.7378C6.7592 20.1141 6.14453 19.8422 6.20601 19.3504L6.49979 17.0001H4.0869C3.65972 17.0001 3.42927 16.499 3.70727 16.1747Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-7.14 -6.38)">
|
||||
<path d="M13.7969 17.1665C14.465 17.1665 15.1067 17.2803 15.7045 17.4872L14.7247 20.9165H12.9636C11.8131 20.9167 10.8803 21.8493 10.8803 22.9998C10.8803 23.2963 10.9436 23.5778 11.0552 23.8332H8.79695C8.33682 23.833 7.95953 23.4587 8.00349 23.0007C8.30296 19.8813 10.2937 17.1666 13.7969 17.1665Z" fill="currentColor"/>
|
||||
<path d="M25.4632 16.3333C25.7246 16.3333 25.9715 16.4558 26.1289 16.6645C26.2668 16.8473 26.3224 17.0777 26.286 17.3008L26.2648 17.3953L24.5981 23.2286C24.4959 23.5863 24.1686 23.8333 23.7965 23.8333H12.9632C12.5031 23.8331 12.1299 23.4601 12.1299 22.9999C12.1299 22.5398 12.5031 22.1668 12.9632 22.1666H15.6675L17.1616 16.9379L17.2105 16.8093C17.3465 16.5223 17.6376 16.3333 17.9632 16.3333H25.4632Z" fill="currentColor"/>
|
||||
<path d="M14.2132 9.25C16.0541 9.25 17.5465 10.7424 17.5465 12.5833C17.5465 14.4243 16.0541 15.9167 14.2132 15.9167C12.3724 15.9165 10.8799 14.4242 10.8799 12.5833C10.8799 10.7425 12.3724 9.25013 14.2132 9.25Z" fill="currentColor"/>
|
||||
<path d="M22.5474 8C22.7539 8.00029 22.9276 8.15533 22.951 8.36052C23.0941 9.62402 23.8103 10.3975 25.093 10.5114C25.3029 10.53 25.4635 10.7067 25.4632 10.9175C25.4628 11.128 25.3019 11.3037 25.0921 11.3219C23.8276 11.4314 23.0613 12.1977 22.9518 13.4622C22.9335 13.672 22.758 13.833 22.5474 13.8333C22.3367 13.8336 22.1609 13.6728 22.1421 13.4631C22.0281 12.1805 21.2546 11.4635 19.9912 11.3203C19.786 11.2971 19.6303 11.124 19.6299 10.9175C19.6297 10.7108 19.7851 10.5367 19.9904 10.513C21.2719 10.3651 21.9951 9.64128 22.1429 8.3597C22.1667 8.15453 22.3408 7.99979 22.5474 8Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z" fill="currentColor"/>
|
||||
<path d="M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(2 2.333)">
|
||||
<path d="M1.33333 2.33333C1.33333 1.78105 1.78105 1.33333 2.33333 1.33333C2.88562 1.33333 3.33333 1.78105 3.33333 2.33333C3.33333 2.88562 2.88562 3.33333 2.33333 3.33333C1.78105 3.33333 1.33333 2.88562 1.33333 2.33333ZM2.33333 0C1.04467 0 0 1.04467 0 2.33333C0 3.622 1.04467 4.66667 2.33333 4.66667C3.622 4.66667 4.66667 3.622 4.66667 2.33333C4.66667 1.04467 3.622 0 2.33333 0ZM6 3H11.3333V1.66667H6V3ZM8.66667 9C8.66667 8.44773 9.1144 8 9.66667 8C10.2189 8 10.6667 8.44773 10.6667 9C10.6667 9.55227 10.2189 10 9.66667 10C9.1144 10 8.66667 9.55227 8.66667 9ZM9.66667 6.66667C8.378 6.66667 7.33333 7.71133 7.33333 9C7.33333 10.2887 8.378 11.3333 9.66667 11.3333C10.9553 11.3333 12 10.2887 12 9C12 7.71133 10.9553 6.66667 9.66667 6.66667ZM0.666667 8.33333V9.66667H6V8.33333H0.666667Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 956 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="14" height="14" viewBox="0 0 13.3333 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.66667 10C7.03486 10 7.33333 10.2985 7.33333 10.6667V12.6667C7.33333 13.0349 7.03486 13.3333 6.66667 13.3333C6.29848 13.3333 6 13.0349 6 12.6667V10.6667C6 10.2985 6.29848 10 6.66667 10Z" fill="currentColor"/>
|
||||
<path d="M1.95247 1.95247C2.21277 1.69219 2.63483 1.69229 2.89518 1.95247L7.13867 6.19531C7.19313 6.24981 7.23528 6.3119 7.26693 6.3776C7.28146 6.40773 7.29411 6.43892 7.30404 6.47135C7.33671 6.57832 7.34196 6.69172 7.31966 6.80078C7.30858 6.85501 7.2903 6.90651 7.26693 6.95508C7.23526 7.02098 7.19325 7.08337 7.13867 7.13802L2.89518 11.3809C2.63483 11.6409 2.21272 11.641 1.95247 11.3809C1.6924 11.1206 1.6925 10.6985 1.95247 10.4382L5.05794 7.33333H0.666667C0.298477 7.33333 0 7.03486 0 6.66667C0 6.29848 0.298477 6 0.666667 6H5.05794L1.95247 2.89518C1.69236 2.63487 1.69235 2.21277 1.95247 1.95247Z" fill="currentColor"/>
|
||||
<path d="M9.02409 9.02344C9.28445 8.76325 9.7065 8.76314 9.9668 9.02344L11.3809 10.4375C11.641 10.6978 11.641 11.1199 11.3809 11.3802C11.1206 11.6405 10.6985 11.6404 10.4382 11.3802L9.02409 9.96615C8.76374 9.7058 8.76374 9.28379 9.02409 9.02344Z" fill="currentColor"/>
|
||||
<path d="M12.6667 6C13.0349 6 13.3333 6.29848 13.3333 6.66667C13.3333 7.03486 13.0349 7.33333 12.6667 7.33333H10.6667C10.2985 7.33333 10 7.03486 10 6.66667C10 6.29848 10.2985 6 10.6667 6H12.6667Z" fill="currentColor"/>
|
||||
<path d="M10.4382 1.95247C10.6985 1.69225 11.1205 1.69226 11.3809 1.95247C11.6411 2.21281 11.6411 2.63486 11.3809 2.89518L9.9668 4.30924C9.70647 4.56956 9.28444 4.56951 9.02409 4.30924C8.76381 4.04888 8.76376 3.62686 9.02409 3.36654L10.4382 1.95247Z" fill="currentColor"/>
|
||||
<path d="M6.66667 0C7.03486 0 7.33333 0.298477 7.33333 0.666667V2.66667C7.33333 3.03486 7.03486 3.33333 6.66667 3.33333C6.29848 3.33333 6 3.03486 6 2.66667V0.666667C6 0.298477 6.29848 0 6.66667 0Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(2.5904 1.333)">
|
||||
<path d="M7.07562 3C7.07562 2.07953 6.32943 1.33333 5.40895 1.33333C4.48848 1.33333 3.74229 2.07953 3.74229 3C3.74229 3.92047 4.48848 4.66667 5.40895 4.66667C6.32943 4.66667 7.07562 3.92047 7.07562 3ZM8.40895 3C8.40895 4.65685 7.06581 6 5.40895 6C3.7521 6 2.40895 4.65685 2.40895 3C2.40895 1.34315 3.7521 0 5.40895 0C7.06581 0 8.40895 1.34315 8.40895 3Z" fill="currentColor"/>
|
||||
<path d="M5.40961 6.66667C5.94859 6.66667 6.46414 6.7352 6.94997 6.86523C7.30564 6.96044 7.51718 7.32597 7.42198 7.68164C7.32674 8.03718 6.96114 8.24808 6.60557 8.153C6.23516 8.05385 5.83538 8 5.40961 8C3.44272 8.00003 1.96767 9.15509 1.35687 10.8066C1.31131 10.9299 1.33437 11.0372 1.41677 11.1354C1.50836 11.2446 1.67346 11.3333 1.87445 11.3333H4.07627C4.44446 11.3333 4.74294 11.6318 4.74294 12C4.74294 12.3682 4.44446 12.6667 4.07627 12.6667H1.87445C1.28978 12.6667 0.746862 12.4112 0.395283 11.9922C0.0346179 11.5622 -0.120709 10.9573 0.106221 10.3438C0.900152 8.19736 2.85517 6.6667 5.40961 6.66667Z" fill="currentColor"/>
|
||||
<path d="M8.9376 8.1952C9.56613 7.56673 10.5851 7.56673 11.2136 8.1952C11.8422 8.82374 11.8421 9.84269 11.2136 10.4712L8.9376 12.7473C8.56255 13.1223 8.05388 13.3332 7.52354 13.3332H6.74229C6.3741 13.3332 6.07562 13.0347 6.07562 12.6666V11.886C6.07562 11.3555 6.28646 10.8463 6.66156 10.4712L8.9376 8.1952ZM7.40895 11.9999H7.52354C7.70025 11.9998 7.86989 11.9296 7.99489 11.8046L10.2709 9.52854C10.3787 9.42068 10.3788 9.24575 10.2709 9.13791C10.1631 9.03013 9.98814 9.03013 9.88031 9.13791L7.60426 11.414C7.47923 11.539 7.40895 11.7091 7.40895 11.886V11.9999Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"name": "Dify Custom Vender",
|
||||
"total": 281,
|
||||
"total": 318,
|
||||
"version": "0.0.0-private",
|
||||
"author": {
|
||||
"name": "LangGenius, Inc.",
|
||||
|
||||