XnLemon commented on code in PR #1046:
URL: https://github.com/apache/dubbo-go-pixiu/pull/1046#discussion_r4214570745


##########
admin/controller/configInfo/route_binding.go:
##########
@@ -0,0 +1,471 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package configInfo
+
+import (
+       "encoding/json"
+       "errors"
+       "fmt"
+       "io"
+       "net/http"
+       "strconv"
+       "strings"
+)
+
+import (
+       "github.com/gin-gonic/gin"
+)
+
+import (
+       adminconfig "github.com/apache/dubbo-go-pixiu/admin/config"
+       "github.com/apache/dubbo-go-pixiu/admin/logic"
+       "github.com/apache/dubbo-go-pixiu/pkg/config/schema"
+)
+
+const maxRouteBindingRequestBytes = 4 << 20
+
+type routeBindingRequest struct {
+       Object           *schema.AdminObject `json:"object"`
+       ExpectedRevision int64               `json:"expectedRevision"`
+}
+
+type routeBindingValidationResponse struct {
+       Valid  bool               `json:"valid"`
+       Object schema.AdminObject `json:"object"`
+}
+
+type routeBindingPreviewResponse struct {
+       Object schema.AdminObject `json:"object"`
+       YAML   string             `json:"yaml"`
+}
+
+type routeBindingErrorResponse struct {
+       Message string                   `json:"message"`
+       Issues  []schema.ValidationIssue `json:"issues,omitempty"`
+}
+
+// GetRouteBindingSchema returns the AdminRouteBinding form and validation
+// schema. It has no etcd dependency so the UI can render before a config
+// namespace contains any routes.
+//
+// @Tags Config
+// @Summary get API route binding schema
+// @Produce application/json
+// @Success 200 {object} string
+// @Router /config/api/route/schema [get]
+func GetRouteBindingSchema(c *gin.Context) {
+       registry, err := schema.NewBuiltinRegistry()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(registry.List()))
+}
+
+// GetRouteBindingList returns high-level route bindings from the draft or
+// published namespace. The default scope is draft; pass scope=published or
+// unpublished=0 to read the published snapshot.
+//
+// @Tags Config
+// @Summary get API route binding list
+// @Produce application/json
+// @Param scope query string false "draft or published"
+// @Param unpublished query string false "1 for draft, 0 for published"
+// @Success 200 {object} string
+// @Router /config/api/route/list [get]
+func GetRouteBindingList(c *gin.Context) {
+       store, err := logic.NewAdminRouteBindingStore()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       bindings, err := store.List(c.Request.Context(), 
routeBindingUnpublished(c))
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(bindings))
+}
+
+// GetRouteBindingDetail returns one high-level route binding by metadata.name.
+//
+// @Tags Config
+// @Summary get API route binding detail
+// @Produce application/json
+// @Param name query string true "Route binding name"
+// @Param scope query string false "draft or published"
+// @Param unpublished query string false "1 for draft, 0 for published"
+// @Success 200 {object} string
+// @Router /config/api/route/detail [get]
+func GetRouteBindingDetail(c *gin.Context) {
+       name := c.Query("name")
+       if name == "" {
+               name = c.Param("name")
+       }
+       store, err := logic.NewAdminRouteBindingStore()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       binding, err := store.Get(c.Request.Context(), name, 
routeBindingUnpublished(c))
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(binding))
+}
+
+// CreateRouteBinding validates and saves one route binding in the draft
+// namespace. The JSON body may be either an AdminObject or
+// {"object": AdminObject, "expectedRevision": number}; a form content field
+// containing the same object as YAML is also accepted for Admin compatibility.
+//
+// @Tags Config
+// @Summary create API route binding draft
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} string
+// @Router /config/api/route [post]
+func CreateRouteBinding(c *gin.Context) {
+       object, expectedRevision, err := decodeRouteBindingRequest(c)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       store, err := logic.NewAdminRouteBindingStore()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       binding, err := store.SaveDraft(c.Request.Context(), object, true, 
expectedRevision)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(binding))
+}
+
+// ModifyRouteBinding validates and replaces one route binding draft. If the
+// draft does not exist but a published binding with the same name exists, its
+// stable runtime identity is reused.
+//
+// @Tags Config
+// @Summary modify API route binding draft
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} string
+// @Router /config/api/route [put]
+func ModifyRouteBinding(c *gin.Context) {
+       object, expectedRevision, err := decodeRouteBindingRequest(c)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       store, err := logic.NewAdminRouteBindingStore()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       binding, err := store.SaveDraft(c.Request.Context(), object, false, 
expectedRevision)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(binding))
+}
+
+// DeleteRouteBinding removes one route draft and its published runtime state
+// in one atomic transaction after the caller confirms the deletion.
+//
+// @Tags Config
+// @Summary delete and publish one API route binding
+// @Produce application/json
+// @Param name query string true "Route binding name"
+// @Param expectedRevision query int false "Draft key mod revision"
+// @Success 200 {object} string
+// @Router /config/api/route [delete]
+func DeleteRouteBinding(c *gin.Context) {
+       name := c.Query("name")
+       expectedRevision, err := routeBindingExpectedRevision(c)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       store, err := logic.NewAdminRouteBindingStore()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       result, err := store.DeleteAndPublish(c.Request.Context(), name, 
expectedRevision)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(result))
+}
+
+// ValidateRouteBinding validates an object and returns schema defaults without
+// writing anything to etcd.
+//
+// @Tags Config
+// @Summary validate API route binding
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} string
+// @Router /config/api/route/validate [post]
+func ValidateRouteBinding(c *gin.Context) {
+       object, _, err := decodeRouteBindingRequest(c)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       registry, err := schema.NewBuiltinRegistry()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       normalized, err := registry.Normalize(object)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, 
adminconfig.WithRet(routeBindingValidationResponse{Valid: true, Object: 
normalized}))
+}
+
+// PreviewRouteBinding validates and compiles an object to the legacy
+// api_config.yaml shape without writing anything to etcd.
+//
+// @Tags Config
+// @Summary preview API route binding
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} string
+// @Router /config/api/route/preview [post]
+func PreviewRouteBinding(c *gin.Context) {
+       object, _, err := decodeRouteBindingRequest(c)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       registry, err := schema.NewBuiltinRegistry()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       normalized, err := registry.Normalize(object)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       compiled, err := schema.CompileAdminRouteBinding(registry, normalized)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       preview, err := compiled.PreviewYAML()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, 
adminconfig.WithRet(routeBindingPreviewResponse{Object: normalized, YAML: 
string(preview)}))
+}
+
+// PublishRouteBinding publishes one route draft in one etcd transaction. Pass
+// name and expectedRevision to reject a stale UI snapshot.
+//
+// @Tags Config
+// @Summary atomically publish one API route binding
+// @Produce application/json
+// @Param name query string true "Route binding name"
+// @Param expectedRevision query int false "Draft route key mod revision"
+// @Success 200 {object} string
+// @Router /config/api/route/publish [put]
+func PublishRouteBinding(c *gin.Context) {
+       name := c.Query("name")
+       if name == "" {
+               name = c.Param("name")
+       }
+       if strings.TrimSpace(name) == "" {
+               writeRouteBindingError(c, errors.New("route binding name is 
required for publish"))
+               return
+       }
+       expectedRevision, err := routeBindingExpectedRevision(c)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       store, err := logic.NewAdminRouteBindingStore()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       result, err := store.Publish(c.Request.Context(), name, 
expectedRevision)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(result))
+}
+
+// GetRouteBindingStatus returns the independent draft and published
+// revisions for one route binding.
+//
+// @Tags Config
+// @Summary get API route binding status
+// @Produce application/json
+// @Param name query string true "Route binding name"
+// @Success 200 {object} string
+// @Router /config/api/route/status [get]
+func GetRouteBindingStatus(c *gin.Context) {
+       name := c.Query("name")
+       if name == "" {
+               name = c.Param("name")
+       }
+       if strings.TrimSpace(name) == "" {
+               writeRouteBindingError(c, errors.New("route binding name is 
required for status"))
+               return
+       }
+       store, err := logic.NewAdminRouteBindingStore()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       status, err := store.Status(c.Request.Context(), name)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(status))
+}
+
+// GetRouteBindingDiff returns the field-level diff between one route's draft
+// and published objects.
+//
+// @Tags Config
+// @Summary get API route binding diff
+// @Produce application/json
+// @Param name query string true "Route binding name"
+// @Success 200 {object} string
+// @Router /config/api/route/diff [get]
+func GetRouteBindingDiff(c *gin.Context) {
+       name := c.Query("name")
+       if name == "" {
+               name = c.Param("name")
+       }
+       if strings.TrimSpace(name) == "" {
+               writeRouteBindingError(c, errors.New("route binding name is 
required for diff"))
+               return
+       }
+       store, err := logic.NewAdminRouteBindingStore()
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       diff, err := store.Diff(c.Request.Context(), name)
+       if err != nil {
+               writeRouteBindingError(c, err)
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithRet(diff))
+}
+
+func decodeRouteBindingRequest(c *gin.Context) (schema.AdminObject, int64, 
error) {
+       if content := c.PostForm("content"); strings.TrimSpace(content) != "" {
+               object, err := schema.DecodeAdminObjectYAML([]byte(content))
+               if err != nil {
+                       return schema.AdminObject{}, 0, err
+               }
+               expectedRevision, err := routeBindingExpectedRevision(c)
+               return object, expectedRevision, err
+       }
+
+       body, err := io.ReadAll(io.LimitReader(c.Request.Body, 
maxRouteBindingRequestBytes+1))
+       if err != nil {
+               return schema.AdminObject{}, 0, fmt.Errorf("read route binding 
request: %w", err)
+       }
+       if len(body) > maxRouteBindingRequestBytes {
+               return schema.AdminObject{}, 0, fmt.Errorf("route binding 
request exceeds %d bytes", maxRouteBindingRequestBytes)
+       }
+       body = []byte(strings.TrimSpace(string(body)))
+       if len(body) == 0 {
+               return schema.AdminObject{}, 0, errors.New("route binding 
request body is empty")
+       }
+
+       var request routeBindingRequest
+       if err := json.Unmarshal(body, &request); err == nil && request.Object 
!= nil {
+               expectedRevision := request.ExpectedRevision
+               if expectedRevision == 0 {
+                       expectedRevision, err = routeBindingExpectedRevision(c)
+               }
+               return *request.Object, expectedRevision, err
+       }
+
+       var object schema.AdminObject
+       if err := json.Unmarshal(body, &object); err == nil && (object.Kind != 
"" || object.Metadata.Name != "" || object.Spec != nil) {
+               expectedRevision, revisionErr := routeBindingExpectedRevision(c)
+               return object, expectedRevision, revisionErr
+       }
+
+       object, err = schema.DecodeAdminObjectYAML(body)
+       if err != nil {
+               return schema.AdminObject{}, 0, fmt.Errorf("decode route 
binding request: %w", err)
+       }
+       expectedRevision, revisionErr := routeBindingExpectedRevision(c)
+       return object, expectedRevision, revisionErr
+}
+
+func routeBindingExpectedRevision(c *gin.Context) (int64, error) {
+       value := strings.TrimSpace(c.Query("expectedRevision"))
+       if value == "" {
+               value = 
strings.Trim(strings.TrimSpace(c.GetHeader("If-Match")), `"`)
+       }
+       if value == "" {
+               return 0, nil
+       }
+       revision, err := strconv.ParseInt(value, 10, 64)
+       if err != nil || revision < 0 {
+               return 0, fmt.Errorf("expected revision %q is invalid", value)
+       }
+       return revision, nil
+}
+
+func routeBindingUnpublished(c *gin.Context) bool {
+       if strings.EqualFold(strings.TrimSpace(c.Query("scope")), "published") {
+               return false
+       }
+       if strings.EqualFold(strings.TrimSpace(c.Query("scope")), "draft") {
+               return true
+       }
+       return c.Query("unpublished") != "0"
+}
+
+func writeRouteBindingError(c *gin.Context, err error) {
+       if err == nil {
+               err = errors.New("unknown route binding error")
+       }
+       var validationErrors schema.ValidationErrors
+       if errors.As(err, &validationErrors) {
+               issues := append([]schema.ValidationIssue(nil), 
validationErrors...)
+               c.JSON(http.StatusOK, adminconfig.RetData{
+                       Code: adminconfig.ERR,
+                       Data: routeBindingErrorResponse{Message: err.Error(), 
Issues: issues},
+               })
+               return
+       }
+       c.JSON(http.StatusOK, adminconfig.WithError(err))

Review Comment:
   Addressed on the PR branch. `writeRouteBindingError` maps both route-binding 
conflict errors to `adminconfig.RETRY`, and 
`TestWriteRouteBindingErrorReturnsRetryCode` covers direct and wrapped 
conflicts. Verified at head `12888c81`.
   
   <details><summary>中文</summary>
   已在 PR 分支处理:`writeRouteBindingError` 会将两类路由冲突映射为 
`adminconfig.RETRY`,`TestWriteRouteBindingErrorReturnsRetryCode` 
覆盖了直接冲突和包装后的冲突。已在最新提交 `12888c81` 验证。
   </details>



##########
admin/web/src/features/config/RouteBindingEditorSections.tsx:
##########
@@ -0,0 +1,1153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  AlertCircle,
+  ArrowLeft,
+  CheckCircle2,
+  Eye,
+  FileCode2,
+  GitCompareArrows,
+  Plus,
+  Save,
+  Send,
+  Trash2,
+} from 'lucide-react'
+import { translateText } from '../../i18n'
+import type {
+  AdminRouteBindingObject,
+  RouteBindingDiff,
+  RouteBindingParam,
+  RouteBindingPublishStatus,
+} from '../../types/api'
+
+export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 
'OPTIONS', 'HEAD']
+export const PARAM_TYPES = [
+  'string',
+  'char',
+  'short',
+  'int',
+  'long',
+  'float',
+  'double',
+  'boolean',
+  'byte',
+  'date',
+  'object',
+  'java.lang.String',
+  'java.lang.Character',
+  'java.lang.Short',
+  'java.lang.Integer',
+  'java.lang.Long',
+  'java.lang.Float',
+  'java.lang.Double',
+  'java.lang.Boolean',
+  'java.lang.Byte',
+  'java.lang.Object',
+  'java.util.Date',
+]
+
+export type EditorTab = 'form' | 'preview' | 'diff' | 'yaml'
+export type BusyAction = '' | 'save' | 'publish' | 'validate' | 'preview' | 
'diff'
+export type Notice = { tone: 'success' | 'error'; text: string }
+export type IssueMessage = (path: string) => string
+export type RouteEntry = AdminRouteBindingObject['spec']['entry']
+export type RouteTarget = AdminRouteBindingObject['spec']['target']
+type EditableTargetKey = Exclude<keyof RouteTarget, 'protocol'>
+
+type HeaderProps = {
+  readonly isEnglish: boolean
+  readonly routeLabel: string
+  readonly entry: RouteEntry
+  readonly publishStatus: RouteBindingPublishStatus | null
+  readonly dirty: boolean
+  readonly published: boolean
+  readonly enabled: boolean
+  readonly currentStatus: string
+  readonly lifecycleStatus: string
+  readonly busy: BusyAction
+  readonly yamlError: string
+  readonly onBack: () => void
+  readonly onValidate: () => void
+  readonly onSave: () => void
+  readonly onPublish: () => void
+}
+
+type TabsProps = {
+  readonly isEnglish: boolean
+  readonly activeTab: EditorTab
+  readonly onChange: (tab: EditorTab) => void
+}
+
+type IdentityCardProps = {
+  readonly isEnglish: boolean
+  readonly mode: 'create' | 'edit'
+  readonly entry: RouteEntry
+  readonly name: string
+  readonly issueMessage: IssueMessage
+  readonly onNameChange: (value: string) => void
+  readonly onPathChange: (value: string) => void
+  readonly onMethodChange: (value: string) => void
+}
+
+type TargetCardProps = {
+  readonly isEnglish: boolean
+  readonly target: RouteTarget
+  readonly issueMessage: IssueMessage
+  readonly onChange: (key: EditableTargetKey, value: string) => void
+}
+
+type ParamCardProps = {
+  readonly isEnglish: boolean
+  readonly params: RouteBindingParam[]
+  readonly issueMessage: IssueMessage
+  readonly onAdd: () => void
+  readonly onUpdate: (index: number, patch: Partial<RouteBindingParam>) => void
+  readonly onRemove: (index: number) => void
+}
+
+type PublishCardProps = {
+  readonly isEnglish: boolean
+  readonly enabled: boolean
+  readonly validate: boolean
+  readonly onEnabledChange: (enabled: boolean) => void
+  readonly onValidateChange: (validate: boolean) => void
+}
+
+type FormViewProps = IdentityCardProps & TargetCardProps & ParamCardProps & 
PublishCardProps
+
+type DiffViewProps = {
+  readonly isEnglish: boolean
+  readonly diffData: RouteBindingDiff | null
+  readonly busy: BusyAction
+  readonly onRefresh: () => void
+}
+
+type PreviewViewProps = {
+  readonly isEnglish: boolean
+  readonly activeTab: 'preview' | 'yaml'
+  readonly busy: BusyAction
+  readonly previewYaml: string
+  readonly routeYaml: string
+  readonly yamlError: string
+  readonly onApplyYaml: () => void
+  readonly onChangeYaml: (value: string) => void
+  readonly onPreview: () => void
+}
+
+type ContentProps = {
+  readonly isEnglish: boolean
+  readonly loading: boolean
+  readonly activeTab: EditorTab
+  readonly mode: 'create' | 'edit'
+  readonly object: AdminRouteBindingObject
+  readonly busy: BusyAction
+  readonly diffData: RouteBindingDiff | null
+  readonly previewYaml: string
+  readonly routeYaml: string
+  readonly yamlError: string
+  readonly issueMessage: IssueMessage
+  readonly onNameChange: (value: string) => void
+  readonly onEntryChange: (key: 'path' | 'method', value: string) => void
+  readonly onTargetChange: (key: EditableTargetKey, value: string) => void
+  readonly onParamAdd: () => void
+  readonly onParamUpdate: (index: number, patch: Partial<RouteBindingParam>) 
=> void
+  readonly onParamRemove: (index: number) => void
+  readonly onEnabledChange: (enabled: boolean) => void
+  readonly onValidateChange: (validate: boolean) => void
+  readonly onRefreshDiff: () => void
+  readonly onApplyYaml: () => void
+  readonly onChangeYaml: (value: string) => void
+  readonly onPreview: () => void
+}
+
+function actionLabel(
+  isEnglish: boolean,
+  busy: BusyAction,
+  action: Exclude<BusyAction, ''>,
+  idleLabel: string,
+) {
+  if (busy !== action) return idleLabel
+  const labels: Record<Exclude<BusyAction, ''>, [string, string]> = {
+    save: ['Saving...', '保存中...'],
+    publish: ['Publishing...', '发布中...'],
+    validate: ['Checking...', '校验中...'],
+    preview: ['Generating...', '生成中...'],
+    diff: ['Loading...', '加载中...'],
+  }
+  return isEnglish ? labels[action][0] : labels[action][1]
+}
+
+function routeNameHelp(isEnglish: boolean, mode: 'create' | 'edit') {
+  if (mode === 'edit')
+    return isEnglish ? 'The key stays stable while editing.' : '编辑时保持路由键稳定。'
+  return isEnglish
+    ? 'Used as the AdminRouteBinding metadata.name.'
+    : '对应 AdminRouteBinding 的 metadata.name。'
+}
+
+function previewTitle(isEnglish: boolean, activeTab: 'preview' | 'yaml') {
+  if (activeTab === 'yaml') return 'YAML'
+  return isEnglish ? 'Runtime preview' : '运行时预览'
+}
+
+function previewDescription(isEnglish: boolean, activeTab: 'preview' | 'yaml') 
{
+  if (activeTab === 'yaml') {
+    return isEnglish
+      ? 'Edit the AdminRouteBinding source. Valid changes sync back to the 
form.'
+      : '编辑 AdminRouteBinding 源配置,语法有效的改动会同步回表单。'
+  }
+  return isEnglish
+    ? 'Generated by the current AdminRouteBinding compiler.'
+    : '由当前 AdminRouteBinding 编译器生成。'
+}
+
+function routeStateClass(dirty: boolean, published: boolean) {
+  if (dirty || !published) return 'draft'
+  return 'published'
+}
+
+function yamlSyncLabel(isEnglish: boolean, yamlError: string) {
+  if (yamlError) return isEnglish ? 'Syntax error' : '语法错误'
+  return isEnglish ? 'Synced with form' : '已与表单同步'
+}
+
+function yamlSyncClass(yamlError: string) {
+  return yamlError ? 'error' : 'synced'
+}
+
+export function RouteEditorHeader({
+  isEnglish,
+  routeLabel,
+  entry,
+  publishStatus,
+  dirty,
+  published,
+  enabled,
+  currentStatus,
+  lifecycleStatus,
+  busy,
+  yamlError,
+  onBack,
+  onValidate,
+  onSave,
+  onPublish,
+}: HeaderProps) {
+  return (
+    <>
+      <div className="route-editor-backbar">
+        <button className="route-editor-back" type="button" onClick={onBack}>
+          <ArrowLeft size={15} />
+          {isEnglish ? 'Back to API routes' : '返回 API 路由'}
+        </button>
+        <div
+          className="route-editor-revisions"
+          aria-label={isEnglish ? 'Route revisions' : '路由版本'}
+        >
+          <span className="route-revision draft">
+            {isEnglish ? 'Draft rev' : '草稿 rev'} {publishStatus?.draftRevision 
|| '-'}
+          </span>
+          <span className="route-revision published">
+            {isEnglish ? 'Published rev' : '已发布 rev'} 
{publishStatus?.publishedRevision || '-'}
+          </span>
+        </div>
+      </div>
+
+      <div className="route-editor-titlebar">
+        <div>
+          <p className="eyebrow">API ROUTE</p>
+          <h1>{routeLabel}</h1>
+          <div className="route-editor-summary">
+            <span className={`method 
${entry.method.toLowerCase()}`}>{entry.method || 'GET'}</span>
+            <span className="route-editor-path">
+              {entry.path || (isEnglish ? 'Path required' : '需要填写路径')}
+            </span>
+            <span className={`route-editor-state ${routeStateClass(dirty, 
published)}`}>
+              <i /> {currentStatus}
+            </span>
+            <span className={`route-editor-lifecycle ${enabled ? 'enabled' : 
'disabled'}`}>
+              <i /> {lifecycleStatus}
+            </span>
+          </div>
+        </div>
+        <div className="route-editor-actions">
+          <button
+            className="secondary"
+            type="button"
+            disabled={busy !== '' || Boolean(yamlError)}
+            onClick={onValidate}
+          >
+            <CheckCircle2 size={14} />
+            {actionLabel(
+              isEnglish,
+              busy,
+              'validate',
+              translateText(isEnglish ? 'en-US' : 'zh-CN', '校验'),
+            )}
+          </button>
+          <button
+            className="secondary"
+            type="button"
+            disabled={busy !== '' || Boolean(yamlError)}
+            onClick={onSave}
+          >
+            <Save size={14} />
+            {actionLabel(isEnglish, busy, 'save', isEnglish ? 'Save draft' : 
'保存草稿')}
+          </button>
+          <button
+            className="primary"
+            type="button"
+            disabled={busy !== '' || Boolean(yamlError)}
+            onClick={onPublish}
+          >
+            <Send size={14} />
+            {actionLabel(isEnglish, busy, 'publish', isEnglish ? 'Publish' : 
'发布')}
+          </button>
+        </div>
+      </div>
+    </>
+  )
+}
+
+type ContractProps = {
+  readonly isEnglish: boolean
+}
+
+export function RouteEditorContract({ isEnglish }: ContractProps) {
+  return (
+    <div className="route-editor-contract">
+      <span className="route-contract-mark">{isEnglish ? 'ATOMIC PUBLISH' : 
'原子发布'}</span>
+      <span>
+        {isEnglish
+          ? 'Saving updates this route draft. Publishing replaces only this 
route in one etcd transaction.'
+          : '保存只更新当前路由草稿。发布会通过一次 etcd 事务只替换当前路由。'}
+      </span>
+    </div>
+  )
+}
+
+type NoticeProps = {
+  readonly notice: Notice
+}
+
+export function RouteEditorNotice({ notice }: NoticeProps) {
+  if (notice.tone === 'error') {
+    return (
+      <div className="route-editor-notice error" role="alert">
+        <AlertCircle size={16} />
+        <span>{notice.text}</span>
+      </div>
+    )
+  }
+  return (
+    <output className="route-editor-notice success">
+      <CheckCircle2 size={16} />
+      <span>{notice.text}</span>
+    </output>
+  )
+}
+
+export function RouteEditorTabs({ isEnglish, activeTab, onChange }: TabsProps) 
{
+  return (
+    <div
+      className="route-editor-tabs"
+      role="tablist"
+      aria-label={isEnglish ? 'Route editor views' : '路由编辑视图'}
+    >
+      <button
+        className={activeTab === 'form' ? 'active' : ''}
+        type="button"
+        role="tab"
+        aria-selected={activeTab === 'form'}
+        onClick={() => onChange('form')}
+      >
+        {isEnglish ? 'Form' : '表单'}
+      </button>
+      <button
+        className={activeTab === 'preview' ? 'active' : ''}
+        type="button"
+        role="tab"
+        aria-selected={activeTab === 'preview'}
+        onClick={() => onChange('preview')}
+      >
+        <Eye size={14} /> {isEnglish ? 'Preview' : '预览'}
+      </button>
+      <button
+        className={activeTab === 'diff' ? 'active' : ''}
+        type="button"
+        role="tab"
+        aria-selected={activeTab === 'diff'}
+        onClick={() => onChange('diff')}
+      >
+        <GitCompareArrows size={14} /> Diff
+      </button>
+      <button
+        className="unavailable-tab"
+        type="button"
+        disabled
+        title={
+          isEnglish
+            ? 'History is not exposed by the current Admin API.'
+            : '当前 Admin API 尚未提供历史接口。'
+        }
+      >
+        {isEnglish ? 'History' : '历史'}
+      </button>
+      <button
+        className={activeTab === 'yaml' ? 'active' : ''}
+        type="button"
+        role="tab"
+        aria-selected={activeTab === 'yaml'}
+        onClick={() => onChange('yaml')}
+      >
+        <FileCode2 size={14} /> YAML
+      </button>
+    </div>
+  )
+}
+
+function RouteIdentityCard({
+  isEnglish,
+  mode,
+  entry,
+  name,
+  issueMessage,
+  onNameChange,
+  onPathChange,
+  onMethodChange,
+}: IdentityCardProps) {
+  const nameIssue = issueMessage('metadata.name')
+  const pathIssue = issueMessage('spec.entry.path')
+  const methodIssue = issueMessage('spec.entry.method')
+  return (
+    <section className="panel route-editor-card">
+      <div className="panel-head">
+        <div>
+          <h2>{isEnglish ? 'Identity and entry' : '身份与入口'}</h2>
+          <span>
+            {isEnglish
+              ? 'Define the HTTP route exposed by Pixiu.'
+              : '定义 Pixiu 对外暴露的 HTTP 路由。'}
+          </span>
+        </div>
+      </div>
+      <div className="route-entry-fields">
+        <label className="route-field">
+          <span>{isEnglish ? 'Route name' : '路由名称'}</span>
+          <input
+            value={name}
+            readOnly={mode === 'edit'}
+            aria-invalid={Boolean(nameIssue)}
+            aria-describedby="route-name-help route-name-error"
+            placeholder="create-user"
+            onChange={(event) => onNameChange(event.target.value)}
+          />
+          <small id="route-name-help">{routeNameHelp(isEnglish, mode)}</small>
+          {nameIssue && (
+            <small id="route-name-error" className="route-field-error">
+              {nameIssue}
+            </small>
+          )}
+        </label>
+        <label className="route-field">
+          <span>{isEnglish ? 'Entry protocol' : '入口协议'}</span>
+          <select value={entry.protocol} disabled>
+            <option value="http">HTTP</option>
+          </select>
+          <small>
+            {isEnglish ? 'The current schema accepts HTTP only.' : '当前 schema 
仅支持 HTTP。'}
+          </small>
+        </label>
+        <label className="route-field route-field-wide">
+          <span>{isEnglish ? 'Path' : '路径'}</span>
+          <input
+            value={entry.path}
+            aria-invalid={Boolean(pathIssue)}
+            placeholder="/api/v1/users/:id"
+            onChange={(event) => onPathChange(event.target.value)}
+          />
+          {pathIssue && <small 
className="route-field-error">{pathIssue}</small>}
+        </label>
+        <fieldset className="route-field route-field-wide 
route-method-fieldset">
+          <legend>{isEnglish ? 'HTTP method' : '请求方法'}</legend>
+          <div className="route-method-options">
+            {HTTP_METHODS.map((method) => (
+              <button
+                className={entry.method === method ? 'active' : ''}
+                key={method}
+                type="button"
+                aria-pressed={entry.method === method}
+                onClick={() => onMethodChange(method)}
+              >
+                {method}
+              </button>
+            ))}
+          </div>
+          {methodIssue && <small 
className="route-field-error">{methodIssue}</small>}
+        </fieldset>
+      </div>
+    </section>
+  )
+}
+
+type TargetFieldProps = {
+  readonly label: string
+  readonly value: string
+  readonly placeholder: string
+  readonly issue: string
+  readonly onChange: (value: string) => void
+}
+
+function TargetField({ label, value, placeholder, issue, onChange }: 
TargetFieldProps) {
+  return (
+    <label className="route-field">
+      <span>{label}</span>
+      <input
+        value={value}
+        placeholder={placeholder}
+        onChange={(event) => onChange(event.target.value)}
+      />
+      {issue && <small className="route-field-error">{issue}</small>}
+    </label>
+  )
+}
+
+function RouteTargetCard({ isEnglish, target, issueMessage, onChange }: 
TargetCardProps) {
+  const fields: Array<{
+    key: EditableTargetKey
+    label: string
+    placeholder: string
+    issuePath: string
+  }> = [
+    {
+      key: 'cluster',
+      label: 'Cluster',
+      placeholder: 'user-service',
+      issuePath: 'spec.target.cluster',
+    },
+    {
+      key: 'application',
+      label: 'Application',
+      placeholder: 'UserProvider',
+      issuePath: 'spec.target.application',
+    },
+    {
+      key: 'interface',
+      label: 'Interface',
+      placeholder: 'com.example.user.UserService',
+      issuePath: 'spec.target.interface',
+    },
+    { key: 'method', label: 'Method', placeholder: 'GetUser', issuePath: 
'spec.target.method' },
+    { key: 'group', label: 'Group', placeholder: 'stable', issuePath: '' },
+    { key: 'version', label: 'Version', placeholder: '1.0.0', issuePath: '' },
+  ]
+  return (
+    <section className="panel route-editor-card">
+      <div className="panel-head">
+        <div>
+          <h2>{isEnglish ? 'Backend target' : '后端目标'}</h2>
+          <span>
+            {isEnglish
+              ? 'Map the request to one Dubbo service method.'
+              : '将请求映射到一个 Dubbo 服务方法。'}
+          </span>
+        </div>
+      </div>
+      <div className="route-target-fields">
+        <label className="route-field">
+          <span>{isEnglish ? 'Target protocol' : '目标协议'}</span>
+          <select value={target.protocol} disabled>
+            <option value="dubbo">Dubbo</option>
+          </select>
+          <small>
+            {isEnglish ? 'The current compiler targets Dubbo.' : '当前编译器目标为 
Dubbo。'}
+          </small>
+        </label>
+        {fields.map((field) => (
+          <TargetField
+            key={field.key}
+            label={field.label}
+            value={target[field.key]}
+            placeholder={field.placeholder}
+            issue={field.issuePath ? issueMessage(field.issuePath) : ''}
+            onChange={(value) => onChange(field.key, value)}
+          />
+        ))}
+      </div>
+    </section>
+  )
+}
+
+type ParamRowProps = {
+  readonly isEnglish: boolean
+  readonly param: RouteBindingParam
+  readonly index: number
+  readonly issueMessage: IssueMessage
+  readonly onUpdate: (index: number, patch: Partial<RouteBindingParam>) => void
+  readonly onRemove: (index: number) => void
+}
+
+function RouteParamRow({
+  isEnglish,
+  param,
+  index,
+  issueMessage,
+  onUpdate,
+  onRemove,
+}: ParamRowProps) {
+  const fromIssue = issueMessage(`spec.params[${index}].from`)
+  const toIssue = issueMessage(`spec.params[${index}].to`)
+  const typeIssue = issueMessage(`spec.params[${index}].type`)
+  return (
+    <div className="route-param-row" key={`${index}-${param.from}`}>
+      <label className="route-param-field">
+        <span>{isEnglish ? 'HTTP source' : 'HTTP 来源'}</span>
+        <input
+          value={param.from}
+          aria-invalid={Boolean(fromIssue)}
+          placeholder="queryStrings.page"
+          onChange={(event) => onUpdate(index, { from: event.target.value })}
+        />
+        {fromIssue && <small className="route-field-error">{fromIssue}</small>}
+      </label>
+      <label className="route-param-field">
+        <span>{isEnglish ? 'Argument index' : '参数位置'}</span>
+        <input
+          type="number"
+          min="0"
+          value={param.to}
+          aria-invalid={Boolean(toIssue)}
+          onChange={(event) => onUpdate(index, { to: 
Number(event.target.value) })}
+        />
+        {toIssue && <small className="route-field-error">{toIssue}</small>}
+      </label>
+      <label className="route-param-field">
+        <span>{isEnglish ? 'Dubbo type' : 'Dubbo 类型'}</span>
+        <select
+          value={param.type}
+          onChange={(event) => onUpdate(index, { type: event.target.value })}
+        >
+          {PARAM_TYPES.map((type) => (
+            <option key={type} value={type}>
+              {type}
+            </option>
+          ))}
+        </select>
+        {typeIssue && <small className="route-field-error">{typeIssue}</small>}
+      </label>
+      <button
+        className="route-icon-action"
+        type="button"
+        aria-label={isEnglish ? `Remove parameter ${index + 1}` : `删除第 ${index 
+ 1} 个参数`}
+        onClick={() => onRemove(index)}
+      >
+        <Trash2 size={15} />
+      </button>
+    </div>
+  )
+}
+
+function RouteParamsCard({
+  isEnglish,
+  params,
+  issueMessage,
+  onAdd,
+  onUpdate,
+  onRemove,
+}: ParamCardProps) {
+  return (
+    <section className="panel route-param-panel">
+      <div className="panel-head route-param-head">
+        <div>
+          <h2>{isEnglish ? 'Inbound request' : '入站请求'}</h2>
+          <span>
+            {isEnglish
+              ? 'Map HTTP values to zero-based Dubbo argument indexes.'
+              : '将 HTTP 参数映射到从 0 开始的 Dubbo 参数位置。'}
+          </span>
+        </div>
+        <button className="secondary route-add-param" type="button" 
onClick={onAdd}>
+          <Plus size={14} /> {isEnglish ? 'Add parameter' : '添加参数'}
+        </button>
+      </div>
+      <div className="route-param-table">
+        {params.length === 0 ? (
+          <div className="route-param-empty">
+            <span>{isEnglish ? 'No parameter mappings yet.' : 
'暂未配置参数映射。'}</span>
+            <small>
+              {isEnglish
+                ? 'Add a mapping when the Dubbo method expects HTTP input.'
+                : '当 Dubbo 方法需要接收 HTTP 参数时,再添加映射。'}
+            </small>
+          </div>
+        ) : (
+          <>
+            <div className="route-param-header" aria-hidden="true">
+              <span>{isEnglish ? 'HTTP source' : 'HTTP 来源'}</span>
+              <span>{isEnglish ? 'Argument index' : '参数位置'}</span>
+              <span>{isEnglish ? 'Dubbo type' : 'Dubbo 类型'}</span>
+              <span />
+            </div>
+            {params.map((param, index) => (
+              <RouteParamRow
+                key={`${index}-${param.from}`}
+                isEnglish={isEnglish}
+                param={param}
+                index={index}
+                issueMessage={issueMessage}
+                onUpdate={onUpdate}
+                onRemove={onRemove}
+              />
+            ))}
+          </>
+        )}
+      </div>
+    </section>
+  )
+}
+
+function RoutePublishCard({
+  isEnglish,
+  enabled,
+  validate,
+  onEnabledChange,
+  onValidateChange,
+}: PublishCardProps) {
+  return (
+    <section className="panel route-publish-panel">
+      <div>
+        <h2>{isEnglish ? 'Release and lifecycle' : '发布与生命周期'}</h2>
+        <span>
+          {isEnglish
+            ? 'Both lifecycle changes and validation preferences are published 
with this route.'
+            : '路由启停和校验偏好都会随当前路由一起发布。'}
+        </span>
+      </div>
+      <div className="route-switches">
+        <label className="route-switch">
+          <input
+            type="checkbox"
+            checked={enabled}
+            aria-label={isEnglish ? 'Enable route' : '启用路由'}
+            onChange={(event) => onEnabledChange(event.target.checked)}
+          />
+          <span className="route-switch-track" />
+          <span>{isEnglish ? 'Route enabled' : '启用路由'}</span>
+        </label>
+        <label className="route-switch">
+          <input
+            type="checkbox"
+            checked={validate}

Review Comment:
   Addressed on the PR branch. The no-op pre-publish validation control and 
`spec.publish` schema field have been removed; publish continues to validate on 
the server. The editor handles old YAML explicitly instead of presenting the 
setting as effective. Verified at head `12888c81`.
   
   <details><summary>中文</summary>
   已在 PR 分支处理:移除了无实际作用的“发布前校验”控件和 `spec.publish` Schema 
字段;发布仍由服务端执行校验。编辑器会显式处理旧 YAML,不再把该设置展示为有效配置。已在最新提交 `12888c81` 验证。
   </details>



##########
admin/logic/route_binding.go:
##########
@@ -0,0 +1,1245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package logic
+
+import (
+       "context"
+       "encoding/json"
+       "errors"
+       "fmt"
+       "net/url"
+       "reflect"
+       "sort"
+       "strconv"
+       "strings"
+       "time"
+)
+
+import (
+       clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+import (
+       adminconfig "github.com/apache/dubbo-go-pixiu/admin/config"
+       commonyaml "github.com/apache/dubbo-go-pixiu/pkg/common/yaml"
+       legacyconfig "github.com/apache/dubbo-go-pixiu/pkg/config"
+       "github.com/apache/dubbo-go-pixiu/pkg/config/schema"
+)
+
+const (
+       routeBindingPathSegment        = "route-bindings"
+       routeBindingRevisionSegment    = "route-bindings-revision"
+       routeBindingRuntimeResourceDir = "resources"
+       maxRouteBindingIDRetries       = 16
+)
+
+var (
+       ErrRouteBindingNotFound        = errors.New("admin route binding not 
found")
+       ErrRouteBindingAlreadyExists   = errors.New("admin route binding 
already exists")
+       ErrRouteBindingNameImmutable   = errors.New("admin route binding 
metadata.name is immutable")
+       ErrRouteBindingConflict        = errors.New("admin route binding was 
modified concurrently")
+       ErrRouteBindingPublishConflict = errors.New("admin route binding 
publish conflict")
+       ErrRouteBindingRuntimeConflict = errors.New("admin route binding 
conflicts with an existing runtime route")
+)
+
+// routeBindingKV is the small part of clientv3.KV needed by the route store.
+// Keeping this boundary narrow makes the planning and transaction logic easy
+// to exercise without coupling it to the gost wrapper.
+type routeBindingKV interface {
+       Get(context.Context, string, ...clientv3.OpOption) 
(*clientv3.GetResponse, error)
+       Txn(context.Context) clientv3.Txn
+}
+
+// RouteBinding is the Admin-facing response shape. ResourceID and MethodID
+// are stable runtime identities used by Pixiu's legacy etcd watcher; they are
+// not part of the user-editable AdminRouteBinding object.
+type RouteBinding struct {
+       Object        schema.AdminObject         `json:"object"`
+       ResourceID    int                        `json:"resourceId"`
+       MethodID      int                        `json:"methodId"`
+       Revision      int64                      `json:"revision"`
+       PublishStatus *RouteBindingPublishStatus 
`json:"publishStatus,omitempty"`
+}
+
+// RouteBindingPublishResult describes one atomic publish transaction.
+type RouteBindingPublishResult struct {
+       Name              string `json:"name"`
+       Revision          int64  `json:"revision"`
+       DraftRevision     int64  `json:"draftRevision"`
+       PublishedRevision int64  `json:"publishedRevision"`
+       PublishedCount    int    `json:"publishedCount"`
+       DeletedCount      int    `json:"deletedCount"`
+}
+
+// RouteBindingPublishStatus exposes one route's draft and published state.
+// Revisions are the etcd key mod revisions for that route, not a global
+// configuration revision.
+type RouteBindingPublishStatus struct {
+       Name              string `json:"name"`
+       DraftRevision     int64  `json:"draftRevision"`
+       PublishedRevision int64  `json:"publishedRevision"`
+       DraftExists       bool   `json:"draftExists"`
+       PublishedExists   bool   `json:"publishedExists"`
+       Dirty             bool   `json:"dirty"`
+}
+
+type RouteBindingDiffChange struct {
+       Path   string `json:"path"`
+       Before any    `json:"before"`
+       After  any    `json:"after"`
+}
+
+// RouteBindingDiff compares one route's draft object with its published
+// object. A nil side represents a route that only exists in the other scope.
+type RouteBindingDiff struct {
+       Name      string                   `json:"name"`
+       Draft     *RouteBinding            `json:"draft,omitempty"`
+       Published *RouteBinding            `json:"published,omitempty"`
+       Changes   []RouteBindingDiffChange `json:"changes"`
+}
+
+type routeBindingRecord struct {
+       Object     schema.AdminObject `json:"object"`
+       ResourceID int                `json:"resourceId"`
+       MethodID   int                `json:"methodId"`
+}
+
+type routeBindingEntry struct {
+       record   routeBindingRecord
+       key      string
+       revision int64
+}
+
+// RouteBindingStore owns the AdminRouteBinding persistence and publish
+// boundary. Published runtime data is deliberately written in the existing
+// Resource/Method layout so the current Pixiu watcher can consume it.
+type RouteBindingStore struct {
+       kv       routeBindingKV
+       root     string
+       registry *schema.Registry
+}
+
+// NewRouteBindingStore creates a store over an etcd v3 client.
+func NewRouteBindingStore(kv routeBindingKV, root string, registry 
*schema.Registry) (*RouteBindingStore, error) {
+       if kv == nil {
+               return nil, errors.New("route binding etcd client is nil")
+       }
+       root = strings.TrimRight(strings.TrimSpace(root), "/")
+       if root == "" {
+               return nil, errors.New("route binding etcd root path is empty")
+       }
+       if registry == nil {
+               var err error
+               registry, err = schema.NewBuiltinRegistry()
+               if err != nil {
+                       return nil, fmt.Errorf("create route binding schema 
registry: %w", err)
+               }
+       }
+       return &RouteBindingStore{kv: kv, root: root, registry: registry}, nil
+}
+
+// NewAdminRouteBindingStore creates the production store from the Admin
+// process' configured etcd client and root path.
+func NewAdminRouteBindingStore() (*RouteBindingStore, error) {
+       if adminconfig.Bootstrap == nil {
+               return nil, errors.New("admin bootstrap is nil")
+       }
+       if adminconfig.Client == nil {
+               return nil, errors.New("admin etcd client is nil")
+       }
+       rawClient := adminconfig.Client.GetRawClient()
+       if rawClient == nil {
+               return nil, errors.New("admin raw etcd client is nil")
+       }
+       return NewRouteBindingStore(rawClient, adminconfig.Bootstrap.GetPath(), 
nil)
+}
+
+func routeBindingContext(ctx context.Context) context.Context {
+       if ctx == nil {
+               return context.Background()
+       }
+       return ctx
+}
+
+// Registry returns the schema registry used by this store.
+func (s *RouteBindingStore) Registry() *schema.Registry {
+       return s.registry
+}
+
+// List returns high-level route bindings from either the draft or published
+// namespace. Empty namespaces are returned as an empty slice.
+func (s *RouteBindingStore) List(ctx context.Context, unpublished bool) 
([]RouteBinding, error) {
+       ctx = routeBindingContext(ctx)
+       entries, err := s.listEntries(ctx, s.bindingPrefix(unpublished))
+       if err != nil {
+               return nil, err
+       }
+       var statuses map[string]RouteBindingPublishStatus
+       if unpublished {
+               publishedEntries, err := s.listEntries(ctx, 
s.bindingPrefix(false))
+               if err != nil {
+                       return nil, err
+               }
+               statuses = routeBindingStatuses(entries, publishedEntries)
+       }
+       result := make([]RouteBinding, 0, len(entries))
+       for _, entry := range entries {
+               view := routeBindingView(entry)
+               if status, exists := 
statuses[entry.record.Object.Metadata.Name]; exists {
+                       view.PublishStatus = &status
+               }
+               result = append(result, view)
+       }
+       return result, nil
+}
+
+func routeBindingStatuses(draftEntries, publishedEntries []routeBindingEntry) 
map[string]RouteBindingPublishStatus {
+       publishedByName := make(map[string]routeBindingEntry, 
len(publishedEntries))
+       for _, entry := range publishedEntries {
+               publishedByName[entry.record.Object.Metadata.Name] = entry
+       }
+
+       statuses := make(map[string]RouteBindingPublishStatus, 
len(draftEntries))
+       for _, draft := range draftEntries {
+               name := draft.record.Object.Metadata.Name
+               published, publishedExists := publishedByName[name]
+               status := RouteBindingPublishStatus{
+                       Name:              name,
+                       DraftRevision:     draft.revision,
+                       DraftExists:       true,
+                       PublishedExists:   publishedExists,
+                       PublishedRevision: published.revision,
+                       Dirty:             !publishedExists || 
!reflect.DeepEqual(draft.record.Object, published.record.Object),
+               }
+               statuses[name] = status
+       }
+       return statuses
+}
+
+// Get returns one high-level route binding from either the draft or published
+// namespace.
+func (s *RouteBindingStore) Get(ctx context.Context, name string, unpublished 
bool) (RouteBinding, error) {
+       ctx = routeBindingContext(ctx)
+       name, err := normalizeRouteBindingName(name)
+       if err != nil {
+               return RouteBinding{}, err
+       }
+       entry, exists, _, err := s.getEntry(ctx, s.bindingPrefix(unpublished), 
name)
+       if err != nil {
+               return RouteBinding{}, err
+       }
+       if !exists {
+               return RouteBinding{}, fmt.Errorf("%w: %s", 
ErrRouteBindingNotFound, name)
+       }
+       return routeBindingView(entry), nil
+}
+
+// Normalize validates an object and applies all registered schema defaults.
+// It is useful to the controller's validate and preview endpoints and does
+// not require an etcd write.
+func (s *RouteBindingStore) Normalize(object schema.AdminObject) 
(schema.AdminObject, error) {
+       normalized, err := s.registry.Normalize(object)
+       if err != nil {
+               return normalized, err
+       }
+       name, err := normalizeRouteBindingName(normalized.Metadata.Name)
+       if err != nil {
+               return normalized, err
+       }
+       normalized.Metadata.Name = name
+       return normalized, nil
+}
+
+// Preview validates and compiles an Admin object without writing it.
+func (s *RouteBindingStore) Preview(object schema.AdminObject) 
(schema.AdminObject, []byte, error) {
+       normalized, err := s.Normalize(object)
+       if err != nil {
+               return normalized, nil, err
+       }
+       compiled, err := schema.CompileAdminRouteBinding(s.registry, normalized)
+       if err != nil {
+               return normalized, nil, err
+       }
+       preview, err := compiled.PreviewYAML()
+       if err != nil {
+               return normalized, nil, err
+       }
+       return normalized, preview, nil
+}
+
+// SaveDraft validates and atomically stores one AdminRouteBinding in the
+// draft namespace. POST callers use create=true; PUT callers use create=false.
+// expectedRevision is the draft key's mod revision and may be zero to allow
+// an unconditional write.
+func (s *RouteBindingStore) SaveDraft(ctx context.Context, object 
schema.AdminObject, create bool, expectedRevision int64) (RouteBinding, error) {
+       ctx = routeBindingContext(ctx)
+       if err := validateSaveDraftRequest(create, expectedRevision); err != 
nil {
+               return RouteBinding{}, err
+       }
+       normalized, err := s.Normalize(object)
+       if err != nil {
+               return RouteBinding{}, err
+       }
+       name := normalized.Metadata.Name
+       draftPrefix := s.bindingPrefix(true)
+       draftEntry, draftExists, draftRevision, err := s.getEntry(ctx, 
draftPrefix, name)
+       if err != nil {
+               return RouteBinding{}, err
+       }
+       if err := validateDraftSaveState(name, create, expectedRevision, 
draftExists, draftRevision); err != nil {
+               return RouteBinding{}, err
+       }
+
+       resourceID, methodID, err := s.resolveRuntimeIdentity(ctx, name, 
draftEntry, draftExists)
+       if err != nil {
+               return RouteBinding{}, err
+       }
+
+       record := routeBindingRecord{
+               Object:     normalized,
+               ResourceID: resourceID,
+               MethodID:   methodID,
+       }
+       value, err := json.Marshal(record)
+       if err != nil {
+               return RouteBinding{}, fmt.Errorf("encode route binding %q: 
%w", name, err)
+       }
+       if err := s.commitDraft(ctx, name, draftPrefix, value, create, 
expectedRevision); err != nil {
+               return RouteBinding{}, err
+       }
+
+       saved, exists, revision, err := s.getEntry(ctx, draftPrefix, name)
+       if err != nil {
+               return RouteBinding{}, err
+       }
+       if !exists {
+               return RouteBinding{}, fmt.Errorf("route binding %q disappeared 
after save", name)
+       }
+       saved.revision = revision
+       return routeBindingView(saved), nil
+}
+
+// UpdateDraft updates one existing route binding draft without allowing its
+// metadata.name to change. The original name is the stable identity supplied
+// by the caller, while the object name is user-editable in the YAML editor.
+func (s *RouteBindingStore) UpdateDraft(ctx context.Context, originalName 
string, object schema.AdminObject, expectedRevision int64) (RouteBinding, 
error) {
+       originalName, err := normalizeRouteBindingName(originalName)
+       if err != nil {
+               return RouteBinding{}, err
+       }
+       normalized, err := s.Normalize(object)
+       if err != nil {
+               return RouteBinding{}, err
+       }
+       if normalized.Metadata.Name != originalName {
+               return RouteBinding{}, fmt.Errorf("%w: %q cannot be changed to 
%q", ErrRouteBindingNameImmutable, originalName, normalized.Metadata.Name)
+       }
+       return s.SaveDraft(ctx, normalized, false, expectedRevision)
+}
+
+func validateSaveDraftRequest(create bool, expectedRevision int64) error {
+       if expectedRevision < 0 {
+               return errors.New("expected revision must not be negative")
+       }
+       if create && expectedRevision > 0 {
+               return errors.New("create route binding does not accept an 
expected revision")
+       }
+       return nil
+}
+
+func validateDraftSaveState(name string, create bool, expectedRevision int64, 
exists bool, revision int64) error {
+       if create && exists {
+               return fmt.Errorf("%w: %s", ErrRouteBindingAlreadyExists, name)
+       }
+       if expectedRevision > 0 && (!exists || revision != expectedRevision) {
+               return fmt.Errorf("%w: %s", ErrRouteBindingConflict, name)
+       }
+       return nil
+}
+
+func (s *RouteBindingStore) resolveRuntimeIdentity(ctx context.Context, name 
string, draft routeBindingEntry, draftExists bool) (int, int, error) {
+       resourceID, methodID := draft.record.ResourceID, draft.record.MethodID
+       if !draftExists {
+               published, exists, _, err := s.getEntry(ctx, 
s.bindingPrefix(false), name)
+               if err != nil {
+                       return 0, 0, err
+               }
+               if exists {
+                       resourceID, methodID = published.record.ResourceID, 
published.record.MethodID
+               }
+       }
+       if resourceID <= 0 {
+               var err error
+               resourceID, err = s.allocateRuntimeID(ctx)
+               if err != nil {
+                       return 0, 0, err
+               }
+       }
+       if methodID <= 0 {
+               // One AdminRouteBinding currently compiles to exactly one 
Method. Method
+               // IDs are scoped below the resource, so sharing the stable 
route ID is
+               // safe and avoids a second allocation transaction.
+               methodID = resourceID
+       }
+       return resourceID, methodID, nil
+}
+
+func (s *RouteBindingStore) commitDraft(ctx context.Context, name, draftPrefix 
string, value []byte, create bool, expectedRevision int64) error {
+       comparisons := make([]clientv3.Cmp, 0, 1)
+       if create {
+               comparisons = append(comparisons, 
clientv3.Compare(clientv3.CreateRevision(s.bindingKey(draftPrefix, name)), "=", 
0))
+       } else if expectedRevision > 0 {
+               comparisons = append(comparisons, 
clientv3.Compare(clientv3.ModRevision(s.bindingKey(draftPrefix, name)), "=", 
expectedRevision))
+       }
+       operations := []clientv3.Op{
+               clientv3.OpPut(s.bindingKey(draftPrefix, name), string(value)),
+       }
+       transaction := s.kv.Txn(ctx)
+       if len(comparisons) > 0 {
+               transaction = transaction.If(comparisons...)
+       }
+       response, err := transaction.Then(operations...).Commit()
+       if err != nil {
+               return fmt.Errorf("save route binding %q: %w", name, err)
+       }
+       if response.Succeeded {
+               return nil
+       }
+       if create {
+               return fmt.Errorf("%w: %s", ErrRouteBindingAlreadyExists, name)
+       }
+       return fmt.Errorf("%w: %s", ErrRouteBindingConflict, name)
+}
+
+// DeleteAndPublish removes one route draft and its published runtime state in
+// the same etcd transaction. This keeps the delete flow atomic for callers:
+// there is no intermediate state where the draft is gone but the route still
+// remains active in Pixiu.
+func (s *RouteBindingStore) DeleteAndPublish(ctx context.Context, name string, 
expectedRevision int64) (RouteBindingPublishResult, error) {
+       ctx = routeBindingContext(ctx)
+       if expectedRevision < 0 {
+               return RouteBindingPublishResult{}, errors.New("expected 
revision must not be negative")
+       }
+       name, err := normalizeRouteBindingName(name)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       state, err := s.readPublishState(ctx, name)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       if expectedRevision > 0 && (!state.draftExists || state.draftRevision 
!= expectedRevision) {
+               return RouteBindingPublishResult{}, fmt.Errorf("%w: %s", 
ErrRouteBindingConflict, name)
+       }
+       comparisons, err := s.buildPublishComparisons(ctx, state)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       operations := make([]clientv3.Op, 0, 4)
+       if state.draftExists {
+               operations = append(operations, 
clientv3.OpDelete(s.bindingKey(state.draftPrefix, name)))
+       }
+       deletedCount := 0
+       if state.publishedExists {
+               if state.published.record.ResourceID <= 0 || 
state.published.record.MethodID <= 0 {
+                       return RouteBindingPublishResult{}, 
fmt.Errorf("published route binding %q has invalid runtime identity", name)
+               }
+               operations = append(operations,
+                       
clientv3.OpDelete(s.runtimeResourceKey(state.published.record.ResourceID), 
clientv3.WithPrefix()),
+                       clientv3.OpDelete(s.bindingKey(state.publishedPrefix, 
name)),
+               )
+               deletedCount = 1
+       }
+       operations = append(operations,
+               clientv3.OpPut(s.publishedRevisionKey(), 
strconv.FormatInt(time.Now().UnixNano(), 10)))
+       response, err := 
s.kv.Txn(ctx).If(comparisons...).Then(operations...).Commit()
+       if err != nil {
+               return RouteBindingPublishResult{}, fmt.Errorf("delete route 
binding %q: %w", name, err)
+       }
+       if !response.Succeeded {
+               return RouteBindingPublishResult{}, fmt.Errorf("%w: %s", 
ErrRouteBindingPublishConflict, name)
+       }
+       return RouteBindingPublishResult{
+               Name:              name,
+               Revision:          response.Header.Revision,
+               DraftRevision:     state.draftRevision,
+               PublishedRevision: 0,
+               DeletedCount:      deletedCount,
+       }, nil
+}
+
+// Publish publishes exactly one route binding. The generated legacy resource
+// and method, the published high-level binding, and the internal generation
+// guard are written or deleted in one etcd transaction. Other routes' draft
+// and published values are left untouched.
+func (s *RouteBindingStore) Publish(ctx context.Context, name string, 
expectedDraftRevision int64) (RouteBindingPublishResult, error) {
+       ctx = routeBindingContext(ctx)
+       if expectedDraftRevision < 0 {
+               return RouteBindingPublishResult{}, errors.New("expected draft 
revision must not be negative")
+       }
+       name, err := normalizeRouteBindingName(name)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       state, err := s.readPublishState(ctx, name)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       if expectedDraftRevision > 0 && (!state.draftExists || 
state.draftRevision != expectedDraftRevision) {
+               return RouteBindingPublishResult{}, fmt.Errorf("%w: %s", 
ErrRouteBindingPublishConflict, name)
+       }
+       publishedEntries, err := s.listEntries(ctx, state.publishedPrefix)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       candidate, err := s.compilePublishCandidate(name, state.draft, 
state.draftExists, publishedEntries)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       if err := s.checkRuntimeRouteConflict(ctx, candidate, state); err != 
nil {
+               return RouteBindingPublishResult{}, err
+       }
+       comparisons, err := s.buildPublishComparisons(ctx, state)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       operations, publishedCount, deletedCount, err := 
s.buildSinglePublishOperations(state, candidate)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       operations = append(operations,
+               clientv3.OpPut(s.publishedRevisionKey(), 
strconv.FormatInt(time.Now().UnixNano(), 10)))
+       response, err := 
s.kv.Txn(ctx).If(comparisons...).Then(operations...).Commit()
+       if err != nil {
+               return RouteBindingPublishResult{}, fmt.Errorf("publish route 
binding %q: %w", name, err)
+       }
+       if !response.Succeeded {
+               return RouteBindingPublishResult{}, fmt.Errorf("%w: %s", 
ErrRouteBindingPublishConflict, name)
+       }
+       publishedRevision, err := s.publishedRouteRevision(ctx, 
state.publishedPrefix, name, candidate)
+       if err != nil {
+               return RouteBindingPublishResult{}, err
+       }
+       return RouteBindingPublishResult{
+               Name:              name,
+               Revision:          response.Header.Revision,
+               DraftRevision:     state.draftRevision,
+               PublishedRevision: publishedRevision,
+               PublishedCount:    publishedCount,
+               DeletedCount:      deletedCount,
+       }, nil
+}
+
+type routeBindingPublishState struct {
+       draftPrefix       string
+       publishedPrefix   string
+       draft             routeBindingEntry
+       draftExists       bool
+       draftRevision     int64
+       published         routeBindingEntry
+       publishedExists   bool
+       publishedRevision int64
+}
+
+func (s *RouteBindingStore) readPublishState(ctx context.Context, name string) 
(routeBindingPublishState, error) {
+       state := routeBindingPublishState{
+               draftPrefix:     s.bindingPrefix(true),
+               publishedPrefix: s.bindingPrefix(false),
+       }
+       var err error
+       state.draft, state.draftExists, state.draftRevision, err = 
s.getEntry(ctx, state.draftPrefix, name)
+       if err != nil {
+               return routeBindingPublishState{}, err
+       }
+       state.published, state.publishedExists, state.publishedRevision, err = 
s.getEntry(ctx, state.publishedPrefix, name)
+       if err != nil {
+               return routeBindingPublishState{}, err
+       }
+       if !state.draftExists && !state.publishedExists {
+               return routeBindingPublishState{}, fmt.Errorf("%w: %s", 
ErrRouteBindingNotFound, name)
+       }
+       return state, nil
+}
+
+func (s *RouteBindingStore) compilePublishCandidate(name string, draft 
routeBindingEntry, draftExists bool, published []routeBindingEntry) 
(*compiledRouteEntry, error) {
+       if !draftExists {
+               return nil, nil
+       }
+       entries := make([]routeBindingEntry, 0, len(published)+1)
+       for _, entry := range published {
+               if entry.record.Object.Metadata.Name != name {
+                       entries = append(entries, entry)
+               }
+       }
+       entries = append(entries, draft)
+       compiled, err := s.compileSnapshot(entries)
+       if err != nil {
+               return nil, err
+       }
+       for index := range compiled {
+               if compiled[index].object.Metadata.Name == name {
+                       return &compiled[index], nil
+               }
+       }
+       return nil, fmt.Errorf("route binding %q was not compiled", name)
+}
+
+func (s *RouteBindingStore) buildPublishComparisons(ctx context.Context, state 
routeBindingPublishState) ([]clientv3.Cmp, error) {
+       comparisons := make([]clientv3.Cmp, 0, 2)
+       if state.draftExists {
+               // Always compare the value observed above, even when the 
caller omitted
+               // expectedDraftRevision. This closes the read/modify/write 
race.
+               comparisons = append(comparisons,
+                       
clientv3.Compare(clientv3.ModRevision(s.bindingKey(state.draftPrefix, 
state.draft.record.Object.Metadata.Name)), "=", state.draftRevision))
+               if state.publishedExists {
+                       comparisons = append(comparisons,
+                               
clientv3.Compare(clientv3.ModRevision(s.bindingKey(state.publishedPrefix, 
state.published.record.Object.Metadata.Name)), "=", state.publishedRevision))
+               } else {
+                       comparisons = append(comparisons,
+                               
clientv3.Compare(clientv3.CreateRevision(s.bindingKey(state.publishedPrefix, 
state.draft.record.Object.Metadata.Name)), "=", 0))
+               }
+       } else {
+               comparisons = append(comparisons,
+                       
clientv3.Compare(clientv3.ModRevision(s.bindingKey(state.publishedPrefix, 
state.published.record.Object.Metadata.Name)), "=", state.publishedRevision))
+       }
+       publishedMarkerRevision, publishedMarkerExists, err := 
s.markerRevision(ctx, s.publishedRevisionKey())
+       if err != nil {
+               return nil, err
+       }
+       if publishedMarkerExists {
+               comparisons = append(comparisons,
+                       
clientv3.Compare(clientv3.ModRevision(s.publishedRevisionKey()), "=", 
publishedMarkerRevision))
+       } else {
+               comparisons = append(comparisons,
+                       
clientv3.Compare(clientv3.CreateRevision(s.publishedRevisionKey()), "=", 0))
+       }
+       return comparisons, nil
+}
+
+func (s *RouteBindingStore) buildSinglePublishOperations(state 
routeBindingPublishState, candidate *compiledRouteEntry) ([]clientv3.Op, int, 
int, error) {
+       if candidate == nil {
+               return []clientv3.Op{
+                       
clientv3.OpDelete(s.runtimeResourceKey(state.published.record.ResourceID), 
clientv3.WithPrefix()),
+                       clientv3.OpDelete(s.bindingKey(state.publishedPrefix, 
state.published.record.Object.Metadata.Name)),
+               }, 0, 1, nil
+       }
+       compiled := candidate.legacy
+       operations := make([]clientv3.Op, 0, 4)
+       if state.publishedExists && state.published.record.ResourceID != 
compiled.Resource.ID {
+               operations = append(operations,
+                       
clientv3.OpDelete(s.runtimeResourceKey(state.published.record.ResourceID), 
clientv3.WithPrefix()))
+       } else if state.publishedExists && state.published.record.MethodID != 
compiled.Method.ID {
+               operations = append(operations,
+                       
clientv3.OpDelete(s.runtimeMethodKey(state.published.record.ResourceID, 
state.published.record.MethodID)))
+       }
+       resourceValue, methodValue, err := marshalCompiledRuntimeRoute(compiled)
+       if err != nil {
+               return nil, 0, 0, fmt.Errorf("encode route binding %q for 
runtime: %w", candidate.object.Metadata.Name, err)
+       }
+       recordValue, err := json.Marshal(routeBindingRecord{
+               Object:     candidate.object,
+               ResourceID: compiled.Resource.ID,
+               MethodID:   compiled.Method.ID,
+       })
+       if err != nil {
+               return nil, 0, 0, fmt.Errorf("encode published route binding 
%q: %w", candidate.object.Metadata.Name, err)
+       }
+       operations = append(operations,
+               clientv3.OpPut(s.runtimeResourceKey(compiled.Resource.ID), 
string(resourceValue)),
+               clientv3.OpPut(s.runtimeMethodKey(compiled.Resource.ID, 
compiled.Method.ID), string(methodValue)),
+               clientv3.OpPut(s.bindingKey(state.publishedPrefix, 
candidate.object.Metadata.Name), string(recordValue)))
+       return operations, 1, 0, nil
+}
+
+func (s *RouteBindingStore) publishedRouteRevision(ctx context.Context, 
prefix, name string, candidate *compiledRouteEntry) (int64, error) {
+       if candidate == nil {
+               return 0, nil
+       }
+       _, exists, revision, err := s.getEntry(ctx, prefix, name)
+       if err != nil {
+               return 0, err
+       }
+       if !exists {
+               return 0, fmt.Errorf("route binding %q disappeared after 
publish", name)
+       }
+       return revision, nil
+}
+
+// checkRuntimeRouteConflict protects the breaking migration boundary between
+// the legacy Resource/Method keys and AdminRouteBinding. Existing runtime
+// method keys are still consumed by Pixiu's watcher, even when they are not
+// visible through the new Admin API. Publishing a new route with the same
+// HTTP method and path would otherwise commit successfully and leave the
+// watcher with duplicate runtime routes.
+func (s *RouteBindingStore) checkRuntimeRouteConflict(ctx context.Context, 
candidate *compiledRouteEntry, state routeBindingPublishState) error {
+       if candidate == nil {
+               return nil
+       }
+       prefix := s.runtimeResourcePrefix()
+       response, err := s.kv.Get(ctx, prefix, clientv3.WithPrefix())
+       if err != nil {
+               return fmt.Errorf("list existing runtime methods: %w", err)
+       }
+       candidatePath := candidate.legacy.Resource.Path
+       candidateVerb := strings.TrimSpace(candidate.legacy.Method.HTTPVerb)
+       for _, kv := range response.Kvs {
+               resourceID, methodID, ok, err := runtimeMethodIDs(prefix, 
string(kv.Key))
+               if err != nil {
+                       return err
+               }
+               if !ok {
+                       continue
+               }
+               // A republish of the same published AdminRouteBinding owns 
this runtime
+               // identity; a new route must not bypass the check by reusing 
an orphaned
+               // legacy method key with the same numeric IDs.
+               if state.publishedExists && resourceID == 
state.published.record.ResourceID && methodID == 
state.published.record.MethodID {
+                       continue
+               }
+               var method legacyconfig.Method
+               if err := commonyaml.UnmarshalYML(kv.Value, &method); err != 
nil {
+                       return fmt.Errorf("decode existing runtime method %q: 
%w", string(kv.Key), err)
+               }
+               if method.ResourcePath == candidatePath && 
strings.EqualFold(method.HTTPVerb, candidateVerb) {
+                       return fmt.Errorf("%w: %s %s already exists at %q", 
ErrRouteBindingRuntimeConflict, candidateVerb, candidatePath, string(kv.Key))

Review Comment:
   Addressed and covered by regression tests. The checker derives effective 
paths from nested Resource hierarchy when `Method.ResourcePath` is empty, and 
normalizes path and verb comparisons to match runtime routing. Tests cover an 
empty method path under nested resources and case differences. Verified with 
`go test ./admin/logic` at head `12888c81`.
   
   <details><summary>中文</summary>
   已处理并补充回归测试:当 `Method.ResourcePath` 为空时,冲突检查会从嵌套 Resource 
层级推导有效路径;路径和方法比较也按运行时规则进行规范化。测试覆盖了嵌套路由空路径字段及大小写差异。最新提交 `12888c81` 下 `go test 
./admin/logic` 通过。
   </details>



##########
pkg/config/schema/README.md:
##########
@@ -0,0 +1,43 @@
+# Admin route binding model
+
+This package is an initial code-level model for replacing raw Resource and
+Method YAML editing with one Admin-facing route binding.
+
+The supported flow is:
+
+```text
+AdminRouteBinding
+    -> schema defaults and validation
+    -> legacy Resource + Method compilation
+    -> api_config.yaml preview
+    -> future draft and publish storage
+```
+
+## Scope
+
+The first version deliberately covers one HTTP entry mapped to one
+registry-backed Dubbo method. Listener, Cluster, PluginGroup, direct provider
+URLs, and the actual etcd publish transaction are outside this package.
+
+`Resource` and `Method` are generated Pixiu runtime structures. They are not
+registered as Admin form objects.
+
+## Object boundary
+
+`AdminObject` keeps only a stable `kind`, `metadata`, and dynamic
+`spec map[string]any`. `ObjectSchema` and `FieldSchema` define the form,
+defaults, and validation rules for the dynamic values.
+
+The built-in `AdminRouteBinding` schema has five user-facing sections:
+
+- `entry`: HTTP protocol, path, and method.
+- `target`: Dubbo application, interface, method, version, group, and cluster.
+- `params`: ordered HTTP-source to Dubbo-argument mappings.
+- `enabled`: whether the published route accepts requests; it defaults to 
`true`.
+- `publish`: Admin control-plane intent; it is never emitted into Pixiu YAML.

Review Comment:
   Addressed on the PR branch. The schema README now states that 
draft/published lifecycle state is tracked by Admin separately and that 
`spec.publish` is not a user-facing field. Verified at head `12888c81`.
   
   <details><summary>中文</summary>
   已在 PR 分支处理:Schema README 现已说明草稿/发布生命周期由 Admin 单独管理,`spec.publish` 
不是面向用户的配置字段。已在最新提交 `12888c81` 验证。
   </details>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to