dify/cli/src/commands/get/app/run.ts
Xiyuan Chen 1502a57381
feat(api,cli): strict UUID validation for app-id and workspace-id (#37212)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-09 07:35:18 +00:00

171 lines
5.4 KiB
TypeScript

import type { AppDescribeResponse, AppListResponse, AppMode } from '@dify/contracts/api/openapi/types.gen'
import type { ActiveContext } from '@/auth/hosts'
import type { HttpClient } from '@/http/types'
import type { IOStreams } from '@/sys/io/streams'
import { AppsClient } from '@/api/apps'
import { WorkspacesClient } from '@/api/workspaces'
import { LIMIT_DEFAULT, parseLimit } from '@/limit/limit'
import { getEnv } from '@/sys/index'
import { runWithSpinner } from '@/sys/io/spinner'
import { nullStreams } from '@/sys/io/streams'
import { resolveWorkspaceId } from '@/workspace/resolver'
import { AppListOutput, AppRow } from './handlers.js'
export type GetAppOptions = {
readonly appId?: string
readonly workspace?: string
readonly allWorkspaces?: boolean
readonly page?: number
readonly limitRaw?: string
readonly mode?: string
readonly name?: string
readonly tag?: string
readonly format?: string
}
export type GetAppDeps = {
readonly active: ActiveContext
readonly http: HttpClient
readonly io?: IOStreams
readonly envLookup?: (k: string) => string | undefined
readonly appsFactory?: (http: HttpClient) => AppsClient
readonly workspacesFactory?: (http: HttpClient) => WorkspacesClient
}
const ALL_WORKSPACES_CONCURRENCY = 4
export type GetAppResult = {
readonly data: AppListOutput
}
export async function runGetApp(opts: GetAppOptions, deps: GetAppDeps): Promise<GetAppResult> {
const env = deps.envLookup ?? getEnv
const appsFactory = deps.appsFactory ?? ((h: HttpClient) => new AppsClient(h))
const wsFactory = deps.workspacesFactory ?? ((h: HttpClient) => new WorkspacesClient(h))
const apps = appsFactory(deps.http)
const pageSize = resolveLimit(opts.limitRaw, env)
const page = opts.page === undefined || opts.page <= 0 ? 1 : opts.page
const label = opts.appId !== undefined && opts.appId !== '' ? 'Fetching app' : 'Fetching apps'
const io = deps.io ?? nullStreams()
const envelope = await runWithSpinner(
{ io, label },
async (): Promise<AppListResponse> => {
if (opts.allWorkspaces === true) {
const ws = wsFactory(deps.http)
return runAllWorkspaces(apps, ws, opts, page, pageSize)
}
if (opts.appId !== undefined && opts.appId !== '') {
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
const wsName = workspaceNameForId(deps.active, wsId)
const desc = await apps.describe(opts.appId, ['info'])
return describeToEnvelope(desc, wsId, wsName)
}
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
return apps.list({
workspaceId: wsId,
page,
limit: pageSize,
mode: opts.mode,
name: opts.name,
tag: opts.tag,
})
},
)
return {
data: new AppListOutput(envelope.data.map(row => new AppRow(row)), envelope),
}
}
function resolveLimit(raw: string | undefined, env: (k: string) => string | undefined): number {
if (raw !== undefined && raw !== '')
return parseLimit(raw, '--limit')
const envValue = env('DIFY_LIMIT')
if (envValue !== undefined && envValue !== '')
return parseLimit(envValue, 'DIFY_LIMIT')
return LIMIT_DEFAULT
}
function describeToEnvelope(desc: AppDescribeResponse, wsId: string, wsName: string): AppListResponse {
if (desc.info === null || desc.info === undefined) {
return { page: 1, limit: 1, total: 0, has_more: false, data: [] }
}
return {
page: 1,
limit: 1,
total: 1,
has_more: false,
data: [{
id: desc.info.id,
name: desc.info.name,
description: desc.info.description,
mode: desc.info.mode as AppMode,
tags: desc.info.tags,
updated_at: desc.info.updated_at,
created_by_name: desc.info.author === '' ? undefined : desc.info.author,
workspace_id: wsId,
workspace_name: wsName === '' ? undefined : wsName,
}],
}
}
function workspaceNameForId(active: ActiveContext, id: string): string {
if (id === '')
return ''
const ctx = active.ctx
if (ctx.workspace?.id === id)
return ctx.workspace.name
for (const w of ctx.available_workspaces ?? []) {
if (w.id === id)
return w.name
}
return ''
}
async function runAllWorkspaces(
apps: AppsClient,
ws: WorkspacesClient,
opts: GetAppOptions,
page: number,
limit: number,
): Promise<AppListResponse> {
const wsResp = await ws.list()
if (wsResp.workspaces.length === 0)
return { page: 1, limit, total: 0, has_more: false, data: [] }
const merged: AppListResponse = { page: 1, limit, total: 0, has_more: false, data: [] }
const queue = [...wsResp.workspaces]
const workers: Promise<void>[] = []
const fetchOne = async (wsId: string): Promise<void> => {
const env = await apps.list({
workspaceId: wsId,
page,
limit,
mode: opts.mode,
name: opts.name,
tag: opts.tag,
})
merged.total += env.total
merged.data = [...merged.data, ...env.data]
}
const runner = async (): Promise<void> => {
while (true) {
const next = queue.shift()
if (next === undefined)
return
await fetchOne(next.id)
}
}
const N = Math.min(ALL_WORKSPACES_CONCURRENCY, wsResp.workspaces.length)
for (let i = 0; i < N; i++) workers.push(runner())
await Promise.all(workers)
merged.data = [...merged.data].sort((a, b) => a.id.localeCompare(b.id))
return merged
}