mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 16:36:28 +08:00
Merge branch 'main' into jzh
This commit is contained in:
@@ -137,6 +137,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
workflow=self._workflow,
|
||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
)
|
||||
else:
|
||||
inputs = self.application_generate_entity.inputs
|
||||
|
||||
@@ -106,6 +106,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
workflow=workflow,
|
||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
)
|
||||
else:
|
||||
inputs = self.application_generate_entity.inputs
|
||||
|
||||
@@ -92,6 +92,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
workflow=self._workflow,
|
||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
)
|
||||
else:
|
||||
inputs = self.application_generate_entity.inputs
|
||||
|
||||
@@ -164,6 +164,8 @@ class WorkflowBasedAppRunner:
|
||||
workflow: Workflow,
|
||||
single_iteration_run: Any | None = None,
|
||||
single_loop_run: Any | None = None,
|
||||
*,
|
||||
user_id: str,
|
||||
) -> tuple[Graph, VariablePool, GraphRuntimeState]:
|
||||
"""
|
||||
Prepare graph, variable pool, and runtime state for single node execution
|
||||
@@ -200,6 +202,7 @@ class WorkflowBasedAppRunner:
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
node_type_filter_key="iteration_id",
|
||||
node_type_label="iteration",
|
||||
user_id=user_id,
|
||||
)
|
||||
elif single_loop_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,
|
||||
node_type_filter_key="loop_id",
|
||||
node_type_label="loop",
|
||||
user_id=user_id,
|
||||
)
|
||||
else:
|
||||
raise ValueError("Neither single_iteration_run nor single_loop_run is specified")
|
||||
@@ -225,6 +229,8 @@ class WorkflowBasedAppRunner:
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
node_type_filter_key: str, # 'iteration_id' or 'loop_id'
|
||||
node_type_label: str = "node", # 'iteration' or 'loop' for error messages
|
||||
*,
|
||||
user_id: str = "",
|
||||
) -> tuple[Graph, VariablePool]:
|
||||
"""
|
||||
Get graph and variable pool for single node execution (iteration or loop).
|
||||
@@ -290,7 +296,7 @@ class WorkflowBasedAppRunner:
|
||||
run_context=build_dify_run_context(
|
||||
tenant_id=workflow.tenant_id,
|
||||
app_id=self._app_id,
|
||||
user_id="",
|
||||
user_id=user_id,
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
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.model_entities import ModelFeature, ModelType
|
||||
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 models import UploadFile
|
||||
from models.dataset import (
|
||||
@@ -1024,8 +1025,13 @@ class DatasetRetrieval:
|
||||
"""
|
||||
if not query and not attachment_ids:
|
||||
return
|
||||
created_by_role = self._resolve_creator_user_role(user_from)
|
||||
if created_by_role is None:
|
||||
created_by = parse_uuid_str_or_none(user_id)
|
||||
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
|
||||
dataset_queries = []
|
||||
for dataset_id in dataset_ids:
|
||||
@@ -1041,8 +1047,8 @@ class DatasetRetrieval:
|
||||
content=json.dumps(contents),
|
||||
source=DatasetQuerySource.APP,
|
||||
source_app_id=app_id,
|
||||
created_by_role=created_by_role,
|
||||
created_by=user_id,
|
||||
created_by_role=CreatorUserRole(user_from),
|
||||
created_by=created_by,
|
||||
)
|
||||
dataset_queries.append(dataset_query)
|
||||
if dataset_queries:
|
||||
|
||||
@@ -174,6 +174,18 @@ def normalize_uuid(value: str | UUID) -> str:
|
||||
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)]
|
||||
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestWorkflowBasedAppRunner:
|
||||
workflow = SimpleNamespace(environment_variables=[], graph_dict={})
|
||||
|
||||
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):
|
||||
runner = WorkflowBasedAppRunner(queue_manager=SimpleNamespace(), app_id="app")
|
||||
@@ -136,6 +136,7 @@ class TestWorkflowBasedAppRunner:
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
node_type_filter_key="iteration_id",
|
||||
node_type_label="iteration",
|
||||
user_id="00000000-0000-0000-0000-000000000001",
|
||||
)
|
||||
|
||||
assert graph is not None
|
||||
|
||||
@@ -100,6 +100,7 @@ def test_run_uses_single_node_execution_branch(
|
||||
workflow=workflow,
|
||||
single_iteration_run=single_iteration_run,
|
||||
single_loop_run=single_loop_run,
|
||||
user_id="user",
|
||||
)
|
||||
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,
|
||||
node_type_filter_key="loop_id",
|
||||
node_type_label="loop",
|
||||
user_id="00000000-0000-0000-0000-000000000001",
|
||||
)
|
||||
|
||||
assert seen_configs == [workflow.graph_dict["nodes"][0]]
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"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:
|
||||
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:
|
||||
|
||||
@@ -735,7 +736,7 @@ packages:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
picomatch: 4.0.4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
@@ -963,8 +964,8 @@ packages:
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@4.0.3:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
picomatch@4.0.4:
|
||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pirates@4.0.7:
|
||||
@@ -1829,9 +1830,9 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
fdir@6.5.0(picomatch@4.0.4):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
picomatch: 4.0.4
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
dependencies:
|
||||
@@ -2038,7 +2039,7 @@ snapshots:
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
picomatch@4.0.4: {}
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
@@ -2149,8 +2150,8 @@ snapshots:
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tinyrainbow@3.0.3: {}
|
||||
|
||||
@@ -2207,8 +2208,8 @@ snapshots:
|
||||
vite@7.3.1(@types/node@25.4.0):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.8
|
||||
rollup: 4.59.0
|
||||
tinyglobby: 0.2.15
|
||||
@@ -2230,7 +2231,7 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
picomatch: 4.0.4
|
||||
std-env: 3.10.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.0.2
|
||||
|
||||
@@ -51,8 +51,6 @@ NEXT_PUBLIC_ALLOW_EMBED=
|
||||
# Allow rendering unsafe URLs which have "data:" scheme.
|
||||
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.
|
||||
NEXT_PUBLIC_TOP_K_MAX_VALUE=10
|
||||
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
* upload handling, and task status polling. Verifies the complete plugin
|
||||
* installation pipeline from source discovery to completion.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
GITHUB_ACCESS_TOKEN: '',
|
||||
}))
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { checkForUpdates, fetchReleases, handleUpload } from '@/app/components/plugins/install-plugin/hooks'
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
@@ -30,10 +28,6 @@ vi.mock('@/service/plugins', () => ({
|
||||
checkTaskStatus: vi.fn(),
|
||||
}))
|
||||
|
||||
const { useGitHubReleases, useGitHubUpload } = await import(
|
||||
'@/app/components/plugins/install-plugin/hooks',
|
||||
)
|
||||
|
||||
describe('Plugin Installation Flow Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -44,22 +38,22 @@ describe('Plugin Installation Flow Integration', () => {
|
||||
it('fetches releases, checks for updates, and uploads the new version', async () => {
|
||||
const mockReleases = [
|
||||
{
|
||||
tag_name: 'v2.0.0',
|
||||
assets: [{ browser_download_url: 'https://github.com/test/v2.difypkg', name: 'plugin-v2.difypkg' }],
|
||||
tag: 'v2.0.0',
|
||||
assets: [{ downloadUrl: 'https://github.com/test/v2.difypkg' }],
|
||||
},
|
||||
{
|
||||
tag_name: 'v1.5.0',
|
||||
assets: [{ browser_download_url: 'https://github.com/test/v1.5.difypkg', name: 'plugin-v1.5.difypkg' }],
|
||||
tag: 'v1.5.0',
|
||||
assets: [{ downloadUrl: 'https://github.com/test/v1.5.difypkg' }],
|
||||
},
|
||||
{
|
||||
tag_name: 'v1.0.0',
|
||||
assets: [{ browser_download_url: 'https://github.com/test/v1.difypkg', name: 'plugin-v1.difypkg' }],
|
||||
tag: 'v1.0.0',
|
||||
assets: [{ downloadUrl: 'https://github.com/test/v1.difypkg' }],
|
||||
},
|
||||
]
|
||||
|
||||
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockReleases),
|
||||
json: () => Promise.resolve({ releases: mockReleases }),
|
||||
})
|
||||
|
||||
mockUploadGitHub.mockResolvedValue({
|
||||
@@ -67,8 +61,6 @@ describe('Plugin Installation Flow Integration', () => {
|
||||
unique_identifier: 'test-plugin:2.0.0',
|
||||
})
|
||||
|
||||
const { fetchReleases, checkForUpdates } = useGitHubReleases()
|
||||
|
||||
const releases = await fetchReleases('test-org', 'test-repo')
|
||||
expect(releases).toHaveLength(3)
|
||||
expect(releases[0].tag_name).toBe('v2.0.0')
|
||||
@@ -77,7 +69,6 @@ describe('Plugin Installation Flow Integration', () => {
|
||||
expect(needUpdate).toBe(true)
|
||||
expect(toastProps.message).toContain('v2.0.0')
|
||||
|
||||
const { handleUpload } = useGitHubUpload()
|
||||
const onSuccess = vi.fn()
|
||||
const result = await handleUpload(
|
||||
'https://github.com/test-org/test-repo',
|
||||
@@ -104,18 +95,16 @@ describe('Plugin Installation Flow Integration', () => {
|
||||
it('handles no new version available', async () => {
|
||||
const mockReleases = [
|
||||
{
|
||||
tag_name: 'v1.0.0',
|
||||
assets: [{ browser_download_url: 'https://github.com/test/v1.difypkg', name: 'plugin-v1.difypkg' }],
|
||||
tag: 'v1.0.0',
|
||||
assets: [{ downloadUrl: 'https://github.com/test/v1.difypkg' }],
|
||||
},
|
||||
]
|
||||
|
||||
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockReleases),
|
||||
json: () => Promise.resolve({ releases: mockReleases }),
|
||||
})
|
||||
|
||||
const { fetchReleases, checkForUpdates } = useGitHubReleases()
|
||||
|
||||
const releases = await fetchReleases('test-org', 'test-repo')
|
||||
const { needUpdate, toastProps } = checkForUpdates(releases, 'v1.0.0')
|
||||
|
||||
@@ -127,11 +116,9 @@ describe('Plugin Installation Flow Integration', () => {
|
||||
it('handles empty releases', async () => {
|
||||
;(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
json: () => Promise.resolve({ releases: [] }),
|
||||
})
|
||||
|
||||
const { fetchReleases, checkForUpdates } = useGitHubReleases()
|
||||
|
||||
const releases = await fetchReleases('test-org', 'test-repo')
|
||||
expect(releases).toHaveLength(0)
|
||||
|
||||
@@ -147,7 +134,6 @@ describe('Plugin Installation Flow Integration', () => {
|
||||
status: 404,
|
||||
})
|
||||
|
||||
const { fetchReleases } = useGitHubReleases()
|
||||
const releases = await fetchReleases('nonexistent-org', 'nonexistent-repo')
|
||||
|
||||
expect(releases).toEqual([])
|
||||
@@ -159,7 +145,6 @@ describe('Plugin Installation Flow Integration', () => {
|
||||
it('handles upload failure gracefully', async () => {
|
||||
mockUploadGitHub.mockRejectedValue(new Error('Upload failed'))
|
||||
|
||||
const { handleUpload } = useGitHubUpload()
|
||||
const onSuccess = vi.fn()
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import nock from 'nock'
|
||||
import * as React from 'react'
|
||||
import GithubStar from '../index'
|
||||
|
||||
const GITHUB_HOST = 'https://api.github.com'
|
||||
const GITHUB_PATH = '/repos/langgenius/dify'
|
||||
const GITHUB_STAR_URL = 'https://ungh.cc/repos/langgenius/dify'
|
||||
|
||||
const renderWithQueryClient = () => {
|
||||
const queryClient = new QueryClient({
|
||||
@@ -18,40 +15,66 @@ const renderWithQueryClient = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const mockGithubStar = (status: number, body: Record<string, unknown>, delayMs = 0) => {
|
||||
return nock(GITHUB_HOST).get(GITHUB_PATH).delay(delayMs).reply(status, body)
|
||||
const createJsonResponse = (body: Record<string, unknown>, status = 200) => {
|
||||
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', () => {
|
||||
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 () => {
|
||||
mockGithubStar(200, { stargazers_count: 123456 })
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
createJsonResponse({ repo: { stars: 123456 } }),
|
||||
)
|
||||
|
||||
renderWithQueryClient()
|
||||
|
||||
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 () => {
|
||||
mockGithubStar(500, {})
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
createJsonResponse({}, 500),
|
||||
)
|
||||
|
||||
renderWithQueryClient()
|
||||
|
||||
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 () => {
|
||||
mockGithubStar(200, { stargazers_count: 222222 }, 50)
|
||||
const deferred = createDeferred<Response>()
|
||||
vi.spyOn(globalThis, 'fetch').mockReturnValueOnce(deferred.promise)
|
||||
|
||||
const { container } = renderWithQueryClient()
|
||||
|
||||
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'
|
||||
import type { FC } from 'react'
|
||||
import type { GithubRepo } from '@/models/common'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { IS_DEV } from '@/config'
|
||||
|
||||
const defaultData = {
|
||||
stargazers_count: 110918,
|
||||
type GithubStarResponse = {
|
||||
repo: {
|
||||
stars: number
|
||||
}
|
||||
}
|
||||
|
||||
const defaultData: GithubStarResponse = {
|
||||
repo: { stars: 110918 },
|
||||
}
|
||||
|
||||
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)
|
||||
throw new Error('Failed to fetch github star')
|
||||
@@ -19,21 +22,20 @@ const getStar = async () => {
|
||||
}
|
||||
|
||||
const GithubStar: FC<{ className: string }> = (props) => {
|
||||
const { isFetching, isError, data } = useQuery<GithubRepo>({
|
||||
const { isFetching, isError, data } = useQuery<GithubStarResponse>({
|
||||
queryKey: ['github-star'],
|
||||
queryFn: getStar,
|
||||
enabled: !IS_DEV,
|
||||
retry: false,
|
||||
placeholderData: defaultData,
|
||||
})
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useGitHubReleases, useGitHubUpload } from '../hooks'
|
||||
import { checkForUpdates, fetchReleases, handleUpload } from '../hooks'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
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()
|
||||
vi.mock('@/service/plugins', () => ({
|
||||
uploadGitHub: (...args: unknown[]) => mockUploadGitHub(...args),
|
||||
@@ -37,17 +32,17 @@ describe('install-plugin/hooks', () => {
|
||||
it('fetches releases from GitHub API and formats them', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([
|
||||
{
|
||||
tag_name: 'v1.0.0',
|
||||
assets: [{ browser_download_url: 'https://example.com/v1.zip', name: 'plugin.zip' }],
|
||||
body: 'Release notes',
|
||||
},
|
||||
]),
|
||||
json: () => Promise.resolve({
|
||||
releases: [
|
||||
{
|
||||
tag: 'v1.0.0',
|
||||
assets: [{ downloadUrl: 'https://example.com/plugin.zip' }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
||||
const releases = await fetchReleases('owner', 'repo')
|
||||
|
||||
expect(releases).toHaveLength(1)
|
||||
expect(releases[0].tag_name).toBe('v1.0.0')
|
||||
@@ -60,8 +55,7 @@ describe('install-plugin/hooks', () => {
|
||||
ok: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
||||
const releases = await fetchReleases('owner', 'repo')
|
||||
|
||||
expect(releases).toEqual([])
|
||||
expect(mockNotify).toHaveBeenCalledWith('Failed to fetch repository releases')
|
||||
@@ -70,29 +64,26 @@ describe('install-plugin/hooks', () => {
|
||||
|
||||
describe('checkForUpdates', () => {
|
||||
it('detects newer version available', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = [
|
||||
{ tag_name: 'v1.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(toastProps.message).toContain('v2.0.0')
|
||||
})
|
||||
|
||||
it('returns no update when current is latest', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = [
|
||||
{ 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(toastProps.type).toBe('info')
|
||||
})
|
||||
|
||||
it('returns error for empty releases', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const { needUpdate, toastProps } = result.current.checkForUpdates([], 'v1.0.0')
|
||||
const { needUpdate, toastProps } = checkForUpdates([], 'v1.0.0')
|
||||
expect(needUpdate).toBe(false)
|
||||
expect(toastProps.type).toBe('error')
|
||||
expect(toastProps.message).toContain('empty')
|
||||
@@ -109,8 +100,7 @@ describe('install-plugin/hooks', () => {
|
||||
})
|
||||
const onSuccess = vi.fn()
|
||||
|
||||
const { result } = renderHook(() => useGitHubUpload())
|
||||
const pkg = await result.current.handleUpload(
|
||||
const pkg = await handleUpload(
|
||||
'https://github.com/owner/repo',
|
||||
'v1.0.0',
|
||||
'plugin.difypkg',
|
||||
@@ -132,9 +122,8 @@ describe('install-plugin/hooks', () => {
|
||||
it('shows toast on upload error', async () => {
|
||||
mockUploadGitHub.mockRejectedValue(new Error('Upload failed'))
|
||||
|
||||
const { result } = renderHook(() => useGitHubUpload())
|
||||
await expect(
|
||||
result.current.handleUpload('url', 'v1', 'pkg'),
|
||||
handleUpload('url', 'v1', 'pkg'),
|
||||
).rejects.toThrow('Upload failed')
|
||||
expect(mockNotify).toHaveBeenCalledWith('Error uploading package')
|
||||
})
|
||||
|
||||
@@ -1,101 +1,87 @@
|
||||
import type { GitHubRepoReleaseResponse } from '../types'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
||||
import { uploadGitHub } from '@/service/plugins'
|
||||
import { compareVersion, getLatestVersion } from '@/utils/semver'
|
||||
|
||||
const normalizeAssetName = (downloadUrl: string) => {
|
||||
const parts = downloadUrl.split('/')
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
const formatReleases = (releases: any) => {
|
||||
return releases.map((release: any) => ({
|
||||
tag_name: release.tag_name,
|
||||
tag_name: release.tag,
|
||||
assets: release.assets.map((asset: any) => ({
|
||||
browser_download_url: asset.browser_download_url,
|
||||
name: asset.name,
|
||||
browser_download_url: asset.downloadUrl,
|
||||
name: normalizeAssetName(asset.downloadUrl),
|
||||
})),
|
||||
}))
|
||||
}
|
||||
|
||||
export const useGitHubReleases = () => {
|
||||
const fetchReleases = async (owner: string, repo: string) => {
|
||||
try {
|
||||
if (!GITHUB_ACCESS_TOKEN) {
|
||||
// Fetch releases without authentication from client
|
||||
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`)
|
||||
if (!res.ok)
|
||||
throw new Error('Failed to fetch repository 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 []
|
||||
}
|
||||
export const fetchReleases = async (owner: string, repo: string) => {
|
||||
try {
|
||||
// Fetch releases without authentication from client
|
||||
const res = await fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`)
|
||||
if (!res.ok)
|
||||
throw new Error('Failed to fetch repository releases')
|
||||
const data = await res.json()
|
||||
return formatReleases(data.releases)
|
||||
}
|
||||
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) => {
|
||||
let needUpdate = false
|
||||
const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
|
||||
type: 'info',
|
||||
message: 'No new version available',
|
||||
}
|
||||
if (fetchedReleases.length === 0) {
|
||||
toastProps.type = 'error'
|
||||
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'
|
||||
}
|
||||
export const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
|
||||
let needUpdate = false
|
||||
const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
|
||||
type: 'info',
|
||||
message: 'No new version available',
|
||||
}
|
||||
if (fetchedReleases.length === 0) {
|
||||
toastProps.type = 'error'
|
||||
toastProps.message = 'Input releases is empty'
|
||||
return { needUpdate, toastProps }
|
||||
}
|
||||
|
||||
return { fetchReleases, checkForUpdates }
|
||||
}
|
||||
|
||||
export const useGitHubUpload = () => {
|
||||
const handleUpload = async (
|
||||
repoUrl: string,
|
||||
selectedVersion: string,
|
||||
selectedPackage: string,
|
||||
onSuccess?: (GitHubPackage: { manifest: any, unique_identifier: string }) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
||||
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
|
||||
}
|
||||
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 }
|
||||
}
|
||||
|
||||
export const handleUpload = async (
|
||||
repoUrl: string,
|
||||
selectedVersion: string,
|
||||
selectedPackage: string,
|
||||
onSuccess?: (GitHubPackage: { manifest: any, unique_identifier: string }) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
||||
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 }),
|
||||
}))
|
||||
|
||||
const mockFetchReleases = vi.fn()
|
||||
vi.mock('../../hooks', () => ({
|
||||
useGitHubReleases: () => ({ fetchReleases: mockFetchReleases }),
|
||||
const { mockFetchReleases } = vi.hoisted(() => ({
|
||||
mockFetchReleases: vi.fn(),
|
||||
}))
|
||||
vi.mock('../../hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
fetchReleases: mockFetchReleases,
|
||||
}
|
||||
})
|
||||
|
||||
const mockRefreshPluginList = vi.fn()
|
||||
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 { InstallStepFromGitHub } from '../../types'
|
||||
import Installed from '../base/installed'
|
||||
import { useGitHubReleases } from '../hooks'
|
||||
import { fetchReleases } from '../hooks'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
|
||||
@@ -31,7 +31,6 @@ type InstallFromGitHubProps = {
|
||||
const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, onClose, onSuccess }) => {
|
||||
const { t } = useTranslation()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const { fetchReleases } = useGitHubReleases()
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
|
||||
const {
|
||||
|
||||
@@ -5,11 +5,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../../types'
|
||||
import SelectPackage from '../selectPackage'
|
||||
|
||||
// Mock the useGitHubUpload hook
|
||||
const mockHandleUpload = vi.fn()
|
||||
vi.mock('../../../hooks', () => ({
|
||||
useGitHubUpload: () => ({ handleUpload: mockHandleUpload }),
|
||||
// Mock upload helper from hooks module
|
||||
const { mockHandleUpload } = vi.hoisted(() => ({
|
||||
mockHandleUpload: vi.fn(),
|
||||
}))
|
||||
vi.mock('../../../hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
handleUpload: mockHandleUpload,
|
||||
}
|
||||
})
|
||||
|
||||
// Factory functions
|
||||
const createMockManifest = (): PluginDeclaration => ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { useGitHubUpload } from '../../hooks'
|
||||
import { handleUpload } from '../../hooks'
|
||||
|
||||
const i18nPrefix = 'installFromGitHub'
|
||||
|
||||
@@ -43,7 +43,6 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const isEdit = Boolean(updatePayload)
|
||||
const [isUploading, setIsUploading] = React.useState(false)
|
||||
const { handleUpload } = useGitHubUpload()
|
||||
|
||||
const handleUploadPackage = async () => {
|
||||
if (isUploading)
|
||||
|
||||
@@ -103,12 +103,14 @@ vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
||||
}))
|
||||
|
||||
vi.mock('../../install-plugin/hooks', () => ({
|
||||
useGitHubReleases: () => ({
|
||||
vi.mock('../../install-plugin/hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../install-plugin/hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
checkForUpdates: mockCheckForUpdates,
|
||||
fetchReleases: mockFetchReleases,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// Auto upgrade settings mock
|
||||
let mockAutoUpgradeInfo: {
|
||||
|
||||
@@ -72,12 +72,14 @@ vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../install-plugin/hooks', () => ({
|
||||
useGitHubReleases: () => ({
|
||||
vi.mock('../../../../install-plugin/hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../../install-plugin/hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
checkForUpdates: mockCheckForUpdates,
|
||||
fetchReleases: mockFetchReleases,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
const createPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => ({
|
||||
id: 'test-id',
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import { useInvalidateCheckInstalled } from '@/service/use-plugins'
|
||||
import { useInvalidateAllToolProviders } from '@/service/use-tools'
|
||||
import { useGitHubReleases } from '../../../install-plugin/hooks'
|
||||
import { checkForUpdates, fetchReleases } from '../../../install-plugin/hooks'
|
||||
import { PluginCategoryEnum, PluginSource } from '../../../types'
|
||||
|
||||
type UsePluginOperationsParams = {
|
||||
@@ -39,7 +39,6 @@ export const usePluginOperations = ({
|
||||
onUpdate,
|
||||
}: UsePluginOperationsParams): UsePluginOperationsReturn => {
|
||||
const { t } = useTranslation()
|
||||
const { checkForUpdates, fetchReleases } = useGitHubReleases()
|
||||
const { setShowUpdatePluginModal } = useModalContext()
|
||||
const { refreshModelProviders } = useProviderContext()
|
||||
const invalidateCheckInstalled = useInvalidateCheckInstalled()
|
||||
|
||||
@@ -46,13 +46,15 @@ vi.mock('@/service/plugins', () => ({
|
||||
uninstallPlugin: (id: string) => mockUninstallPlugin(id),
|
||||
}))
|
||||
|
||||
// Mock GitHub releases hook
|
||||
vi.mock('../../install-plugin/hooks', () => ({
|
||||
useGitHubReleases: () => ({
|
||||
// Mock GitHub release helpers
|
||||
vi.mock('../../install-plugin/hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../install-plugin/hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
fetchReleases: mockFetchReleases,
|
||||
checkForUpdates: mockCheckForUpdates,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// Mock modal context
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import ActionButton from '../../base/action-button'
|
||||
import Confirm from '../../base/confirm'
|
||||
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 { PluginSource } from '../types'
|
||||
|
||||
@@ -54,7 +54,6 @@ const Action: FC<Props> = ({
|
||||
setTrue: showDeleting,
|
||||
setFalse: hideDeleting,
|
||||
}] = useBoolean(false)
|
||||
const { checkForUpdates, fetchReleases } = useGitHubReleases()
|
||||
const { setShowUpdatePluginModal } = useModalContext()
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ export type QAChunks = {
|
||||
|
||||
export type ChunkInfo = GeneralChunks | ParentChildChunks | QAChunks
|
||||
|
||||
export enum QAItemType {
|
||||
Question = 'question',
|
||||
Answer = 'answer',
|
||||
}
|
||||
export const QAItemType = {
|
||||
Question: 'question',
|
||||
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'
|
||||
|
||||
export enum TestRunStep {
|
||||
dataSource = 'dataSource',
|
||||
documentProcessing = 'documentProcessing',
|
||||
}
|
||||
export const TestRunStep = {
|
||||
dataSource: 'dataSource',
|
||||
documentProcessing: 'documentProcessing',
|
||||
} as const
|
||||
export type TestRunStep = typeof TestRunStep[keyof typeof TestRunStep]
|
||||
|
||||
export type DataSourceOption = {
|
||||
label: string
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('SelectionContextmenu', () => {
|
||||
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 = [
|
||||
createNode({ id: 'n1', selected: true, 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(() => {
|
||||
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 () => {
|
||||
@@ -165,7 +167,7 @@ describe('SelectionContextmenu', () => {
|
||||
const { store } = renderSelectionMenu({ nodes })
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
||||
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -190,7 +192,7 @@ describe('SelectionContextmenu', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 100, top: 100 } })
|
||||
store.setState({ selectionMenu: { clientX: 100, clientY: 100 } })
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||
@@ -220,7 +222,7 @@ describe('SelectionContextmenu', () => {
|
||||
const { store } = renderSelectionMenu({ nodes })
|
||||
|
||||
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')
|
||||
@@ -230,12 +232,12 @@ describe('SelectionContextmenu', () => {
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-copy'))
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
||||
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||
})
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-duplicate'))
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
||||
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||
})
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-delete'))
|
||||
|
||||
@@ -261,7 +263,7 @@ describe('SelectionContextmenu', () => {
|
||||
const { store } = renderSelectionMenu({ nodes, edges })
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 120, top: 120 } })
|
||||
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-createSnippet'))
|
||||
@@ -324,7 +326,7 @@ describe('SelectionContextmenu', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 160, top: 120 } })
|
||||
store.setState({ selectionMenu: { clientX: 160, clientY: 120 } })
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-distributeHorizontal'))
|
||||
@@ -363,7 +365,7 @@ describe('SelectionContextmenu', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 180, top: 120 } })
|
||||
store.setState({ selectionMenu: { clientX: 180, clientY: 120 } })
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||
@@ -382,7 +384,7 @@ describe('SelectionContextmenu', () => {
|
||||
const { store } = renderSelectionMenu({ nodes })
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 100, top: 100 } })
|
||||
store.setState({ selectionMenu: { clientX: 100, clientY: 100 } })
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||
@@ -400,7 +402,7 @@ describe('SelectionContextmenu', () => {
|
||||
const { store } = renderSelectionMenu({ nodes })
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 100, top: 100 } })
|
||||
store.setState({ selectionMenu: { clientX: 100, clientY: 100 } })
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||
@@ -425,7 +427,7 @@ describe('SelectionContextmenu', () => {
|
||||
const { store } = renderSelectionMenu({ nodes })
|
||||
|
||||
act(() => {
|
||||
store.setState({ selectionMenu: { left: 100, top: 100 } })
|
||||
store.setState({ selectionMenu: { clientX: 100, clientY: 100 } })
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('selection-contextmenu-item-left'))
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('usePanelInteractions', () => {
|
||||
const { result, store } = renderWorkflowHook(() => usePanelInteractions(), {
|
||||
initialStoreState: {
|
||||
nodeMenu: { top: 20, left: 40, nodeId: 'n1' },
|
||||
selectionMenu: { top: 30, left: 50 },
|
||||
selectionMenu: { clientX: 30, clientY: 50 },
|
||||
edgeMenu: { clientX: 320, clientY: 180, edgeId: 'e1' },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -200,8 +200,8 @@ describe('useSelectionInteractions', () => {
|
||||
})
|
||||
|
||||
expect(store.getState().selectionMenu).toEqual({
|
||||
top: 150,
|
||||
left: 200,
|
||||
clientX: 300,
|
||||
clientY: 200,
|
||||
})
|
||||
expect(store.getState().nodeMenu).toBeUndefined()
|
||||
expect(store.getState().panelMenu).toBeUndefined()
|
||||
@@ -210,7 +210,7 @@ describe('useSelectionInteractions', () => {
|
||||
|
||||
it('handleSelectionContextmenuCancel should clear selectionMenu', () => {
|
||||
const { result, store } = renderSelectionInteractions({
|
||||
selectionMenu: { top: 50, left: 60 },
|
||||
selectionMenu: { clientX: 50, clientY: 60 },
|
||||
})
|
||||
|
||||
act(() => {
|
||||
|
||||
@@ -137,15 +137,13 @@ export const useSelectionInteractions = () => {
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const { x, y } = container!.getBoundingClientRect()
|
||||
workflowStore.setState({
|
||||
nodeMenu: undefined,
|
||||
panelMenu: undefined,
|
||||
edgeMenu: undefined,
|
||||
selectionMenu: {
|
||||
top: e.clientY - y,
|
||||
left: e.clientX - x,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
},
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { CommonNodeType } from '../types'
|
||||
|
||||
export enum NoteTheme {
|
||||
blue = 'blue',
|
||||
cyan = 'cyan',
|
||||
green = 'green',
|
||||
yellow = 'yellow',
|
||||
pink = 'pink',
|
||||
violet = 'violet',
|
||||
}
|
||||
export const NoteTheme = {
|
||||
blue: 'blue',
|
||||
cyan: 'cyan',
|
||||
green: 'green',
|
||||
yellow: 'yellow',
|
||||
pink: 'pink',
|
||||
violet: 'violet',
|
||||
} as const
|
||||
export type NoteTheme = typeof NoteTheme[keyof typeof NoteTheme]
|
||||
|
||||
export type NoteNodeType = CommonNodeType & {
|
||||
text: string
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import type { CreateSnippetDialogPayload } from './create-snippet-dialog'
|
||||
import type { Edge, Node } from './types'
|
||||
import type { SnippetCanvasData } from '@/models/snippet'
|
||||
@@ -53,13 +52,6 @@ const AlignType = {
|
||||
|
||||
type AlignTypeValue = (typeof AlignType)[keyof typeof AlignType]
|
||||
|
||||
type SelectionMenuPosition = {
|
||||
left: number
|
||||
top: number
|
||||
}
|
||||
|
||||
type ContainerRect = Pick<DOMRect, 'width' | 'height'>
|
||||
|
||||
type AlignBounds = {
|
||||
minX: number
|
||||
maxX: number
|
||||
@@ -69,7 +61,7 @@ type AlignBounds = {
|
||||
|
||||
type AlignMenuItem = {
|
||||
alignType: AlignTypeValue
|
||||
icon: ComponentType<{ className?: string }>
|
||||
icon: string
|
||||
iconClassName?: string
|
||||
translationKey: string
|
||||
}
|
||||
@@ -86,39 +78,16 @@ const MENU_HEIGHT = 240
|
||||
const DEFAULT_SNIPPET_VIEWPORT: SnippetCanvasData['viewport'] = { x: 0, y: 0, zoom: 1 }
|
||||
|
||||
const alignMenuItems: AlignMenuItem[] = [
|
||||
{ alignType: AlignType.Left, icon: RiAlignItemLeftLine, translationKey: 'operator.alignLeft' },
|
||||
{ alignType: AlignType.Center, icon: RiAlignItemHorizontalCenterLine, translationKey: 'operator.alignCenter' },
|
||||
{ alignType: AlignType.Right, icon: RiAlignItemRightLine, translationKey: 'operator.alignRight' },
|
||||
{ alignType: AlignType.Top, icon: RiAlignItemTopLine, translationKey: 'operator.alignTop' },
|
||||
{ alignType: AlignType.Middle, icon: RiAlignItemVerticalCenterLine, iconClassName: 'rotate-90', translationKey: 'operator.alignMiddle' },
|
||||
{ alignType: AlignType.Bottom, icon: RiAlignItemBottomLine, translationKey: 'operator.alignBottom' },
|
||||
{ alignType: AlignType.DistributeHorizontal, icon: RiAlignJustify, translationKey: 'operator.distributeHorizontal' },
|
||||
{ alignType: AlignType.DistributeVertical, icon: RiAlignJustify, iconClassName: 'rotate-90', translationKey: 'operator.distributeVertical' },
|
||||
{ alignType: AlignType.Left, icon: 'i-ri-align-item-left-line', translationKey: 'operator.alignLeft' },
|
||||
{ alignType: AlignType.Center, icon: 'i-ri-align-item-horizontal-center-line', translationKey: 'operator.alignCenter' },
|
||||
{ alignType: AlignType.Right, icon: 'i-ri-align-item-right-line', translationKey: 'operator.alignRight' },
|
||||
{ alignType: AlignType.Top, icon: 'i-ri-align-item-top-line', translationKey: 'operator.alignTop' },
|
||||
{ alignType: AlignType.Middle, icon: 'i-ri-align-item-vertical-center-line', iconClassName: 'rotate-90', translationKey: 'operator.alignMiddle' },
|
||||
{ alignType: AlignType.Bottom, icon: 'i-ri-align-item-bottom-line', translationKey: 'operator.alignBottom' },
|
||||
{ alignType: AlignType.DistributeHorizontal, icon: 'i-ri-align-justify-line', translationKey: 'operator.distributeHorizontal' },
|
||||
{ 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 selectedNodeIds = new Set(selectedNodes.map(node => node.id))
|
||||
const childNodeIds = new Set<string>()
|
||||
@@ -356,29 +325,19 @@ const SelectionContextmenu = () => {
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
|
||||
const menuPosition = useMemo(() => {
|
||||
const container = document.querySelector('#workflow-container')
|
||||
return getMenuPosition(selectionMenu, container?.getBoundingClientRect())
|
||||
}, [selectionMenu])
|
||||
|
||||
const anchor = useMemo(() => {
|
||||
if (!selectionMenu)
|
||||
return null
|
||||
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const containerRect = container?.getBoundingClientRect()
|
||||
if (!containerRect)
|
||||
return null
|
||||
return undefined
|
||||
|
||||
return {
|
||||
getBoundingClientRect: () => DOMRect.fromRect({
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: containerRect.left + menuPosition.left,
|
||||
y: containerRect.top + menuPosition.top,
|
||||
x: selectionMenu.clientX,
|
||||
y: selectionMenu.clientY,
|
||||
}),
|
||||
}
|
||||
}, [menuPosition.left, menuPosition.top, selectionMenu])
|
||||
}, [selectionMenu])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionMenu && selectedNodes.length <= 1)
|
||||
@@ -574,7 +533,7 @@ const SelectionContextmenu = () => {
|
||||
}}
|
||||
>
|
||||
<ContextMenuContent
|
||||
positionerProps={{ anchor }}
|
||||
positionerProps={anchor ? { anchor } : undefined}
|
||||
popupClassName="w-[240px] py-0"
|
||||
>
|
||||
<div className="p-1">
|
||||
@@ -612,7 +571,7 @@ const SelectionContextmenu = () => {
|
||||
data-testid={`selection-contextmenu-item-${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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('createWorkflowStore', () => {
|
||||
['showInputsPanel', 'setShowInputsPanel', true],
|
||||
['showDebugAndPreviewPanel', 'setShowDebugAndPreviewPanel', true],
|
||||
['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' }],
|
||||
['showVariableInspectPanel', 'setShowVariableInspectPanel', true],
|
||||
['initShowLastRunTab', 'setInitShowLastRunTab', true],
|
||||
|
||||
@@ -16,8 +16,8 @@ export type PanelSliceShape = {
|
||||
}
|
||||
setPanelMenu: (panelMenu: PanelSliceShape['panelMenu']) => void
|
||||
selectionMenu?: {
|
||||
top: number
|
||||
left: number
|
||||
clientX: number
|
||||
clientY: number
|
||||
}
|
||||
setSelectionMenu: (selectionMenu: PanelSliceShape['selectionMenu']) => void
|
||||
edgeMenu?: {
|
||||
|
||||
@@ -174,7 +174,7 @@ const Right = ({
|
||||
{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
|
||||
variableCategory={currentNodeVar.nodeType as VarInInspectType}
|
||||
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 enum ViewMode {
|
||||
Code = 'code',
|
||||
Preview = 'preview',
|
||||
}
|
||||
export const ViewMode = {
|
||||
Code: 'code',
|
||||
Preview: 'preview',
|
||||
} as const
|
||||
export type ViewMode = typeof ViewMode[keyof typeof ViewMode]
|
||||
|
||||
export enum PreviewType {
|
||||
Markdown = 'markdown',
|
||||
Chunks = 'chunks',
|
||||
}
|
||||
export const PreviewType = {
|
||||
Markdown: 'markdown',
|
||||
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 GITHUB_ACCESS_TOKEN
|
||||
= env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN
|
||||
|
||||
export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl'
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
### Example Structure
|
||||
|
||||
@@ -66,10 +66,6 @@ const clientSchema = {
|
||||
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: coercedBoolean.default(true),
|
||||
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: coercedBoolean.default(true),
|
||||
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
|
||||
*/
|
||||
@@ -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_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_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_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'),
|
||||
|
||||
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 hyoban from 'eslint-plugin-hyoban'
|
||||
import markdownPreferences from 'eslint-plugin-markdown-preferences'
|
||||
import noBarrelFiles from 'eslint-plugin-no-barrel-files'
|
||||
import { reactRefresh } from 'eslint-plugin-react-refresh'
|
||||
import sonar from 'eslint-plugin-sonarjs'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
@@ -30,12 +31,17 @@ const plugins = pluginReact.configs.all.plugins
|
||||
export default antfu(
|
||||
{
|
||||
react: false,
|
||||
nextjs: true,
|
||||
nextjs: {
|
||||
overrides: {
|
||||
'next/no-img-element': 'off',
|
||||
},
|
||||
},
|
||||
ignores: ['public', 'types/doc-paths.ts', 'eslint-suppressions.json'],
|
||||
typescript: {
|
||||
overrides: {
|
||||
'ts/consistent-type-definitions': ['error', 'type'],
|
||||
'ts/no-explicit-any': 'error',
|
||||
'ts/no-redeclare': 'off',
|
||||
},
|
||||
erasableOnly: true,
|
||||
},
|
||||
@@ -66,12 +72,23 @@ export default antfu(
|
||||
...pluginReact.configs['recommended-typescript'].rules,
|
||||
'react/prefer-namespace-import': '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'],
|
||||
rules: {
|
||||
'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(),
|
||||
@@ -98,7 +115,6 @@ export default antfu(
|
||||
{
|
||||
rules: {
|
||||
'node/prefer-global/process': 'off',
|
||||
'next/no-img-element': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -160,7 +176,7 @@ export default antfu(
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/package.json'],
|
||||
files: ['package.json'],
|
||||
rules: {
|
||||
'hyoban/no-dependency-version-prefix': 'error',
|
||||
},
|
||||
|
||||
@@ -220,10 +220,6 @@ export type DataSources = {
|
||||
sources: DataSourceItem[]
|
||||
}
|
||||
|
||||
export type GithubRepo = {
|
||||
stargazers_count: number
|
||||
}
|
||||
|
||||
export type PluginProvider = {
|
||||
tool_name: string
|
||||
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/utils": "0.42.0",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@octokit/core": "7.0.6",
|
||||
"@octokit/request-error": "7.1.0",
|
||||
"@orpc/client": "1.13.9",
|
||||
"@orpc/contract": "1.13.9",
|
||||
"@orpc/openapi-client": "1.13.9",
|
||||
@@ -217,6 +215,7 @@
|
||||
"eslint-plugin-better-tailwindcss": "4.3.2",
|
||||
"eslint-plugin-hyoban": "0.14.1",
|
||||
"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-refresh": "0.5.2",
|
||||
"eslint-plugin-sonarjs": "4.0.2",
|
||||
@@ -228,7 +227,6 @@
|
||||
"jsdom-testing-mocks": "1.16.0",
|
||||
"knip": "6.0.2",
|
||||
"lint-staged": "16.4.0",
|
||||
"nock": "14.0.11",
|
||||
"postcss": "8.5.8",
|
||||
"postcss-js": "5.1.0",
|
||||
"react-server-dom-webpack": "19.2.4",
|
||||
@@ -278,6 +276,8 @@
|
||||
"object.values": "npm:@nolyfill/object.values@^1.0.44",
|
||||
"pbkdf2": "~3.1.5",
|
||||
"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.0": "1.30.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",
|
||||
"safer-buffer": "npm:@nolyfill/safer-buffer@^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",
|
||||
"string-width": "~8.2.0",
|
||||
"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",
|
||||
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.13",
|
||||
"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"
|
||||
},
|
||||
"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'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NextResponse } from 'next/server'
|
||||
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) => {
|
||||
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
* Model provider quota types - shared type definitions for API responses
|
||||
* These represent the provider identifiers that support paid/trial quotas
|
||||
*/
|
||||
export enum ModelProviderQuotaGetPaid {
|
||||
ANTHROPIC = 'langgenius/anthropic/anthropic',
|
||||
OPENAI = 'langgenius/openai/openai',
|
||||
// AZURE_OPENAI = 'langgenius/azure_openai/azure_openai',
|
||||
GEMINI = 'langgenius/gemini/google',
|
||||
X = 'langgenius/x/x',
|
||||
DEEPSEEK = 'langgenius/deepseek/deepseek',
|
||||
TONGYI = 'langgenius/tongyi/tongyi',
|
||||
}
|
||||
export const ModelProviderQuotaGetPaid = {
|
||||
ANTHROPIC: 'langgenius/anthropic/anthropic',
|
||||
OPENAI: 'langgenius/openai/openai',
|
||||
// AZURE_OPENAI: 'langgenius/azure_openai/azure_openai',
|
||||
GEMINI: 'langgenius/gemini/google',
|
||||
X: 'langgenius/x/x',
|
||||
DEEPSEEK: 'langgenius/deepseek/deepseek',
|
||||
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
|
||||
|
||||
// Var Inspect
|
||||
export enum VarInInspectType {
|
||||
conversation = 'conversation',
|
||||
environment = 'env',
|
||||
node = 'node',
|
||||
system = 'sys',
|
||||
}
|
||||
export const VarInInspectType = {
|
||||
conversation: 'conversation',
|
||||
environment: 'env',
|
||||
node: 'node',
|
||||
system: 'sys',
|
||||
} as const
|
||||
export type VarInInspectType = typeof VarInInspectType[keyof typeof VarInInspectType]
|
||||
|
||||
export type FullContent = {
|
||||
size_bytes: number
|
||||
|
||||
Reference in New Issue
Block a user