From 433f170d95c0200f4f643065978bd949e188c31c Mon Sep 17 00:00:00 2001 From: yaozichen2025 Date: Sat, 11 Jul 2026 18:59:10 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(extension):=20=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=88=86=E5=B1=82=E8=A7=86=E5=9B=BE=E6=94=AF=E6=8C=81=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E6=9F=A5=E7=9C=8B=20Query=20/=20Pageflow=20=E4=BA=A7?= =?UTF-8?q?=E7=89=A9=20(NAV-01)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 动机:应用页签分层浏览会列出 AppGen 产出的 query/pageflow 条目,但点击 跳转仅支持 page/vo/eapi/fe-eapi/entity 五类,query/pageflow 点击无响应, 用户能看到产物存在却无法查看内容。issue IK09C6 中维护者已确认先做只读形态。 改动: - 模型页签新增只读源码类 entry(query/pageflow),复用既有"文件"视图渲染 YAML,此类 entry 强制文件视图并隐藏图形视图切换; - browser-generate 新增 generateAppWithSourcesInBrowser 返回生成结果与 query/pageflow 源码 YAML,原函数签名不变(委托实现,调用方零破坏); - 应用分层导航对 query/pageflow 走"点击时直接带 YAML 打开"路径; dbo/java/webcmp 等生成代码类条目保持现状; - openModelCount 统计口径同步纳入 query/pageflow。 验证:extension typecheck 相对 main 基线零新增错误; bun test app-preview-model.test.ts 5 pass(含新增用例)。 关联 issue: https://gitee.com/ubml/code-agent/issues/IK09C6 Co-Authored-By: Claude Fable 5 --- .../src/components/app-preview-content.tsx | 12 +++++-- .../extension/src/lib/app-layer-navigation.ts | 22 +++++++++++-- .../src/lib/app-preview-model.test.ts | 25 +++++++++++++++ .../extension/src/lib/app-preview-model.ts | 9 ++++++ .../extension/src/lib/model-preview-model.ts | 12 ++++++- .../extension/src/lib/ubml-model-bridge.ts | 24 ++++++++++++++ .../extension/src/lib/ubml-model-tab-store.ts | 22 +++++++++++-- .../src/pages/workbench/ubml-app-editor.tsx | 6 ++-- .../src/pages/workbench/ubml-model-panel.tsx | 32 ++++++++++++------- .../src/ubml-app/browser-generate.ts | 31 ++++++++++++++++-- 10 files changed, 172 insertions(+), 23 deletions(-) diff --git a/packages/extension/src/components/app-preview-content.tsx b/packages/extension/src/components/app-preview-content.tsx index 1213ed55..0d3fe9bb 100644 --- a/packages/extension/src/components/app-preview-content.tsx +++ b/packages/extension/src/components/app-preview-content.tsx @@ -3,7 +3,15 @@ import type { AppLayerItem, AppPreviewModel } from "../lib/app-preview-model" import { tryOpenAppLayerItem } from "../lib/app-layer-navigation" function isOpenableLayerItem(item: AppLayerItem): boolean { - return item.kind === "page" || item.kind === "vo" || item.kind === "eapi" || item.kind === "fe-eapi" || item.kind === "entity" + return ( + item.kind === "page" || + item.kind === "vo" || + item.kind === "eapi" || + item.kind === "fe-eapi" || + item.kind === "entity" || + item.kind === "query" || + item.kind === "pageflow" + ) } export function AppPreviewContent(props: { model: AppPreviewModel }) { @@ -11,7 +19,7 @@ export function AppPreviewContent(props: { model: AppPreviewModel }) { function onLayerItemClick(item: AppLayerItem) { if (!isOpenableLayerItem(item)) return - const result = tryOpenAppLayerItem(item) + const result = tryOpenAppLayerItem(item, props.model.sources) setNavMessage(result.ok ? null : (result.message ?? "无法打开模型")) } diff --git a/packages/extension/src/lib/app-layer-navigation.ts b/packages/extension/src/lib/app-layer-navigation.ts index eccd7d77..1a4739a2 100644 --- a/packages/extension/src/lib/app-layer-navigation.ts +++ b/packages/extension/src/lib/app-layer-navigation.ts @@ -1,5 +1,6 @@ import type { AppLayerItem } from "./app-preview-model" -import { navigateUbmlModelRef } from "./ubml-model-bridge" +import type { AppPreviewSources } from "./app-preview-model" +import { navigateUbmlModelRef, openUbmlModelTab } from "./ubml-model-bridge" import type { UbmlModelKind } from "./ubml-model-tab-store" function layerKindToModelKind(kind: AppLayerItem["kind"]): UbmlModelKind | undefined { @@ -10,7 +11,24 @@ function layerKindToModelKind(kind: AppLayerItem["kind"]): UbmlModelKind | undef return undefined } -export function tryOpenAppLayerItem(item: AppLayerItem): { ok: boolean; message?: string } { +export function tryOpenAppLayerItem( + item: AppLayerItem, + sources?: AppPreviewSources, +): { ok: boolean; message?: string } { + if (item.kind === "query" || item.kind === "pageflow") { + const yaml = item.kind === "query" ? sources?.queryYamls[item.code] : sources?.pageflowYaml + if (!yaml) { + return { ok: false, message: `${item.kind}/${item.code} 源码不可用,请重新生成应用预览` } + } + openUbmlModelTab({ + kind: item.kind, + key: `${item.kind}:${item.code}`, + title: item.code, + yaml, + viewMode: "file", + }) + return { ok: true } + } const modelKind = layerKindToModelKind(item.kind) if (!modelKind) { return { ok: false, message: `${item.kind}/${item.code} 暂不支持在模型页签打开` } diff --git a/packages/extension/src/lib/app-preview-model.test.ts b/packages/extension/src/lib/app-preview-model.test.ts index f8ade2d9..05c894d1 100644 --- a/packages/extension/src/lib/app-preview-model.test.ts +++ b/packages/extension/src/lib/app-preview-model.test.ts @@ -70,4 +70,29 @@ describe("app-preview-model", () => { expect(backendKinds).toContain("entity") expect(backendKinds).toContain("eapi") }) + + test("buildAppPreviewModel carries sources and counts query/pageflow as openable", async () => { + const yaml = await readFile(EXEMPLAR, "utf8") + const result = await generateAppFromYaml(yaml) + const sources = { queryYamls: { bxdqo: "query: {}" }, pageflowYaml: "pageflow: {}" } + const model = buildAppPreviewModel(result, sources) + expect(model.sources).toEqual(sources) + + const backendKinds = model.layers.find((l) => l.id === "backend")?.groups.map((g) => g.kind) ?? [] + expect(backendKinds).toContain("query") + expect(backendKinds).toContain("pageflow") + + const withoutSources = buildAppPreviewModel(result) + expect(withoutSources.sources).toBeUndefined() + + const openableKinds = new Set(["page", "vo", "eapi", "fe-eapi", "entity", "query", "pageflow"]) + const expectedOpenable = model.layers + .flatMap((l) => l.groups) + .filter((g) => openableKinds.has(g.kind)) + .reduce((n, g) => n + g.items.length, 0) + expect(model.layerSummary.openModelCount).toBe(expectedOpenable) + const queryCount = + model.layers.flatMap((l) => l.groups).find((g) => g.kind === "query")?.items.length ?? 0 + expect(queryCount).toBeGreaterThan(0) + }) }) diff --git a/packages/extension/src/lib/app-preview-model.ts b/packages/extension/src/lib/app-preview-model.ts index 9f2a3547..7f860b25 100644 --- a/packages/extension/src/lib/app-preview-model.ts +++ b/packages/extension/src/lib/app-preview-model.ts @@ -37,6 +37,11 @@ export type AppLayer = { groups: AppLayerGroup[] } +export type AppPreviewSources = { + queryYamls: Record + pageflowYaml?: string +} + export type AppPreviewModel = { bizobject: string namespace: string @@ -44,6 +49,7 @@ export type AppPreviewModel = { stages: { id: string; valid: boolean; issueCount: number }[] artifacts: { kind: string; code: string }[] layers: AppLayer[] + sources?: AppPreviewSources layerSummary: { frontendCount: number backendCount: number @@ -244,6 +250,7 @@ export function countOpenableLayerModels(layers: AppLayer[]): number { count += group.items.length } if (group.kind === "fe-eapi") count += group.items.length + if (group.kind === "query" || group.kind === "pageflow") count += group.items.length } } return count @@ -251,12 +258,14 @@ export function countOpenableLayerModels(layers: AppLayer[]): number { export function buildAppPreviewModel( result: import("@code-agent/ubml-codegen/app-browser").AppGenerateResult, + sources?: AppPreviewSources, ): AppPreviewModel { const layers = buildAppLayers(result) return { bizobject: result.dsl.app.bizobject, namespace: result.dsl.app.namespace, valid: result.validation.valid, + ...(sources ? { sources } : {}), stages: result.stages.map((s) => ({ id: s.id, valid: s.valid, diff --git a/packages/extension/src/lib/model-preview-model.ts b/packages/extension/src/lib/model-preview-model.ts index f89d45ed..1a22250e 100644 --- a/packages/extension/src/lib/model-preview-model.ts +++ b/packages/extension/src/lib/model-preview-model.ts @@ -20,9 +20,18 @@ const KIND_LABELS: Record = { page: "页面 pages/", vo: "视图对象 vo/", eapi: "API eapi/", + query: "查询对象 qo/", + pageflow: "页面流 pf/", } export function navItemFromEntry(entry: UbmlModelEntry): ModelNavItem { + if (entry.kind === "query" || entry.kind === "pageflow") { + return { + key: entry.key, + code: entry.label, + name: entry.kind === "query" ? "查询对象(只读)" : "页面流(只读)", + } + } if (entry.kind === "page") { return { key: entry.key, @@ -84,7 +93,7 @@ export function buildModelNavigation( list.push(navItemFromEntry(entry)) groups.set(entry.kind, list) } - const order: UbmlModelKind[] = ["page", "vo", "eapi", "entity"] + const order: UbmlModelKind[] = ["page", "vo", "eapi", "entity", "query", "pageflow"] const navGroups = order .filter((kind) => groups.has(kind)) .map((kind) => ({ @@ -124,5 +133,6 @@ export function graphViewLabel(kind: UbmlModelKind): string { if (kind === "entity") return "关系图" if (kind === "vo") return "映射" if (kind === "eapi") return "接口" + if (kind === "query" || kind === "pageflow") return "文件" return "结构" } diff --git a/packages/extension/src/lib/ubml-model-bridge.ts b/packages/extension/src/lib/ubml-model-bridge.ts index 36ed5b2b..fd993b1c 100644 --- a/packages/extension/src/lib/ubml-model-bridge.ts +++ b/packages/extension/src/lib/ubml-model-bridge.ts @@ -65,11 +65,25 @@ export type OpenUbmlEapiModelTabInput = { relativePath?: string } +type OpenUbmlSourceModelTabInputBase = { + key: string + title: string + yaml: string + viewMode?: UbmlModelViewMode + logicalKey?: string + relativePath?: string +} + +export type OpenUbmlSourceModelTabInput = + | (OpenUbmlSourceModelTabInputBase & { kind: "query" }) + | (OpenUbmlSourceModelTabInputBase & { kind: "pageflow" }) + export type OpenUbmlModelTabInput = | OpenUbmlEntityModelTabInput | OpenUbmlPageModelTabInput | OpenUbmlVoModelTabInput | OpenUbmlEapiModelTabInput + | OpenUbmlSourceModelTabInput export type UbmlModelRef = { kind: UbmlModelKind @@ -171,6 +185,16 @@ function toEntry(input: OpenUbmlModelTabInput): UbmlModelEntry { ...pathField, } } + if (input.kind === "query" || input.kind === "pageflow") { + return { + key: input.key.trim(), + label: input.title.trim() || input.key.trim(), + kind: input.kind, + yaml: input.yaml, + ...(logicalKey ? { logicalKey } : {}), + ...pathField, + } + } return { key: input.key.trim(), label: input.title.trim() || input.model.entityCode, diff --git a/packages/extension/src/lib/ubml-model-tab-store.ts b/packages/extension/src/lib/ubml-model-tab-store.ts index 3bbbf259..ad7931ba 100644 --- a/packages/extension/src/lib/ubml-model-tab-store.ts +++ b/packages/extension/src/lib/ubml-model-tab-store.ts @@ -3,7 +3,7 @@ import type { EapiPreviewModel } from "./eapi-preview-model" import type { PagePreviewModel } from "./page-preview-model" import type { VoPreviewModel } from "./vo-preview-model" -export type UbmlModelKind = "entity" | "page" | "vo" | "eapi" +export type UbmlModelKind = "entity" | "page" | "vo" | "eapi" | "query" | "pageflow" export type UbmlEntityModelEntry = { key: string @@ -49,7 +49,25 @@ export type UbmlEapiModelEntry = { relativePath?: string } -export type UbmlModelEntry = UbmlEntityModelEntry | UbmlPageModelEntry | UbmlVoModelEntry | UbmlEapiModelEntry +/** 只读源码类产物(query/pageflow):无结构化预览模型,仅在“文件”视图展示 YAML。 */ +type UbmlSourceModelEntryBase = { + key: string + label: string + yaml: string + logicalKey?: string + relativePath?: string +} + +export type UbmlSourceModelEntry = + | (UbmlSourceModelEntryBase & { kind: "query" }) + | (UbmlSourceModelEntryBase & { kind: "pageflow" }) + +export type UbmlModelEntry = + | UbmlEntityModelEntry + | UbmlPageModelEntry + | UbmlVoModelEntry + | UbmlEapiModelEntry + | UbmlSourceModelEntry export type UbmlModelViewMode = "graph" | "preview" | "actions" | "file" diff --git a/packages/extension/src/pages/workbench/ubml-app-editor.tsx b/packages/extension/src/pages/workbench/ubml-app-editor.tsx index 5bf48a32..c4ca3505 100644 --- a/packages/extension/src/pages/workbench/ubml-app-editor.tsx +++ b/packages/extension/src/pages/workbench/ubml-app-editor.tsx @@ -17,7 +17,7 @@ import { repairLimitMessage, } from "../../lib/ubml-appgen-repair-loop" import { consumeDevLabPendingYaml } from "../../lib/ubml-dev-lab-bridge" -import { generateAppFromYamlInBrowser } from "../../ubml-app/browser-generate" +import { generateAppWithSourcesInBrowser } from "../../ubml-app/browser-generate" import { getAppExemplar, listAppExemplarIds } from "../../ubml-app/catalog" import { openUbmlAppWritebackDialog, UbmlAppWritebackDialog } from "../../components/ubml-app-writeback-dialog" import type { AppPreviewModel } from "../../lib/app-preview-model" @@ -142,7 +142,7 @@ export function UbmlAppEditor(props: UbmlAppEditorProps = {}) { async function runGenerate() { try { - const result = await generateAppFromYamlInBrowser(yaml()) + const { result, sources } = await generateAppWithSourcesInBrowser(yaml()) const manifest = buildAppManifest(result) as Record setLastResult(result) setLastManifest(manifest) @@ -152,7 +152,7 @@ export function UbmlAppEditor(props: UbmlAppEditorProps = {}) { setStatus(formatStatus(result)) setIssues(JSON.stringify(result.validation.issues, null, 2)) if (props.onPreview) { - props.onPreview(buildAppPreviewModel(result)) + props.onPreview(buildAppPreviewModel(result, sources)) } } catch (e) { setStatus(e instanceof Error ? e.message : String(e)) diff --git a/packages/extension/src/pages/workbench/ubml-model-panel.tsx b/packages/extension/src/pages/workbench/ubml-model-panel.tsx index ff2b9da5..4a2782cf 100644 --- a/packages/extension/src/pages/workbench/ubml-model-panel.tsx +++ b/packages/extension/src/pages/workbench/ubml-model-panel.tsx @@ -115,6 +115,11 @@ export function UbmlModelPanel(props: { tabActive?: boolean } = {}) { buildModelNavigation(state().entries, getBundleNavigationRoot()), ) + const sourceOnly = createMemo(() => { + const kind = selected()?.kind + return kind === "query" || kind === "pageflow" + }) + createEffect(() => { const entry = selected() const mode = state().viewMode @@ -124,6 +129,9 @@ export function UbmlModelPanel(props: { tabActive?: boolean } = {}) { if (entry?.kind === "eapi" && (mode === "preview" || mode === "actions")) { setUbmlModelViewMode("graph") } + if ((entry?.kind === "query" || entry?.kind === "pageflow") && mode !== "file") { + setUbmlModelViewMode("file") + } }) const selectedKey = createMemo(() => selected()?.key) @@ -205,17 +213,19 @@ export function UbmlModelPanel(props: { tabActive?: boolean } = {}) {
- + + +