From c3f56fcc9a8a2a25d7290f329bfce791bd86a3ea Mon Sep 17 00:00:00 2001 From: GareArc Date: Sun, 21 Jun 2026 21:04:03 -0700 Subject: [PATCH] refactor(openapi/cli): drop tag/author from app usage-face noun The app noun is the usage face; tags and author are build/management metadata that belong to studio-app, not here. Remove them end to end: - backend: drop tags/created_by_name from AppListRow, tag from AppListQuery, the TagItem model, and the tag-name filter lookup; stop hardcoding the cross-tenant blanks in the permitted-external list - cli: remove the --tag flag, TAGS/AUTHOR columns, and tag from the list query; single get app no longer fabricates the fields - regenerate openapi contracts (types/zod) and markdown docs get app and get app now agree (neither surfaces tags/author), resolving the list-vs-single divergence raised in review. --- api/controllers/openapi/__init__.py | 2 - api/controllers/openapi/_models.py | 7 -- api/controllers/openapi/apps.py | 14 ---- .../openapi/apps_permitted_external.py | 2 - api/openapi/markdown/openapi-openapi.md | 10 --- .../openapi/test_app_list_query.py | 11 +-- cli/src/api/apps.test.ts | 6 +- cli/src/api/apps.ts | 2 - cli/src/commands/get/app/handlers.ts | 10 +-- cli/src/commands/get/app/index.ts | 2 - cli/src/commands/get/app/run.test.ts | 18 +---- cli/src/commands/get/app/run.ts | 5 -- .../generated/api/openapi/types.gen.ts | 8 -- .../generated/api/openapi/zod.gen.ts | 81 ++++++++----------- 14 files changed, 42 insertions(+), 136 deletions(-) diff --git a/api/controllers/openapi/__init__.py b/api/controllers/openapi/__init__.py index 2f23baa11a9..c11019cf627 100644 --- a/api/controllers/openapi/__init__.py +++ b/api/controllers/openapi/__init__.py @@ -62,7 +62,6 @@ from controllers.openapi._models import ( SessionListQuery, SessionListResponse, SessionRow, - TagItem, TaskStopResponse, UsageInfo, WorkflowRunData, @@ -96,7 +95,6 @@ register_response_schema_models( openapi_ns, ErrorBody, EventStreamResponse, - TagItem, UsageInfo, MessageMetadata, AppListRow, diff --git a/api/controllers/openapi/_models.py b/api/controllers/openapi/_models.py index b1a432abe3c..e846db3ea75 100644 --- a/api/controllers/openapi/_models.py +++ b/api/controllers/openapi/_models.py @@ -38,18 +38,12 @@ class PaginationEnvelope[T](BaseModel): return cls(page=page, limit=limit, total=total, has_more=page * limit < total, data=items) -class TagItem(BaseModel): - name: str - - class AppListRow(BaseModel): id: str name: str description: str | None = None mode: AppMode - tags: list[TagItem] = [] updated_at: str | None = None - created_by_name: str | None = None workspace_id: str | None = None workspace_name: str | None = None @@ -292,7 +286,6 @@ class AppListQuery(BaseModel): limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT) mode: AppMode | None = None name: str | None = Field(None, max_length=200) - tag: str | None = Field(None, max_length=100) class AppRunRequest(BaseModel): diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index c1b9b5eed06..eadc359acce 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -20,7 +20,6 @@ from controllers.openapi._models import ( AppListQuery, AppListResponse, AppListRow, - TagItem, ) from controllers.openapi.auth.composition import auth_router from controllers.openapi.auth.data import AuthData @@ -32,7 +31,6 @@ from models import App from models.model import AppMode from services.account_service import TenantService from services.app_service import AppListParams, AppService -from services.tag_service import TagService _ALLOWED_DESCRIBE_FIELDS: frozenset[str] = frozenset({"info", "parameters", "input_schema"}) @@ -164,28 +162,18 @@ class AppListApi(Resource): name=app.name, description=app.description, mode=app.mode, - tags=[TagItem(name=t.name) for t in app.tags], updated_at=app.updated_at.isoformat() if app.updated_at else None, - created_by_name=getattr(app, "author_name", None), workspace_id=str(workspace_id), workspace_name=tenant_name, ) env = AppListResponse(page=1, limit=1, total=1, has_more=False, data=[item]) return env - tag_ids: list[str] | None = None - if query.tag: - tags = TagService.get_tag_by_tag_name("app", workspace_id, query.tag, db.session) - if not tags: - return empty - tag_ids = [tag.id for tag in tags] - params = AppListParams( page=query.page, limit=query.limit, mode=query.mode.value if query.mode else "all", # type:ignore name=query.name, - tag_ids=tag_ids, status="normal", # Visibility gate pushed into the query — pagination.total stays # consistent across pages because invisible rows never count. @@ -206,9 +194,7 @@ class AppListApi(Resource): name=r.name, description=r.description, mode=r.mode, - tags=[TagItem(name=t.name) for t in r.tags], updated_at=r.updated_at.isoformat() if r.updated_at else None, - created_by_name=getattr(r, "author_name", None), workspace_id=str(workspace_id), workspace_name=tenant_name, ) diff --git a/api/controllers/openapi/apps_permitted_external.py b/api/controllers/openapi/apps_permitted_external.py index 949aa6a38d4..9bc400e5cc7 100644 --- a/api/controllers/openapi/apps_permitted_external.py +++ b/api/controllers/openapi/apps_permitted_external.py @@ -71,9 +71,7 @@ class PermittedExternalAppsListApi(Resource): name=app.name, description=app.description, mode=app.mode, - tags=[], # tenant-scoped; not surfaced cross-tenant updated_at=app.updated_at.isoformat() if app.updated_at else None, - created_by_name=None, # cross-tenant author leak prevention workspace_id=str(app.tenant_id), workspace_name=tenant.name if tenant else None, ) diff --git a/api/openapi/markdown/openapi-openapi.md b/api/openapi/markdown/openapi-openapi.md index 6841be77ff1..bd93557edcf 100644 --- a/api/openapi/markdown/openapi-openapi.md +++ b/api/openapi/markdown/openapi-openapi.md @@ -83,7 +83,6 @@ User-scoped operations | mode | query | | No | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "channel", "chat", "completion", "rag-pipeline", "workflow" | | name | query | | No | string | | page | query | | No | integer,
**Default:** 1 | -| tag | query | | No | string | | workspace_id | query | | Yes | string | #### Responses @@ -601,7 +600,6 @@ mode is a closed enum. | mode | [AppMode](#appmode) | | No | | name | string | | No | | page | integer,
**Default:** 1 | | No | -| tag | string | | No | | workspace_id | string | | Yes | #### AppListResponse @@ -618,12 +616,10 @@ mode is a closed enum. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| created_by_name | string | | No | | description | string | | No | | id | string | | Yes | | mode | [AppMode](#appmode) | | Yes | | name | string | | Yes | -| tags | [ [TagItem](#tagitem) ],
**Default:** | | No | | updated_at | string | | No | | workspace_id | string | | No | | workspace_name | string | | No | @@ -994,12 +990,6 @@ Pagination for GET /account/sessions. Strict (extra='forbid'). | last_used_at | string | | No | | prefix | string | | Yes | -#### TagItem - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| name | string | | Yes | - #### TaskStopResponse 200 body for POST /apps//tasks//stop. The handler always returns diff --git a/api/tests/unit_tests/controllers/openapi/test_app_list_query.py b/api/tests/unit_tests/controllers/openapi/test_app_list_query.py index 9d207b1930a..e0b15585323 100644 --- a/api/tests/unit_tests/controllers/openapi/test_app_list_query.py +++ b/api/tests/unit_tests/controllers/openapi/test_app_list_query.py @@ -5,7 +5,7 @@ Runs against the model directly, not the HTTP layer. Pins: - workspace_id is required. - numeric bounds enforced (page >= 1, limit in [1, MAX_PAGE_LIMIT]). - mode validates against the AppMode enum. -- name and tag have length caps. +- name has a length cap. """ from __future__ import annotations @@ -24,7 +24,6 @@ def test_defaults(): assert q.limit == 20 assert q.mode is None assert q.name is None - assert q.tag is None def test_workspace_id_required(): @@ -80,12 +79,6 @@ def test_name_length_capped(): AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 201}) -def test_tag_length_capped(): - AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 100}) - with pytest.raises(ValidationError): - 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( @@ -95,7 +88,6 @@ def test_all_fields_accept_valid_values(): "limit": 50, "mode": "workflow", "name": "search", - "tag": "prod", } ) assert q.workspace_id == "00000000-0000-0000-0000-000000000001" @@ -104,4 +96,3 @@ def test_all_fields_accept_valid_values(): assert q.mode is not None assert q.mode.value == "workflow" assert q.name == "search" - assert q.tag == "prod" diff --git a/cli/src/api/apps.test.ts b/cli/src/api/apps.test.ts index 68e7bcc86a3..861f60feb26 100644 --- a/cli/src/api/apps.test.ts +++ b/cli/src/api/apps.test.ts @@ -36,7 +36,6 @@ describe('AppsClient.list', () => { // Optional filters are omitted entirely when not supplied. expect(q.has('mode')).toBe(false) expect(q.has('name')).toBe(false) - expect(q.has('tag')).toBe(false) }) it('forwards explicit pagination and filters', async () => { @@ -48,7 +47,6 @@ describe('AppsClient.list', () => { limit: 50, mode: 'chat', name: 'support bot', - tag: 'prod', }) const q = queryOf(stub.captured.url) @@ -56,18 +54,16 @@ describe('AppsClient.list', () => { expect(q.get('limit')).toBe('50') expect(q.get('mode')).toBe('chat') expect(q.get('name')).toBe('support bot') - expect(q.get('tag')).toBe('prod') }) it('treats empty-string filters as absent (not blank query params)', async () => { stub = await startStubServer(cap => jsonResponder(200, LIST_BODY, cap)) - await makeClient(stub.url).list({ workspaceId: 'ws-1', mode: '', name: '', tag: '' }) + await makeClient(stub.url).list({ workspaceId: 'ws-1', mode: '', name: '' }) const q = queryOf(stub.captured.url) expect(q.has('mode')).toBe(false) expect(q.has('name')).toBe(false) - expect(q.has('tag')).toBe(false) }) it('propagates server 403 as a classified BaseError', async () => { diff --git a/cli/src/api/apps.ts b/cli/src/api/apps.ts index bf672f1f45d..01b18d9a9da 100644 --- a/cli/src/api/apps.ts +++ b/cli/src/api/apps.ts @@ -10,7 +10,6 @@ export type ListQuery = { readonly limit?: number readonly mode?: AppMode | '' readonly name?: string - readonly tag?: string } // An absent or empty mode filter means "any mode" — collapse both to undefined for the query. @@ -33,7 +32,6 @@ export class AppsClient implements AppReader { limit: q.limit ?? 20, mode: normalizeMode(q.mode), name: q.name !== undefined && q.name !== '' ? q.name : undefined, - tag: q.tag !== undefined && q.tag !== '' ? q.tag : undefined, }, }) } diff --git a/cli/src/commands/get/app/handlers.ts b/cli/src/commands/get/app/handlers.ts index ac7008fa537..9c91057d26d 100644 --- a/cli/src/commands/get/app/handlers.ts +++ b/cli/src/commands/get/app/handlers.ts @@ -1,4 +1,4 @@ -import type { AppListResponse, AppListRow, TagItem } from '@dify/contracts/api/openapi/types.gen' +import type { AppListResponse, AppListRow } from '@dify/contracts/api/openapi/types.gen' import type { TableCell, TableColumn } from '@/framework/output' export const APP_MODE_KEY = 'app' @@ -7,9 +7,7 @@ export const APP_COLUMNS: readonly TableColumn[] = [ { name: 'NAME', priority: 0 }, { name: 'ID', priority: 0 }, { name: 'MODE', priority: 0 }, - { name: 'TAGS', priority: 0 }, { name: 'UPDATED', priority: 0 }, - { name: 'AUTHOR', priority: 1 }, { name: 'WORKSPACE', priority: 1 }, ] @@ -25,9 +23,7 @@ export class AppRow { this.data.name, this.data.id, this.data.mode, - joinTags(this.data.tags ?? []), this.data.updated_at ?? '', - this.data.created_by_name ?? '', this.data.workspace_name ?? '', ] } @@ -70,7 +66,3 @@ export class AppListOutput { return this.envelope } } - -function joinTags(tags: readonly TagItem[]): string { - return tags.map(t => t.name).join(',') -} diff --git a/cli/src/commands/get/app/index.ts b/cli/src/commands/get/app/index.ts index 47594813704..ffce31b7c49 100644 --- a/cli/src/commands/get/app/index.ts +++ b/cli/src/commands/get/app/index.ts @@ -42,7 +42,6 @@ export default class GetApp extends DifyCommand { 'limit': Flags.string({ description: 'page size [1..200]' }), 'mode': Flags.string({ description: 'filter by app mode', options: APP_MODE_VALUES }), 'name': Flags.string({ description: 'filter by app name (server-side substring)' }), - 'tag': Flags.string({ description: 'filter by tag name (server-side exact match)' }), 'http-retry': httpRetryFlag, 'output': Flags.outputFormat({ options: [OutputFormat.JSON, OutputFormat.YAML, OutputFormat.NAME, OutputFormat.WIDE], default: '' }), } @@ -59,7 +58,6 @@ export default class GetApp extends DifyCommand { limitRaw: flags.limit, mode: flags.mode as AppMode | undefined, name: flags.name, - tag: flags.tag, format, }, { active: ctx.active, http: ctx.http, io: ctx.io }) return table({ diff --git a/cli/src/commands/get/app/run.test.ts b/cli/src/commands/get/app/run.test.ts index 094fdc69c93..99a58cdbd71 100644 --- a/cli/src/commands/get/app/run.test.ts +++ b/cli/src/commands/get/app/run.test.ts @@ -42,13 +42,12 @@ describe('runGetApp', () => { })) } - it('list (no id, default format) renders table with NAME ID MODE TAGS UPDATED', async () => { + it('list (no id, default format) renders table with NAME ID MODE UPDATED', async () => { const out = await render() - expect(out).toMatch(/^NAME\s+ID\s+MODE\s+TAGS\s+UPDATED/) + expect(out).toMatch(/^NAME\s+ID\s+MODE\s+UPDATED/) expect(out).toContain('Greeter') expect(out).toContain('app-1') expect(out).toContain('chat') - expect(out).toContain('demo') expect(out).toContain('Workflow') expect(out).not.toContain('app-3') }) @@ -58,9 +57,7 @@ describe('runGetApp', () => { 'NAME', 'ID', 'MODE', - 'TAGS', 'UPDATED', - 'AUTHOR', 'WORKSPACE', ]) }) @@ -78,12 +75,6 @@ describe('runGetApp', () => { expect(out).not.toContain('Greeter') }) - it('--tag filters server-side', async () => { - const out = await render({ tag: 'demo' }) - expect(out).toContain('Greeter') - expect(out).not.toContain('Workflow') - }) - it('-A all-workspaces aggregates across workspaces sorted by id', async () => { const out = await render({ allWorkspaces: true }) expect(out).toContain('app-1') @@ -112,10 +103,9 @@ describe('runGetApp', () => { expect(out.trim().split('\n').sort()).toEqual(['app-1', 'app-2']) }) - it('-o wide includes AUTHOR and WORKSPACE columns', async () => { + it('-o wide includes the WORKSPACE column', async () => { const out = await render({ format: 'wide' }) - expect(out).toMatch(/^NAME\s+ID\s+MODE\s+TAGS\s+UPDATED\s+AUTHOR\s+WORKSPACE/) - expect(out).toContain('tester') + expect(out).toMatch(/^NAME\s+ID\s+MODE\s+UPDATED\s+WORKSPACE/) expect(out).toContain('Default') }) diff --git a/cli/src/commands/get/app/run.ts b/cli/src/commands/get/app/run.ts index 308b256bc84..c4a7911e0db 100644 --- a/cli/src/commands/get/app/run.ts +++ b/cli/src/commands/get/app/run.ts @@ -22,7 +22,6 @@ export type GetAppOptions = { readonly limitRaw?: string readonly mode?: AppMode readonly name?: string - readonly tag?: string readonly format?: string } @@ -76,7 +75,6 @@ export async function runGetApp(opts: GetAppOptions, deps: GetAppDeps): Promise< limit: pageSize, mode: opts.mode, name: opts.name, - tag: opts.tag, }) }, ) @@ -109,9 +107,7 @@ function describeToEnvelope(desc: AppDescribeResponse, wsId: string, wsName: str name: desc.info.name, description: desc.info.description, mode: desc.info.mode as AppMode, - tags: [], updated_at: desc.info.updated_at, - created_by_name: undefined, workspace_id: wsId, workspace_name: wsName === '' ? undefined : wsName, }], @@ -146,7 +142,6 @@ async function runAllWorkspaces( limit, mode: opts.mode, name: opts.name, - tag: opts.tag, }) merged.total += env.total merged.data = [...merged.data, ...env.data] diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index b3262944a90..52307ca545c 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -76,7 +76,6 @@ export type AppListQuery = { mode?: AppMode | null name?: string | null page?: number - tag?: string | null workspace_id: string } @@ -89,12 +88,10 @@ export type AppListResponse = { } export type AppListRow = { - created_by_name?: string | null description?: string | null id: string mode: AppMode name: string - tags?: Array updated_at?: string | null workspace_id?: string | null workspace_name?: string | null @@ -406,10 +403,6 @@ export type SessionRow = { prefix: string } -export type TagItem = { - name: string -} - export type TaskStopResponse = { result: 'success' } @@ -605,7 +598,6 @@ export type GetAppsData = { | 'workflow' name?: string page?: number - tag?: string workspace_id: string } url: '/apps' diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index b1f6b1554f1..da0a2fc04e6 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -114,10 +114,33 @@ export const zAppListQuery = z.object({ mode: zAppMode.nullish(), name: z.string().max(200).nullish(), page: z.int().gte(1).optional().default(1), - tag: z.string().max(100).nullish(), workspace_id: z.string(), }) +/** + * AppListRow + */ +export const zAppListRow = z.object({ + description: z.string().nullish(), + id: z.string(), + mode: zAppMode, + name: z.string(), + updated_at: z.string().nullish(), + workspace_id: z.string().nullish(), + workspace_name: z.string().nullish(), +}) + +/** + * AppListResponse + */ +export const zAppListResponse = z.object({ + data: z.array(zAppListRow), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + /** * AppRunRequest */ @@ -439,6 +462,17 @@ export const zPermittedExternalAppsListQuery = z.object({ page: z.int().gte(1).optional().default(1), }) +/** + * PermittedExternalAppsListResponse + */ +export const zPermittedExternalAppsListResponse = z.object({ + data: z.array(zAppListRow), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + /** * RevokeResponse */ @@ -490,50 +524,6 @@ export const zSessionListResponse = z.object({ total: z.int(), }) -/** - * TagItem - */ -export const zTagItem = z.object({ - name: z.string(), -}) - -/** - * AppListRow - */ -export const zAppListRow = z.object({ - created_by_name: z.string().nullish(), - description: z.string().nullish(), - id: z.string(), - mode: zAppMode, - name: z.string(), - tags: z.array(zTagItem).optional().default([]), - updated_at: z.string().nullish(), - workspace_id: z.string().nullish(), - workspace_name: z.string().nullish(), -}) - -/** - * AppListResponse - */ -export const zAppListResponse = z.object({ - data: z.array(zAppListRow), - has_more: z.boolean(), - limit: z.int(), - page: z.int(), - total: z.int(), -}) - -/** - * PermittedExternalAppsListResponse - */ -export const zPermittedExternalAppsListResponse = z.object({ - data: z.array(zAppListRow), - has_more: z.boolean(), - limit: z.int(), - page: z.int(), - total: z.int(), -}) - /** * TaskStopResponse * @@ -720,7 +710,6 @@ export const zGetAppsQuery = z.object({ .optional(), name: z.string().max(200).optional(), page: z.int().gte(1).optional().default(1), - tag: z.string().max(100).optional(), workspace_id: z.string(), })