fix(e2e): align fixtures with generated contracts (#39394)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-07-22 13:09:23 +08:00 committed by GitHub
parent c5d471dbdd
commit 05a25943e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 972 additions and 780 deletions

View File

@ -246,7 +246,7 @@ class ModelConfigPartial(ResponseModel):
return to_timestamp(value)
class ModelConfig(ResponseModel):
class AppModelConfigResponse(ResponseModel):
opening_statement: str | None = None
suggested_questions: Any | None = Field(
default=None, validation_alias=AliasChoices("suggested_questions_list", "suggested_questions")
@ -419,7 +419,7 @@ class AppDetail(AppResponseModel):
icon_background: str | None = None
enable_site: bool
enable_api: bool
model_config_: ModelConfig | None = Field(
model_config_: AppModelConfigResponse | None = Field(
default=None,
validation_alias=AliasChoices("app_model_config", "model_config"),
alias="model_config",
@ -525,7 +525,13 @@ def _enrich_app_list_items(session: Session, *, apps: Sequence[App], tenant_id:
register_enum_models(console_ns, RetrievalMethod, WorkflowExecutionStatus, DatasetPermissionEnum)
register_response_schema_models(
console_ns, RedirectUrlResponse, SimpleResultResponse, AppImportResponse, AppTraceResponse
console_ns,
RedirectUrlResponse,
SimpleResultResponse,
AppImportResponse,
AppTraceResponse,
AppModelConfigResponse,
AppDetail,
)
register_schema_models(
@ -544,10 +550,8 @@ register_schema_models(
Tag,
WorkflowPartial,
ModelConfigPartial,
ModelConfig,
AppDetailSiteResponse,
DeletedTool,
AppDetail,
AppExportResponse,
Segmentation,
PreProcessingRule,

View File

@ -359,6 +359,12 @@ class WorkflowPublishResponse(ResponseModel):
created_at: int
class SyncDraftWorkflowResponse(ResponseModel):
result: str
hash: str
updated_at: int
class WorkflowRestoreResponse(ResponseModel):
result: str
hash: str
@ -441,6 +447,7 @@ register_response_schema_models(
WorkflowOnlineUsersByApp,
WorkflowOnlineUsersResponse,
WorkflowPublishResponse,
SyncDraftWorkflowResponse,
WorkflowRestoreResponse,
DefaultBlockConfigsResponse,
DefaultBlockConfigResponse,
@ -556,14 +563,7 @@ class DraftWorkflowApi(Resource):
@console_ns.response(
200,
"Draft workflow synced successfully",
console_ns.model(
"SyncDraftWorkflowResponse",
{
"result": fields.String,
"hash": fields.String,
"updated_at": fields.String,
},
),
console_ns.models[SyncDraftWorkflowResponse.__name__],
)
@console_ns.response(400, "Invalid workflow configuration")
@console_ns.response(403, "Permission denied")
@ -618,11 +618,14 @@ class DraftWorkflowApi(Resource):
except VariableError as e:
raise InvalidArgumentError(description=str(e))
return {
"result": "success",
"hash": workflow.unique_hash,
"updated_at": TimestampField().format(workflow.updated_at or workflow.created_at),
}
return dump_response(
SyncDraftWorkflowResponse,
{
"result": "success",
"hash": workflow.unique_hash,
"updated_at": TimestampField().format(workflow.updated_at or workflow.created_at),
},
)
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")

View File

@ -13278,7 +13278,7 @@ Model class for AI model.
| maintainer | string | | No |
| max_active_requests | integer | | No |
| mode | string | | Yes |
| model_config | [ModelConfig](#modelconfig) | | No |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No |
| name | string | | Yes |
| permission_keys | [ string ] | | No |
| role | string | | No |
@ -15348,7 +15348,6 @@ This class is used to store the schema information of an api based tool.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| app_model_config | [ModelConfig](#modelconfig) | | No |
| created_at | integer | | No |
| created_by | string | | No |
| description | string | | No |
@ -15358,7 +15357,8 @@ This class is used to store the schema information of an api based tool.
| icon_background | string | | No |
| id | string | | Yes |
| maintainer | string | | No |
| mode_compatible_with_agent | string | | Yes |
| mode | string | | Yes |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No |
| name | string | | Yes |
| permission_keys | [ string ] | | No |
| tags | [ [Tag](#tag) ] | | No |
@ -15420,7 +15420,7 @@ This class is used to store the schema information of an api based tool.
| maintainer | string | | No |
| max_active_requests | integer | | No |
| mode | string | | Yes |
| model_config | [ModelConfig](#modelconfig) | | No |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No |
| name | string | | Yes |
| permission_keys | [ string ] | | No |
| site | [AppDetailSiteResponse](#appdetailsiteresponse) | | No |
@ -15519,6 +15519,35 @@ AppMCPServer Status Enum
| ---- | ---- | ----------- | -------- |
| AppMCPServerStatus | string | AppMCPServer Status Enum | |
#### AppModelConfigResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_mode | | | No |
| annotation_reply | | | No |
| chat_prompt_config | | | No |
| completion_prompt_config | | | No |
| created_at | integer | | No |
| created_by | string | | No |
| dataset_configs | | | No |
| dataset_query_variable | string | | No |
| external_data_tools | | | No |
| file_upload | | | No |
| model | | | No |
| more_like_this | | | No |
| opening_statement | string | | No |
| pre_prompt | string | | No |
| prompt_type | string | | No |
| retriever_resource | | | No |
| sensitive_word_avoidance | | | No |
| speech_to_text | | | No |
| suggested_questions | | | No |
| suggested_questions_after_answer | | | No |
| text_to_speech | | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
| user_input_form | | | No |
#### AppNamePayload
| Name | Type | Description | Required |
@ -21954,9 +21983,9 @@ The subscription constructor of the trigger provider
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| hash | string | | No |
| result | string | | No |
| updated_at | string | | No |
| hash | string | | Yes |
| result | string | | Yes |
| updated_at | integer | | Yes |
#### SystemConfigurationResponse

View File

@ -212,6 +212,17 @@ def test_generate_specs_include_console_contract_shapes_for_schema_migration(tmp
assert {"type": "null"} in app_detail_nullable_schema["anyOf"]
assert schemas["RecommendedAppInfoResponse"]["properties"]["icon_url"]["readOnly"] is True
assert schemas["InstalledAppInfoResponse"]["properties"]["icon_url"]["readOnly"] is True
assert _response_schema(paths["/apps/{app_id}"]["get"])["$ref"] == "#/components/schemas/AppDetailWithSite"
app_model_config = schemas["AppDetailWithSite"]["properties"]["model_config"]
assert {"$ref": "#/components/schemas/AppModelConfigResponse"} in app_model_config["anyOf"]
app_detail = schemas["AppDetail"]
assert "mode" in app_detail["properties"]
assert "mode_compatible_with_agent" not in app_detail["properties"]
sync_draft_workflow = schemas["SyncDraftWorkflowResponse"]
assert _response_schema(paths["/apps/{app_id}/workflows/draft"]["post"])["$ref"] == (
"#/components/schemas/SyncDraftWorkflowResponse"
)
assert sync_draft_workflow["properties"]["updated_at"]["type"] == "integer"
tool_icon_schema = schemas["ExploreAppMetaResponse"]["properties"]["tool_icons"]["additionalProperties"]
assert {"type": "string"} in tool_icon_schema["anyOf"]
assert {"additionalProperties": True, "type": "object"} in tool_icon_schema["anyOf"]

View File

@ -1,12 +1,18 @@
import type {
AgentApiAccessResponse,
AgentApiStatusPayload,
ApiKeyItem,
} from '@dify/contracts/api/console/agent/types.gen'
import type {
ChatRequestPayloadWithUser,
PostChatMessagesResponse,
} from '@dify/contracts/api/service/types.gen'
import { createApiContext, expectApiResponseOK, setAppSiteEnabled } from '../../../support/api'
import {
zPostAgentByAgentIdApiEnableResponse,
zPostAgentByAgentIdApiKeysResponse,
} from '@dify/contracts/api/console/agent/zod.gen'
import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context'
import { setAppSiteEnabled } from '../../../support/api/web-apps'
import { getTestAgent } from './agent'
import { consumeServiceApiSse, SERVICE_API_STREAM_TIMEOUT_MS } from './service-api-sse'
@ -38,47 +44,40 @@ async function parseServiceApiChatResponse(response: Response) {
}
}
export async function setAgentSiteAccessAndGetURL(
agentId: string,
enabled: boolean,
): Promise<string> {
export async function setAgentSiteAccess(agentId: string, enabled: boolean): Promise<void> {
const agent = await getTestAgent(agentId)
const appId = agent.app_id ?? agent.backing_app_id
if (!appId) throw new Error(`Agent v2 ${agentId} does not expose a backing app ID.`)
const appDetail = await setAppSiteEnabled(appId, enabled)
const token = agent.site?.access_token ?? agent.site?.code ?? appDetail.site?.access_token
const baseURL = agent.site?.app_base_url ?? appDetail.site?.app_base_url
if (!token || !baseURL) throw new Error(`Agent v2 ${agentId} does not expose a Web App URL.`)
return `${baseURL.replace(/\/$/, '')}/agent/${token}`
await setAppSiteEnabled(appId, enabled)
}
export async function setAgentApiAccess(
agentId: string,
enabled: boolean,
): Promise<AgentApiAccessResponse> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const data = { enable_api: enabled } satisfies AgentApiStatusPayload
const response = await ctx.post(`/console/api/agent/${agentId}/api-enable`, {
data: { enable_api: enabled },
data,
})
await expectApiResponseOK(
response,
`${enabled ? 'Enable' : 'Disable'} Agent v2 API access for ${agentId}`,
)
return (await response.json()) as AgentApiAccessResponse
return zPostAgentByAgentIdApiEnableResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function createAgentApiKey(agentId: string): Promise<ApiKeyItem> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.post(`/console/api/agent/${agentId}/api-keys`)
await expectApiResponseOK(response, `Create Agent v2 API key for ${agentId}`)
return (await response.json()) as ApiKeyItem
return zPostAgentByAgentIdApiKeysResponse.parse(await response.json())
} finally {
await ctx.dispose()
}

View File

@ -2,10 +2,10 @@ import type {
AgentBuildDraftResponse,
AgentSoulConfig,
} from '@dify/contracts/api/console/agent/types.gen'
import { createApiContext, expectApiResponseOK } from '../../../support/api'
import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context'
export async function checkoutAgentBuildDraft(agentId: string): Promise<AgentBuildDraftResponse> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.post(`/console/api/agent/${agentId}/build-draft/checkout`, {
data: { force: true },
@ -21,7 +21,7 @@ export async function saveAgentBuildDraft(
agentId: string,
agentSoul: AgentSoulConfig,
): Promise<AgentBuildDraftResponse> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.put(`/console/api/agent/${agentId}/build-draft`, {
data: {
@ -38,7 +38,7 @@ export async function saveAgentBuildDraft(
}
export async function agentBuildDraftExists(agentId: string): Promise<boolean> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agentId}/build-draft`)
if (response.status() === 404) return false
@ -51,7 +51,7 @@ export async function agentBuildDraftExists(agentId: string): Promise<boolean> {
}
export async function getAgentBuildDraft(agentId: string): Promise<AgentBuildDraftResponse> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agentId}/build-draft`)
await expectApiResponseOK(response, `Get Agent v2 build draft for ${agentId}`)
@ -62,7 +62,7 @@ export async function getAgentBuildDraft(agentId: string): Promise<AgentBuildDra
}
export async function discardAgentBuildDraft(agentId: string): Promise<void> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.delete(`/console/api/agent/${agentId}/build-draft`)
await expectApiResponseOK(response, `Discard Agent v2 build draft for ${agentId}`)

View File

@ -10,7 +10,7 @@ import type {
import { Buffer } from 'node:buffer'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { createApiContext, expectApiResponseOK } from '../../../support/api'
import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context'
export type UploadedConsoleFile = {
id: string
@ -126,7 +126,7 @@ export async function uploadAgentDriveSkill({
fileName: string
filePath: string
}): Promise<AgentSkillUploadResponse> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const upload = await toSkillArchiveUpload({ fileName, filePath })
const response = await ctx.post(`/console/api/agent/${agentId}/skills/upload`, {
@ -154,7 +154,7 @@ export async function uploadAgentConfigFileToDraft({
fileName: string
filePath: string
}): Promise<AgentConfigFileRefConfig> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const uploadResponse = await ctx.post('/console/api/files/upload', {
multipart: {
@ -203,7 +203,7 @@ export async function uploadAgentConfigSkillToDraft({
fileName: string
filePath: string
}): Promise<AgentConfigSkillRefConfig> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const upload = await toSkillArchiveUpload({ fileName, filePath })
const response = await ctx.post(`/console/api/agent/${agentId}/config/skills/upload`, {
@ -236,7 +236,7 @@ export async function uploadAgentConfigSkillToDraft({
}
export async function getAgentDriveSkills(agentId: string): Promise<AgentDriveSkillItemResponse[]> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agentId}/drive/skills`)
await expectApiResponseOK(response, `Get Agent v2 drive skills for ${agentId}`)
@ -248,7 +248,7 @@ export async function getAgentDriveSkills(agentId: string): Promise<AgentDriveSk
}
export async function deleteAgentConfigFile(agentId: string, name: string): Promise<void> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.delete(
`/console/api/agent/${agentId}/config/files/${encodeURIComponent(name)}`,
@ -260,7 +260,7 @@ export async function deleteAgentConfigFile(agentId: string, name: string): Prom
}
export async function deleteAgentConfigSkill(agentId: string, name: string): Promise<void> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.delete(
`/console/api/agent/${agentId}/config/skills/${encodeURIComponent(name)}`,
@ -272,7 +272,7 @@ export async function deleteAgentConfigSkill(agentId: string, name: string): Pro
}
export async function deleteAgentDriveFile(agentId: string, key: string): Promise<void> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const searchParams = new URLSearchParams({ key })
const response = await ctx.delete(`/console/api/agent/${agentId}/files?${searchParams}`)

View File

@ -1,11 +1,16 @@
import type {
AgentAppComposerResponse,
AgentAppCreatePayload,
AgentAppDetailWithSite,
AgentReferencingWorkflowResponse,
AgentReferencingWorkflowsResponse,
AgentSoulConfig,
} from '@dify/contracts/api/console/agent/types.gen'
import { createApiContext, expectApiResponseOK } from '../../../support/api'
import {
zGetAgentByAgentIdResponse,
zPostAgentResponse,
} from '@dify/contracts/api/console/agent/zod.gen'
import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context'
import { assertE2EResourceName, createE2EResourceName } from '../../../support/naming'
import {
createPublishableAgentSoulConfig,
@ -13,21 +18,6 @@ import {
normalAgentSoulConfig,
} from './agent-soul'
export type AgentSeed = Pick<
AgentAppDetailWithSite,
| 'active_config_is_published'
| 'app_id'
| 'backing_app_id'
| 'description'
| 'enable_site'
| 'id'
| 'name'
| 'role'
| 'site'
> & {
active_config_snapshot_id?: string | null
}
export type CreateTestAgentOptions = {
description?: string
name?: string
@ -41,22 +31,23 @@ export async function createTestAgent({
description = 'Created by Dify E2E.',
name = createE2EResourceName('Agent'),
role = 'E2E test assistant',
}: CreateTestAgentOptions = {}): Promise<AgentSeed> {
}: CreateTestAgentOptions = {}): Promise<AgentAppDetailWithSite> {
assertE2EResourceName(name, 'Agent')
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const data = {
description,
icon: '🤖',
icon_background: '#FFEAD5',
icon_type: 'emoji',
name,
role,
} satisfies AgentAppCreatePayload
const response = await ctx.post('/console/api/agent', {
data: {
description,
icon: '🤖',
icon_background: '#FFEAD5',
icon_type: 'emoji',
name,
role,
},
data,
})
await expectApiResponseOK(response, 'Create Agent v2 test agent')
return (await response.json()) as AgentSeed
return zPostAgentResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
@ -68,25 +59,25 @@ export async function createConfiguredTestAgent({
}: {
agentSoul?: AgentSoulConfig
seed?: CreateTestAgentOptions
} = {}): Promise<AgentSeed> {
} = {}): Promise<AgentAppDetailWithSite> {
const agent = await createTestAgent(seed)
await saveAgentComposerDraft(agent.id, agentSoul)
return agent
}
export async function getTestAgent(agentId: string): Promise<AgentSeed> {
const ctx = await createApiContext()
export async function getTestAgent(agentId: string): Promise<AgentAppDetailWithSite> {
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agentId}`)
await expectApiResponseOK(response, `Get Agent v2 test agent ${agentId}`)
return (await response.json()) as AgentSeed
return zGetAgentByAgentIdResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function deleteTestAgent(agentId: string): Promise<void> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.delete(`/console/api/agent/${agentId}`)
await expectApiResponseOK(response, `Delete Agent v2 test agent ${agentId}`)
@ -99,7 +90,7 @@ export async function saveAgentComposerDraft(
agentId: string,
agentSoul: AgentSoulConfig = defaultAgentSoulConfig,
): Promise<AgentAppComposerResponse> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.put(`/console/api/agent/${agentId}/composer`, {
data: {
@ -118,7 +109,7 @@ export async function saveAgentComposerDraft(
export async function getAgentReferencingWorkflows(
agentId: string,
): Promise<AgentReferencingWorkflowResponse[]> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agentId}/referencing-workflows`)
await expectApiResponseOK(response, `Get Agent v2 referencing workflows for ${agentId}`)
@ -130,7 +121,7 @@ export async function getAgentReferencingWorkflows(
}
export async function getAgentComposerDraft(agentId: string): Promise<AgentAppComposerResponse> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agentId}/composer`)
await expectApiResponseOK(response, `Get Agent v2 composer draft for ${agentId}`)
@ -150,7 +141,7 @@ export async function ensureAgentComposerDraftIsPublishable(agentId: string): Pr
}
export async function publishAgent(agentId: string, versionNote = 'E2E publish'): Promise<void> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.post(`/console/api/agent/${agentId}/publish`, {
data: { version_note: versionNote },

View File

@ -1,7 +1,10 @@
import type { AgentReferencingWorkflowsResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { DifyWorld } from '../../../support/world'
import type { PreseededResource } from './common'
import { createApiContext, expectApiResponseOK } from '../../../../support/api'
import {
createConsoleApiContext,
expectApiResponseOK,
} from '../../../../support/api/console-context'
import { requirePreseededAgent, requirePreseededWorkflow } from './agents'
import { failFixturePrerequisite } from './common'
@ -14,7 +17,7 @@ export async function requirePreseededAgentWorkflowReference(
const workflow = await requirePreseededWorkflow(world, workflowName)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agent.id}/referencing-workflows`)
await expectApiResponseOK(response, `Check preseeded Agent workflow reference ${agentName}`)

View File

@ -4,7 +4,10 @@ import type {
} from '@dify/contracts/api/console/agent/types.gen'
import type { DifyWorld } from '../../../support/world'
import type { PreseededResource } from './common'
import { createApiContext, expectApiResponseOK } from '../../../../support/api'
import {
createConsoleApiContext,
expectApiResponseOK,
} from '../../../../support/api/console-context'
import {
agentBuilderExpectedTokens,
agentBuilderFixedInputs,
@ -124,7 +127,7 @@ export async function requirePreseededAgentDriveSkill(
): Promise<PreseededResource> {
const agent = await requirePreseededAgent(world, agentName)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agent.id}/drive/skills`)
await expectApiResponseOK(response, `Check preseeded Agent skill ${skillName}`)
@ -169,7 +172,7 @@ export async function requirePreseededFullConfigAgentCoreConfiguration(
agentBuilderPreseededResources.agentKnowledgeBase,
)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agent.id}/composer`)
await expectApiResponseOK(response, `Check preseeded Agent core configuration ${agentName}`)
@ -246,7 +249,7 @@ export async function requirePreseededToolStatesAgentConfiguration(
agentBuilderPreseededResources.tavilySearchTool,
)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agent.id}/composer`)
await expectApiResponseOK(response, `Check preseeded Agent tool states ${agentName}`)
@ -320,7 +323,7 @@ export async function requirePreseededDualRetrievalAgentConfiguration(
agentBuilderPreseededResources.agentKnowledgeBase,
)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/agent/${agent.id}/composer`)
await expectApiResponseOK(response, `Check preseeded Agent dual retrieval ${agentName}`)

View File

@ -1,5 +1,8 @@
import type { DifyWorld } from '../../../support/world'
import { createApiContext, expectApiResponseOK } from '../../../../support/api'
import {
createConsoleApiContext,
expectApiResponseOK,
} from '../../../../support/api/console-context'
export type PreseededResource = NonNullable<
DifyWorld['agentBuilder']['fixtures']['preseededResources'][string]
@ -45,7 +48,7 @@ export const findConsoleResourceByName = async <T extends NamedResource = NamedR
path: string
resourceName: string
}) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(path)
await expectApiResponseOK(response, action)

View File

@ -6,7 +6,10 @@ import type {
} from '@dify/contracts/api/console/datasets/types.gen'
import type { DifyWorld } from '../../../support/world'
import type { PreseededResource } from './common'
import { createApiContext, expectApiResponseOK } from '../../../../support/api'
import {
createConsoleApiContext,
expectApiResponseOK,
} from '../../../../support/api/console-context'
import {
agentBuilderExpectedTokens,
agentBuilderFixedInputs,
@ -34,7 +37,7 @@ export const getPreseededDataset = async (resourceName: string) => {
}
const getDatasetIndexingStatuses = async (datasetId: string, resourceName: string) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/datasets/${datasetId}/indexing-status`)
await expectApiResponseOK(response, `Check preseeded dataset indexing status ${resourceName}`)
@ -48,7 +51,7 @@ const getDatasetIndexingStatuses = async (datasetId: string, resourceName: strin
const getDatasetDocuments = async (datasetId: string, resourceName: string) => {
const documents: DocumentWithSegmentsListResponse['data'] = []
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
let page = 1
let hasMore = true
@ -76,7 +79,7 @@ const datasetHasEnabledSegmentContainingTokens = async (
expectedTokens: string[],
) => {
const documents = await getDatasetDocuments(datasetId, resourceName)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
for (const document of documents) {
const query = buildQuery({

View File

@ -3,7 +3,10 @@ import type {
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { DifyWorld } from '../../../support/world'
import { createApiContext, expectApiResponseOK } from '../../../../support/api'
import {
createConsoleApiContext,
expectApiResponseOK,
} from '../../../../support/api/console-context'
import { agentBuilderPreseededResources } from '../agent-builder-resources'
import { failFixturePrerequisite } from './common'
@ -82,7 +85,7 @@ async function requireAgentBuilderModel(
): Promise<NonNullable<DifyWorld['agentBuilder']['fixtures']['stableModel']>> {
if (!config.ok) return failFixturePrerequisite(world, config.reason)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(
`/console/api/workspaces/current/models/model-types/${config.type}`,
@ -132,7 +135,7 @@ export async function requireAgentBuilderStableChatModel(
export async function requireAgentBuilderSpeechToTextModel(
world: DifyWorld,
): Promise<NonNullable<DifyWorld['agentBuilder']['fixtures']['speechToTextModel']>> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
let defaultModel: NonNullable<DefaultModelDataResponse['data']>
try {

View File

@ -1,6 +1,9 @@
import type { DifyWorld } from '../../../support/world'
import type { LocalizedLabel, PreseededResource } from './common'
import { createApiContext, expectApiResponseOK } from '../../../../support/api'
import {
createConsoleApiContext,
expectApiResponseOK,
} from '../../../../support/api/console-context'
import { asRecord, asString, failFixturePrerequisite, matchesNameOrLabel } from './common'
type BuiltinToolProvider = {
@ -93,7 +96,7 @@ export async function requirePreseededTool(
const parsed = splitToolDisplayName(resourceName)
if (!parsed.ok) return failFixturePrerequisite(world, parsed.reason)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get('/console/api/workspaces/current/tools/builtin')
await expectApiResponseOK(response, `Check preseeded tool ${resourceName}`)

View File

@ -18,13 +18,9 @@ import type {
import type { SeedContext, SeedResource, SeedTask } from '../../../support/seed'
import type { UploadedConsoleFile } from './agent-drive'
import { readFile } from 'node:fs/promises'
import {
createApiContext,
createTestApp,
expectApiResponseOK,
publishWorkflowApp,
syncAgentV2WorkflowDraft,
} from '../../../support/api'
import { createTestApp } from '../../../support/api/apps'
import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context'
import { publishWorkflowApp } from '../../../support/api/workflows'
import { bootstrapMarketplacePlugins } from '../../../support/marketplace-plugins'
import { sleep } from '../../../support/process'
import { blocked, created, skipped, updated, verified } from '../../../support/seed'
@ -53,6 +49,7 @@ import {
} from './fixtures/common'
import { splitToolDisplayName } from './fixtures/tools'
import { agentBuilderTestMaterials, getAgentBuilderTestMaterialPath } from './test-materials'
import { syncAgentV2WorkflowDraft } from './workflow'
type StableModel = {
name: string
@ -127,7 +124,7 @@ const parseJsonEnv = (envName: string) => {
}
const findModel = async (config: StableModel, title: string) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(
`/console/api/workspaces/current/models/model-types/${config.type}`,
@ -156,7 +153,7 @@ const findModel = async (config: StableModel, title: string) => {
}
const resolveProvider = async (config: StableModel) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(
`/console/api/workspaces/current/model-providers?${buildQuery({ model_type: config.type })}`,
@ -178,7 +175,7 @@ const resolveProvider = async (config: StableModel) => {
}
const selectCustomProviderCredential = async (provider: string, credentialId?: string) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
if (credentialId) {
const switchResponse = await ctx.post(
@ -210,7 +207,7 @@ const upsertStableProviderCredential = async (
credentials: Record<string, unknown>,
credentialId?: string,
) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
if (credentialId) {
const updateResponse = await ctx.put(
@ -336,7 +333,7 @@ const seedAgentDecisionModel = async (context: SeedContext) =>
})
const getDefaultModel = async (modelType: string) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(
`/console/api/workspaces/current/default-model?${buildQuery({ model_type: modelType })}`,
@ -350,7 +347,7 @@ const getDefaultModel = async (modelType: string) => {
}
const setDefaultModel = async (model: StableModel) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.post('/console/api/workspaces/current/default-model', {
data: {
@ -431,7 +428,7 @@ const findBuiltinTool = async (displayName: string) => {
const parsed = splitToolDisplayName(displayName)
if (!parsed.ok) return { ok: false as const, reason: parsed.reason }
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get('/console/api/workspaces/current/tools/builtin')
await expectApiResponseOK(response, `Check built-in tool ${displayName}`)
@ -476,7 +473,7 @@ const uploadConsoleFile = async (
fileName: string,
filePath: string,
): Promise<UploadedConsoleFile> => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.post('/console/api/files/upload', {
multipart: {
@ -504,7 +501,7 @@ const findDataset = (name: string) => {
}
const getDatasetDocuments = async (datasetId: string) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(
`/console/api/datasets/${datasetId}/documents?${buildQuery({ limit: '100', page: '1' })}`,
@ -525,7 +522,7 @@ const requiredKnowledgeSegmentTokens = [
const datasetHasKnowledgeSegment = async (datasetId: string) => {
const documents = await getDatasetDocuments(datasetId)
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
for (const document of documents) {
const response = await ctx.get(
@ -563,7 +560,7 @@ const waitForDatasetCompleted = async (datasetId: string) => {
let status = 'missing'
while (Date.now() < deadline) {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/datasets/${datasetId}/indexing-status`)
await expectApiResponseOK(response, `Check dataset indexing ${datasetId}`)
@ -614,7 +611,7 @@ const addKnowledgeDocument = async (datasetId: string) => {
},
} satisfies KnowledgeConfig
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.post(`/console/api/datasets/${datasetId}/documents`, { data: body })
await expectApiResponseOK(response, `Seed knowledge document ${datasetId}`)
@ -624,7 +621,7 @@ const addKnowledgeDocument = async (datasetId: string) => {
}
const createDataset = async (name: string) => {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.post('/console/api/datasets', {
data: {

View File

@ -0,0 +1,72 @@
import type { SyncDraftWorkflowPayload } from '@dify/contracts/api/console/apps/types.gen'
import { zPostAppsByAppIdWorkflowsDraftResponse } from '@dify/contracts/api/console/apps/zod.gen'
import * as z from 'zod'
import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context'
import { getWorkflowDraft } from '../../../support/api/workflows'
const agentV2WorkflowNodeId = 'agent-v2'
const zWorkflowGraph = z.object({
nodes: z.array(
z.object({
data: z.record(z.string(), z.unknown()).optional(),
id: z.string(),
}),
),
})
export async function getAgentV2WorkflowNodeData(appId: string) {
const draft = await getWorkflowDraft(appId)
const graph = zWorkflowGraph.parse(draft.graph)
const agentNode = graph.nodes.find((node) => node.id === agentV2WorkflowNodeId)
if (!agentNode)
throw new Error(
`Workflow draft ${appId} does not include Agent v2 node ${agentV2WorkflowNodeId}.`,
)
return agentNode.data ?? {}
}
export async function syncAgentV2WorkflowDraft(appId: string, agentId: string): Promise<void> {
const ctx = await createConsoleApiContext()
try {
const data = {
graph: {
nodes: [
{
id: 'start',
type: 'custom',
position: { x: 80, y: 282 },
data: { id: 'start', type: 'start', title: 'Start', variables: [] },
},
{
id: 'agent-v2',
type: 'custom',
position: { x: 420, y: 282 },
data: {
id: 'agent-v2',
type: 'agent',
title: 'Agent',
desc: '',
agent_binding: {
binding_type: 'roster_agent',
agent_id: agentId,
},
agent_node_kind: 'dify_agent',
version: '2',
},
},
],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
features: {},
environment_variables: [],
conversation_variables: [],
} satisfies SyncDraftWorkflowPayload
const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { data })
await expectApiResponseOK(response, `Sync Agent v2 workflow draft for ${appId}`)
zPostAppsByAppIdWorkflowsDraftResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}

View File

@ -28,11 +28,11 @@ Feature: Agent v2 tools
Then the Agent v2 Backend service API response should include the JSON Replace E2E marker
@core
Scenario: Tool selector shows an empty state for a missing tool search
Scenario: Tool selector recovers after an unavailable installed-tool search
Given I am signed in as the default E2E admin
And a basic configured Agent v2 test agent has been created via API
When I open the Agent v2 configure page
And I search for the missing Agent v2 tool from the Tools selector
Then I should see the Agent v2 tool selector empty state
Then I should see the unavailable Agent v2 installed-tool search applied
When I clear the Agent v2 tool selector search
Then I should see the Agent v2 tool selector ready for another search

View File

@ -2,7 +2,7 @@ import type { DifyWorld } from '../../support/world'
import type { AccessSurfaceName } from './access-point-helpers'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { setAgentApiAccess, setAgentSiteAccessAndGetURL } from '../../agent-v2/support/access-point'
import { setAgentApiAccess, setAgentSiteAccess } from '../../agent-v2/support/access-point'
import { publishAgentWithPublishableDraft } from '../../agent-v2/support/agent'
import {
getAccessRegion,
@ -19,10 +19,7 @@ Given(
/^Agent v2 (Web app|Backend service API) access has been enabled via API$/,
async function (this: DifyWorld, surface: AccessSurfaceName) {
if (surface === 'Web app') {
this.agentBuilder.accessPoint.webAppURL = await setAgentSiteAccessAndGetURL(
getCurrentAgentId(this),
true,
)
await setAgentSiteAccess(getCurrentAgentId(this), true)
return
}

View File

@ -2,10 +2,11 @@ import type { DataTable } from '@cucumber/cucumber'
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
import type { AgentV2WorkflowOutputVariable, DifyWorld } from '../../support/world'
import { Then, When } from '@cucumber/cucumber'
import { zDeclaredOutputConfig } from '@dify/contracts/api/console/apps/zod.gen'
import { expect } from '@playwright/test'
import { getWorkflowDraft } from '../../../support/api'
import * as z from 'zod'
import { getAgentV2WorkflowNodeData } from '../../agent-v2/support/workflow'
const agentV2WorkflowNodeId = 'agent-v2'
const taskOutputName = 'e2e_report'
const renamedTaskOutputName = 'e2e_final_report'
@ -18,23 +19,12 @@ const getCurrentAppId = (world: DifyWorld) => {
return appId
}
const getAgentV2WorkflowNodeData = async (appId: string) => {
const draft = await getWorkflowDraft(appId)
const agentNode = draft.graph.nodes.find((node) => node.id === agentV2WorkflowNodeId)
if (!agentNode)
throw new Error(
`Workflow draft ${appId} does not include Agent v2 node ${agentV2WorkflowNodeId}.`,
)
return agentNode.data ?? {}
}
const parseDeclaredOutputs = (value: unknown): DeclaredOutputConfig[] =>
z.array(zDeclaredOutputConfig).optional().default([]).parse(value)
const getDeclaredOutputsFromDraft = async (appId: string): Promise<DeclaredOutputConfig[]> => {
const data = await getAgentV2WorkflowNodeData(appId)
const outputs = data.agent_declared_outputs
if (!Array.isArray(outputs)) return []
return outputs as DeclaredOutputConfig[]
return parseDeclaredOutputs(data.agent_declared_outputs)
}
const getOutputVariablesFromDraft = async (appId: string) => getDeclaredOutputsFromDraft(appId)
@ -310,9 +300,7 @@ async function expectAgentTaskOutputReference(
.poll(
async () => {
const data = await getAgentV2WorkflowNodeData(appId)
const outputs = Array.isArray(data.agent_declared_outputs)
? (data.agent_declared_outputs as DeclaredOutputConfig[])
: []
const outputs = parseDeclaredOutputs(data.agent_declared_outputs)
const expectedOutput = outputs.find((output) => output.name === expectedName)
return {

View File

@ -249,15 +249,16 @@ Then(
},
)
Then('I should see the Agent v2 tool selector empty state', async function (this: DifyWorld) {
const page = this.getPage()
Then(
'I should see the unavailable Agent v2 installed-tool search applied',
async function (this: DifyWorld) {
const page = this.getPage()
const search = getToolSelectorSearch(this)
await expect(page.getByText('No integrations were found')).toBeVisible({ timeout: 30_000 })
await expect(page.getByRole('link', { name: 'Requests to the community' })).toBeVisible()
await expect(
page.getByText(agentBuilderFixedInputs.missingToolSearchWithSuffix),
).not.toBeVisible()
})
await expect(search).toHaveValue(agentBuilderFixedInputs.missingToolSearchWithSuffix)
await expect(page.getByText('All tools', { exact: true })).not.toBeVisible()
},
)
Then(
'I should see the Agent v2 tool selector ready for another search',

View File

@ -1,7 +1,7 @@
import type { DifyWorld } from '../../support/world'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { createTestApp, syncAgentV2WorkflowDraft } from '../../../support/api'
import { createTestApp } from '../../../support/api/apps'
import { createE2EResourceName } from '../../../support/naming'
import { createConfiguredTestAgent, publishAgent } from '../../agent-v2/support/agent'
import {
@ -9,6 +9,7 @@ import {
normalAgentPrompt,
normalAgentSoulConfig,
} from '../../agent-v2/support/agent-soul'
import { syncAgentV2WorkflowDraft } from '../../agent-v2/support/workflow'
Given(
'a workflow app with an Agent v2 node has been created via API',

View File

@ -1,7 +1,7 @@
import type { DifyWorld } from '../../support/world'
import { Given, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { createTestApp } from '../../../support/api'
import { createTestApp } from '../../../support/api/apps'
import { createE2EResourceName } from '../../../support/naming'
Given('there is an existing E2E app available for testing', async function (this: DifyWorld) {

View File

@ -1,12 +1,9 @@
import type { DifyWorld } from '../../support/world'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import {
createTestApp,
enableAppSiteAndGetURL,
publishWorkflowApp,
syncRunnableWorkflowDraft,
} from '../../../support/api'
import { createTestApp } from '../../../support/api/apps'
import { enableAppSiteAndGetURL } from '../../../support/api/web-apps'
import { publishWorkflowApp, syncRunnableWorkflowDraft } from '../../../support/api/workflows'
import { createE2EResourceName } from '../../../support/naming'
Given('a workflow app has been published and shared via API', async function (this: DifyWorld) {

View File

@ -1,7 +1,7 @@
import type { DifyWorld } from '../../support/world'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { createTestApp } from '../../../support/api'
import { createTestApp } from '../../../support/api/apps'
import { createE2EResourceName } from '../../../support/naming'
Given(

View File

@ -1,13 +1,9 @@
import type { DifyWorld } from '../../support/world'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import {
createTestApp,
getAppSiteDetail,
getAppSiteURL,
publishWorkflowApp,
syncRunnableWorkflowDraft,
} from '../../../support/api'
import { createTestApp } from '../../../support/api/apps'
import { getAppSiteDetail, getAppSiteURL } from '../../../support/api/web-apps'
import { publishWorkflowApp, syncRunnableWorkflowDraft } from '../../../support/api/workflows'
import { createE2EResourceName } from '../../../support/naming'
import { baseURL, defaultLocale } from '../../../test-env'

View File

@ -1,7 +1,7 @@
import type { DifyWorld } from '../../support/world'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { syncRunnableWorkflowDraft } from '../../../support/api'
import { syncRunnableWorkflowDraft } from '../../../support/api/workflows'
Given('a minimal runnable workflow draft has been synced', async function (this: DifyWorld) {
const appId = this.createdAppIds.at(-1)

View File

@ -1,12 +1,15 @@
import type { DifyWorld } from '../../support/world'
import { Given, When } from '@cucumber/cucumber'
import { zCreateAppPayload } from '@dify/contracts/api/console/apps/zod.gen'
import { expect } from '@playwright/test'
import { createTestApp, syncMinimalWorkflowDraft } from '../../../support/api'
import { createTestApp } from '../../../support/api/apps'
import { syncMinimalWorkflowDraft } from '../../../support/api/workflows'
import { waitForAppsConsole } from '../../../support/apps'
import { createE2EResourceName } from '../../../support/naming'
Given('a {string} app has been created via API', async function (this: DifyWorld, mode: string) {
const app = await createTestApp(createE2EResourceName('App', mode), mode)
const appMode = zCreateAppPayload.shape.mode.parse(mode)
const app = await createTestApp(createE2EResourceName('App', appMode), appMode)
this.createdAppIds.push(app.id)
this.lastCreatedAppName = app.name
})

View File

@ -8,11 +8,11 @@ import { fileURLToPath } from 'node:url'
import { After, AfterAll, Before, setDefaultTimeout, Status } from '@cucumber/cucumber'
import { chromium, webkit } from '@playwright/test'
import { AUTH_BOOTSTRAP_TIMEOUT_MS, ensureAuthenticatedState } from '../../fixtures/auth'
import { deleteTestApp } from '../../support/api'
import { deleteTestApp } from '../../support/api/apps'
import { deleteTestDataset } from '../../support/api/datasets'
import { deleteBuiltinToolCredential } from '../../support/api/tools'
import { runCleanupTasks, shouldFailForCleanupErrors } from '../../support/cleanup'
import { deleteTestDataset } from '../../support/datasets'
import { getVoiceInputTestMaterialPath } from '../../support/test-materials'
import { deleteBuiltinToolCredential } from '../../support/tools'
import { baseURL, cucumberHeadless, cucumberSlowMo, e2eBrowser } from '../../test-env'
import { deleteTestAgent } from '../agent-v2/support/agent'
import {

View File

@ -1,270 +0,0 @@
import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen'
import type { APIResponse } from '@playwright/test'
import { readFile } from 'node:fs/promises'
import { zAppDetailWithSite } from '@dify/contracts/api/console/apps/zod.gen'
import { request } from '@playwright/test'
import { authStatePath } from '../fixtures/auth'
import { apiURL } from '../test-env'
import { assertE2EResourceName, createE2EResourceName } from './naming'
type StorageState = {
cookies: Array<{ name: string; value: string }>
}
export async function createApiContext() {
const state = JSON.parse(await readFile(authStatePath, 'utf8')) as StorageState
const csrfToken = state.cookies.find((c) => c.name.endsWith('csrf_token'))?.value ?? ''
return request.newContext({
baseURL: apiURL,
extraHTTPHeaders: { 'X-CSRF-Token': csrfToken },
storageState: authStatePath,
})
}
export async function expectApiResponseOK(response: APIResponse, action: string): Promise<void> {
if (response.ok()) return
const body = await response.text().catch(() => '')
throw new Error(`${action} failed with ${response.status()} ${response.statusText()}: ${body}`)
}
export type AppSeed = {
id: string
name: string
}
export type WorkflowDraft = {
graph: {
edges: Array<Record<string, unknown>>
nodes: Array<{
data?: Record<string, unknown>
id: string
type: string
}>
viewport?: Record<string, unknown>
}
}
export async function createTestApp(
name = createE2EResourceName('App'),
mode = 'workflow',
): Promise<AppSeed> {
assertE2EResourceName(name, 'App')
const ctx = await createApiContext()
try {
const response = await ctx.post('/console/api/apps', {
data: {
name,
mode,
icon_type: 'emoji',
icon: '🤖',
icon_background: '#FFEAD5',
},
})
await expectApiResponseOK(response, `Create ${mode} app ${name}`)
const body = (await response.json()) as AppSeed
return body
} finally {
await ctx.dispose()
}
}
export async function getWorkflowDraft(appId: string): Promise<WorkflowDraft> {
const ctx = await createApiContext()
try {
const response = await ctx.get(`/console/api/apps/${appId}/workflows/draft`)
await expectApiResponseOK(response, `Get workflow draft for ${appId}`)
return (await response.json()) as WorkflowDraft
} finally {
await ctx.dispose()
}
}
export async function syncMinimalWorkflowDraft(appId: string): Promise<void> {
const ctx = await createApiContext()
try {
const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, {
data: {
graph: {
nodes: [
{
id: '1',
type: 'custom',
position: { x: 80, y: 282 },
data: { id: '1', type: 'start', title: 'Start', variables: [] },
},
],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
features: {},
environment_variables: [],
conversation_variables: [],
},
})
await expectApiResponseOK(response, `Sync minimal workflow draft for ${appId}`)
} finally {
await ctx.dispose()
}
}
export async function syncAgentV2WorkflowDraft(appId: string, agentId: string): Promise<void> {
const ctx = await createApiContext()
try {
const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, {
data: {
graph: {
nodes: [
{
id: 'start',
type: 'custom',
position: { x: 80, y: 282 },
data: { id: 'start', type: 'start', title: 'Start', variables: [] },
},
{
id: 'agent-v2',
type: 'custom',
position: { x: 420, y: 282 },
data: {
id: 'agent-v2',
type: 'agent',
title: 'Agent',
desc: '',
agent_binding: {
binding_type: 'roster_agent',
agent_id: agentId,
},
agent_node_kind: 'dify_agent',
version: '2',
},
},
],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
features: {},
environment_variables: [],
conversation_variables: [],
},
})
await expectApiResponseOK(response, `Sync Agent v2 workflow draft for ${appId}`)
} finally {
await ctx.dispose()
}
}
export async function deleteTestApp(id: string): Promise<void> {
const ctx = await createApiContext()
try {
const response = await ctx.delete(`/console/api/apps/${id}`)
await expectApiResponseOK(response, `Delete app ${id}`)
} finally {
await ctx.dispose()
}
}
export async function syncRunnableWorkflowDraft(appId: string): Promise<void> {
const ctx = await createApiContext()
try {
const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, {
data: {
graph: {
nodes: [
{
id: 'start',
type: 'custom',
position: { x: 80, y: 282 },
data: { id: 'start', type: 'start', title: 'Start', variables: [] },
},
{
id: 'end',
type: 'custom',
position: { x: 480, y: 282 },
data: {
id: 'end',
type: 'end',
title: 'End',
outputs: [{ variable: 'result', value_selector: ['sys', 'workflow_run_id'] }],
},
},
],
edges: [
{
id: 'start-end',
type: 'custom',
source: 'start',
target: 'end',
sourceHandle: 'source',
targetHandle: 'target',
},
],
viewport: { x: 0, y: 0, zoom: 1 },
},
features: {},
environment_variables: [],
conversation_variables: [],
},
})
await expectApiResponseOK(response, `Sync runnable workflow draft for ${appId}`)
} finally {
await ctx.dispose()
}
}
export async function publishWorkflowApp(appId: string): Promise<void> {
const ctx = await createApiContext()
try {
const response = await ctx.post(`/console/api/apps/${appId}/workflows/publish`, {
data: { marked_name: '', marked_comment: '' },
})
await expectApiResponseOK(response, `Publish workflow app ${appId}`)
} finally {
await ctx.dispose()
}
}
export function getAppSiteURL({ mode, site }: AppDetailWithSite): string {
if (!site?.app_base_url || !site.access_token)
throw new Error('App detail does not include a Web App URL.')
const webAppMode = (() => {
if (mode === 'completion' || mode === 'workflow') return mode
if (mode === 'advanced-chat' || mode === 'agent-chat' || mode === 'chat') return 'chat'
throw new Error(`Unsupported Web App mode: ${mode}`)
})()
return `${site.app_base_url}/${webAppMode}/${site.access_token}`
}
export async function getAppSiteDetail(appId: string): Promise<AppDetailWithSite> {
const ctx = await createApiContext()
try {
const response = await ctx.get(`/console/api/apps/${appId}`)
await expectApiResponseOK(response, `Get app site detail for ${appId}`)
return zAppDetailWithSite.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function enableAppSiteAndGetURL(appId: string): Promise<string> {
return getAppSiteURL(await setAppSiteEnabled(appId, true))
}
export async function setAppSiteEnabled(
appId: string,
enabled: boolean,
): Promise<AppDetailWithSite> {
const ctx = await createApiContext()
try {
const enableResponse = await ctx.post(`/console/api/apps/${appId}/site-enable`, {
data: { enable_site: enabled },
})
await expectApiResponseOK(enableResponse, `${enabled ? 'Enable' : 'Disable'} app site ${appId}`)
} finally {
await ctx.dispose()
}
return getAppSiteDetail(appId)
}

36
e2e/support/api/apps.ts Normal file
View File

@ -0,0 +1,36 @@
import type { CreateAppPayload, PostAppsResponse } from '@dify/contracts/api/console/apps/types.gen'
import { zPostAppsResponse } from '@dify/contracts/api/console/apps/zod.gen'
import { assertE2EResourceName, createE2EResourceName } from '../naming'
import { createConsoleApiContext, expectApiResponseOK } from './console-context'
export async function createTestApp(
name = createE2EResourceName('App'),
mode: CreateAppPayload['mode'] = 'workflow',
): Promise<PostAppsResponse> {
assertE2EResourceName(name, 'App')
const ctx = await createConsoleApiContext()
try {
const data = {
name,
mode,
icon_type: 'emoji',
icon: '🤖',
icon_background: '#FFEAD5',
} satisfies CreateAppPayload
const response = await ctx.post('/console/api/apps', { data })
await expectApiResponseOK(response, `Create ${mode} app ${name}`)
return zPostAppsResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function deleteTestApp(id: string): Promise<void> {
const ctx = await createConsoleApiContext()
try {
const response = await ctx.delete(`/console/api/apps/${id}`)
await expectApiResponseOK(response, `Delete app ${id}`)
} finally {
await ctx.dispose()
}
}

View File

@ -0,0 +1,34 @@
import type { APIResponse } from '@playwright/test'
import { readFile } from 'node:fs/promises'
import { request } from '@playwright/test'
import * as z from 'zod'
import { authStatePath } from '../../fixtures/auth'
import { apiURL } from '../../test-env'
const zStorageState = z.object({
cookies: z.array(
z.object({
name: z.string(),
value: z.string(),
}),
),
})
export async function createConsoleApiContext() {
const state = zStorageState.parse(JSON.parse(await readFile(authStatePath, 'utf8')))
const csrfToken = state.cookies.find((cookie) => cookie.name.endsWith('csrf_token'))?.value
if (!csrfToken) throw new Error(`No CSRF token found in E2E auth state: ${authStatePath}`)
return request.newContext({
baseURL: apiURL,
extraHTTPHeaders: { 'X-CSRF-Token': csrfToken },
storageState: authStatePath,
})
}
export async function expectApiResponseOK(response: APIResponse, action: string): Promise<void> {
if (response.ok()) return
const body = await response.text().catch(() => '')
throw new Error(`${action} failed with ${response.status()} ${response.statusText()}: ${body}`)
}

View File

@ -1,7 +1,7 @@
import { createApiContext, expectApiResponseOK } from './api'
import { createConsoleApiContext, expectApiResponseOK } from './console-context'
export async function deleteTestDataset(datasetId: string): Promise<void> {
const ctx = await createApiContext()
const ctx = await createConsoleApiContext()
try {
const response = await ctx.delete(`/console/api/datasets/${datasetId}`)
await expectApiResponseOK(response, `Delete dataset ${datasetId}`)

View File

@ -0,0 +1,122 @@
import type {
GetWorkspacesCurrentPluginTasksByTaskIdResponse,
ParserLatest,
ParserPluginIdentifiers,
PostWorkspacesCurrentPluginInstallMarketplaceResponse,
PostWorkspacesCurrentPluginInstallPkgResponse,
PostWorkspacesCurrentPluginListInstallationsIdsResponse,
PostWorkspacesCurrentPluginListLatestVersionsResponse,
PostWorkspacesCurrentPluginUploadPkgResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { Buffer } from 'node:buffer'
import {
zGetWorkspacesCurrentPluginTasksByTaskIdResponse,
zPostWorkspacesCurrentPluginInstallMarketplaceResponse,
zPostWorkspacesCurrentPluginInstallPkgResponse,
zPostWorkspacesCurrentPluginListInstallationsIdsResponse,
zPostWorkspacesCurrentPluginListLatestVersionsResponse,
zPostWorkspacesCurrentPluginUploadPkgResponse,
} from '@dify/contracts/api/console/workspaces/zod.gen'
import { createConsoleApiContext, expectApiResponseOK } from './console-context'
export async function getLatestMarketplacePluginVersions(
pluginIds: string[],
): Promise<PostWorkspacesCurrentPluginListLatestVersionsResponse> {
const ctx = await createConsoleApiContext()
try {
const data = { plugin_ids: pluginIds } satisfies ParserLatest
const response = await ctx.post('/console/api/workspaces/current/plugin/list/latest-versions', {
data,
})
await expectApiResponseOK(response, 'Resolve latest marketplace plugin versions')
return zPostWorkspacesCurrentPluginListLatestVersionsResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function getInstalledMarketplacePlugins(
pluginIds: string[],
): Promise<PostWorkspacesCurrentPluginListInstallationsIdsResponse> {
const ctx = await createConsoleApiContext()
try {
const data = { plugin_ids: pluginIds } satisfies ParserLatest
const response = await ctx.post(
'/console/api/workspaces/current/plugin/list/installations/ids',
{ data },
)
await expectApiResponseOK(response, 'List installed marketplace plugins')
return zPostWorkspacesCurrentPluginListInstallationsIdsResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function getMarketplacePluginInstallTask(
taskId: string,
): Promise<GetWorkspacesCurrentPluginTasksByTaskIdResponse> {
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/workspaces/current/plugin/tasks/${taskId}`)
await expectApiResponseOK(response, `Fetch marketplace plugin install task ${taskId}`)
return zGetWorkspacesCurrentPluginTasksByTaskIdResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function startMarketplacePluginInstall(
pluginUniqueIdentifiers: string[],
): Promise<PostWorkspacesCurrentPluginInstallMarketplaceResponse> {
const ctx = await createConsoleApiContext()
try {
const data = {
plugin_unique_identifiers: pluginUniqueIdentifiers,
} satisfies ParserPluginIdentifiers
const response = await ctx.post('/console/api/workspaces/current/plugin/install/marketplace', {
data,
})
await expectApiResponseOK(response, 'Install marketplace plugins')
return zPostWorkspacesCurrentPluginInstallMarketplaceResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function uploadMarketplacePluginPackageFile(
pkg: Buffer,
fileName: string,
): Promise<PostWorkspacesCurrentPluginUploadPkgResponse> {
const ctx = await createConsoleApiContext()
try {
const response = await ctx.post('/console/api/workspaces/current/plugin/upload/pkg', {
multipart: {
pkg: {
buffer: pkg,
mimeType: 'application/octet-stream',
name: fileName,
},
},
})
await expectApiResponseOK(response, `Upload marketplace package ${fileName}`)
return zPostWorkspacesCurrentPluginUploadPkgResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function startUploadedPluginPackageInstall(
pluginUniqueIdentifiers: string[],
): Promise<PostWorkspacesCurrentPluginInstallPkgResponse> {
const ctx = await createConsoleApiContext()
try {
const data = {
plugin_unique_identifiers: pluginUniqueIdentifiers,
} satisfies ParserPluginIdentifiers
const response = await ctx.post('/console/api/workspaces/current/plugin/install/pkg', { data })
await expectApiResponseOK(response, 'Install uploaded plugin packages')
return zPostWorkspacesCurrentPluginInstallPkgResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}

24
e2e/support/api/tools.ts Normal file
View File

@ -0,0 +1,24 @@
import type { BuiltinToolCredentialDeletePayload } from '@dify/contracts/api/console/workspaces/types.gen'
import { zPostWorkspacesCurrentToolProviderBuiltinByProviderDeleteResponse } from '@dify/contracts/api/console/workspaces/zod.gen'
import { createConsoleApiContext, expectApiResponseOK } from './console-context'
export async function deleteBuiltinToolCredential(
provider: string,
credentialId: string,
): Promise<void> {
const ctx = await createConsoleApiContext()
try {
const data = { credential_id: credentialId } satisfies BuiltinToolCredentialDeletePayload
const response = await ctx.post(
`/console/api/workspaces/current/tool-provider/builtin/${provider}/delete`,
{ data },
)
await expectApiResponseOK(
response,
`Delete built-in tool credential ${credentialId} for ${provider}`,
)
zPostWorkspacesCurrentToolProviderBuiltinByProviderDeleteResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}

View File

@ -0,0 +1,47 @@
import type {
AppDetailWithSite,
AppSiteStatusPayload,
GetAppsByAppIdResponse,
} from '@dify/contracts/api/console/apps/types.gen'
import { zGetAppsByAppIdResponse } from '@dify/contracts/api/console/apps/zod.gen'
import { createConsoleApiContext, expectApiResponseOK } from './console-context'
export function getAppSiteURL({ mode, site }: AppDetailWithSite): string {
if (!site?.app_base_url || !site.access_token)
throw new Error('App detail does not include a Web App URL.')
const webAppMode = (() => {
if (mode === 'completion' || mode === 'workflow') return mode
if (mode === 'advanced-chat' || mode === 'agent-chat' || mode === 'chat') return 'chat'
throw new Error(`Unsupported Web App mode: ${mode}`)
})()
return `${site.app_base_url}/${webAppMode}/${site.access_token}`
}
export async function getAppSiteDetail(appId: string): Promise<GetAppsByAppIdResponse> {
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/apps/${appId}`)
await expectApiResponseOK(response, `Get app site detail for ${appId}`)
return zGetAppsByAppIdResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function enableAppSiteAndGetURL(appId: string): Promise<string> {
await setAppSiteEnabled(appId, true)
return getAppSiteURL(await getAppSiteDetail(appId))
}
export async function setAppSiteEnabled(appId: string, enabled: boolean): Promise<void> {
const ctx = await createConsoleApiContext()
try {
const data = { enable_site: enabled } satisfies AppSiteStatusPayload
const enableResponse = await ctx.post(`/console/api/apps/${appId}/site-enable`, { data })
await expectApiResponseOK(enableResponse, `${enabled ? 'Enable' : 'Disable'} app site ${appId}`)
} finally {
await ctx.dispose()
}
}

View File

@ -0,0 +1,115 @@
import type {
GetAppsByAppIdWorkflowsDraftResponse,
PublishWorkflowPayload,
SyncDraftWorkflowPayload,
} from '@dify/contracts/api/console/apps/types.gen'
import {
zGetAppsByAppIdWorkflowsDraftResponse,
zPostAppsByAppIdWorkflowsDraftResponse,
zPostAppsByAppIdWorkflowsPublishResponse,
} from '@dify/contracts/api/console/apps/zod.gen'
import { createConsoleApiContext, expectApiResponseOK } from './console-context'
export async function getWorkflowDraft(
appId: string,
): Promise<GetAppsByAppIdWorkflowsDraftResponse> {
const ctx = await createConsoleApiContext()
try {
const response = await ctx.get(`/console/api/apps/${appId}/workflows/draft`)
await expectApiResponseOK(response, `Get workflow draft for ${appId}`)
return zGetAppsByAppIdWorkflowsDraftResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function syncMinimalWorkflowDraft(appId: string): Promise<void> {
const ctx = await createConsoleApiContext()
try {
const data = {
graph: {
nodes: [
{
id: '1',
type: 'custom',
position: { x: 80, y: 282 },
data: { id: '1', type: 'start', title: 'Start', variables: [] },
},
],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
features: {},
environment_variables: [],
conversation_variables: [],
} satisfies SyncDraftWorkflowPayload
const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { data })
await expectApiResponseOK(response, `Sync minimal workflow draft for ${appId}`)
zPostAppsByAppIdWorkflowsDraftResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function syncRunnableWorkflowDraft(appId: string): Promise<void> {
const ctx = await createConsoleApiContext()
try {
const data = {
graph: {
nodes: [
{
id: 'start',
type: 'custom',
position: { x: 80, y: 282 },
data: { id: 'start', type: 'start', title: 'Start', variables: [] },
},
{
id: 'end',
type: 'custom',
position: { x: 480, y: 282 },
data: {
id: 'end',
type: 'end',
title: 'End',
outputs: [{ variable: 'result', value_selector: ['sys', 'workflow_run_id'] }],
},
},
],
edges: [
{
id: 'start-end',
type: 'custom',
source: 'start',
target: 'end',
sourceHandle: 'source',
targetHandle: 'target',
},
],
viewport: { x: 0, y: 0, zoom: 1 },
},
features: {},
environment_variables: [],
conversation_variables: [],
} satisfies SyncDraftWorkflowPayload
const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { data })
await expectApiResponseOK(response, `Sync runnable workflow draft for ${appId}`)
zPostAppsByAppIdWorkflowsDraftResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}
export async function publishWorkflowApp(appId: string): Promise<void> {
const ctx = await createConsoleApiContext()
try {
const data = {
marked_name: '',
marked_comment: '',
} satisfies PublishWorkflowPayload
const response = await ctx.post(`/console/api/apps/${appId}/workflows/publish`, { data })
await expectApiResponseOK(response, `Publish workflow app ${appId}`)
zPostAppsByAppIdWorkflowsPublishResponse.parse(await response.json())
} finally {
await ctx.dispose()
}
}

View File

@ -1,36 +1,17 @@
import type { PluginInstallTask } from '@dify/contracts/api/console/workspaces/types.gen'
import type { SeedContext, SeedResult } from './seed'
import { Buffer } from 'node:buffer'
import { createApiContext, expectApiResponseOK } from './api'
import {
getInstalledMarketplacePlugins,
getLatestMarketplacePluginVersions,
getMarketplacePluginInstallTask,
startMarketplacePluginInstall,
startUploadedPluginPackageInstall,
uploadMarketplacePluginPackageFile,
} from './api/marketplace-plugins'
import { sleep } from './process'
import { blocked, created, skipped, verified } from './seed'
type LatestPlugin = {
unique_identifier?: string
version?: string
}
type PluginInstallation = {
plugin_id: string
plugin_unique_identifier: string
}
type PluginInstallTask = {
id?: string
plugins?: Array<{
message?: string
plugin_id?: string
plugin_unique_identifier?: string
status?: string
}>
status?: string
}
type PluginInstallStartResponse = {
all_installed?: boolean
task?: PluginInstallTask | null
task_id?: string
}
type MarketplacePluginBootstrapConfig = {
defaultPluginIds: string[]
pluginIdsEnv: string
@ -56,45 +37,23 @@ const getPluginId = (pluginUniqueIdentifier: string) =>
const resolveLatestPluginIdentifiers = async (pluginIds: string[]) => {
if (pluginIds.length === 0) return { identifiers: [] as string[], missing: [] as string[] }
const ctx = await createApiContext()
try {
const response = await ctx.post('/console/api/workspaces/current/plugin/list/latest-versions', {
data: { plugin_ids: pluginIds },
})
await expectApiResponseOK(response, 'Resolve latest marketplace plugin versions')
const body = (await response.json()) as { versions?: Record<string, LatestPlugin | null> }
const identifiers: string[] = []
const missing: string[] = []
const body = await getLatestMarketplacePluginVersions(pluginIds)
const identifiers: string[] = []
const missing: string[] = []
for (const pluginId of pluginIds) {
const latest = body.versions?.[pluginId]
if (latest?.unique_identifier) identifiers.push(latest.unique_identifier)
else missing.push(pluginId)
}
return { identifiers, missing }
} finally {
await ctx.dispose()
for (const pluginId of pluginIds) {
const latest = body.versions[pluginId]
if (latest?.unique_identifier) identifiers.push(latest.unique_identifier)
else missing.push(pluginId)
}
return { identifiers, missing }
}
const listInstalledPlugins = async (pluginIds: string[]) => {
if (pluginIds.length === 0) return [] as PluginInstallation[]
if (pluginIds.length === 0) return []
const ctx = await createApiContext()
try {
const response = await ctx.post(
'/console/api/workspaces/current/plugin/list/installations/ids',
{
data: { plugin_ids: pluginIds },
},
)
await expectApiResponseOK(response, 'List installed marketplace plugins')
const body = (await response.json()) as { plugins?: PluginInstallation[] }
return body.plugins ?? []
} finally {
await ctx.dispose()
}
return (await getInstalledMarketplacePlugins(pluginIds)).plugins
}
const waitForPluginInstallTask = async (taskId: string, timeoutMs = 300_000) => {
@ -102,15 +61,7 @@ const waitForPluginInstallTask = async (taskId: string, timeoutMs = 300_000) =>
let lastTask: PluginInstallTask | undefined
while (Date.now() < deadline) {
const ctx = await createApiContext()
try {
const response = await ctx.get(`/console/api/workspaces/current/plugin/tasks/${taskId}`)
await expectApiResponseOK(response, `Fetch marketplace plugin install task ${taskId}`)
const body = (await response.json()) as { task?: PluginInstallTask }
lastTask = body.task
} finally {
await ctx.dispose()
}
lastTask = (await getMarketplacePluginInstallTask(taskId)).task
if (lastTask?.status === terminalSuccessTaskStatus) return { ok: true as const, task: lastTask }
@ -140,16 +91,7 @@ const waitForPluginInstallTask = async (taskId: string, timeoutMs = 300_000) =>
}
const installMarketplacePlugins = async (pluginUniqueIdentifiers: string[]) => {
const ctx = await createApiContext()
try {
const response = await ctx.post('/console/api/workspaces/current/plugin/install/marketplace', {
data: { plugin_unique_identifiers: pluginUniqueIdentifiers },
})
await expectApiResponseOK(response, 'Install marketplace plugins')
return (await response.json()) as PluginInstallStartResponse
} finally {
await ctx.dispose()
}
return startMarketplacePluginInstall(pluginUniqueIdentifiers)
}
const getMarketplaceDownloadUrl = (pluginUniqueIdentifier: string) => {
@ -174,44 +116,12 @@ const downloadMarketplacePluginPackage = async (pluginUniqueIdentifier: string)
const uploadMarketplacePluginPackage = async (pluginUniqueIdentifier: string) => {
const pkg = await downloadMarketplacePluginPackage(pluginUniqueIdentifier)
const ctx = await createApiContext()
try {
const response = await ctx.post('/console/api/workspaces/current/plugin/upload/pkg', {
multipart: {
pkg: {
buffer: pkg,
mimeType: 'application/octet-stream',
name: `${getPluginId(pluginUniqueIdentifier).replaceAll('/', '-')}.difypkg`,
},
},
})
await expectApiResponseOK(
response,
`Upload marketplace package ${getPluginId(pluginUniqueIdentifier)}`,
)
const body = (await response.json()) as { unique_identifier?: string }
if (!body.unique_identifier)
throw new Error(
`Upload marketplace package ${getPluginId(pluginUniqueIdentifier)} did not return a unique identifier.`,
)
return body.unique_identifier
} finally {
await ctx.dispose()
}
const fileName = `${getPluginId(pluginUniqueIdentifier).replaceAll('/', '-')}.difypkg`
return (await uploadMarketplacePluginPackageFile(pkg, fileName)).unique_identifier
}
const installLocalPluginPackages = async (pluginUniqueIdentifiers: string[]) => {
const ctx = await createApiContext()
try {
const response = await ctx.post('/console/api/workspaces/current/plugin/install/pkg', {
data: { plugin_unique_identifiers: pluginUniqueIdentifiers },
})
await expectApiResponseOK(response, 'Install uploaded plugin packages')
return (await response.json()) as PluginInstallStartResponse
} finally {
await ctx.dispose()
}
return startUploadedPluginPackageInstall(pluginUniqueIdentifiers)
}
const shouldFallbackToLocalPackageInstall = (error: string) =>

View File

@ -1,22 +0,0 @@
import { createApiContext, expectApiResponseOK } from './api'
export async function deleteBuiltinToolCredential(
provider: string,
credentialId: string,
): Promise<void> {
const ctx = await createApiContext()
try {
const response = await ctx.post(
`/console/api/workspaces/current/tool-provider/builtin/${provider}/delete`,
{
data: { credential_id: credentialId },
},
)
await expectApiResponseOK(
response,
`Delete built-in tool credential ${credentialId} for ${provider}`,
)
} finally {
await ctx.dispose()
}
}

View File

@ -46,7 +46,7 @@ export type AgentAppDetailWithSite = {
maintainer?: string | null
max_active_requests?: number | null
mode: string
model_config?: ModelConfig | null
model_config?: AppModelConfigResponse | null
name: string
permission_keys?: Array<string>
role?: string | null
@ -536,13 +536,31 @@ export type DeletedTool = {
type: string
}
export type ModelConfig = {
completion_params?: {
[key: string]: unknown
}
mode: LlmMode
name: string
provider: string
export type AppModelConfigResponse = {
agent_mode?: unknown | null
annotation_reply?: unknown | null
chat_prompt_config?: unknown | null
completion_prompt_config?: unknown | null
created_at?: number | null
created_by?: string | null
dataset_configs?: unknown | null
dataset_query_variable?: string | null
external_data_tools?: unknown | null
file_upload?: unknown | null
model?: unknown | null
more_like_this?: unknown | null
opening_statement?: string | null
pre_prompt?: string | null
prompt_type?: string | null
retriever_resource?: unknown | null
sensitive_word_avoidance?: unknown | null
speech_to_text?: unknown | null
suggested_questions?: unknown | null
suggested_questions_after_answer?: unknown | null
text_to_speech?: unknown | null
updated_at?: number | null
updated_by?: string | null
user_input_form?: unknown | null
}
export type AppDetailSiteResponse = {
@ -1095,8 +1113,6 @@ export type AgentAppPublishedReferenceResponse = {
app_name: string
}
export type LlmMode = 'chat' | 'completion'
export type AgentKind = 'dify_agent'
export type AgentPublishedReferenceResponse = {
@ -1894,7 +1910,7 @@ export type AgentAppDetailWithSiteWritable = {
maintainer?: string | null
max_active_requests?: number | null
mode: string
model_config?: ModelConfig | null
model_config?: AppModelConfigResponse | null
name: string
permission_keys?: Array<string>
role?: string | null

View File

@ -288,6 +288,36 @@ export const zDeletedTool = z.object({
type: z.string(),
})
/**
* AppModelConfigResponse
*/
export const zAppModelConfigResponse = z.object({
agent_mode: z.unknown().nullish(),
annotation_reply: z.unknown().nullish(),
chat_prompt_config: z.unknown().nullish(),
completion_prompt_config: z.unknown().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
dataset_configs: z.unknown().nullish(),
dataset_query_variable: z.string().nullish(),
external_data_tools: z.unknown().nullish(),
file_upload: z.unknown().nullish(),
model: z.unknown().nullish(),
more_like_this: z.unknown().nullish(),
opening_statement: z.string().nullish(),
pre_prompt: z.string().nullish(),
prompt_type: z.string().nullish(),
retriever_resource: z.unknown().nullish(),
sensitive_word_avoidance: z.unknown().nullish(),
speech_to_text: z.unknown().nullish(),
suggested_questions: z.unknown().nullish(),
suggested_questions_after_answer: z.unknown().nullish(),
text_to_speech: z.unknown().nullish(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
user_input_form: z.unknown().nullish(),
})
/**
* AppDetailSiteResponse
*/
@ -339,6 +369,47 @@ export const zWorkflowPartial = z.object({
updated_by: z.string().nullish(),
})
/**
* AgentAppDetailWithSite
*/
export const zAgentAppDetailWithSite = z.object({
access_mode: z.string().nullish(),
active_config_is_published: z.boolean().optional().default(false),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
backing_app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
debug_conversation_has_messages: z.boolean().optional().default(false),
debug_conversation_id: z.string().nullish(),
debug_conversation_message_count: z.int().optional().default(0),
deleted_tools: z.array(zDeletedTool).optional(),
description: z.string().nullish(),
enable_api: z.boolean(),
enable_site: z.boolean(),
hidden_app_backed: z.boolean().optional().default(false),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
icon_type: z.string().nullish(),
icon_url: z.string().nullable(),
id: z.string(),
maintainer: z.string().nullish(),
max_active_requests: z.int().nullish(),
mode: z.string(),
model_config: zAppModelConfigResponse.nullish(),
name: z.string(),
permission_keys: z.array(z.string()).optional(),
role: z.string().nullish(),
site: zAppDetailSiteResponse.nullish(),
tags: z.array(zTag).optional(),
tracing: z.unknown().nullish(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
use_icon_as_answer_icon: z.boolean().nullish(),
workflow: zWorkflowPartial.nullish(),
})
/**
* ComposerBindingPayload
*/
@ -1005,64 +1076,6 @@ export const zAgentAppPagination = z.object({
total: z.int(),
})
/**
* LLMMode
*
* Enum class for large language model mode.
*/
export const zLlmMode = z.enum(['chat', 'completion'])
/**
* ModelConfig
*/
export const zModelConfig = z.object({
completion_params: z.record(z.string(), z.unknown()).optional(),
mode: zLlmMode,
name: z.string(),
provider: z.string(),
})
/**
* AgentAppDetailWithSite
*/
export const zAgentAppDetailWithSite = z.object({
access_mode: z.string().nullish(),
active_config_is_published: z.boolean().optional().default(false),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
backing_app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
debug_conversation_has_messages: z.boolean().optional().default(false),
debug_conversation_id: z.string().nullish(),
debug_conversation_message_count: z.int().optional().default(0),
deleted_tools: z.array(zDeletedTool).optional(),
description: z.string().nullish(),
enable_api: z.boolean(),
enable_site: z.boolean(),
hidden_app_backed: z.boolean().optional().default(false),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
icon_type: z.string().nullish(),
icon_url: z.string().nullable(),
id: z.string(),
maintainer: z.string().nullish(),
max_active_requests: z.int().nullish(),
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
permission_keys: z.array(z.string()).optional(),
role: z.string().nullish(),
site: zAppDetailSiteResponse.nullish(),
tags: z.array(zTag).optional(),
tracing: z.unknown().nullish(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
use_icon_as_answer_icon: z.boolean().nullish(),
workflow: zWorkflowPartial.nullish(),
})
/**
* AgentKind
*
@ -2732,7 +2745,7 @@ export const zAgentAppDetailWithSiteWritable = z.object({
maintainer: z.string().nullish(),
max_active_requests: z.int().nullish(),
mode: z.string(),
model_config: zModelConfig.nullish(),
model_config: zAppModelConfigResponse.nullish(),
name: z.string(),
permission_keys: z.array(z.string()).optional(),
role: z.string().nullish(),

View File

@ -40,7 +40,7 @@ export type AppDetailWithSite = {
maintainer?: string | null
max_active_requests?: number | null
mode: string
model_config?: ModelConfig | null
model_config?: AppModelConfigResponse | null
name: string
permission_keys?: Array<string>
site?: AppDetailSiteResponse | null
@ -415,7 +415,6 @@ export type AppApiStatusPayload = {
export type AppDetail = {
access_mode?: string | null
app_model_config?: ModelConfig | null
created_at?: number | null
created_by?: string | null
description?: string | null
@ -425,7 +424,8 @@ export type AppDetail = {
icon_background?: string | null
id: string
maintainer?: string | null
mode_compatible_with_agent: string
mode: string
model_config?: AppModelConfigResponse | null
name: string
permission_keys?: Array<string>
tags?: Array<Tag>
@ -1020,9 +1020,9 @@ export type SyncDraftWorkflowPayload = {
}
export type SyncDraftWorkflowResponse = {
hash?: string
result?: string
updated_at?: string
hash: string
result: string
updated_at: number
}
export type WorkflowDraftVariableList = {
@ -1322,13 +1322,31 @@ export type DeletedTool = {
type: string
}
export type ModelConfig = {
completion_params?: {
[key: string]: unknown
}
mode: LlmMode
name: string
provider: string
export type AppModelConfigResponse = {
agent_mode?: unknown | null
annotation_reply?: unknown | null
chat_prompt_config?: unknown | null
completion_prompt_config?: unknown | null
created_at?: number | null
created_by?: string | null
dataset_configs?: unknown | null
dataset_query_variable?: string | null
external_data_tools?: unknown | null
file_upload?: unknown | null
model?: unknown | null
more_like_this?: unknown | null
opening_statement?: string | null
pre_prompt?: string | null
prompt_type?: string | null
retriever_resource?: unknown | null
sensitive_word_avoidance?: unknown | null
speech_to_text?: unknown | null
suggested_questions?: unknown | null
suggested_questions_after_answer?: unknown | null
text_to_speech?: unknown | null
updated_at?: number | null
updated_by?: string | null
user_input_form?: unknown | null
}
export type AppDetailSiteResponse = {
@ -1608,6 +1626,15 @@ export type FeedbackStat = {
like: number
}
export type ModelConfig = {
completion_params?: {
[key: string]: unknown
}
mode: LlmMode
name: string
provider: string
}
export type Conversation = {
admin_feedback_stats?: FeedbackStat | null
annotation?: ConversationAnnotation | null
@ -2196,8 +2223,6 @@ export type ModelConfigPartial = {
updated_by?: string | null
}
export type LlmMode = 'chat' | 'completion'
export type PluginDependencyType = 'github' | 'marketplace' | 'package'
export type Github = {
@ -2259,6 +2284,8 @@ export type StatusCount = {
success: number
}
export type LlmMode = 'chat' | 'completion'
export type SimpleMessageDetail = {
answer: string
inputs: {
@ -3075,7 +3102,7 @@ export type AppDetailWithSiteWritable = {
maintainer?: string | null
max_active_requests?: number | null
mode: string
model_config?: ModelConfig | null
model_config?: AppModelConfigResponse | null
name: string
permission_keys?: Array<string>
site?: AppDetailSiteResponseWritable | null

View File

@ -663,10 +663,13 @@ export const zSyncDraftWorkflowPayload = z.object({
hash: z.string().nullish(),
})
/**
* SyncDraftWorkflowResponse
*/
export const zSyncDraftWorkflowResponse = z.object({
hash: z.string().optional(),
result: z.string().optional(),
updated_at: z.string().optional(),
hash: z.string(),
result: z.string(),
updated_at: z.int(),
})
/**
@ -886,6 +889,36 @@ export const zDeletedTool = z.object({
type: z.string(),
})
/**
* AppModelConfigResponse
*/
export const zAppModelConfigResponse = z.object({
agent_mode: z.unknown().nullish(),
annotation_reply: z.unknown().nullish(),
chat_prompt_config: z.unknown().nullish(),
completion_prompt_config: z.unknown().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
dataset_configs: z.unknown().nullish(),
dataset_query_variable: z.string().nullish(),
external_data_tools: z.unknown().nullish(),
file_upload: z.unknown().nullish(),
model: z.unknown().nullish(),
more_like_this: z.unknown().nullish(),
opening_statement: z.string().nullish(),
pre_prompt: z.string().nullish(),
prompt_type: z.string().nullish(),
retriever_resource: z.unknown().nullish(),
sensitive_word_avoidance: z.unknown().nullish(),
speech_to_text: z.unknown().nullish(),
suggested_questions: z.unknown().nullish(),
suggested_questions_after_answer: z.unknown().nullish(),
text_to_speech: z.unknown().nullish(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
user_input_form: z.unknown().nullish(),
})
/**
* AppDetailSiteResponse
*/
@ -937,6 +970,66 @@ export const zWorkflowPartial = z.object({
updated_by: z.string().nullish(),
})
/**
* AppDetailWithSite
*/
export const zAppDetailWithSite = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
deleted_tools: z.array(zDeletedTool).optional(),
description: z.string().nullish(),
enable_api: z.boolean(),
enable_site: z.boolean(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
icon_type: z.string().nullish(),
icon_url: z.string().nullable(),
id: z.string(),
maintainer: z.string().nullish(),
max_active_requests: z.int().nullish(),
mode: z.string(),
model_config: zAppModelConfigResponse.nullish(),
name: z.string(),
permission_keys: z.array(z.string()).optional(),
site: zAppDetailSiteResponse.nullish(),
tags: z.array(zTag).optional(),
tracing: z.unknown().nullish(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
use_icon_as_answer_icon: z.boolean().nullish(),
workflow: zWorkflowPartial.nullish(),
})
/**
* AppDetail
*/
export const zAppDetail = z.object({
access_mode: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
description: z.string().nullish(),
enable_api: z.boolean(),
enable_site: z.boolean(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
id: z.string(),
maintainer: z.string().nullish(),
mode: z.string(),
model_config: zAppModelConfigResponse.nullish(),
name: z.string(),
permission_keys: z.array(z.string()).optional(),
tags: z.array(zTag).optional(),
tracing: z.unknown().nullish(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
use_icon_as_answer_icon: z.boolean().nullish(),
workflow: zWorkflowPartial.nullish(),
})
/**
* ImportStatus
*/
@ -2251,102 +2344,6 @@ export const zAppPagination = z.object({
total: z.int(),
})
/**
* LLMMode
*
* Enum class for large language model mode.
*/
export const zLlmMode = z.enum(['chat', 'completion'])
/**
* ModelConfig
*/
export const zModelConfig = z.object({
completion_params: z.record(z.string(), z.unknown()).optional(),
mode: zLlmMode,
name: z.string(),
provider: z.string(),
})
/**
* AppDetailWithSite
*/
export const zAppDetailWithSite = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
deleted_tools: z.array(zDeletedTool).optional(),
description: z.string().nullish(),
enable_api: z.boolean(),
enable_site: z.boolean(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
icon_type: z.string().nullish(),
icon_url: z.string().nullable(),
id: z.string(),
maintainer: z.string().nullish(),
max_active_requests: z.int().nullish(),
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
permission_keys: z.array(z.string()).optional(),
site: zAppDetailSiteResponse.nullish(),
tags: z.array(zTag).optional(),
tracing: z.unknown().nullish(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
use_icon_as_answer_icon: z.boolean().nullish(),
workflow: zWorkflowPartial.nullish(),
})
/**
* AppDetail
*/
export const zAppDetail = z.object({
access_mode: z.string().nullish(),
app_model_config: zModelConfig.nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
description: z.string().nullish(),
enable_api: z.boolean(),
enable_site: z.boolean(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
id: z.string(),
maintainer: z.string().nullish(),
mode_compatible_with_agent: z.string(),
name: z.string(),
permission_keys: z.array(z.string()).optional(),
tags: z.array(zTag).optional(),
tracing: z.unknown().nullish(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
use_icon_as_answer_icon: z.boolean().nullish(),
workflow: zWorkflowPartial.nullish(),
})
/**
* ConversationDetail
*/
export const zConversationDetail = z.object({
admin_feedback_stats: zFeedbackStat.nullish(),
annotated: z.boolean(),
created_at: z.int().nullish(),
from_account_id: z.string().nullish(),
from_end_user_id: z.string().nullish(),
from_source: z.string(),
id: z.string(),
introduction: z.string().nullish(),
message_count: z.int(),
model_config: zModelConfig.nullish(),
status: z.string(),
updated_at: z.int().nullish(),
user_feedback_stats: zFeedbackStat.nullish(),
})
/**
* PluginDependencyType
*/
@ -2537,6 +2534,42 @@ export const zConversationWithSummaryPagination = z.object({
total: z.int(),
})
/**
* LLMMode
*
* Enum class for large language model mode.
*/
export const zLlmMode = z.enum(['chat', 'completion'])
/**
* ModelConfig
*/
export const zModelConfig = z.object({
completion_params: z.record(z.string(), z.unknown()).optional(),
mode: zLlmMode,
name: z.string(),
provider: z.string(),
})
/**
* ConversationDetail
*/
export const zConversationDetail = z.object({
admin_feedback_stats: zFeedbackStat.nullish(),
annotated: z.boolean(),
created_at: z.int().nullish(),
from_account_id: z.string().nullish(),
from_end_user_id: z.string().nullish(),
from_source: z.string(),
id: z.string(),
introduction: z.string().nullish(),
message_count: z.int(),
model_config: zModelConfig.nullish(),
status: z.string(),
updated_at: z.int().nullish(),
user_feedback_stats: zFeedbackStat.nullish(),
})
/**
* SimpleMessageDetail
*/
@ -4234,7 +4267,7 @@ export const zAppDetailWithSiteWritable = z.object({
maintainer: z.string().nullish(),
max_active_requests: z.int().nullish(),
mode: z.string(),
model_config: zModelConfig.nullish(),
model_config: zAppModelConfigResponse.nullish(),
name: z.string(),
permission_keys: z.array(z.string()).optional(),
site: zAppDetailSiteResponseWritable.nullish(),