mirror of
https://github.com/langgenius/dify.git
synced 2026-06-10 18:24:09 +08:00
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>
This commit is contained in:
parent
686e643632
commit
1502a57381
@ -6,7 +6,7 @@ from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from libs.helper import EmailStr, UUIDStrOrEmpty, uuid_value
|
||||
from libs.helper import EmailStr, UUIDStr, UUIDStrOrEmpty, uuid_value
|
||||
from models.model import AppMode
|
||||
|
||||
# Server-side cap on `limit` query param for /openapi/v1/* list endpoints.
|
||||
@ -262,22 +262,6 @@ class AppDescribeQuery(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
fields: set[str] | None = Field(default=None, json_schema_extra=_csv_string_query_schema)
|
||||
workspace_id: str | None = None
|
||||
|
||||
@field_validator("workspace_id", mode="before")
|
||||
@classmethod
|
||||
def _validate_workspace_id(cls, v: object) -> str | None:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
if not isinstance(v, str):
|
||||
raise ValueError("workspace_id must be a string")
|
||||
try:
|
||||
import uuid as _uuid
|
||||
|
||||
_uuid.UUID(v)
|
||||
except ValueError:
|
||||
raise ValueError("workspace_id must be a valid UUID")
|
||||
return v
|
||||
|
||||
@field_validator("fields", mode="before")
|
||||
@classmethod
|
||||
@ -297,7 +281,7 @@ class AppDescribeQuery(BaseModel):
|
||||
class AppListQuery(BaseModel):
|
||||
"""mode is a closed enum."""
|
||||
|
||||
workspace_id: str
|
||||
workspace_id: UUIDStr
|
||||
page: int = Field(1, ge=1)
|
||||
limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT)
|
||||
mode: AppMode | None = None
|
||||
|
||||
@ -97,7 +97,7 @@ class AppDescribeApi(AppReadResource):
|
||||
except ValidationError as exc:
|
||||
raise UnprocessableEntity(exc.json())
|
||||
|
||||
app = self._load(app_id, workspace_id=query.workspace_id)
|
||||
app = self._load(app_id)
|
||||
|
||||
requested = query.fields
|
||||
want_info = requested is None or "info" in requested
|
||||
|
||||
@ -19,6 +19,10 @@ def load_app(data: AuthData) -> None:
|
||||
if data.app is not None:
|
||||
return
|
||||
app_id = data.path_params["app_id"]
|
||||
try:
|
||||
uuid.UUID(app_id)
|
||||
except ValueError:
|
||||
raise NotFound("app not found")
|
||||
app = AppService.get_app_by_id(db.session, app_id)
|
||||
if not app or app.status != "normal":
|
||||
raise NotFound("app not found")
|
||||
|
||||
@ -273,6 +273,18 @@ def parse_uuid_str_or_none(value: str | None) -> str | None:
|
||||
UUIDStrOrEmpty = Annotated[str, AfterValidator(normalize_uuid)]
|
||||
|
||||
|
||||
def _strict_uuid(value: str | UUID) -> str:
|
||||
if not value:
|
||||
raise ValueError("must be a non-empty valid UUID")
|
||||
try:
|
||||
return uuid_value(value)
|
||||
except ValueError as exc:
|
||||
raise ValueError("must be a valid UUID") from exc
|
||||
|
||||
|
||||
UUIDStr = Annotated[str, AfterValidator(_strict_uuid)]
|
||||
|
||||
|
||||
def alphanumeric(value: str):
|
||||
# check if the value is alphanumeric and underlined
|
||||
if re.match(r"^[a-zA-Z0-9_]+$", value):
|
||||
|
||||
@ -112,7 +112,6 @@ User-scoped operations
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
| fields | query | | No | string |
|
||||
| workspace_id | query | | No | string |
|
||||
|
||||
##### Responses
|
||||
|
||||
@ -454,7 +453,6 @@ Empty / omitted → all blocks. Unknown member → ValidationError → 422.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| fields | string | | No |
|
||||
| workspace_id | string | | No |
|
||||
|
||||
#### AppDescribeResponse
|
||||
|
||||
|
||||
@ -32,18 +32,27 @@ def _make_auth_data(**kwargs) -> AuthData:
|
||||
return data
|
||||
|
||||
|
||||
_VALID_APP_UUID = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
|
||||
def test_load_app_writes_app_to_data():
|
||||
app = MagicMock()
|
||||
app.status = "normal"
|
||||
app.enable_api = True
|
||||
data = _make_auth_data(path_params={"app_id": "abc"})
|
||||
data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID})
|
||||
with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app):
|
||||
load_app(data)
|
||||
assert data.app is app
|
||||
|
||||
|
||||
def test_load_app_raises_not_found_for_non_uuid_app_id():
|
||||
data = _make_auth_data(path_params={"app_id": "not-a-uuid"})
|
||||
with pytest.raises(NotFound):
|
||||
load_app(data)
|
||||
|
||||
|
||||
def test_load_app_raises_not_found_when_missing():
|
||||
data = _make_auth_data(path_params={"app_id": "missing"})
|
||||
data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID})
|
||||
with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=None):
|
||||
with pytest.raises(NotFound):
|
||||
load_app(data)
|
||||
@ -52,7 +61,7 @@ def test_load_app_raises_not_found_when_missing():
|
||||
def test_load_app_raises_not_found_when_not_normal():
|
||||
app = MagicMock()
|
||||
app.status = "archived"
|
||||
data = _make_auth_data(path_params={"app_id": "abc"})
|
||||
data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID})
|
||||
with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app):
|
||||
with pytest.raises(NotFound):
|
||||
load_app(data)
|
||||
@ -62,7 +71,7 @@ def test_load_app_stashes_app_even_when_api_disabled():
|
||||
app = MagicMock()
|
||||
app.status = "normal"
|
||||
app.enable_api = False
|
||||
data = _make_auth_data(path_params={"app_id": "abc"})
|
||||
data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID})
|
||||
with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app):
|
||||
load_app(data)
|
||||
assert data.app is app
|
||||
|
||||
@ -18,8 +18,8 @@ from controllers.openapi.apps import AppListQuery
|
||||
|
||||
|
||||
def test_defaults():
|
||||
q = AppListQuery.model_validate({"workspace_id": "ws-1"})
|
||||
assert q.workspace_id == "ws-1"
|
||||
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001"})
|
||||
assert q.workspace_id == "00000000-0000-0000-0000-000000000001"
|
||||
assert q.page == 1
|
||||
assert q.limit == 20
|
||||
assert q.mode is None
|
||||
@ -34,61 +34,63 @@ def test_workspace_id_required():
|
||||
|
||||
def test_page_must_be_positive():
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "page": 0})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": 0})
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "page": -1})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": -1})
|
||||
|
||||
|
||||
def test_page_rejects_non_integer_string():
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "page": "abc"})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": "abc"})
|
||||
|
||||
|
||||
def test_limit_must_be_positive():
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "limit": 0})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": 0})
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "limit": -1})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": -1})
|
||||
|
||||
|
||||
def test_limit_caps_at_max_page_limit():
|
||||
# Boundary accepts.
|
||||
q = AppListQuery.model_validate({"workspace_id": "ws-1", "limit": MAX_PAGE_LIMIT})
|
||||
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": MAX_PAGE_LIMIT})
|
||||
assert q.limit == MAX_PAGE_LIMIT
|
||||
|
||||
# Just over rejects.
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "limit": MAX_PAGE_LIMIT + 1})
|
||||
AppListQuery.model_validate(
|
||||
{"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": MAX_PAGE_LIMIT + 1}
|
||||
)
|
||||
|
||||
|
||||
def test_mode_whitelisted_against_app_mode():
|
||||
# Valid mode passes.
|
||||
q = AppListQuery.model_validate({"workspace_id": "ws-1", "mode": "chat"})
|
||||
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "mode": "chat"})
|
||||
assert q.mode is not None
|
||||
assert q.mode.value == "chat"
|
||||
|
||||
# Invalid mode rejects.
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "mode": "not-a-mode"})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "mode": "not-a-mode"})
|
||||
|
||||
|
||||
def test_name_length_capped():
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "name": "x" * 200})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 200})
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "name": "x" * 201})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 201})
|
||||
|
||||
|
||||
def test_tag_length_capped():
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "tag": "x" * 100})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 100})
|
||||
with pytest.raises(ValidationError):
|
||||
AppListQuery.model_validate({"workspace_id": "ws-1", "tag": "x" * 101})
|
||||
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 101})
|
||||
|
||||
|
||||
def test_all_fields_accept_valid_values():
|
||||
"""Pin the happy-path acceptance for every field in one place."""
|
||||
q = AppListQuery.model_validate(
|
||||
{
|
||||
"workspace_id": "ws-1",
|
||||
"workspace_id": "00000000-0000-0000-0000-000000000001",
|
||||
"page": 5,
|
||||
"limit": 50,
|
||||
"mode": "workflow",
|
||||
@ -96,7 +98,7 @@ def test_all_fields_accept_valid_values():
|
||||
"tag": "prod",
|
||||
}
|
||||
)
|
||||
assert q.workspace_id == "ws-1"
|
||||
assert q.workspace_id == "00000000-0000-0000-0000-000000000001"
|
||||
assert q.page == 5
|
||||
assert q.limit == 50
|
||||
assert q.mode is not None
|
||||
|
||||
@ -37,11 +37,11 @@ describe('AppMetaClient', () => {
|
||||
const spy = vi.spyOn(apps, 'describe')
|
||||
const client = new AppMetaClient({ apps, host: mock.url, cache })
|
||||
|
||||
const m1 = await client.get('app-1', 'ws-1', [FieldInfo])
|
||||
const m1 = await client.get('app-1', [FieldInfo])
|
||||
expect(m1.info?.id).toBe('app-1')
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
const m2 = await client.get('app-1', 'ws-1', [FieldInfo])
|
||||
const m2 = await client.get('app-1', [FieldInfo])
|
||||
expect(m2.info?.id).toBe('app-1')
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -52,10 +52,10 @@ describe('AppMetaClient', () => {
|
||||
const spy = vi.spyOn(apps, 'describe')
|
||||
const client = new AppMetaClient({ apps, host: mock.url, cache })
|
||||
|
||||
await client.get('app-1', 'ws-1', [FieldInfo])
|
||||
await client.get('app-1', [FieldInfo])
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
const full = await client.get('app-1', 'ws-1', [FieldInfo, FieldParameters])
|
||||
const full = await client.get('app-1', [FieldInfo, FieldParameters])
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
expect(full.coveredFields.has(FieldParameters)).toBe(true)
|
||||
})
|
||||
@ -66,11 +66,11 @@ describe('AppMetaClient', () => {
|
||||
const spy = vi.spyOn(apps, 'describe')
|
||||
const client = new AppMetaClient({ apps, host: mock.url, cache, now: () => new Date('2026-05-09T00:00:00Z') })
|
||||
|
||||
await client.get('app-1', 'ws-1', [FieldInfo])
|
||||
await client.get('app-1', [FieldInfo])
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
const client2 = new AppMetaClient({ apps, host: mock.url, cache, now: () => new Date('2026-05-09T00:00:01Z') })
|
||||
await client2.get('app-1', 'ws-1', [FieldInfo])
|
||||
await client2.get('app-1', [FieldInfo])
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
@ -80,11 +80,11 @@ describe('AppMetaClient', () => {
|
||||
const spy = vi.spyOn(apps, 'describe')
|
||||
const client = new AppMetaClient({ apps, host: mock.url, cache })
|
||||
|
||||
await client.get('app-1', 'ws-1', [FieldInfo])
|
||||
await client.get('app-1', [FieldInfo])
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
await client.invalidate('app-1')
|
||||
await client.get('app-1', 'ws-1', [FieldInfo])
|
||||
await client.get('app-1', [FieldInfo])
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
@ -93,8 +93,8 @@ describe('AppMetaClient', () => {
|
||||
const spy = vi.spyOn(apps, 'describe')
|
||||
const client = new AppMetaClient({ apps, host: mock.url })
|
||||
|
||||
await client.get('app-1', 'ws-1', [FieldInfo])
|
||||
await client.get('app-1', 'ws-1', [FieldInfo])
|
||||
await client.get('app-1', [FieldInfo])
|
||||
await client.get('app-1', [FieldInfo])
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
@ -23,12 +23,12 @@ export class AppMetaClient {
|
||||
this.now = opts.now ?? (() => new Date())
|
||||
}
|
||||
|
||||
async get(appId: string, workspaceId: string, fields: readonly AppMetaFieldKey[] = []): Promise<AppMeta> {
|
||||
async get(appId: string, fields: readonly AppMetaFieldKey[] = []): Promise<AppMeta> {
|
||||
const cached = this.cache?.get(this.host, appId)
|
||||
if (cached !== undefined && this.cache?.isFresh(cached, this.now()) === true && covers(cached.meta, fields))
|
||||
return cached.meta
|
||||
|
||||
const resp = await this.apps.describe(appId, workspaceId, fields.length === 0 ? undefined : fields)
|
||||
const resp = await this.apps.describe(appId, fields.length === 0 ? undefined : fields)
|
||||
const fresh = fromDescribe(resp, fields)
|
||||
const merged = cached !== undefined && this.cache?.isFresh(cached, this.now()) === true
|
||||
? mergeMeta(cached.meta, fresh)
|
||||
|
||||
@ -86,14 +86,14 @@ describe('AppsClient.describe', () => {
|
||||
await stub?.stop()
|
||||
})
|
||||
|
||||
it('hits /apps/<id>/describe, sends workspace_id, omits fields when none given', async () => {
|
||||
it('hits /apps/<id>/describe, omits workspace_id and fields when not given', async () => {
|
||||
stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap))
|
||||
|
||||
const res = await makeClient(stub.url).describe('app-1', 'ws-1')
|
||||
const res = await makeClient(stub.url).describe('app-1')
|
||||
|
||||
expect(stub.captured.url?.split('?')[0]).toBe('/openapi/v1/apps/app-1/describe')
|
||||
const q = queryOf(stub.captured.url)
|
||||
expect(q.get('workspace_id')).toBe('ws-1')
|
||||
expect(q.has('workspace_id')).toBe(false)
|
||||
expect(q.has('fields')).toBe(false)
|
||||
expect(res.info?.id).toBe('app-1')
|
||||
})
|
||||
@ -101,7 +101,7 @@ describe('AppsClient.describe', () => {
|
||||
it('joins fields with commas', async () => {
|
||||
stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap))
|
||||
|
||||
await makeClient(stub.url).describe('app-1', 'ws-1', ['parameters', 'input_schema'])
|
||||
await makeClient(stub.url).describe('app-1', ['parameters', 'input_schema'])
|
||||
|
||||
expect(queryOf(stub.captured.url).get('fields')).toBe('parameters,input_schema')
|
||||
})
|
||||
@ -109,7 +109,7 @@ describe('AppsClient.describe', () => {
|
||||
it('URL-encodes the app id', async () => {
|
||||
stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap))
|
||||
|
||||
await makeClient(stub.url).describe('app/with space', 'ws-1')
|
||||
await makeClient(stub.url).describe('app/with space')
|
||||
|
||||
expect(stub.captured.url?.split('?')[0]).toBe('/openapi/v1/apps/app%2Fwith%20space/describe')
|
||||
})
|
||||
|
||||
@ -32,13 +32,10 @@ export class AppsClient {
|
||||
})
|
||||
}
|
||||
|
||||
async describe(appId: string, workspaceId: string, fields?: readonly string[]): Promise<AppDescribeResponse> {
|
||||
async describe(appId: string, fields?: readonly string[]): Promise<AppDescribeResponse> {
|
||||
return this.orpc.apps.byAppId.describe.get({
|
||||
params: { app_id: appId },
|
||||
query: {
|
||||
workspace_id: workspaceId,
|
||||
// The backend parses a comma-separated string (validator splits on ','); the contract
|
||||
// types `fields` as a string accordingly, so join here rather than send an array.
|
||||
fields: fields !== undefined && fields.length > 0 ? fields.join(',') : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { DifyCommand } from '@/commands/_shared/dify-command'
|
||||
import { httpRetryFlag } from '@/commands/_shared/global-flags'
|
||||
import { BaseError } from '@/errors/base'
|
||||
import { ErrorCode } from '@/errors/codes'
|
||||
import { Args, Flags } from '@/framework/flags'
|
||||
import { formatted, OutputFormat } from '@/framework/output'
|
||||
import { isValidUuid } from '@/workspace/resolver'
|
||||
import { agentGuide } from './guide'
|
||||
import { runDescribeApp } from './run'
|
||||
|
||||
@ -9,13 +12,13 @@ export default class DescribeApp extends DifyCommand {
|
||||
static override description = 'Describe a single app (kubectl-describe-style)'
|
||||
|
||||
static override examples = [
|
||||
'<%= config.bin %> describe app app-1',
|
||||
'<%= config.bin %> describe app app-1 -o json',
|
||||
'<%= config.bin %> describe app app-1 --refresh',
|
||||
'<%= config.bin %> describe app <uuid>',
|
||||
'<%= config.bin %> describe app <uuid> -o json',
|
||||
'<%= config.bin %> describe app <uuid> --refresh',
|
||||
]
|
||||
|
||||
static override args = {
|
||||
id: Args.string({ description: 'app id', required: true }),
|
||||
id: Args.string({ description: 'app UUID', required: true }),
|
||||
}
|
||||
|
||||
static override flags = {
|
||||
@ -27,6 +30,8 @@ export default class DescribeApp extends DifyCommand {
|
||||
|
||||
async run(argv: string[]) {
|
||||
const { args, flags } = this.parse(DescribeApp, argv)
|
||||
if (!isValidUuid(args.id))
|
||||
throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `${JSON.stringify(args.id)} is not a valid app UUID` })
|
||||
const format = flags.output
|
||||
const ctx = await this.authedCtx({ retryFlag: flags['http-retry'], withCache: true, format })
|
||||
return formatted({
|
||||
|
||||
@ -4,11 +4,9 @@ import type { HttpClient } from '@/http/types'
|
||||
import type { IOStreams } from '@/sys/io/streams'
|
||||
import { AppMetaClient } from '@/api/app-meta'
|
||||
import { AppsClient } from '@/api/apps'
|
||||
import { getEnv } from '@/sys/index'
|
||||
import { runWithSpinner } from '@/sys/io/spinner'
|
||||
import { nullStreams } from '@/sys/io/streams'
|
||||
import { FieldInfo, FieldInputSchema, FieldParameters } from '@/types/app-meta'
|
||||
import { resolveWorkspaceId } from '@/workspace/resolver'
|
||||
import { AppDescribeOutput } from './handlers.js'
|
||||
|
||||
export type DescribeAppOptions = {
|
||||
@ -28,8 +26,6 @@ export type DescribeAppDeps = {
|
||||
}
|
||||
|
||||
export async function runDescribeApp(opts: DescribeAppOptions, deps: DescribeAppDeps): Promise<AppDescribeOutput> {
|
||||
const env = deps.envLookup ?? getEnv
|
||||
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
|
||||
const apps = new AppsClient(deps.http)
|
||||
const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache })
|
||||
const io = deps.io ?? nullStreams()
|
||||
@ -38,7 +34,7 @@ export async function runDescribeApp(opts: DescribeAppOptions, deps: DescribeApp
|
||||
async () => {
|
||||
if (opts.refresh === true)
|
||||
await meta.invalidate(opts.appId)
|
||||
return meta.get(opts.appId, wsId, [FieldInfo, FieldParameters, FieldInputSchema])
|
||||
return meta.get(opts.appId, [FieldInfo, FieldParameters, FieldInputSchema])
|
||||
},
|
||||
)
|
||||
return new AppDescribeOutput(result)
|
||||
|
||||
@ -59,7 +59,7 @@ export async function runGetApp(opts: GetAppOptions, deps: GetAppDeps): Promise<
|
||||
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, wsId, ['info'])
|
||||
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 })
|
||||
|
||||
@ -8,10 +8,9 @@ import { AppRunClient } from '@/api/app-run'
|
||||
import { AppsClient } from '@/api/apps'
|
||||
import { pickStrategy } from '@/commands/run/app/_strategies/index'
|
||||
import { RUN_MODES } from '@/commands/run/app/handlers'
|
||||
import { getEnv, processExit } from '@/sys/index'
|
||||
import { processExit } from '@/sys/index'
|
||||
import { colorEnabled, colorScheme } from '@/sys/io/color'
|
||||
import { FieldInfo } from '@/types/app-meta'
|
||||
import { resolveWorkspaceId } from '@/workspace/resolver'
|
||||
|
||||
export type ResumeAppOptions = {
|
||||
readonly appId: string
|
||||
@ -76,12 +75,9 @@ async function resolveInputs(
|
||||
}
|
||||
|
||||
export async function resumeApp(opts: ResumeAppOptions, deps: ResumeAppDeps): Promise<void> {
|
||||
const env = deps.envLookup ?? getEnv
|
||||
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
|
||||
|
||||
const apps = new AppsClient(deps.http)
|
||||
const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache })
|
||||
const m = await meta.get(opts.appId, wsId, [FieldInfo])
|
||||
const m = await meta.get(opts.appId, [FieldInfo])
|
||||
const mode = m.info?.mode ?? RUN_MODES.Workflow
|
||||
|
||||
const runClient = new AppRunClient(deps.http)
|
||||
|
||||
@ -9,9 +9,8 @@ import { FileUploadClient } from '@/api/file-upload'
|
||||
import { pickStrategy } from '@/commands/run/app/_strategies/index'
|
||||
import { BaseError, HttpClientError } from '@/errors/base'
|
||||
import { ErrorCode } from '@/errors/codes'
|
||||
import { getEnv, processExit } from '@/sys/index'
|
||||
import { processExit } from '@/sys/index'
|
||||
import { FieldInfo } from '@/types/app-meta'
|
||||
import { resolveWorkspaceId } from '@/workspace/resolver'
|
||||
import { resolveFileInputs } from './file-flags.js'
|
||||
import { RUN_MODES } from './handlers.js'
|
||||
|
||||
@ -78,13 +77,11 @@ async function resolveInputs(
|
||||
}
|
||||
|
||||
export async function runApp(opts: RunAppOptions, deps: RunAppDeps): Promise<void> {
|
||||
const env = deps.envLookup ?? getEnv
|
||||
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
|
||||
const apps = new AppsClient(deps.http)
|
||||
const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache })
|
||||
|
||||
try {
|
||||
await executeRun(opts, deps, meta, wsId)
|
||||
await executeRun(opts, deps, meta)
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof HttpClientError && err.httpStatus === 422) {
|
||||
@ -99,9 +96,8 @@ async function executeRun(
|
||||
opts: RunAppOptions,
|
||||
deps: RunAppDeps,
|
||||
meta: AppMetaClient,
|
||||
wsId: string,
|
||||
): Promise<void> {
|
||||
const m = await meta.get(opts.appId, wsId, [FieldInfo])
|
||||
const m = await meta.get(opts.appId, [FieldInfo])
|
||||
const mode = m.info?.mode ?? ''
|
||||
if (mode === '')
|
||||
throw new Error(`app ${opts.appId}: mode missing from /describe`)
|
||||
|
||||
@ -8,11 +8,23 @@ export type WorkspaceResolveInputs = {
|
||||
readonly active?: ActiveContext
|
||||
}
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
export function isValidUuid(v: string): boolean {
|
||||
return UUID_RE.test(v)
|
||||
}
|
||||
|
||||
export function resolveWorkspaceId(inputs: WorkspaceResolveInputs): string {
|
||||
if (truthy(inputs.flag))
|
||||
if (truthy(inputs.flag)) {
|
||||
if (!isValidUuid(inputs.flag))
|
||||
throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `--workspace value ${JSON.stringify(inputs.flag)} is not a valid UUID` })
|
||||
return inputs.flag
|
||||
if (truthy(inputs.env))
|
||||
}
|
||||
if (truthy(inputs.env)) {
|
||||
if (!isValidUuid(inputs.env))
|
||||
throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `DIFY_WORKSPACE_ID value ${JSON.stringify(inputs.env)} is not a valid UUID` })
|
||||
return inputs.env
|
||||
}
|
||||
const ctx = inputs.active?.ctx
|
||||
if (ctx !== undefined) {
|
||||
if (truthy(ctx.workspace?.id))
|
||||
|
||||
@ -33,7 +33,6 @@ export type AppDescribeInfo = {
|
||||
|
||||
export type AppDescribeQuery = {
|
||||
fields?: string
|
||||
workspace_id?: string | null
|
||||
}
|
||||
|
||||
export type AppDescribeResponse = {
|
||||
@ -445,7 +444,6 @@ export type GetAppsByAppIdDescribeData = {
|
||||
}
|
||||
query?: {
|
||||
fields?: string
|
||||
workspace_id?: string
|
||||
}
|
||||
url: '/apps/{app_id}/describe'
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ export const zAccountPayload = z.object({
|
||||
*/
|
||||
export const zAppDescribeQuery = z.object({
|
||||
fields: z.string().optional(),
|
||||
workspace_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
@ -533,7 +532,6 @@ export const zGetAppsByAppIdDescribePath = z.object({
|
||||
|
||||
export const zGetAppsByAppIdDescribeQuery = z.object({
|
||||
fields: z.string().optional(),
|
||||
workspace_id: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user