mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
Merge branch 'main' into jzh
This commit is contained in:
commit
2514e181ba
@ -137,6 +137,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
|||||||
workflow=self._workflow,
|
workflow=self._workflow,
|
||||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||||
|
user_id=self.application_generate_entity.user_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
inputs = self.application_generate_entity.inputs
|
inputs = self.application_generate_entity.inputs
|
||||||
|
|||||||
@ -106,6 +106,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
|||||||
workflow=workflow,
|
workflow=workflow,
|
||||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||||
|
user_id=self.application_generate_entity.user_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
inputs = self.application_generate_entity.inputs
|
inputs = self.application_generate_entity.inputs
|
||||||
|
|||||||
@ -92,6 +92,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
|||||||
workflow=self._workflow,
|
workflow=self._workflow,
|
||||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||||
|
user_id=self.application_generate_entity.user_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
inputs = self.application_generate_entity.inputs
|
inputs = self.application_generate_entity.inputs
|
||||||
|
|||||||
@ -164,6 +164,8 @@ class WorkflowBasedAppRunner:
|
|||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
single_iteration_run: Any | None = None,
|
single_iteration_run: Any | None = None,
|
||||||
single_loop_run: Any | None = None,
|
single_loop_run: Any | None = None,
|
||||||
|
*,
|
||||||
|
user_id: str,
|
||||||
) -> tuple[Graph, VariablePool, GraphRuntimeState]:
|
) -> tuple[Graph, VariablePool, GraphRuntimeState]:
|
||||||
"""
|
"""
|
||||||
Prepare graph, variable pool, and runtime state for single node execution
|
Prepare graph, variable pool, and runtime state for single node execution
|
||||||
@ -200,6 +202,7 @@ class WorkflowBasedAppRunner:
|
|||||||
graph_runtime_state=graph_runtime_state,
|
graph_runtime_state=graph_runtime_state,
|
||||||
node_type_filter_key="iteration_id",
|
node_type_filter_key="iteration_id",
|
||||||
node_type_label="iteration",
|
node_type_label="iteration",
|
||||||
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
elif single_loop_run:
|
elif single_loop_run:
|
||||||
graph, variable_pool = self._get_graph_and_variable_pool_for_single_node_run(
|
graph, variable_pool = self._get_graph_and_variable_pool_for_single_node_run(
|
||||||
@ -209,6 +212,7 @@ class WorkflowBasedAppRunner:
|
|||||||
graph_runtime_state=graph_runtime_state,
|
graph_runtime_state=graph_runtime_state,
|
||||||
node_type_filter_key="loop_id",
|
node_type_filter_key="loop_id",
|
||||||
node_type_label="loop",
|
node_type_label="loop",
|
||||||
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Neither single_iteration_run nor single_loop_run is specified")
|
raise ValueError("Neither single_iteration_run nor single_loop_run is specified")
|
||||||
@ -225,6 +229,8 @@ class WorkflowBasedAppRunner:
|
|||||||
graph_runtime_state: GraphRuntimeState,
|
graph_runtime_state: GraphRuntimeState,
|
||||||
node_type_filter_key: str, # 'iteration_id' or 'loop_id'
|
node_type_filter_key: str, # 'iteration_id' or 'loop_id'
|
||||||
node_type_label: str = "node", # 'iteration' or 'loop' for error messages
|
node_type_label: str = "node", # 'iteration' or 'loop' for error messages
|
||||||
|
*,
|
||||||
|
user_id: str = "",
|
||||||
) -> tuple[Graph, VariablePool]:
|
) -> tuple[Graph, VariablePool]:
|
||||||
"""
|
"""
|
||||||
Get graph and variable pool for single node execution (iteration or loop).
|
Get graph and variable pool for single node execution (iteration or loop).
|
||||||
@ -290,7 +296,7 @@ class WorkflowBasedAppRunner:
|
|||||||
run_context=build_dify_run_context(
|
run_context=build_dify_run_context(
|
||||||
tenant_id=workflow.tenant_id,
|
tenant_id=workflow.tenant_id,
|
||||||
app_id=self._app_id,
|
app_id=self._app_id,
|
||||||
user_id="",
|
user_id=user_id,
|
||||||
user_from=UserFrom.ACCOUNT,
|
user_from=UserFrom.ACCOUNT,
|
||||||
invoke_from=InvokeFrom.DEBUGGER,
|
invoke_from=InvokeFrom.DEBUGGER,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -71,6 +71,7 @@ from graphon.model_runtime.entities.llm_entities import LLMMode, LLMResult, LLMU
|
|||||||
from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageRole, PromptMessageTool
|
from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageRole, PromptMessageTool
|
||||||
from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType
|
from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType
|
||||||
from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
||||||
|
from libs.helper import parse_uuid_str_or_none
|
||||||
from libs.json_in_md_parser import parse_and_check_json_markdown
|
from libs.json_in_md_parser import parse_and_check_json_markdown
|
||||||
from models import UploadFile
|
from models import UploadFile
|
||||||
from models.dataset import (
|
from models.dataset import (
|
||||||
@ -1024,8 +1025,13 @@ class DatasetRetrieval:
|
|||||||
"""
|
"""
|
||||||
if not query and not attachment_ids:
|
if not query and not attachment_ids:
|
||||||
return
|
return
|
||||||
created_by_role = self._resolve_creator_user_role(user_from)
|
created_by = parse_uuid_str_or_none(user_id)
|
||||||
if created_by_role is None:
|
if created_by is None:
|
||||||
|
logger.debug(
|
||||||
|
"Skipping dataset query log: empty created_by user_id (user_from=%s, app_id=%s)",
|
||||||
|
user_from,
|
||||||
|
app_id,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
dataset_queries = []
|
dataset_queries = []
|
||||||
for dataset_id in dataset_ids:
|
for dataset_id in dataset_ids:
|
||||||
@ -1041,8 +1047,8 @@ class DatasetRetrieval:
|
|||||||
content=json.dumps(contents),
|
content=json.dumps(contents),
|
||||||
source=DatasetQuerySource.APP,
|
source=DatasetQuerySource.APP,
|
||||||
source_app_id=app_id,
|
source_app_id=app_id,
|
||||||
created_by_role=created_by_role,
|
created_by_role=CreatorUserRole(user_from),
|
||||||
created_by=user_id,
|
created_by=created_by,
|
||||||
)
|
)
|
||||||
dataset_queries.append(dataset_query)
|
dataset_queries.append(dataset_query)
|
||||||
if dataset_queries:
|
if dataset_queries:
|
||||||
|
|||||||
@ -174,6 +174,18 @@ def normalize_uuid(value: str | UUID) -> str:
|
|||||||
raise ValueError("must be a valid UUID") from exc
|
raise ValueError("must be a valid UUID") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def parse_uuid_str_or_none(value: str | None) -> str | None:
|
||||||
|
"""
|
||||||
|
Return None for missing/empty UUID-like values.
|
||||||
|
|
||||||
|
Keep non-empty values unchanged to avoid changing behavior in paths that
|
||||||
|
currently pass placeholder IDs in tests/mocks.
|
||||||
|
"""
|
||||||
|
if value is None or not str(value).strip():
|
||||||
|
return None
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
UUIDStrOrEmpty = Annotated[str, AfterValidator(normalize_uuid)]
|
UUIDStrOrEmpty = Annotated[str, AfterValidator(normalize_uuid)]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -88,7 +88,7 @@ class TestWorkflowBasedAppRunner:
|
|||||||
workflow = SimpleNamespace(environment_variables=[], graph_dict={})
|
workflow = SimpleNamespace(environment_variables=[], graph_dict={})
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Neither single_iteration_run nor single_loop_run"):
|
with pytest.raises(ValueError, match="Neither single_iteration_run nor single_loop_run"):
|
||||||
runner._prepare_single_node_execution(workflow, None, None)
|
runner._prepare_single_node_execution(workflow, None, None, user_id="00000000-0000-0000-0000-000000000001")
|
||||||
|
|
||||||
def test_get_graph_and_variable_pool_for_single_node_run(self, monkeypatch):
|
def test_get_graph_and_variable_pool_for_single_node_run(self, monkeypatch):
|
||||||
runner = WorkflowBasedAppRunner(queue_manager=SimpleNamespace(), app_id="app")
|
runner = WorkflowBasedAppRunner(queue_manager=SimpleNamespace(), app_id="app")
|
||||||
@ -136,6 +136,7 @@ class TestWorkflowBasedAppRunner:
|
|||||||
graph_runtime_state=graph_runtime_state,
|
graph_runtime_state=graph_runtime_state,
|
||||||
node_type_filter_key="iteration_id",
|
node_type_filter_key="iteration_id",
|
||||||
node_type_label="iteration",
|
node_type_label="iteration",
|
||||||
|
user_id="00000000-0000-0000-0000-000000000001",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert graph is not None
|
assert graph is not None
|
||||||
|
|||||||
@ -100,6 +100,7 @@ def test_run_uses_single_node_execution_branch(
|
|||||||
workflow=workflow,
|
workflow=workflow,
|
||||||
single_iteration_run=single_iteration_run,
|
single_iteration_run=single_iteration_run,
|
||||||
single_loop_run=single_loop_run,
|
single_loop_run=single_loop_run,
|
||||||
|
user_id="user",
|
||||||
)
|
)
|
||||||
init_graph.assert_not_called()
|
init_graph.assert_not_called()
|
||||||
|
|
||||||
@ -158,6 +159,7 @@ def test_single_node_run_validates_target_node_config(monkeypatch) -> None:
|
|||||||
graph_runtime_state=graph_runtime_state,
|
graph_runtime_state=graph_runtime_state,
|
||||||
node_type_filter_key="loop_id",
|
node_type_filter_key="loop_id",
|
||||||
node_type_label="loop",
|
node_type_label="loop",
|
||||||
|
user_id="00000000-0000-0000-0000-000000000001",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert seen_configs == [workflow.graph_dict["nodes"][0]]
|
assert seen_configs == [workflow.graph_dict["nodes"][0]]
|
||||||
|
|||||||
@ -70,7 +70,8 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"flatted@<=3.4.1": "3.4.2",
|
"flatted@<=3.4.1": "3.4.2",
|
||||||
"rollup@>=4.0.0,<4.59.0": "4.59.0"
|
"picomatch@>=4.0.0 <4.0.4": "4.0.4",
|
||||||
|
"rollup@>=4.0.0 <4.59.0": "4.59.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
sdks/nodejs-client/pnpm-lock.yaml
generated
25
sdks/nodejs-client/pnpm-lock.yaml
generated
@ -6,7 +6,8 @@ settings:
|
|||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
flatted@<=3.4.1: 3.4.2
|
flatted@<=3.4.1: 3.4.2
|
||||||
rollup@>=4.0.0,<4.59.0: 4.59.0
|
picomatch@>=4.0.0 <4.0.4: 4.0.4
|
||||||
|
rollup@>=4.0.0 <4.59.0: 4.59.0
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@ -735,7 +736,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
picomatch: ^3 || ^4
|
picomatch: 4.0.4
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
@ -963,8 +964,8 @@ packages:
|
|||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
picomatch@4.0.3:
|
picomatch@4.0.4:
|
||||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
pirates@4.0.7:
|
pirates@4.0.7:
|
||||||
@ -1829,9 +1830,9 @@ snapshots:
|
|||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.3):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.4
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2038,7 +2039,7 @@ snapshots:
|
|||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.4: {}
|
||||||
|
|
||||||
pirates@4.0.7: {}
|
pirates@4.0.7: {}
|
||||||
|
|
||||||
@ -2149,8 +2150,8 @@ snapshots:
|
|||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.4
|
||||||
|
|
||||||
tinyrainbow@3.0.3: {}
|
tinyrainbow@3.0.3: {}
|
||||||
|
|
||||||
@ -2207,8 +2208,8 @@ snapshots:
|
|||||||
vite@7.3.1(@types/node@25.4.0):
|
vite@7.3.1(@types/node@25.4.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.3
|
esbuild: 0.27.3
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.4
|
||||||
postcss: 8.5.8
|
postcss: 8.5.8
|
||||||
rollup: 4.59.0
|
rollup: 4.59.0
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
@ -2230,7 +2231,7 @@ snapshots:
|
|||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
obug: 2.1.1
|
obug: 2.1.1
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.4
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
tinybench: 2.9.0
|
tinybench: 2.9.0
|
||||||
tinyexec: 1.0.2
|
tinyexec: 1.0.2
|
||||||
|
|||||||
@ -51,8 +51,6 @@ NEXT_PUBLIC_ALLOW_EMBED=
|
|||||||
# Allow rendering unsafe URLs which have "data:" scheme.
|
# Allow rendering unsafe URLs which have "data:" scheme.
|
||||||
NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false
|
NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false
|
||||||
|
|
||||||
# Github Access Token, used for invoking Github API
|
|
||||||
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN=
|
|
||||||
# The maximum number of top-k value for RAG.
|
# The maximum number of top-k value for RAG.
|
||||||
NEXT_PUBLIC_TOP_K_MAX_VALUE=10
|
NEXT_PUBLIC_TOP_K_MAX_VALUE=10
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,9 @@
|
|||||||
* upload handling, and task status polling. Verifies the complete plugin
|
* upload handling, and task status polling. Verifies the complete plugin
|
||||||
* installation pipeline from source discovery to completion.
|
* installation pipeline from source discovery to completion.
|
||||||
*/
|
*/
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
GITHUB_ACCESS_TOKEN: '',
|
import { checkForUpdates, fetchReleases, handleUpload } from '@/app/components/plugins/install-plugin/hooks'
|
||||||
}))
|
|
||||||
|
|
||||||
const mockToastNotify = vi.fn()
|
const mockToastNotify = vi.fn()
|
||||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||||
@ -30,10 +28,6 @@ vi.mock('@/service/plugins', () => ({
|
|||||||
checkTaskStatus: vi.fn(),
|
checkTaskStatus: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { useGitHubReleases, useGitHubUpload } = await import(
|
|
||||||
'@/app/components/plugins/install-plugin/hooks',
|
|
||||||
)
|
|
||||||
|
|
||||||
describe('Plugin Installation Flow Integration', () => {
|
describe('Plugin Installation Flow Integration', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
@ -44,22 +38,22 @@ describe('Plugin Installation Flow Integration', () => {
|
|||||||
it('fetches releases, checks for updates, and uploads the new version', async () => {
|
it('fetches releases, checks for updates, and uploads the new version', async () => {
|
||||||
const mockReleases = [
|
const mockReleases = [
|
||||||
{
|
{
|
||||||
tag_name: 'v2.0.0',
|
tag: 'v2.0.0',
|
||||||
assets: [{ browser_download_url: 'https://github.com/test/v2.difypkg', name: 'plugin-v2.difypkg' }],
|
assets: [{ downloadUrl: 'https://github.com/test/v2.difypkg' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag_name: 'v1.5.0',
|
tag: 'v1.5.0',
|
||||||
assets: [{ browser_download_url: 'https://github.com/test/v1.5.difypkg', name: 'plugin-v1.5.difypkg' }],
|
assets: [{ downloadUrl: 'https://github.com/test/v1.5.difypkg' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag_name: 'v1.0.0',
|
tag: 'v1.0.0',
|
||||||
assets: [{ browser_download_url: 'https://github.com/test/v1.difypkg', name: 'plugin-v1.difypkg' }],
|
assets: [{ downloadUrl: 'https://github.com/test/v1.difypkg' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: () => Promise.resolve(mockReleases),
|
json: () => Promise.resolve({ releases: mockReleases }),
|
||||||
})
|
})
|
||||||
|
|
||||||
mockUploadGitHub.mockResolvedValue({
|
mockUploadGitHub.mockResolvedValue({
|
||||||
@ -67,8 +61,6 @@ describe('Plugin Installation Flow Integration', () => {
|
|||||||
unique_identifier: 'test-plugin:2.0.0',
|
unique_identifier: 'test-plugin:2.0.0',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { fetchReleases, checkForUpdates } = useGitHubReleases()
|
|
||||||
|
|
||||||
const releases = await fetchReleases('test-org', 'test-repo')
|
const releases = await fetchReleases('test-org', 'test-repo')
|
||||||
expect(releases).toHaveLength(3)
|
expect(releases).toHaveLength(3)
|
||||||
expect(releases[0].tag_name).toBe('v2.0.0')
|
expect(releases[0].tag_name).toBe('v2.0.0')
|
||||||
@ -77,7 +69,6 @@ describe('Plugin Installation Flow Integration', () => {
|
|||||||
expect(needUpdate).toBe(true)
|
expect(needUpdate).toBe(true)
|
||||||
expect(toastProps.message).toContain('v2.0.0')
|
expect(toastProps.message).toContain('v2.0.0')
|
||||||
|
|
||||||
const { handleUpload } = useGitHubUpload()
|
|
||||||
const onSuccess = vi.fn()
|
const onSuccess = vi.fn()
|
||||||
const result = await handleUpload(
|
const result = await handleUpload(
|
||||||
'https://github.com/test-org/test-repo',
|
'https://github.com/test-org/test-repo',
|
||||||
@ -104,18 +95,16 @@ describe('Plugin Installation Flow Integration', () => {
|
|||||||
it('handles no new version available', async () => {
|
it('handles no new version available', async () => {
|
||||||
const mockReleases = [
|
const mockReleases = [
|
||||||
{
|
{
|
||||||
tag_name: 'v1.0.0',
|
tag: 'v1.0.0',
|
||||||
assets: [{ browser_download_url: 'https://github.com/test/v1.difypkg', name: 'plugin-v1.difypkg' }],
|
assets: [{ downloadUrl: 'https://github.com/test/v1.difypkg' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: () => Promise.resolve(mockReleases),
|
json: () => Promise.resolve({ releases: mockReleases }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { fetchReleases, checkForUpdates } = useGitHubReleases()
|
|
||||||
|
|
||||||
const releases = await fetchReleases('test-org', 'test-repo')
|
const releases = await fetchReleases('test-org', 'test-repo')
|
||||||
const { needUpdate, toastProps } = checkForUpdates(releases, 'v1.0.0')
|
const { needUpdate, toastProps } = checkForUpdates(releases, 'v1.0.0')
|
||||||
|
|
||||||
@ -127,11 +116,9 @@ describe('Plugin Installation Flow Integration', () => {
|
|||||||
it('handles empty releases', async () => {
|
it('handles empty releases', async () => {
|
||||||
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: () => Promise.resolve([]),
|
json: () => Promise.resolve({ releases: [] }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { fetchReleases, checkForUpdates } = useGitHubReleases()
|
|
||||||
|
|
||||||
const releases = await fetchReleases('test-org', 'test-repo')
|
const releases = await fetchReleases('test-org', 'test-repo')
|
||||||
expect(releases).toHaveLength(0)
|
expect(releases).toHaveLength(0)
|
||||||
|
|
||||||
@ -147,7 +134,6 @@ describe('Plugin Installation Flow Integration', () => {
|
|||||||
status: 404,
|
status: 404,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { fetchReleases } = useGitHubReleases()
|
|
||||||
const releases = await fetchReleases('nonexistent-org', 'nonexistent-repo')
|
const releases = await fetchReleases('nonexistent-org', 'nonexistent-repo')
|
||||||
|
|
||||||
expect(releases).toEqual([])
|
expect(releases).toEqual([])
|
||||||
@ -159,7 +145,6 @@ describe('Plugin Installation Flow Integration', () => {
|
|||||||
it('handles upload failure gracefully', async () => {
|
it('handles upload failure gracefully', async () => {
|
||||||
mockUploadGitHub.mockRejectedValue(new Error('Upload failed'))
|
mockUploadGitHub.mockRejectedValue(new Error('Upload failed'))
|
||||||
|
|
||||||
const { handleUpload } = useGitHubUpload()
|
|
||||||
const onSuccess = vi.fn()
|
const onSuccess = vi.fn()
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { render, screen, waitFor } from '@testing-library/react'
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
import nock from 'nock'
|
|
||||||
import * as React from 'react'
|
|
||||||
import GithubStar from '../index'
|
import GithubStar from '../index'
|
||||||
|
|
||||||
const GITHUB_HOST = 'https://api.github.com'
|
const GITHUB_STAR_URL = 'https://ungh.cc/repos/langgenius/dify'
|
||||||
const GITHUB_PATH = '/repos/langgenius/dify'
|
|
||||||
|
|
||||||
const renderWithQueryClient = () => {
|
const renderWithQueryClient = () => {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@ -18,40 +15,66 @@ const renderWithQueryClient = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockGithubStar = (status: number, body: Record<string, unknown>, delayMs = 0) => {
|
const createJsonResponse = (body: Record<string, unknown>, status = 200) => {
|
||||||
return nock(GITHUB_HOST).get(GITHUB_PATH).delay(delayMs).reply(status, body)
|
return new Response(JSON.stringify(body), {
|
||||||
|
status,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDeferred = <T,>() => {
|
||||||
|
let resolve!: (value: T | PromiseLike<T>) => void
|
||||||
|
let reject!: (reason?: unknown) => void
|
||||||
|
const promise = new Promise<T>((res, rej) => {
|
||||||
|
resolve = res
|
||||||
|
reject = rej
|
||||||
|
})
|
||||||
|
|
||||||
|
return { promise, resolve, reject }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('GithubStar', () => {
|
describe('GithubStar', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
nock.cleanAll()
|
vi.restoreAllMocks()
|
||||||
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Shows fetched star count when request succeeds
|
// Covers the fetched star count shown after a successful request.
|
||||||
it('should render fetched star count', async () => {
|
it('should render fetched star count', async () => {
|
||||||
mockGithubStar(200, { stargazers_count: 123456 })
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||||
|
createJsonResponse({ repo: { stars: 123456 } }),
|
||||||
|
)
|
||||||
|
|
||||||
renderWithQueryClient()
|
renderWithQueryClient()
|
||||||
|
|
||||||
expect(await screen.findByText('123,456')).toBeInTheDocument()
|
expect(await screen.findByText('123,456')).toBeInTheDocument()
|
||||||
|
expect(fetchSpy).toHaveBeenCalledWith(GITHUB_STAR_URL)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Falls back to default star count when request fails
|
// Covers the fallback star count shown when the request fails.
|
||||||
it('should render default star count on error', async () => {
|
it('should render default star count on error', async () => {
|
||||||
mockGithubStar(500, {})
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||||
|
createJsonResponse({}, 500),
|
||||||
|
)
|
||||||
|
|
||||||
renderWithQueryClient()
|
renderWithQueryClient()
|
||||||
|
|
||||||
expect(await screen.findByText('110,918')).toBeInTheDocument()
|
expect(await screen.findByText('110,918')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Renders loader while fetching data
|
// Covers the loading indicator while the fetch promise is still pending.
|
||||||
it('should show loader while fetching', async () => {
|
it('should show loader while fetching', async () => {
|
||||||
mockGithubStar(200, { stargazers_count: 222222 }, 50)
|
const deferred = createDeferred<Response>()
|
||||||
|
vi.spyOn(globalThis, 'fetch').mockReturnValueOnce(deferred.promise)
|
||||||
|
|
||||||
const { container } = renderWithQueryClient()
|
const { container } = renderWithQueryClient()
|
||||||
|
|
||||||
expect(container.querySelector('.animate-spin')).toBeInTheDocument()
|
expect(container.querySelector('.animate-spin')).toBeInTheDocument()
|
||||||
await waitFor(() => expect(screen.getByText('222,222')).toBeInTheDocument())
|
|
||||||
|
deferred.resolve(createJsonResponse({ repo: { stars: 222222 } }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('222,222')).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { GithubRepo } from '@/models/common'
|
|
||||||
import { RiLoader2Line } from '@remixicon/react'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { IS_DEV } from '@/config'
|
|
||||||
|
|
||||||
const defaultData = {
|
type GithubStarResponse = {
|
||||||
stargazers_count: 110918,
|
repo: {
|
||||||
|
stars: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultData: GithubStarResponse = {
|
||||||
|
repo: { stars: 110918 },
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStar = async () => {
|
const getStar = async () => {
|
||||||
const res = await fetch('https://api.github.com/repos/langgenius/dify')
|
const res = await fetch('https://ungh.cc/repos/langgenius/dify')
|
||||||
|
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
throw new Error('Failed to fetch github star')
|
throw new Error('Failed to fetch github star')
|
||||||
@ -19,21 +22,20 @@ const getStar = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GithubStar: FC<{ className: string }> = (props) => {
|
const GithubStar: FC<{ className: string }> = (props) => {
|
||||||
const { isFetching, isError, data } = useQuery<GithubRepo>({
|
const { isFetching, isError, data } = useQuery<GithubStarResponse>({
|
||||||
queryKey: ['github-star'],
|
queryKey: ['github-star'],
|
||||||
queryFn: getStar,
|
queryFn: getStar,
|
||||||
enabled: !IS_DEV,
|
|
||||||
retry: false,
|
retry: false,
|
||||||
placeholderData: defaultData,
|
placeholderData: defaultData,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isFetching)
|
if (isFetching)
|
||||||
return <RiLoader2Line className="size-3 shrink-0 animate-spin text-text-tertiary" />
|
return <span className="i-ri-loader-2-line size-3 shrink-0 animate-spin text-text-tertiary" />
|
||||||
|
|
||||||
if (isError)
|
if (isError)
|
||||||
return <span {...props}>{defaultData.stargazers_count.toLocaleString()}</span>
|
return <span {...props}>{defaultData.repo.stars.toLocaleString()}</span>
|
||||||
|
|
||||||
return <span {...props}>{data?.stargazers_count.toLocaleString()}</span>
|
return <span {...props}>{data?.repo.stars.toLocaleString()}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GithubStar
|
export default GithubStar
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { renderHook } from '@testing-library/react'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { useGitHubReleases, useGitHubUpload } from '../hooks'
|
import { checkForUpdates, fetchReleases, handleUpload } from '../hooks'
|
||||||
|
|
||||||
const mockNotify = vi.fn()
|
const mockNotify = vi.fn()
|
||||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||||
@ -15,10 +14,6 @@ vi.mock('@/app/components/base/ui/toast', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
|
||||||
GITHUB_ACCESS_TOKEN: '',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const mockUploadGitHub = vi.fn()
|
const mockUploadGitHub = vi.fn()
|
||||||
vi.mock('@/service/plugins', () => ({
|
vi.mock('@/service/plugins', () => ({
|
||||||
uploadGitHub: (...args: unknown[]) => mockUploadGitHub(...args),
|
uploadGitHub: (...args: unknown[]) => mockUploadGitHub(...args),
|
||||||
@ -37,17 +32,17 @@ describe('install-plugin/hooks', () => {
|
|||||||
it('fetches releases from GitHub API and formats them', async () => {
|
it('fetches releases from GitHub API and formats them', async () => {
|
||||||
mockFetch.mockResolvedValue({
|
mockFetch.mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: () => Promise.resolve([
|
json: () => Promise.resolve({
|
||||||
{
|
releases: [
|
||||||
tag_name: 'v1.0.0',
|
{
|
||||||
assets: [{ browser_download_url: 'https://example.com/v1.zip', name: 'plugin.zip' }],
|
tag: 'v1.0.0',
|
||||||
body: 'Release notes',
|
assets: [{ downloadUrl: 'https://example.com/plugin.zip' }],
|
||||||
},
|
},
|
||||||
]),
|
],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useGitHubReleases())
|
const releases = await fetchReleases('owner', 'repo')
|
||||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
|
||||||
|
|
||||||
expect(releases).toHaveLength(1)
|
expect(releases).toHaveLength(1)
|
||||||
expect(releases[0].tag_name).toBe('v1.0.0')
|
expect(releases[0].tag_name).toBe('v1.0.0')
|
||||||
@ -60,8 +55,7 @@ describe('install-plugin/hooks', () => {
|
|||||||
ok: false,
|
ok: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useGitHubReleases())
|
const releases = await fetchReleases('owner', 'repo')
|
||||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
|
||||||
|
|
||||||
expect(releases).toEqual([])
|
expect(releases).toEqual([])
|
||||||
expect(mockNotify).toHaveBeenCalledWith('Failed to fetch repository releases')
|
expect(mockNotify).toHaveBeenCalledWith('Failed to fetch repository releases')
|
||||||
@ -70,29 +64,26 @@ describe('install-plugin/hooks', () => {
|
|||||||
|
|
||||||
describe('checkForUpdates', () => {
|
describe('checkForUpdates', () => {
|
||||||
it('detects newer version available', () => {
|
it('detects newer version available', () => {
|
||||||
const { result } = renderHook(() => useGitHubReleases())
|
|
||||||
const releases = [
|
const releases = [
|
||||||
{ tag_name: 'v1.0.0', assets: [] },
|
{ tag_name: 'v1.0.0', assets: [] },
|
||||||
{ tag_name: 'v2.0.0', assets: [] },
|
{ tag_name: 'v2.0.0', assets: [] },
|
||||||
]
|
]
|
||||||
const { needUpdate, toastProps } = result.current.checkForUpdates(releases, 'v1.0.0')
|
const { needUpdate, toastProps } = checkForUpdates(releases, 'v1.0.0')
|
||||||
expect(needUpdate).toBe(true)
|
expect(needUpdate).toBe(true)
|
||||||
expect(toastProps.message).toContain('v2.0.0')
|
expect(toastProps.message).toContain('v2.0.0')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns no update when current is latest', () => {
|
it('returns no update when current is latest', () => {
|
||||||
const { result } = renderHook(() => useGitHubReleases())
|
|
||||||
const releases = [
|
const releases = [
|
||||||
{ tag_name: 'v1.0.0', assets: [] },
|
{ tag_name: 'v1.0.0', assets: [] },
|
||||||
]
|
]
|
||||||
const { needUpdate, toastProps } = result.current.checkForUpdates(releases, 'v1.0.0')
|
const { needUpdate, toastProps } = checkForUpdates(releases, 'v1.0.0')
|
||||||
expect(needUpdate).toBe(false)
|
expect(needUpdate).toBe(false)
|
||||||
expect(toastProps.type).toBe('info')
|
expect(toastProps.type).toBe('info')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns error for empty releases', () => {
|
it('returns error for empty releases', () => {
|
||||||
const { result } = renderHook(() => useGitHubReleases())
|
const { needUpdate, toastProps } = checkForUpdates([], 'v1.0.0')
|
||||||
const { needUpdate, toastProps } = result.current.checkForUpdates([], 'v1.0.0')
|
|
||||||
expect(needUpdate).toBe(false)
|
expect(needUpdate).toBe(false)
|
||||||
expect(toastProps.type).toBe('error')
|
expect(toastProps.type).toBe('error')
|
||||||
expect(toastProps.message).toContain('empty')
|
expect(toastProps.message).toContain('empty')
|
||||||
@ -109,8 +100,7 @@ describe('install-plugin/hooks', () => {
|
|||||||
})
|
})
|
||||||
const onSuccess = vi.fn()
|
const onSuccess = vi.fn()
|
||||||
|
|
||||||
const { result } = renderHook(() => useGitHubUpload())
|
const pkg = await handleUpload(
|
||||||
const pkg = await result.current.handleUpload(
|
|
||||||
'https://github.com/owner/repo',
|
'https://github.com/owner/repo',
|
||||||
'v1.0.0',
|
'v1.0.0',
|
||||||
'plugin.difypkg',
|
'plugin.difypkg',
|
||||||
@ -132,9 +122,8 @@ describe('install-plugin/hooks', () => {
|
|||||||
it('shows toast on upload error', async () => {
|
it('shows toast on upload error', async () => {
|
||||||
mockUploadGitHub.mockRejectedValue(new Error('Upload failed'))
|
mockUploadGitHub.mockRejectedValue(new Error('Upload failed'))
|
||||||
|
|
||||||
const { result } = renderHook(() => useGitHubUpload())
|
|
||||||
await expect(
|
await expect(
|
||||||
result.current.handleUpload('url', 'v1', 'pkg'),
|
handleUpload('url', 'v1', 'pkg'),
|
||||||
).rejects.toThrow('Upload failed')
|
).rejects.toThrow('Upload failed')
|
||||||
expect(mockNotify).toHaveBeenCalledWith('Error uploading package')
|
expect(mockNotify).toHaveBeenCalledWith('Error uploading package')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,101 +1,87 @@
|
|||||||
import type { GitHubRepoReleaseResponse } from '../types'
|
import type { GitHubRepoReleaseResponse } from '../types'
|
||||||
import { toast } from '@/app/components/base/ui/toast'
|
import { toast } from '@/app/components/base/ui/toast'
|
||||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
|
||||||
import { uploadGitHub } from '@/service/plugins'
|
import { uploadGitHub } from '@/service/plugins'
|
||||||
import { compareVersion, getLatestVersion } from '@/utils/semver'
|
import { compareVersion, getLatestVersion } from '@/utils/semver'
|
||||||
|
|
||||||
|
const normalizeAssetName = (downloadUrl: string) => {
|
||||||
|
const parts = downloadUrl.split('/')
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
const formatReleases = (releases: any) => {
|
const formatReleases = (releases: any) => {
|
||||||
return releases.map((release: any) => ({
|
return releases.map((release: any) => ({
|
||||||
tag_name: release.tag_name,
|
tag_name: release.tag,
|
||||||
assets: release.assets.map((asset: any) => ({
|
assets: release.assets.map((asset: any) => ({
|
||||||
browser_download_url: asset.browser_download_url,
|
browser_download_url: asset.downloadUrl,
|
||||||
name: asset.name,
|
name: normalizeAssetName(asset.downloadUrl),
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGitHubReleases = () => {
|
export const fetchReleases = async (owner: string, repo: string) => {
|
||||||
const fetchReleases = async (owner: string, repo: string) => {
|
try {
|
||||||
try {
|
// Fetch releases without authentication from client
|
||||||
if (!GITHUB_ACCESS_TOKEN) {
|
const res = await fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`)
|
||||||
// Fetch releases without authentication from client
|
if (!res.ok)
|
||||||
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`)
|
throw new Error('Failed to fetch repository releases')
|
||||||
if (!res.ok)
|
const data = await res.json()
|
||||||
throw new Error('Failed to fetch repository releases')
|
return formatReleases(data.releases)
|
||||||
const data = await res.json()
|
|
||||||
return formatReleases(data)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Fetch releases with authentication from server
|
|
||||||
const res = await fetch(`/repos/${owner}/${repo}/releases`)
|
|
||||||
const bodyJson = await res.json()
|
|
||||||
if (bodyJson.status !== 200)
|
|
||||||
throw new Error(bodyJson.data.message)
|
|
||||||
return formatReleases(bodyJson.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
toast.error(error.message)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toast.error('Failed to fetch repository releases')
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(error.message)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Failed to fetch repository releases')
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
|
export const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
|
||||||
let needUpdate = false
|
let needUpdate = false
|
||||||
const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
|
const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: 'No new version available',
|
message: 'No new version available',
|
||||||
}
|
}
|
||||||
if (fetchedReleases.length === 0) {
|
if (fetchedReleases.length === 0) {
|
||||||
toastProps.type = 'error'
|
toastProps.type = 'error'
|
||||||
toastProps.message = 'Input releases is empty'
|
toastProps.message = 'Input releases is empty'
|
||||||
return { needUpdate, toastProps }
|
|
||||||
}
|
|
||||||
const versions = fetchedReleases.map(release => release.tag_name)
|
|
||||||
const latestVersion = getLatestVersion(versions)
|
|
||||||
try {
|
|
||||||
needUpdate = compareVersion(latestVersion, currentVersion) === 1
|
|
||||||
if (needUpdate)
|
|
||||||
toastProps.message = `New version available: ${latestVersion}`
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
needUpdate = false
|
|
||||||
toastProps.type = 'error'
|
|
||||||
toastProps.message = 'Fail to compare versions, please check the version format'
|
|
||||||
}
|
|
||||||
return { needUpdate, toastProps }
|
return { needUpdate, toastProps }
|
||||||
}
|
}
|
||||||
|
const versions = fetchedReleases.map(release => release.tag_name)
|
||||||
return { fetchReleases, checkForUpdates }
|
const latestVersion = getLatestVersion(versions)
|
||||||
}
|
try {
|
||||||
|
needUpdate = compareVersion(latestVersion, currentVersion) === 1
|
||||||
export const useGitHubUpload = () => {
|
if (needUpdate)
|
||||||
const handleUpload = async (
|
toastProps.message = `New version available: ${latestVersion}`
|
||||||
repoUrl: string,
|
}
|
||||||
selectedVersion: string,
|
catch {
|
||||||
selectedPackage: string,
|
needUpdate = false
|
||||||
onSuccess?: (GitHubPackage: { manifest: any, unique_identifier: string }) => void,
|
toastProps.type = 'error'
|
||||||
) => {
|
toastProps.message = 'Fail to compare versions, please check the version format'
|
||||||
try {
|
}
|
||||||
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
return { needUpdate, toastProps }
|
||||||
const GitHubPackage = {
|
}
|
||||||
manifest: response.manifest,
|
|
||||||
unique_identifier: response.unique_identifier,
|
export const handleUpload = async (
|
||||||
}
|
repoUrl: string,
|
||||||
if (onSuccess)
|
selectedVersion: string,
|
||||||
onSuccess(GitHubPackage)
|
selectedPackage: string,
|
||||||
return GitHubPackage
|
onSuccess?: (GitHubPackage: { manifest: any, unique_identifier: string }) => void,
|
||||||
}
|
) => {
|
||||||
catch (error) {
|
try {
|
||||||
toast.error('Error uploading package')
|
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
||||||
throw error
|
const GitHubPackage = {
|
||||||
}
|
manifest: response.manifest,
|
||||||
|
unique_identifier: response.unique_identifier,
|
||||||
|
}
|
||||||
|
if (onSuccess)
|
||||||
|
onSuccess(GitHubPackage)
|
||||||
|
return GitHubPackage
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
toast.error('Error uploading package')
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
return { handleUpload }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,10 +74,16 @@ vi.mock('@/app/components/plugins/install-plugin/base/use-get-icon', () => ({
|
|||||||
default: () => ({ getIconUrl: mockGetIconUrl }),
|
default: () => ({ getIconUrl: mockGetIconUrl }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockFetchReleases = vi.fn()
|
const { mockFetchReleases } = vi.hoisted(() => ({
|
||||||
vi.mock('../../hooks', () => ({
|
mockFetchReleases: vi.fn(),
|
||||||
useGitHubReleases: () => ({ fetchReleases: mockFetchReleases }),
|
|
||||||
}))
|
}))
|
||||||
|
vi.mock('../../hooks', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('../../hooks')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
fetchReleases: mockFetchReleases,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const mockRefreshPluginList = vi.fn()
|
const mockRefreshPluginList = vi.fn()
|
||||||
vi.mock('../../hooks/use-refresh-plugin-list', () => ({
|
vi.mock('../../hooks/use-refresh-plugin-list', () => ({
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-ico
|
|||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import { InstallStepFromGitHub } from '../../types'
|
import { InstallStepFromGitHub } from '../../types'
|
||||||
import Installed from '../base/installed'
|
import Installed from '../base/installed'
|
||||||
import { useGitHubReleases } from '../hooks'
|
import { fetchReleases } from '../hooks'
|
||||||
import useHideLogic from '../hooks/use-hide-logic'
|
import useHideLogic from '../hooks/use-hide-logic'
|
||||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||||
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
|
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
|
||||||
@ -31,7 +31,6 @@ type InstallFromGitHubProps = {
|
|||||||
const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, onClose, onSuccess }) => {
|
const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, onClose, onSuccess }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { getIconUrl } = useGetIcon()
|
const { getIconUrl } = useGetIcon()
|
||||||
const { fetchReleases } = useGitHubReleases()
|
|
||||||
const { refreshPluginList } = useRefreshPluginList()
|
const { refreshPluginList } = useRefreshPluginList()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -5,11 +5,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import { PluginCategoryEnum } from '../../../../types'
|
import { PluginCategoryEnum } from '../../../../types'
|
||||||
import SelectPackage from '../selectPackage'
|
import SelectPackage from '../selectPackage'
|
||||||
|
|
||||||
// Mock the useGitHubUpload hook
|
// Mock upload helper from hooks module
|
||||||
const mockHandleUpload = vi.fn()
|
const { mockHandleUpload } = vi.hoisted(() => ({
|
||||||
vi.mock('../../../hooks', () => ({
|
mockHandleUpload: vi.fn(),
|
||||||
useGitHubUpload: () => ({ handleUpload: mockHandleUpload }),
|
|
||||||
}))
|
}))
|
||||||
|
vi.mock('../../../hooks', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('../../../hooks')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
handleUpload: mockHandleUpload,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Factory functions
|
// Factory functions
|
||||||
const createMockManifest = (): PluginDeclaration => ({
|
const createMockManifest = (): PluginDeclaration => ({
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import * as React from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { PortalSelect } from '@/app/components/base/select'
|
import { PortalSelect } from '@/app/components/base/select'
|
||||||
import { useGitHubUpload } from '../../hooks'
|
import { handleUpload } from '../../hooks'
|
||||||
|
|
||||||
const i18nPrefix = 'installFromGitHub'
|
const i18nPrefix = 'installFromGitHub'
|
||||||
|
|
||||||
@ -43,7 +43,6 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isEdit = Boolean(updatePayload)
|
const isEdit = Boolean(updatePayload)
|
||||||
const [isUploading, setIsUploading] = React.useState(false)
|
const [isUploading, setIsUploading] = React.useState(false)
|
||||||
const { handleUpload } = useGitHubUpload()
|
|
||||||
|
|
||||||
const handleUploadPackage = async () => {
|
const handleUploadPackage = async () => {
|
||||||
if (isUploading)
|
if (isUploading)
|
||||||
|
|||||||
@ -103,12 +103,14 @@ vi.mock('@/service/use-tools', () => ({
|
|||||||
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../../install-plugin/hooks', () => ({
|
vi.mock('../../install-plugin/hooks', async (importOriginal) => {
|
||||||
useGitHubReleases: () => ({
|
const actual = await importOriginal<typeof import('../../install-plugin/hooks')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
checkForUpdates: mockCheckForUpdates,
|
checkForUpdates: mockCheckForUpdates,
|
||||||
fetchReleases: mockFetchReleases,
|
fetchReleases: mockFetchReleases,
|
||||||
}),
|
}
|
||||||
}))
|
})
|
||||||
|
|
||||||
// Auto upgrade settings mock
|
// Auto upgrade settings mock
|
||||||
let mockAutoUpgradeInfo: {
|
let mockAutoUpgradeInfo: {
|
||||||
|
|||||||
@ -72,12 +72,14 @@ vi.mock('@/service/use-tools', () => ({
|
|||||||
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../../../../install-plugin/hooks', () => ({
|
vi.mock('../../../../install-plugin/hooks', async (importOriginal) => {
|
||||||
useGitHubReleases: () => ({
|
const actual = await importOriginal<typeof import('../../../../install-plugin/hooks')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
checkForUpdates: mockCheckForUpdates,
|
checkForUpdates: mockCheckForUpdates,
|
||||||
fetchReleases: mockFetchReleases,
|
fetchReleases: mockFetchReleases,
|
||||||
}),
|
}
|
||||||
}))
|
})
|
||||||
|
|
||||||
const createPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => ({
|
const createPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => ({
|
||||||
id: 'test-id',
|
id: 'test-id',
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { useProviderContext } from '@/context/provider-context'
|
|||||||
import { uninstallPlugin } from '@/service/plugins'
|
import { uninstallPlugin } from '@/service/plugins'
|
||||||
import { useInvalidateCheckInstalled } from '@/service/use-plugins'
|
import { useInvalidateCheckInstalled } from '@/service/use-plugins'
|
||||||
import { useInvalidateAllToolProviders } from '@/service/use-tools'
|
import { useInvalidateAllToolProviders } from '@/service/use-tools'
|
||||||
import { useGitHubReleases } from '../../../install-plugin/hooks'
|
import { checkForUpdates, fetchReleases } from '../../../install-plugin/hooks'
|
||||||
import { PluginCategoryEnum, PluginSource } from '../../../types'
|
import { PluginCategoryEnum, PluginSource } from '../../../types'
|
||||||
|
|
||||||
type UsePluginOperationsParams = {
|
type UsePluginOperationsParams = {
|
||||||
@ -39,7 +39,6 @@ export const usePluginOperations = ({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
}: UsePluginOperationsParams): UsePluginOperationsReturn => {
|
}: UsePluginOperationsParams): UsePluginOperationsReturn => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { checkForUpdates, fetchReleases } = useGitHubReleases()
|
|
||||||
const { setShowUpdatePluginModal } = useModalContext()
|
const { setShowUpdatePluginModal } = useModalContext()
|
||||||
const { refreshModelProviders } = useProviderContext()
|
const { refreshModelProviders } = useProviderContext()
|
||||||
const invalidateCheckInstalled = useInvalidateCheckInstalled()
|
const invalidateCheckInstalled = useInvalidateCheckInstalled()
|
||||||
|
|||||||
@ -46,13 +46,15 @@ vi.mock('@/service/plugins', () => ({
|
|||||||
uninstallPlugin: (id: string) => mockUninstallPlugin(id),
|
uninstallPlugin: (id: string) => mockUninstallPlugin(id),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock GitHub releases hook
|
// Mock GitHub release helpers
|
||||||
vi.mock('../../install-plugin/hooks', () => ({
|
vi.mock('../../install-plugin/hooks', async (importOriginal) => {
|
||||||
useGitHubReleases: () => ({
|
const actual = await importOriginal<typeof import('../../install-plugin/hooks')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
fetchReleases: mockFetchReleases,
|
fetchReleases: mockFetchReleases,
|
||||||
checkForUpdates: mockCheckForUpdates,
|
checkForUpdates: mockCheckForUpdates,
|
||||||
}),
|
}
|
||||||
}))
|
})
|
||||||
|
|
||||||
// Mock modal context
|
// Mock modal context
|
||||||
vi.mock('@/context/modal-context', () => ({
|
vi.mock('@/context/modal-context', () => ({
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
|||||||
import ActionButton from '../../base/action-button'
|
import ActionButton from '../../base/action-button'
|
||||||
import Confirm from '../../base/confirm'
|
import Confirm from '../../base/confirm'
|
||||||
import Tooltip from '../../base/tooltip'
|
import Tooltip from '../../base/tooltip'
|
||||||
import { useGitHubReleases } from '../install-plugin/hooks'
|
import { checkForUpdates, fetchReleases } from '../install-plugin/hooks'
|
||||||
import PluginInfo from '../plugin-page/plugin-info'
|
import PluginInfo from '../plugin-page/plugin-info'
|
||||||
import { PluginSource } from '../types'
|
import { PluginSource } from '../types'
|
||||||
|
|
||||||
@ -54,7 +54,6 @@ const Action: FC<Props> = ({
|
|||||||
setTrue: showDeleting,
|
setTrue: showDeleting,
|
||||||
setFalse: hideDeleting,
|
setFalse: hideDeleting,
|
||||||
}] = useBoolean(false)
|
}] = useBoolean(false)
|
||||||
const { checkForUpdates, fetchReleases } = useGitHubReleases()
|
|
||||||
const { setShowUpdatePluginModal } = useModalContext()
|
const { setShowUpdatePluginModal } = useModalContext()
|
||||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,8 @@ export type QAChunks = {
|
|||||||
|
|
||||||
export type ChunkInfo = GeneralChunks | ParentChildChunks | QAChunks
|
export type ChunkInfo = GeneralChunks | ParentChildChunks | QAChunks
|
||||||
|
|
||||||
export enum QAItemType {
|
export const QAItemType = {
|
||||||
Question = 'question',
|
Question: 'question',
|
||||||
Answer = 'answer',
|
Answer: 'answer',
|
||||||
}
|
} as const
|
||||||
|
export type QAItemType = typeof QAItemType[keyof typeof QAItemType]
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||||
|
|
||||||
export enum TestRunStep {
|
export const TestRunStep = {
|
||||||
dataSource = 'dataSource',
|
dataSource: 'dataSource',
|
||||||
documentProcessing = 'documentProcessing',
|
documentProcessing: 'documentProcessing',
|
||||||
}
|
} as const
|
||||||
|
export type TestRunStep = typeof TestRunStep[keyof typeof TestRunStep]
|
||||||
|
|
||||||
export type DataSourceOption = {
|
export type DataSourceOption = {
|
||||||
label: string
|
label: string
|
||||||
|
|||||||
@ -130,7 +130,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
expect(screen.queryByText('operator.vertical')).not.toBeInTheDocument()
|
expect(screen.queryByText('operator.vertical')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should still render the menu when the requested position exceeds workflow container bounds', () => {
|
it('should render menu items when selectionMenu is present', async () => {
|
||||||
const nodes = [
|
const nodes = [
|
||||||
createNode({ id: 'n1', selected: true, width: 80, height: 40 }),
|
createNode({ id: 'n1', selected: true, width: 80, height: 40 }),
|
||||||
createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }),
|
createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }),
|
||||||
@ -151,10 +151,12 @@ describe('SelectionContextmenu', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 780, top: 590 } })
|
store.setState({ selectionMenu: { clientX: 780, clientY: 590 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(screen.getByTestId('selection-contextmenu-item-left')).toBeInTheDocument()
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('selection-contextmenu-item-left')).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should close itself when only one node is selected', async () => {
|
it('should close itself when only one node is selected', async () => {
|
||||||
@ -165,7 +167,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
const { store } = renderSelectionMenu({ nodes })
|
const { store } = renderSelectionMenu({ nodes })
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -190,7 +192,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 100, top: 100 } })
|
store.setState({ selectionMenu: { clientX: 100, clientY: 100 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||||
@ -220,7 +222,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
const { store } = renderSelectionMenu({ nodes })
|
const { store } = renderSelectionMenu({ nodes })
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(screen.getByTestId('selection-contextmenu-item-copy')).toHaveTextContent('workflow.common.copy')
|
expect(screen.getByTestId('selection-contextmenu-item-copy')).toHaveTextContent('workflow.common.copy')
|
||||||
@ -230,12 +232,12 @@ describe('SelectionContextmenu', () => {
|
|||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-copy'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-copy'))
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||||
})
|
})
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-duplicate'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-duplicate'))
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||||
})
|
})
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-delete'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-delete'))
|
||||||
|
|
||||||
@ -261,7 +263,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
const { store } = renderSelectionMenu({ nodes, edges })
|
const { store } = renderSelectionMenu({ nodes, edges })
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-createSnippet'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-createSnippet'))
|
||||||
@ -324,7 +326,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 160, top: 120 } })
|
store.setState({ selectionMenu: { clientX: 160, clientY: 120 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-distributeHorizontal'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-distributeHorizontal'))
|
||||||
@ -363,7 +365,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 180, top: 120 } })
|
store.setState({ selectionMenu: { clientX: 180, clientY: 120 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||||
@ -382,7 +384,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
const { store } = renderSelectionMenu({ nodes })
|
const { store } = renderSelectionMenu({ nodes })
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 100, top: 100 } })
|
store.setState({ selectionMenu: { clientX: 100, clientY: 100 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||||
@ -400,7 +402,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
const { store } = renderSelectionMenu({ nodes })
|
const { store } = renderSelectionMenu({ nodes })
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 100, top: 100 } })
|
store.setState({ selectionMenu: { clientX: 100, clientY: 100 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||||
@ -425,7 +427,7 @@ describe('SelectionContextmenu', () => {
|
|||||||
const { store } = renderSelectionMenu({ nodes })
|
const { store } = renderSelectionMenu({ nodes })
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
store.setState({ selectionMenu: { left: 100, top: 100 } })
|
store.setState({ selectionMenu: { clientX: 100, clientY: 100 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||||
|
|||||||
@ -29,7 +29,7 @@ describe('usePanelInteractions', () => {
|
|||||||
const { result, store } = renderWorkflowHook(() => usePanelInteractions(), {
|
const { result, store } = renderWorkflowHook(() => usePanelInteractions(), {
|
||||||
initialStoreState: {
|
initialStoreState: {
|
||||||
nodeMenu: { top: 20, left: 40, nodeId: 'n1' },
|
nodeMenu: { top: 20, left: 40, nodeId: 'n1' },
|
||||||
selectionMenu: { top: 30, left: 50 },
|
selectionMenu: { clientX: 30, clientY: 50 },
|
||||||
edgeMenu: { clientX: 320, clientY: 180, edgeId: 'e1' },
|
edgeMenu: { clientX: 320, clientY: 180, edgeId: 'e1' },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -200,8 +200,8 @@ describe('useSelectionInteractions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(store.getState().selectionMenu).toEqual({
|
expect(store.getState().selectionMenu).toEqual({
|
||||||
top: 150,
|
clientX: 300,
|
||||||
left: 200,
|
clientY: 200,
|
||||||
})
|
})
|
||||||
expect(store.getState().nodeMenu).toBeUndefined()
|
expect(store.getState().nodeMenu).toBeUndefined()
|
||||||
expect(store.getState().panelMenu).toBeUndefined()
|
expect(store.getState().panelMenu).toBeUndefined()
|
||||||
@ -210,7 +210,7 @@ describe('useSelectionInteractions', () => {
|
|||||||
|
|
||||||
it('handleSelectionContextmenuCancel should clear selectionMenu', () => {
|
it('handleSelectionContextmenuCancel should clear selectionMenu', () => {
|
||||||
const { result, store } = renderSelectionInteractions({
|
const { result, store } = renderSelectionInteractions({
|
||||||
selectionMenu: { top: 50, left: 60 },
|
selectionMenu: { clientX: 50, clientY: 60 },
|
||||||
})
|
})
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
|||||||
@ -137,15 +137,13 @@ export const useSelectionInteractions = () => {
|
|||||||
return
|
return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const container = document.querySelector('#workflow-container')
|
|
||||||
const { x, y } = container!.getBoundingClientRect()
|
|
||||||
workflowStore.setState({
|
workflowStore.setState({
|
||||||
nodeMenu: undefined,
|
nodeMenu: undefined,
|
||||||
panelMenu: undefined,
|
panelMenu: undefined,
|
||||||
edgeMenu: undefined,
|
edgeMenu: undefined,
|
||||||
selectionMenu: {
|
selectionMenu: {
|
||||||
top: e.clientY - y,
|
clientX: e.clientX,
|
||||||
left: e.clientX - x,
|
clientY: e.clientY,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, [workflowStore])
|
}, [workflowStore])
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import type { CommonNodeType } from '../types'
|
import type { CommonNodeType } from '../types'
|
||||||
|
|
||||||
export enum NoteTheme {
|
export const NoteTheme = {
|
||||||
blue = 'blue',
|
blue: 'blue',
|
||||||
cyan = 'cyan',
|
cyan: 'cyan',
|
||||||
green = 'green',
|
green: 'green',
|
||||||
yellow = 'yellow',
|
yellow: 'yellow',
|
||||||
pink = 'pink',
|
pink: 'pink',
|
||||||
violet = 'violet',
|
violet: 'violet',
|
||||||
}
|
} as const
|
||||||
|
export type NoteTheme = typeof NoteTheme[keyof typeof NoteTheme]
|
||||||
|
|
||||||
export type NoteNodeType = CommonNodeType & {
|
export type NoteNodeType = CommonNodeType & {
|
||||||
text: string
|
text: string
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { ComponentType } from 'react'
|
|
||||||
import type { CreateSnippetDialogPayload } from './create-snippet-dialog'
|
import type { CreateSnippetDialogPayload } from './create-snippet-dialog'
|
||||||
import type { Edge, Node } from './types'
|
import type { Edge, Node } from './types'
|
||||||
import type { SnippetCanvasData } from '@/models/snippet'
|
import type { SnippetCanvasData } from '@/models/snippet'
|
||||||
@ -53,13 +52,6 @@ const AlignType = {
|
|||||||
|
|
||||||
type AlignTypeValue = (typeof AlignType)[keyof typeof AlignType]
|
type AlignTypeValue = (typeof AlignType)[keyof typeof AlignType]
|
||||||
|
|
||||||
type SelectionMenuPosition = {
|
|
||||||
left: number
|
|
||||||
top: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContainerRect = Pick<DOMRect, 'width' | 'height'>
|
|
||||||
|
|
||||||
type AlignBounds = {
|
type AlignBounds = {
|
||||||
minX: number
|
minX: number
|
||||||
maxX: number
|
maxX: number
|
||||||
@ -69,7 +61,7 @@ type AlignBounds = {
|
|||||||
|
|
||||||
type AlignMenuItem = {
|
type AlignMenuItem = {
|
||||||
alignType: AlignTypeValue
|
alignType: AlignTypeValue
|
||||||
icon: ComponentType<{ className?: string }>
|
icon: string
|
||||||
iconClassName?: string
|
iconClassName?: string
|
||||||
translationKey: string
|
translationKey: string
|
||||||
}
|
}
|
||||||
@ -86,39 +78,16 @@ const MENU_HEIGHT = 240
|
|||||||
const DEFAULT_SNIPPET_VIEWPORT: SnippetCanvasData['viewport'] = { x: 0, y: 0, zoom: 1 }
|
const DEFAULT_SNIPPET_VIEWPORT: SnippetCanvasData['viewport'] = { x: 0, y: 0, zoom: 1 }
|
||||||
|
|
||||||
const alignMenuItems: AlignMenuItem[] = [
|
const alignMenuItems: AlignMenuItem[] = [
|
||||||
{ alignType: AlignType.Left, icon: RiAlignItemLeftLine, translationKey: 'operator.alignLeft' },
|
{ alignType: AlignType.Left, icon: 'i-ri-align-item-left-line', translationKey: 'operator.alignLeft' },
|
||||||
{ alignType: AlignType.Center, icon: RiAlignItemHorizontalCenterLine, translationKey: 'operator.alignCenter' },
|
{ alignType: AlignType.Center, icon: 'i-ri-align-item-horizontal-center-line', translationKey: 'operator.alignCenter' },
|
||||||
{ alignType: AlignType.Right, icon: RiAlignItemRightLine, translationKey: 'operator.alignRight' },
|
{ alignType: AlignType.Right, icon: 'i-ri-align-item-right-line', translationKey: 'operator.alignRight' },
|
||||||
{ alignType: AlignType.Top, icon: RiAlignItemTopLine, translationKey: 'operator.alignTop' },
|
{ alignType: AlignType.Top, icon: 'i-ri-align-item-top-line', translationKey: 'operator.alignTop' },
|
||||||
{ alignType: AlignType.Middle, icon: RiAlignItemVerticalCenterLine, iconClassName: 'rotate-90', translationKey: 'operator.alignMiddle' },
|
{ alignType: AlignType.Middle, icon: 'i-ri-align-item-vertical-center-line', iconClassName: 'rotate-90', translationKey: 'operator.alignMiddle' },
|
||||||
{ alignType: AlignType.Bottom, icon: RiAlignItemBottomLine, translationKey: 'operator.alignBottom' },
|
{ alignType: AlignType.Bottom, icon: 'i-ri-align-item-bottom-line', translationKey: 'operator.alignBottom' },
|
||||||
{ alignType: AlignType.DistributeHorizontal, icon: RiAlignJustify, translationKey: 'operator.distributeHorizontal' },
|
{ alignType: AlignType.DistributeHorizontal, icon: 'i-ri-align-justify-line', translationKey: 'operator.distributeHorizontal' },
|
||||||
{ alignType: AlignType.DistributeVertical, icon: RiAlignJustify, iconClassName: 'rotate-90', translationKey: 'operator.distributeVertical' },
|
{ alignType: AlignType.DistributeVertical, icon: 'i-ri-align-justify-line', iconClassName: 'rotate-90', translationKey: 'operator.distributeVertical' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const getMenuPosition = (
|
|
||||||
selectionMenu: SelectionMenuPosition | undefined,
|
|
||||||
containerRect?: ContainerRect | null,
|
|
||||||
) => {
|
|
||||||
if (!selectionMenu)
|
|
||||||
return { left: 0, top: 0 }
|
|
||||||
|
|
||||||
let { left, top } = selectionMenu
|
|
||||||
|
|
||||||
if (containerRect) {
|
|
||||||
if (left + MENU_WIDTH > containerRect.width)
|
|
||||||
left = left - MENU_WIDTH
|
|
||||||
|
|
||||||
if (top + MENU_HEIGHT > containerRect.height)
|
|
||||||
top = top - MENU_HEIGHT
|
|
||||||
|
|
||||||
left = Math.max(0, left)
|
|
||||||
top = Math.max(0, top)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { left, top }
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAlignableNodes = (nodes: Node[], selectedNodes: Node[]) => {
|
const getAlignableNodes = (nodes: Node[], selectedNodes: Node[]) => {
|
||||||
const selectedNodeIds = new Set(selectedNodes.map(node => node.id))
|
const selectedNodeIds = new Set(selectedNodes.map(node => node.id))
|
||||||
const childNodeIds = new Set<string>()
|
const childNodeIds = new Set<string>()
|
||||||
@ -356,29 +325,19 @@ const SelectionContextmenu = () => {
|
|||||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||||
const { saveStateToHistory } = useWorkflowHistory()
|
const { saveStateToHistory } = useWorkflowHistory()
|
||||||
|
|
||||||
const menuPosition = useMemo(() => {
|
|
||||||
const container = document.querySelector('#workflow-container')
|
|
||||||
return getMenuPosition(selectionMenu, container?.getBoundingClientRect())
|
|
||||||
}, [selectionMenu])
|
|
||||||
|
|
||||||
const anchor = useMemo(() => {
|
const anchor = useMemo(() => {
|
||||||
if (!selectionMenu)
|
if (!selectionMenu)
|
||||||
return null
|
return undefined
|
||||||
|
|
||||||
const container = document.querySelector('#workflow-container')
|
|
||||||
const containerRect = container?.getBoundingClientRect()
|
|
||||||
if (!containerRect)
|
|
||||||
return null
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getBoundingClientRect: () => DOMRect.fromRect({
|
getBoundingClientRect: () => DOMRect.fromRect({
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
x: containerRect.left + menuPosition.left,
|
x: selectionMenu.clientX,
|
||||||
y: containerRect.top + menuPosition.top,
|
y: selectionMenu.clientY,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}, [menuPosition.left, menuPosition.top, selectionMenu])
|
}, [selectionMenu])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectionMenu && selectedNodes.length <= 1)
|
if (selectionMenu && selectedNodes.length <= 1)
|
||||||
@ -574,7 +533,7 @@ const SelectionContextmenu = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ContextMenuContent
|
<ContextMenuContent
|
||||||
positionerProps={{ anchor }}
|
positionerProps={anchor ? { anchor } : undefined}
|
||||||
popupClassName="w-[240px] py-0"
|
popupClassName="w-[240px] py-0"
|
||||||
>
|
>
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
@ -612,7 +571,7 @@ const SelectionContextmenu = () => {
|
|||||||
data-testid={`selection-contextmenu-item-${item.alignType}`}
|
data-testid={`selection-contextmenu-item-${item.alignType}`}
|
||||||
onClick={() => handleAlignNodes(item.alignType)}
|
onClick={() => handleAlignNodes(item.alignType)}
|
||||||
>
|
>
|
||||||
<Icon className={`h-[18px] w-[18px] ${item.iconClassName ?? ''}`.trim()} />
|
<span aria-hidden className={`${item.icon} h-4 w-4 ${item.iconClassName ?? ''}`.trim()} />
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -96,7 +96,7 @@ describe('createWorkflowStore', () => {
|
|||||||
['showInputsPanel', 'setShowInputsPanel', true],
|
['showInputsPanel', 'setShowInputsPanel', true],
|
||||||
['showDebugAndPreviewPanel', 'setShowDebugAndPreviewPanel', true],
|
['showDebugAndPreviewPanel', 'setShowDebugAndPreviewPanel', true],
|
||||||
['panelMenu', 'setPanelMenu', { top: 10, left: 20 }],
|
['panelMenu', 'setPanelMenu', { top: 10, left: 20 }],
|
||||||
['selectionMenu', 'setSelectionMenu', { top: 50, left: 60 }],
|
['selectionMenu', 'setSelectionMenu', { clientX: 50, clientY: 60 }],
|
||||||
['edgeMenu', 'setEdgeMenu', { clientX: 320, clientY: 180, edgeId: 'e1' }],
|
['edgeMenu', 'setEdgeMenu', { clientX: 320, clientY: 180, edgeId: 'e1' }],
|
||||||
['showVariableInspectPanel', 'setShowVariableInspectPanel', true],
|
['showVariableInspectPanel', 'setShowVariableInspectPanel', true],
|
||||||
['initShowLastRunTab', 'setInitShowLastRunTab', true],
|
['initShowLastRunTab', 'setInitShowLastRunTab', true],
|
||||||
|
|||||||
@ -16,8 +16,8 @@ export type PanelSliceShape = {
|
|||||||
}
|
}
|
||||||
setPanelMenu: (panelMenu: PanelSliceShape['panelMenu']) => void
|
setPanelMenu: (panelMenu: PanelSliceShape['panelMenu']) => void
|
||||||
selectionMenu?: {
|
selectionMenu?: {
|
||||||
top: number
|
clientX: number
|
||||||
left: number
|
clientY: number
|
||||||
}
|
}
|
||||||
setSelectionMenu: (selectionMenu: PanelSliceShape['selectionMenu']) => void
|
setSelectionMenu: (selectionMenu: PanelSliceShape['selectionMenu']) => void
|
||||||
edgeMenu?: {
|
edgeMenu?: {
|
||||||
|
|||||||
@ -174,7 +174,7 @@ const Right = ({
|
|||||||
{currentNodeVar?.var && (
|
{currentNodeVar?.var && (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
[VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeVar.nodeType as VarInInspectType) && (
|
([VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system] as VarInInspectType[]).includes(currentNodeVar.nodeType as VarInInspectType) && (
|
||||||
<VariableIconWithColor
|
<VariableIconWithColor
|
||||||
variableCategory={currentNodeVar.nodeType as VarInInspectType}
|
variableCategory={currentNodeVar.nodeType as VarInInspectType}
|
||||||
className="size-4"
|
className="size-4"
|
||||||
|
|||||||
@ -2,12 +2,14 @@ export const EVENT_WORKFLOW_STOP = 'WORKFLOW_STOP'
|
|||||||
|
|
||||||
export const CHUNK_SCHEMA_TYPES = ['general_structure', 'parent_child_structure', 'qa_structure']
|
export const CHUNK_SCHEMA_TYPES = ['general_structure', 'parent_child_structure', 'qa_structure']
|
||||||
|
|
||||||
export enum ViewMode {
|
export const ViewMode = {
|
||||||
Code = 'code',
|
Code: 'code',
|
||||||
Preview = 'preview',
|
Preview: 'preview',
|
||||||
}
|
} as const
|
||||||
|
export type ViewMode = typeof ViewMode[keyof typeof ViewMode]
|
||||||
|
|
||||||
export enum PreviewType {
|
export const PreviewType = {
|
||||||
Markdown = 'markdown',
|
Markdown: 'markdown',
|
||||||
Chunks = 'chunks',
|
Chunks: 'chunks',
|
||||||
}
|
} as const
|
||||||
|
export type PreviewType = typeof PreviewType[keyof typeof PreviewType]
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
import type { NextRequest } from '@/next/server'
|
|
||||||
import { Octokit } from '@octokit/core'
|
|
||||||
import { RequestError } from '@octokit/request-error'
|
|
||||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
|
||||||
import { NextResponse } from '@/next/server'
|
|
||||||
|
|
||||||
type Params = {
|
|
||||||
owner: string
|
|
||||||
repo: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const octokit = new Octokit({
|
|
||||||
auth: GITHUB_ACCESS_TOKEN,
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<Params> },
|
|
||||||
) {
|
|
||||||
const { owner, repo } = (await params)
|
|
||||||
try {
|
|
||||||
const releasesRes = await octokit.request('GET /repos/{owner}/{repo}/releases', {
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
headers: {
|
|
||||||
'X-GitHub-Api-Version': '2022-11-28',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return NextResponse.json(releasesRes)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
if (error instanceof RequestError)
|
|
||||||
return NextResponse.json(error.response)
|
|
||||||
else
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -292,9 +292,6 @@ export const resetHITLInputReg = () => HITL_INPUT_REG.lastIndex = 0
|
|||||||
|
|
||||||
export const DISABLE_UPLOAD_IMAGE_AS_ICON = env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON
|
export const DISABLE_UPLOAD_IMAGE_AS_ICON = env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON
|
||||||
|
|
||||||
export const GITHUB_ACCESS_TOKEN
|
|
||||||
= env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN
|
|
||||||
|
|
||||||
export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl'
|
export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl'
|
||||||
export const FULL_DOC_PREVIEW_LENGTH = 50
|
export const FULL_DOC_PREVIEW_LENGTH = 50
|
||||||
|
|
||||||
|
|||||||
@ -283,16 +283,6 @@ Reserve snapshots for static, deterministic fragments (icons, badges, layout chr
|
|||||||
|
|
||||||
**Note**: Dify is a desktop application. **No need for** responsive/mobile testing.
|
**Note**: Dify is a desktop application. **No need for** responsive/mobile testing.
|
||||||
|
|
||||||
### 12. Mock API
|
|
||||||
|
|
||||||
Use Nock to mock API calls. Example:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const mockGithubStar = (status: number, body: Record<string, unknown>, delayMs = 0) => {
|
|
||||||
return nock(GITHUB_HOST).get(GITHUB_PATH).delay(delayMs).reply(status, body)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
### Example Structure
|
### Example Structure
|
||||||
|
|||||||
@ -66,10 +66,6 @@ const clientSchema = {
|
|||||||
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: coercedBoolean.default(true),
|
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: coercedBoolean.default(true),
|
||||||
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: coercedBoolean.default(true),
|
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: coercedBoolean.default(true),
|
||||||
NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL: coercedBoolean.default(false),
|
NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL: coercedBoolean.default(false),
|
||||||
/**
|
|
||||||
* Github Access Token, used for invoking Github API
|
|
||||||
*/
|
|
||||||
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN: z.string().optional(),
|
|
||||||
/**
|
/**
|
||||||
* The maximum number of tokens for segmentation
|
* The maximum number of tokens for segmentation
|
||||||
*/
|
*/
|
||||||
@ -171,7 +167,6 @@ export const env = createEnv({
|
|||||||
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL : getRuntimeEnvFromBody('enableWebsiteFirecrawl'),
|
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL : getRuntimeEnvFromBody('enableWebsiteFirecrawl'),
|
||||||
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER : getRuntimeEnvFromBody('enableWebsiteJinareader'),
|
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER : getRuntimeEnvFromBody('enableWebsiteJinareader'),
|
||||||
NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL : getRuntimeEnvFromBody('enableWebsiteWatercrawl'),
|
NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL : getRuntimeEnvFromBody('enableWebsiteWatercrawl'),
|
||||||
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN: isServer ? process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN : getRuntimeEnvFromBody('githubAccessToken'),
|
|
||||||
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: isServer ? process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH : getRuntimeEnvFromBody('indexingMaxSegmentationTokensLength'),
|
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: isServer ? process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH : getRuntimeEnvFromBody('indexingMaxSegmentationTokensLength'),
|
||||||
NEXT_PUBLIC_IS_MARKETPLACE: isServer ? process.env.NEXT_PUBLIC_IS_MARKETPLACE : getRuntimeEnvFromBody('isMarketplace'),
|
NEXT_PUBLIC_IS_MARKETPLACE: isServer ? process.env.NEXT_PUBLIC_IS_MARKETPLACE : getRuntimeEnvFromBody('isMarketplace'),
|
||||||
NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: isServer ? process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT : getRuntimeEnvFromBody('loopNodeMaxCount'),
|
NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: isServer ? process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT : getRuntimeEnvFromBody('loopNodeMaxCount'),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ import md from 'eslint-markdown'
|
|||||||
import tailwindcss from 'eslint-plugin-better-tailwindcss'
|
import tailwindcss from 'eslint-plugin-better-tailwindcss'
|
||||||
import hyoban from 'eslint-plugin-hyoban'
|
import hyoban from 'eslint-plugin-hyoban'
|
||||||
import markdownPreferences from 'eslint-plugin-markdown-preferences'
|
import markdownPreferences from 'eslint-plugin-markdown-preferences'
|
||||||
|
import noBarrelFiles from 'eslint-plugin-no-barrel-files'
|
||||||
import { reactRefresh } from 'eslint-plugin-react-refresh'
|
import { reactRefresh } from 'eslint-plugin-react-refresh'
|
||||||
import sonar from 'eslint-plugin-sonarjs'
|
import sonar from 'eslint-plugin-sonarjs'
|
||||||
import storybook from 'eslint-plugin-storybook'
|
import storybook from 'eslint-plugin-storybook'
|
||||||
@ -30,12 +31,17 @@ const plugins = pluginReact.configs.all.plugins
|
|||||||
export default antfu(
|
export default antfu(
|
||||||
{
|
{
|
||||||
react: false,
|
react: false,
|
||||||
nextjs: true,
|
nextjs: {
|
||||||
|
overrides: {
|
||||||
|
'next/no-img-element': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
ignores: ['public', 'types/doc-paths.ts', 'eslint-suppressions.json'],
|
ignores: ['public', 'types/doc-paths.ts', 'eslint-suppressions.json'],
|
||||||
typescript: {
|
typescript: {
|
||||||
overrides: {
|
overrides: {
|
||||||
'ts/consistent-type-definitions': ['error', 'type'],
|
'ts/consistent-type-definitions': ['error', 'type'],
|
||||||
'ts/no-explicit-any': 'error',
|
'ts/no-explicit-any': 'error',
|
||||||
|
'ts/no-redeclare': 'off',
|
||||||
},
|
},
|
||||||
erasableOnly: true,
|
erasableOnly: true,
|
||||||
},
|
},
|
||||||
@ -66,12 +72,23 @@ export default antfu(
|
|||||||
...pluginReact.configs['recommended-typescript'].rules,
|
...pluginReact.configs['recommended-typescript'].rules,
|
||||||
'react/prefer-namespace-import': 'error',
|
'react/prefer-namespace-import': 'error',
|
||||||
'react/set-state-in-effect': 'error',
|
'react/set-state-in-effect': 'error',
|
||||||
|
'react/no-unnecessary-use-prefix': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [...GLOB_TESTS, GLOB_MARKDOWN_CODE, 'vitest.setup.ts', 'test/i18n-mock.ts'],
|
files: [...GLOB_TESTS, GLOB_MARKDOWN_CODE, 'vitest.setup.ts', 'test/i18n-mock.ts'],
|
||||||
rules: {
|
rules: {
|
||||||
'react/component-hook-factories': 'off',
|
'react/component-hook-factories': 'off',
|
||||||
|
'react/no-unnecessary-use-prefix': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'no-barrel-files': noBarrelFiles,
|
||||||
|
},
|
||||||
|
ignores: ['next/**'],
|
||||||
|
rules: {
|
||||||
|
'no-barrel-files/no-barrel-files': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reactRefresh.configs.next(),
|
reactRefresh.configs.next(),
|
||||||
@ -98,7 +115,6 @@ export default antfu(
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'node/prefer-global/process': 'off',
|
'node/prefer-global/process': 'off',
|
||||||
'next/no-img-element': 'off',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -160,7 +176,7 @@ export default antfu(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/package.json'],
|
files: ['package.json'],
|
||||||
rules: {
|
rules: {
|
||||||
'hyoban/no-dependency-version-prefix': 'error',
|
'hyoban/no-dependency-version-prefix': 'error',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -220,10 +220,6 @@ export type DataSources = {
|
|||||||
sources: DataSourceItem[]
|
sources: DataSourceItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GithubRepo = {
|
|
||||||
stargazers_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PluginProvider = {
|
export type PluginProvider = {
|
||||||
tool_name: string
|
tool_name: string
|
||||||
is_enabled: boolean
|
is_enabled: boolean
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
export { NextResponse } from 'next/server'
|
|
||||||
export type { NextRequest } from 'next/server'
|
|
||||||
@ -74,8 +74,6 @@
|
|||||||
"@lexical/text": "0.42.0",
|
"@lexical/text": "0.42.0",
|
||||||
"@lexical/utils": "0.42.0",
|
"@lexical/utils": "0.42.0",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
"@octokit/core": "7.0.6",
|
|
||||||
"@octokit/request-error": "7.1.0",
|
|
||||||
"@orpc/client": "1.13.9",
|
"@orpc/client": "1.13.9",
|
||||||
"@orpc/contract": "1.13.9",
|
"@orpc/contract": "1.13.9",
|
||||||
"@orpc/openapi-client": "1.13.9",
|
"@orpc/openapi-client": "1.13.9",
|
||||||
@ -217,6 +215,7 @@
|
|||||||
"eslint-plugin-better-tailwindcss": "4.3.2",
|
"eslint-plugin-better-tailwindcss": "4.3.2",
|
||||||
"eslint-plugin-hyoban": "0.14.1",
|
"eslint-plugin-hyoban": "0.14.1",
|
||||||
"eslint-plugin-markdown-preferences": "0.40.3",
|
"eslint-plugin-markdown-preferences": "0.40.3",
|
||||||
|
"eslint-plugin-no-barrel-files": "1.2.2",
|
||||||
"eslint-plugin-react-hooks": "7.0.1",
|
"eslint-plugin-react-hooks": "7.0.1",
|
||||||
"eslint-plugin-react-refresh": "0.5.2",
|
"eslint-plugin-react-refresh": "0.5.2",
|
||||||
"eslint-plugin-sonarjs": "4.0.2",
|
"eslint-plugin-sonarjs": "4.0.2",
|
||||||
@ -228,7 +227,6 @@
|
|||||||
"jsdom-testing-mocks": "1.16.0",
|
"jsdom-testing-mocks": "1.16.0",
|
||||||
"knip": "6.0.2",
|
"knip": "6.0.2",
|
||||||
"lint-staged": "16.4.0",
|
"lint-staged": "16.4.0",
|
||||||
"nock": "14.0.11",
|
|
||||||
"postcss": "8.5.8",
|
"postcss": "8.5.8",
|
||||||
"postcss-js": "5.1.0",
|
"postcss-js": "5.1.0",
|
||||||
"react-server-dom-webpack": "19.2.4",
|
"react-server-dom-webpack": "19.2.4",
|
||||||
@ -278,6 +276,8 @@
|
|||||||
"object.values": "npm:@nolyfill/object.values@^1.0.44",
|
"object.values": "npm:@nolyfill/object.values@^1.0.44",
|
||||||
"pbkdf2": "~3.1.5",
|
"pbkdf2": "~3.1.5",
|
||||||
"pbkdf2@<3.1.3": "3.1.3",
|
"pbkdf2@<3.1.3": "3.1.3",
|
||||||
|
"picomatch@<2.3.2": "2.3.2",
|
||||||
|
"picomatch@>=4.0.0 <4.0.4": "4.0.4",
|
||||||
"prismjs": "~1.30",
|
"prismjs": "~1.30",
|
||||||
"prismjs@<1.30.0": "1.30.0",
|
"prismjs@<1.30.0": "1.30.0",
|
||||||
"rollup@>=4.0.0 <4.59.0": "4.59.0",
|
"rollup@>=4.0.0 <4.59.0": "4.59.0",
|
||||||
@ -285,6 +285,7 @@
|
|||||||
"safe-regex-test": "npm:@nolyfill/safe-regex-test@^1.0.44",
|
"safe-regex-test": "npm:@nolyfill/safe-regex-test@^1.0.44",
|
||||||
"safer-buffer": "npm:@nolyfill/safer-buffer@^1.0.44",
|
"safer-buffer": "npm:@nolyfill/safer-buffer@^1.0.44",
|
||||||
"side-channel": "npm:@nolyfill/side-channel@^1.0.44",
|
"side-channel": "npm:@nolyfill/side-channel@^1.0.44",
|
||||||
|
"smol-toml@<1.6.1": "1.6.1",
|
||||||
"solid-js": "1.9.11",
|
"solid-js": "1.9.11",
|
||||||
"string-width": "~8.2.0",
|
"string-width": "~8.2.0",
|
||||||
"string.prototype.includes": "npm:@nolyfill/string.prototype.includes@^1.0.44",
|
"string.prototype.includes": "npm:@nolyfill/string.prototype.includes@^1.0.44",
|
||||||
@ -298,6 +299,7 @@
|
|||||||
"vite": "npm:@voidzero-dev/vite-plus-core@0.1.13",
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.1.13",
|
||||||
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.13",
|
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.13",
|
||||||
"which-typed-array": "npm:@nolyfill/which-typed-array@^1.0.44",
|
"which-typed-array": "npm:@nolyfill/which-typed-array@^1.0.44",
|
||||||
|
"yaml@>=2.0.0 <2.8.3": "2.8.3",
|
||||||
"yauzl@<3.2.1": "3.2.1"
|
"yauzl@<3.2.1": "3.2.1"
|
||||||
},
|
},
|
||||||
"ignoredBuiltDependencies": [
|
"ignoredBuiltDependencies": [
|
||||||
|
|||||||
434
web/pnpm-lock.yaml
generated
434
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,11 @@
|
|||||||
import type { NextRequest } from '@/next/server'
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
import { Buffer } from 'node:buffer'
|
import { Buffer } from 'node:buffer'
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
import { env } from '@/env'
|
import { env } from '@/env'
|
||||||
import { NextResponse } from '@/next/server'
|
|
||||||
|
|
||||||
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com https://api2.amplitude.com *.amplitude.com'
|
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://ungh.cc https://api2.amplitude.com *.amplitude.com'
|
||||||
|
|
||||||
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
|
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
|
||||||
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||||
|
|||||||
@ -2,12 +2,13 @@
|
|||||||
* Model provider quota types - shared type definitions for API responses
|
* Model provider quota types - shared type definitions for API responses
|
||||||
* These represent the provider identifiers that support paid/trial quotas
|
* These represent the provider identifiers that support paid/trial quotas
|
||||||
*/
|
*/
|
||||||
export enum ModelProviderQuotaGetPaid {
|
export const ModelProviderQuotaGetPaid = {
|
||||||
ANTHROPIC = 'langgenius/anthropic/anthropic',
|
ANTHROPIC: 'langgenius/anthropic/anthropic',
|
||||||
OPENAI = 'langgenius/openai/openai',
|
OPENAI: 'langgenius/openai/openai',
|
||||||
// AZURE_OPENAI = 'langgenius/azure_openai/azure_openai',
|
// AZURE_OPENAI: 'langgenius/azure_openai/azure_openai',
|
||||||
GEMINI = 'langgenius/gemini/google',
|
GEMINI: 'langgenius/gemini/google',
|
||||||
X = 'langgenius/x/x',
|
X: 'langgenius/x/x',
|
||||||
DEEPSEEK = 'langgenius/deepseek/deepseek',
|
DEEPSEEK: 'langgenius/deepseek/deepseek',
|
||||||
TONGYI = 'langgenius/tongyi/tongyi',
|
TONGYI: 'langgenius/tongyi/tongyi',
|
||||||
}
|
} as const
|
||||||
|
export type ModelProviderQuotaGetPaid = typeof ModelProviderQuotaGetPaid[keyof typeof ModelProviderQuotaGetPaid]
|
||||||
|
|||||||
@ -455,12 +455,13 @@ export type PanelProps = {
|
|||||||
export type NodeRunResult = NodeTracing
|
export type NodeRunResult = NodeTracing
|
||||||
|
|
||||||
// Var Inspect
|
// Var Inspect
|
||||||
export enum VarInInspectType {
|
export const VarInInspectType = {
|
||||||
conversation = 'conversation',
|
conversation: 'conversation',
|
||||||
environment = 'env',
|
environment: 'env',
|
||||||
node = 'node',
|
node: 'node',
|
||||||
system = 'sys',
|
system: 'sys',
|
||||||
}
|
} as const
|
||||||
|
export type VarInInspectType = typeof VarInInspectType[keyof typeof VarInInspectType]
|
||||||
|
|
||||||
export type FullContent = {
|
export type FullContent = {
|
||||||
size_bytes: number
|
size_bytes: number
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user