跳转至
PROPOSED

ADR-AIOS-24 · XiForge UX 完善 5 议题

范围:仅 P0-XiForge stage UX/功能完善 · 不涉及 P1-xilink(由 ADR-21/23 覆盖)· 不涉及 P3-xitune(由 ADR-11 覆盖)· 不涉及 ADR-04 三层架构(L1/L2/L3 已 accepted 零修改)· 不涉及 ADR-20 codegen+算法接入(F1-F6 已全 zombie 零修改)。本 ADR 只补齐 ADR-04+ADR-20 闭环后用户实测发现的 5 个 UX/功能盲区


1. 背景与动机(Context)

ADR-AIOS-04(XiForge 架构 · accepted 2026-05-25)+ ADR-AIOS-20(XiForge 完善 · accepted 2026-06-12 · 7 fork 全 zombie)两轮闭环后,XiForge 完成度从 ~30% → ~80%,用户 2026-06-13 在 XiForge stage 实测发现 5 个未覆盖盲区,Cline-AIOS 已做代码真值核查(2026-06-13 19:50 · search_files + grep · 见 §1.6 真值附录)。

1.1 盲区 ① · design/runtime 模式命名与底层 kind: native/legacy 不一致(代码已 grep 确认)

用户原话(verbatim):

"当前调音工具 module 新建的过程中会显示 design 和 runtime,这两个模式是代表我两种模式么 · Normal:xilink 下的 module 是全功能状态 · 可以走正常的链路信息结构 · legacy 状态 · 也就是兼容调音中使用 · xilink 可以连线但是 port 信息不传递 · 只是象征意义的连接 · 在兼容模式中通过界面来发送 pid 和数据到对应的硬件 dsp"

用户拍板(2026-06-13 19:55):"我本意应该只有两种模式 · 也就是 normal 和 legacy 的定义 · 但是不知道现在为什么变成了 runtime 和 design"

代码真值(grep 确认 · 截至 2026-06-13): - UI 层下拉选项写死 design/runtime(McModulePropsPane.vue:27-28 + ModuleCreator.vue:369-370) - Schema kind 字段为 kind: 'native' | 'legacy'(types/widget.ts + canvas store) - 存在 3 处映射函数(技术债): - useXiForgeModuleOps.ts:36+50:mode: 'design'|'runtime'kind: 'native'|'legacy' - DrawerWorkspace.vue:165-184:mapModeToLabel(mode) → 'design'|'runtime' - ModuleCreator.vue:2096+2139-2146+3128:mapModuleModeToEditorMode + form.mode 默认值由 kind 推 - ThirdPartyModuleDialog.vue:184-185 用 .tp-mode-badge.design / .runtime CSS 类视觉区分

根因:UI 标签层引入了"design/runtime"中文化别名("编辑态/运行态")· 但 schema 层仍用 native/legacy · 双层命名不一致 · 用户混淆 + 代码维护负担高(3 处映射)。

1.2 盲区 ② · 工具布局编辑器"编译预览"按钮 + 主页/悬浮窗双模式预览

用户原话:

"工具布局中编译预览需要实装功能 · 编译预览后弹出当前调音工具的主页模式和悬浮窗口模式"

代码真值(已确认):XiForge stage Tab 三态 'layout' | 'code' | 'simulation'(stages/xiforge/index.vue:31)· 当前 code 模式可能是占位 · 无"编译预览"实弹窗。

根因:layout 编辑器仅设计期画布编辑 · 没有"编译为运行时双模式渲染"的预览入口 · 用户无法在保存前看到工具实际效果。

1.3 盲区 ③ · 选项中针对主页 + 悬浮窗双界面分别配置

用户原话:

"选项中可能需要针对主页和悬浮窗做两个界面"

代码真值:WidgetDef 没有 displayContexts / mainOnly / floatingOnly 字段 · 当前 widget 在所有上下文中均显示。

根因:某些 widget(如大型 spectrogram chart)可能只适合主页 · 某些(如紧凑 fader)只适合悬浮窗 · 但 schema 无法区分。

1.4 盲区 ④ · widget 二维布局缺失(代码已 grep 确认)

用户原话:

"目前还是有些控件的位置不能被表现出来 · 比如 label button toggle 放在不同位置但是实际上显示还是在一行"

用户拍板(2026-06-13 19:55):"需要你去调查我不知道原因 · 只是拖进来有些控件不能按照实际摆放的位置显示"

代码真值(已 grep 确认): - types/widget.ts:51-58 定义 WidgetDef:仅有 position: { x; y } + size: { w; h }(设计期画布坐标)· 没有 row / column / wrap / grid / flex / order / lineBreak / rowIndex 任意一个二维布局字段 - widgets/WidgetGroupLayout.vue:7-15+ 按 widget.type 硬编码分桶 · 每桶 <div class="wg-section"> flex 容器 · 桶内 v-for 横排 - position{x,y} 字段在 WidgetGroupLayout.vue + WidgetRenderer.vue 均未引用(grep 确认)

根因:Schema 表达力不足 + 渲染器算法不读 position。Schema 缺二维布局字段 · 渲染器按 type 分桶横排 · 用户拖动时画布显示了 position{x,y} 但保存后渲染时被忽略 · "label button toggle 放不同位置 · 显示却在同一行(各自 type 桶内)"。

1.5 盲区 ⑤ · 系统模块库只读 + 私有库 + 内部开发入口

用户原话:

"需要做一个机制 · 系统模块库是不可以被加载更改的 · 但是需要有一个入口 · 内部开发可以访问这些模块库并做修改 · 普通用户只能新建或者修改自己的模块库"

用户拍板(2026-06-13 19:55):"暂时按照开发者模式开关吧 · 后续发布的时候 · 这个入口删除在(变)readonly 的方式"

代码真值:当前 module 库无 readonly / system / vendor 标记 · vendor 字段存在(UID 高 16 位 = xistudio/xivst/reserved · ADR-04 已锁)· 但前端 UI 层无禁写控制。

根因:① 缺前端"系统模块库"标记 + 禁写守卫 ② 缺"开发者模式"运行时开关 ③ 缺发布构建 flag 控制开关入口可见性。

1.6 真值附录(2026-06-13 19:50 grep 确认)

真值点 代码位置 行号
design/runtime UI option McModulePropsPane.vue L27-28
design/runtime UI option ModuleCreator.vue L369-370
模式映射函数 #1 useXiForgeModuleOps.ts L36 + L50
模式映射函数 #2 DrawerWorkspace.vue L165-184
模式映射函数 #3 ModuleCreator.vue L2096 + L2139-2146 + L3128
WidgetDef schema 无二维布局字段 types/widget.ts L51-58
渲染器按 type 分桶横排 widgets/WidgetGroupLayout.vue L7-15 + 全文
XiForge Tab 三态 stages/xiforge/index.vue L31

1.7 时序与依赖

  • ADR-04 ✅ accepted 2026-05-25(L1/L2/L3 + UID + codegen + xml-tuning 退役 · 零修改)
  • ADR-20 ✅ accepted+impl 2026-06-12(7 fork 全 zombie · XiForge 30%→80% · 零修改)
  • ADR-24 提议 2026-06-13(本 ADR · 站在 ADR-04+ADR-20 之上的 UX 补齐层)
  • 实施前置:无强依赖 fork(本 ADR 与 ADR-21/22/23 完全文件正交 · ClaudeA 前端 9.5d 串行队列后追加)

2. 决议(Decision)

2.1 总体决策

新起 ADR-AIOS-24 · 在 ADR-04(架构)+ ADR-20(完善)之上,补齐 5 个 UX/功能盲区。不修改 ADR-04 三层架构 / UID 32 位 / codegen 方向。不修改 ADR-20 已 zombie 7 fork 实施。

2.2 五子决议

2.2.1 子决议 A · 模式命名统一(design/runtimeNormal/legacy)

  • A.1 UI 层统一:McModulePropsPane.vue:27-28 + ModuleCreator.vue:369-370 下拉 option value/label 改:
  • <option value="native">Normal(全功能 · xilink 链路)</option>
  • <option value="legacy">Legacy(兼容模式 · UI 直发 PID)</option>
  • A.2 移除 3 处映射函数:
  • useXiForgeModuleOps.ts:36+50 → 直接用 kind(消除 mode → kind 映射)
  • DrawerWorkspace.vue:165-184mapModeToLabel 删除 · 直接用 def.kind
  • ModuleCreator.vue:2096+2139-2146+3128 → 删 mapModuleModeToEditorMode + form.mode → 改 form.kind
  • A.3 schema 字段权威:kind: 'native' | 'legacy' 是唯一权威字段(枚举值不变 · 仅 UI 层 label 改中文)· 后端 / store / persistence 全部不动
  • A.4 ThirdPartyModuleDialog:CSS 类 .tp-mode-badge.design → .native + .runtime → .legacy(命名一致化)
  • A.5 测试基线:vitest 现有 case 中含 'design'/'runtime' 字面量需替换为 'native'/'legacy'(影响 ≤ 5 个 spec)

2.2.2 子决议 B · 编译预览实装(主页 + 悬浮窗双模式)

  • B.1 layout 编辑器加"编译预览"按钮:XiForge stages/xiforge/index.vue Tab 'layout' 内新增 toolbar 按钮 🔍 Compile Preview(图标 + 中文 tooltip "编译预览")
  • B.2 弹窗双 tab 渲染:点击后 → <el-dialog> 弹窗(全屏 80%)· 内部 2 tab:
  • Tab 1: 主页模式 → 渲染 <WidgetGroupLayout :widgets="widgetsForMain" :floatingMode="false" />
  • Tab 2: 悬浮窗模式 → 渲染 <WidgetGroupLayout :widgets="widgetsForFloating" :floatingMode="true" />
  • B.3 数据源:widgetsForMain = widgets.filter(w => w.displayContexts?.includes('main') ?? true)(默认全显示)· widgetsForFloating = widgets.filter(w => w.displayContexts?.includes('floating') ?? true)(详见 C.1)
  • B.4 floatingMode prop:WidgetGroupLayoutfloatingMode?: boolean · true 时容器最大宽度 ≤ 480px(模拟悬浮窗尺寸)+ 紧凑 padding · false 时全宽
  • B.5 实时编译:点击按钮触发 widget schema → 渲染 props 转换(纯前端 · 不调后端)· 修改 widget 后关闭弹窗重新打开即可看到最新

2.2.3 子决议 C · 主页 vs 悬浮窗双界面 schema 选项

  • C.1 WidgetDef 加字段:
    displayContexts?: ('main' | 'floating')[]  // 默认 ['main', 'floating'](两个上下文都显示)
    
  • C.2 ModuleCreator 选项编辑器:每 widget 选项面板加 2 个 checkbox:
  • ☐ 主页显示(对应 'main')
  • ☐ 悬浮窗显示(对应 'floating')
  • 默认两个都勾选 · 用户可单独取消
  • C.3 双界面独立渲染:B.3 已定义 filter 逻辑 · WidgetGroupLayout 不需要改(由父组件 props 注入 filtered widgets)
  • C.4 边界:不引入第 3 个上下文(如 'sidebar' / 'popup')· 留作 ADR-25+ 候选

2.2.4 子决议 D · widget 二维布局(grid layout)

  • D.1 WidgetDef 加字段(向后兼容):
    layout?: {
      row: number;            // 0-based 行号 · 必填
      column?: number;        // 0-based 列号 · 不填则按 row 内追加顺序
      spanColumns?: number;   // 跨列数 · 默认 1
      lineBreak?: boolean;    // 强制换行(等价 row+1, column=0)· 默认 false
    }
    
  • D.2 WidgetGroupLayout.vue 新算法:
  • Step 1:若所有 widget 均无 layout 字段 → 降级现有 type 分桶逻辑(向后兼容 ADR-20 已 zombie 7 fork 数据)
  • Step 2:若任一 widget 有 layout 字段 → 启用新 grid 算法:
    • layout.row 升序排序 · 同 row 按 layout.column 升序(无 column 则按数组顺序)
    • 渲染 <div class="wg-grid"> · 每 row 一个 <div class="wg-row" :style="{ gridTemplateColumns: ... }">
    • 容器 CSS:display: grid; grid-template-columns: repeat(12, 1fr); gap: 8px;(12 列栅格 · 业界标准)
    • widget 渲染:<WidgetRenderer :style="{ gridColumn: 'span ' + (w.layout.spanColumns ?? 1) }" />
  • D.3 ModuleCreator 编辑器 UX:
  • 拖控件到画布时自动计算 layout.row + layout.column(基于 position{x,y})
  • 加"换行"按钮(↵)→ 设置 lineBreak: true(下一 widget 自动换行)
  • 加"行号"+"列号"输入框可手动调整
  • D.4 失败回退:
  • 若 layout 字段冲突(2 widget 同 row 同 column)→ 后追加的自动 column+1
  • 若 row 跳号(0 → 5)→ 中间空行不渲染(节省空间)· 不补占位
  • 若 spanColumns > 12 → clamp 为 12

2.2.5 子决议 E · 系统模块库只读 + 私有库 + 开发者模式开关

  • E.1 模块库分类:
  • SystemLibrary(系统模块库 · vendor=xistudio · 物理路径 <install-dir>/modules/system/ · 默认 read-only)
  • UserLibrary(用户私有库 · 物理路径 <user-data>/modules/user/ · 用户可读写)
  • E.2 前端禁写守卫:DrawerWorkspace + ModuleCreator 在保存 / 编辑 / 删除时检查 module.libraryType === 'system' → 禁用 + tooltip "系统模块库只读 · 请新建到用户库"
  • E.3 开发者模式开关:
  • 设置面板新增 "开发者模式"toggle(默认 OFF · localStorage 持久化 xistudio.devMode)
  • ON 时:① E.2 禁写守卫解除(允许编辑系统库)② DrawerWorkspace 显示"📦 系统模块库"分组(默认隐藏)
  • 设置面板顶部加显著 warning:"⚠️ 开发者模式仅供内部开发 · 修改系统库将影响所有用户"
  • E.4 发布构建 flag:
  • import.meta.env.VITE_DEV_MODE_VISIBLE(.env.production 默认 false · .env.development 默认 true)
  • 设置面板"开发者模式"toggle v-if="VITE_DEV_MODE_VISIBLE" · 发布版完全不显示该入口
  • 兜底:即便绕过 UI 直接改 localStorage devMode=true · 发布版仍读 VITE_DEV_MODE_VISIBLE=false 强制忽略
  • E.5 系统库物理只读(B 级防护):
  • Electron 主进程或 Tauri 后端在启动时将 SystemLibrary 路径设为 read-only(file system 层 + 文件 mode 0444)
  • 前端写入失败时降级 fallback 到 UserLibrary(toast 提示)

2.3 非目标(Non-Goals · 严禁纳入本 ADR)

  • ❌ 不动 ADR-04 三层架构(L1/L2/L3 · 已 accepted 零修改)
  • ❌ 不动 ADR-04 UID 32 位命名空间(zombie 已稳定)
  • ❌ 不动 ADR-20 codegen + 算法接入(7 fork 全 zombie · 零修改)
  • ❌ 不引入新 widget type(本 ADR 仅升级布局/上下文/模式命名)
  • ❌ 不实现 code 模式 / simulation 模式(ADR-04 推迟下季度)
  • ❌ 不引入第 3 个 displayContext('sidebar' / 'popup')· 留作未来
  • ❌ 不引入真用户权限系统(role/admin/user)· 仅开关 + 物理 read-only
  • ❌ 不动 P1-xilink / P3-xitune / P4-xitest 业务

3. 业务行为契约(5 必填段 × 5 子决议)

3.1 子决议 A · 模式命名统一

① 输入/输出契约

// UI 下拉 option 严格枚举
type ModuleKindOption = {
  value: 'native' | 'legacy';
  label: 'Normal(全功能 · xilink 链路)' | 'Legacy(兼容模式 · UI 直发 PID)';
};

// schema 字段(权威 · 不动)
interface ModuleDef {
  kind: 'native' | 'legacy';  // 不再有 mode 字段
}

// 已删除的 3 处映射函数签名
// ❌ fetchCategories(mode: 'design' | 'runtime') → 改用 kind
// ❌ mapModeToLabel(mode?: string) → 删除
// ❌ mapModuleModeToEditorMode(mode?: string) → 删除

② 收敛/成功判据

判据 阈值 含义
UI option 全部更名 100% (design/runtimenative/legacy) grep 后无 'design'\|'runtime' 残留(测试 spec 内 mock 数据除外)
3 处映射函数移除 100% grep mapModeToLabel\|mapModuleModeToEditorMode\|fetchCategories.*mode = 0 命中
schema 字段不动 100% kind: 'native'\|'legacy' 后端 / store / persistence 零修改
测试基线零回归 现有 vitest case 全过 仅 ≤ 5 个 spec 内字面量需 search-replace

③ 失败回退路径(5 类)

# 失败 触发 UI 表现 恢复路径
1 旧持久化数据含 mode: 'design'/'runtime' 字段 用户已存的 module 文件 load 时 fallback mode='design' → kind='native' / mode='runtime' → kind='legacy' · save 时只写 kind 用户重存自动迁移
2 localStorage 含旧 'design'/'runtime' 标签 上一版本残留 启动时 migrate 一次性转换 + 删除旧 key 自动收敛
3 第三方扩展依赖 mode 字段 极端兼容 console.warn + 短期 alias def.mode = def.kind === 'legacy' ? 'runtime' : 'design'(deprecated · 6 个月后删) 扩展开发者升级
4 测试 spec mock 数据用 mode: 'design' mock fixture 老 spec 启动时报错 + 测试守护 grep 批量 search-replace
5 UI 下拉初始值无效 kind 字段缺失 默认 kind='native' + console.warn 自动收敛

④ 用户操作流(端到端 5 步)

  1. 用户打开 ModuleCreator → 看到下拉 "Normal(全功能 · xilink 链路)" / "Legacy(兼容模式 · UI 直发 PID)"
  2. 用户选 "Legacy" → 模块属性面板更新 · canvas store kind = 'legacy'
  3. 用户保存 → JSON 文件含 "kind": "legacy" · 不含 "mode" 字段
  4. 用户重启应用 → 加载 module → 下拉默认显示 "Legacy"(从 kind 读)
  5. 用户加载老版 JSON(含 "mode": "runtime")→ 自动迁移到 kind: "legacy" + 下次保存清掉 mode 字段

⑤ 端到端真值 e2e

  • playwright:加载 fixture 旧 JSON(mode: 'runtime')+ 新 JSON(kind: 'legacy')
  • 断言 ① UI 下拉只显示 "Normal"/"Legacy" 中文 · 无 "design"/"runtime" 字面量
  • ② 保存后磁盘 JSON 不含 mode 字段
  • ③ DrawerWorkspace 系统/兼容分组渲染正确
  • ④ ThirdPartyModuleDialog 徽章颜色 .native/.legacy CSS 类生效
  • ⑤ vitest 全过(基线 ≥ 250 case 零回归 + 5 spec 字面量更新)

3.2 子决议 B · 编译预览实装

① 输入/输出契约

// XiForge index.vue toolbar 按钮触发
interface CompilePreviewEvent {
  type: 'compile-preview';
  widgets: WidgetDef[];      // 当前编辑中的 widget 数组
  moduleKind: 'native' | 'legacy';
}

// CompilePreviewDialog props
interface CompilePreviewDialogProps {
  open: boolean;
  widgets: WidgetDef[];
  onClose: () => void;
}

// WidgetGroupLayout 加 prop
interface WidgetGroupLayoutProps {
  widgets: WidgetDef[];
  floatingMode?: boolean;    // true = 悬浮窗模拟尺寸 ≤ 480px
}

② 收敛/成功判据

判据 阈值 含义
按钮响应时延 ≤ 100ms 点击立即弹窗
双 tab 渲染 100% widget 显示正确(按 displayContexts 过滤) 主页 / 悬浮窗 widget 不混淆
floatingMode 容器宽度 ≤ 480px 视觉模拟悬浮窗
实时编译 修改后重新打开看到最新 不需要后端调用
按钮位置 layout tab 工具栏右侧固定位 用户视线快速找到

③ 失败回退路径(5 类)

# 失败 触发 UI 表现 恢复路径
1 widgets 数组为空 用户未拖任何控件 弹窗显示 "暂无控件 · 拖拽到画布开始编辑" 用户添加控件
2 widget schema 损坏 type 字段缺失 单 widget 渲染失败 + 红 X 占位 + tooltip 错误信息 用户修复或删除
3 主页 widget 全部隐藏 全部 displayContexts=['floating'] 主页 tab 显示 "无主页控件" 提示 用户调整 displayContexts
4 悬浮窗 widget 全部隐藏 全部 displayContexts=['main'] 悬浮窗 tab 显示 "无悬浮窗控件" 提示 用户调整 displayContexts
5 弹窗超出屏幕 小屏 < 800px 自动调整为全屏(100%)· 不留 margin 自动收敛

④ 用户操作流(端到端 5 步)

  1. 用户在 XiForge layout tab 编辑 widgets · 完成后点击 toolbar "🔍 编译预览"
  2. 弹窗打开 · 默认显示 Tab 1 "主页模式"(全宽布局 · displayContexts 含 'main' 的 widget)
  3. 用户切到 Tab 2 "悬浮窗模式" · 容器宽度 ≤ 480px · 仅显示含 'floating' 的 widget
  4. 用户关闭弹窗 · 回到 layout 编辑 · 修改 widget displayContexts(子决议 C)
  5. 用户再点击 "编译预览" → 弹窗内容立即反映最新修改

⑤ 端到端真值 e2e

  • playwright:加载 fixture 8 widget(4 main / 4 floating / 4 both)
  • 断言 ① 点击按钮 弹窗在 ≤ 100ms 出现
  • ② 主页 tab 显示 8 widget(全部默认 main)
  • ③ 悬浮窗 tab 显示 8 widget · 容器宽度 ≤ 480px
  • ④ 修改 displayContexts 后重新打开 · widget 数量正确变化
  • ⑤ 关闭弹窗后 layout 编辑器状态保持

3.3 子决议 C · 主页 vs 悬浮窗双界面 schema 选项

① 输入/输出契约

// WidgetDef 扩展(向后兼容 · optional)
interface WidgetDef {
  // ... 现有字段
  displayContexts?: ('main' | 'floating')[];  // 默认 ['main', 'floating']
}

// 选项面板 checkbox 双向绑定
interface DisplayContextPanelProps {
  modelValue: WidgetDef['displayContexts'];
  onChange: (next: ('main' | 'floating')[]) => void;
}

② 收敛/成功判据

判据 阈值 含义
默认值兼容 100% 老 widget(无字段)= 两个 context 都显示
checkbox 双向绑定 < 50ms 响应 实时反映 schema 变化
至少选 1 个 100% 不允许 displayContexts=[] · UI 强制至少勾 1
编译预览过滤 100% 准确 与 B.3 filter 逻辑一致
持久化 JSON 含 displayContexts save / load 完整往返

③ 失败回退路径(5 类)

# 失败 触发 UI 表现 恢复路径
1 字段缺失(老数据) 旧 JSON fallback ['main', 'floating'](两个都显示) 兼容收敛
2 用户取消所有 checkbox 误操作 强制至少勾 1 个 · 末次取消的 checkbox 自动重新勾上 + warning toast 自动收敛
3 字段值含未知 context 第三方扩展 仅识别 'main'/'floating' · 其余忽略 + console.warn 兼容收敛
4 字段类型错误(string 而非 array) 数据损坏 fallback 默认值 + console.error 用户重存
5 主页悬浮窗均关闭(运行时) bug widget 不渲染 · 编译预览显示提示 用户重新勾选

④ 用户操作流(端到端 5 步)

  1. 用户拖入 widget · 默认 displayContexts=['main', 'floating']
  2. 用户打开选项面板 · 看到 2 个 checkbox 默认全勾选
  3. 用户取消"主页显示" → checkbox 单选"悬浮窗" · widget 仅悬浮窗 tab 可见
  4. 用户编译预览 → 主页 tab 不见该 widget · 悬浮窗 tab 见
  5. 用户保存 module · 重新打开 · displayContexts 正确还原

⑤ 端到端真值 e2e

  • playwright:加载空 module · 拖入 3 widget · 调整 displayContexts(全 main / 全 floating / 默认两个)
  • 断言 ① 默认值 = ['main', 'floating']
  • ② checkbox 单击响应 < 50ms · v-model 同步
  • ③ 强制至少勾 1 个 · 单选取消末次自动恢复
  • ④ 保存后 JSON 含 displayContexts · reload 还原
  • ⑤ 编译预览过滤准确

3.4 子决议 D · widget 二维布局

① 输入/输出契约

// WidgetDef 扩展(向后兼容 · optional)
interface WidgetDef {
  // ... 现有字段
  layout?: {
    row: number;            // 0-based 必填
    column?: number;        // 0-based 可选
    spanColumns?: number;   // 跨列 · 默认 1 · clamp 1-12
    lineBreak?: boolean;    // 强制换行 · 默认 false
  };
}

// WidgetGroupLayout 算法签名
function computeGridLayout(widgets: WidgetDef[]): {
  rows: WidgetDef[][];      // 二维数组 · 按 row 分组
  mode: 'grid' | 'fallback'; // 任一 widget 有 layout = 'grid' · 否则 'fallback'
};

② 收敛/成功判据

判据 阈值 含义
同 row 横排 100% row=0 + row=0 同行
不同 row 换行 100% row=0 + row=1 不同行
spanColumns 生效 100% grid-column: span N
12 列栅格 100% 业界标准容器宽度等分
向后兼容 100% 老数据(无 layout 字段)走 type 分桶 fallback
拖拽 UX 自动 row/column 拖动后 < 100ms 自动赋值 编辑器流畅

③ 失败回退路径(5 类)

# 失败 触发 UI 表现 恢复路径
1 老数据无 layout 字段 ADR-20 zombie 数据 全部走现有 type 分桶横排(向后兼容) 兼容收敛
2 同 row 同 column 冲突 2 widget 都 row=0 column=0 后者自动 column++ + console.warn 自动收敛
3 row 跳号(0 → 5) 用户编辑遗漏 跳号空行不渲染(节省空间) UX 上加"自动整理 row"按钮
4 spanColumns > 12 过大值 clamp 为 12 + console.warn 自动收敛
5 算法异常 内部 bug catch + 降级现有 type 分桶 + 红色 toast 用户上报 + bug fix

④ 用户操作流(端到端 5 步)

  1. 用户拖入 label / button / toggle 3 widget 到画布(分别拖到 row 0 / row 1 / row 2)
  2. 系统自动赋值 layout.row = 0/½ · column = 0
  3. 编译预览 / 实际渲染:3 widget 各自独立一行(不再"全部挤一行")
  4. 用户拖第 4 widget(checkbox)到 row 1 + column 1 · 与 button 同行
  5. 编译预览:row 0 = label / row 1 = button + checkbox 横排 / row 2 = toggle 各占一行

⑤ 端到端真值 e2e

  • playwright:加载 fixture 6 widget(3 行 · 第 2 行 2 个 widget · 含 spanColumns=4 测试)
  • 断言 ① computeGridLayout 返回 mode='grid' + 3 行
  • ② 渲染后 DOM 中 3 个 .wg-row 元素
  • ③ row 1 有 2 个 .wg-widget 横排
  • ④ spanColumns=4 widget DOM style.gridColumn = 'span 4'
  • ⑤ 加载老 fixture(无 layout 字段)→ mode='fallback' · 走 type 分桶 · 零回归 ADR-20 已 zombie 数据

3.5 子决议 E · 系统模块库只读 + 私有库 + 开发者模式开关

① 输入/输出契约

// ModuleDef 扩展
interface ModuleDef {
  // ... 现有字段
  libraryType: 'system' | 'user';  // 必填(老数据自动 'user')
  filePath: string;                // 物理路径(用于权限检查)
}

// 设置面板 store
interface DevModeStore {
  enabled: boolean;                 // localStorage 持久化 'xistudio.devMode'
  visible: boolean;                 // 取自 import.meta.env.VITE_DEV_MODE_VISIBLE
}

// 写入守卫
function canWriteModule(module: ModuleDef, devMode: DevModeStore): {
  allowed: boolean;
  reason?: string;
}

② 收敛/成功判据

判据 阈值 含义
系统库默认禁写 100% UI 编辑/删除按钮禁用 + tooltip
用户库自由读写 100% 完全无限制
开发者模式开关 localStorage 持久化 + 即时生效 切换 < 100ms 解除/启用守卫
发布版隐藏入口 100% VITE_DEV_MODE_VISIBLE=false 后开关不显示
物理 read-only 兜底 100% 即便绕过 UI · 文件系统层 mode 0444 拒写

③ 失败回退路径(5 类)

# 失败 触发 UI 表现 恢复路径
1 系统库写入(发布版) 用户绕过 UI 物理写入失败 → toast "系统库只读 · 请在用户库新建" 用户改写用户库
2 用户库路径不存在 首次启动 自动创建 <user-data>/modules/user/ 自动收敛
3 开发者模式开关绕过(localStorage 注入) 黑客手段 发布版 VITE_DEV_MODE_VISIBLE=false 强制忽略 localStorage 读取 兜底防护
4 系统库 JSON 损坏 安装包问题 UI 标记 ⚠️ + 不阻塞用户库使用 用户重装应用
5 第三方扩展尝试写系统库 扩展开发 拒写 + console.error + 上报扩展 扩展开发者升级

④ 用户操作流(端到端 5 步)

  1. 普通用户启动 → 设置面板看不到"开发者模式"开关(发布版 VITE_DEV_MODE_VISIBLE=false)
  2. 用户打开 DrawerWorkspace · 看到"📦 用户模块库"分组(系统库分组隐藏)· 可自由读写
  3. 内部开发用户启动 dev 版 → 设置面板有"开发者模式"toggle · 默认 OFF
  4. 内部用户打开 toggle → DrawerWorkspace 显示"📦 系统模块库"分组 + 顶部 ⚠️ warning · 可编辑系统库 widget
  5. 内部用户关闭 toggle → 系统库分组隐藏 · 编辑保存按钮禁用

⑤ 端到端真值 e2e

  • playwright(dev 版):
  • 加载应用 · 设置面板有"开发者模式"toggle · 默认 OFF
  • DrawerWorkspace 不显示系统库分组
  • 切 toggle ON · 系统库分组出现 + warning · 编辑/保存按钮启用
  • 切 OFF · 分组消失 + 按钮禁用
  • playwright(production 版):
  • 加载 · 设置面板开发者模式开关
  • 即便 localStorage 注入 xistudio.devMode=true · 系统库分组仍隐藏(VITE_DEV_MODE_VISIBLE=false 兜底)
  • 物理写入测试:模拟绕过 UI 直接写 system 路径 · 文件系统拒绝(mode 0444)

4. 实施清单(fork 表)

F# UID 部门 CPU 工作量 描述
F1 P0.A24.F1-mode-rename-design-runtime-to-native-legacy 前端 P0-XiForge ClaudeA 0.8d 子决议 A · 移除 3 处映射函数(useXiForgeModuleOps + DrawerWorkspace + ModuleCreator)+ UI option 改 native/legacy 中文 + 老数据迁移 + 5 spec 字面量更新 + ThirdPartyModuleDialog CSS 类改名
F2 P0.A24.F2-compile-preview-dual-mode 前端 P0-XiForge ClaudeA 1.5d 子决议 B · CompilePreviewDialog.vue + WidgetGroupLayout.vue 加 floatingMode prop + XiForge index.vue toolbar 按钮 + 双 tab 切换 + 5 类失败回退
F3 P0.A24.F3-display-contexts-schema 前端 P0-XiForge ClaudeA 0.5d 子决议 C · WidgetDef 加 displayContexts 字段 + ModuleCreator 选项面板 2 checkbox + B.3 filter 逻辑接入 + 老数据兼容
F4 P0.A24.F4-widget-grid-layout 前端 P0-XiForge ClaudeA 2.0d 子决议 D · WidgetDef 加 layout 字段 + WidgetGroupLayout.vue 新 grid 算法(12 列栅格)+ ModuleCreator 拖拽自动赋 row/column + 换行按钮 + 5 类失败回退 + ADR-20 已 zombie 数据零回归(fallback type 分桶)
F5 P5.A24.F5-system-library-readonly-backend 后端 + 文件系统 ClaudeB 1.5d 子决议 E 后端层 · ModuleDef 加 libraryType 字段 + 物理路径区分 + 启动时设置 system 路径为 read-only + write API 加权限检查 + 用户库自动创建
F6 P0.A24.F6-dev-mode-toggle-and-system-library-ui 前端 P0-XiForge ClaudeA 1.2d 子决议 E 前端层 · 设置面板 toggle + DrawerWorkspace 系统库分组(条件显示)+ 编辑保存按钮守卫 + warning + VITE_DEV_MODE_VISIBLE flag 接入
F7 P_e2e.A24.F7-truth-e2e-xiforge-ux 🏆 测试编排 ClaudeC 1.5d playwright e2e 5 spec ≥ 25 case 真值断言(子决议 A-E 端到端)· dev 版 + production 版双跑 · 解锁 ADR-24 fulfilled 🏆

fork 总计:7 fork · 9.0d · ClaudeA 5 fork(0.8+1.5+0.5+2.0+1.2 = 6.0d)+ ClaudeB 1 fork(1.5d)+ ClaudeC 1 fork(1.5d)

依赖关系: - F1 独立(模式命名)· 与 F2-F6 完全文件正交 - F2 → F3(F2 编译预览需 F3 displayContexts filter 逻辑 · 但可并行起跑 · F3 zombie 后 F2 接入) - F4 独立(grid layout)· 与 F1-F3 + F5-F6 完全文件正交 - F5 → F6(F6 前端守卫需 F5 后端 libraryType 字段 + write API) - F7 → 全部 F1-F6 zombie(e2e 测试)

派发顺序建议(基于 ClaudeA 当前 9.5d 排满 + ClaudeB 仅 ADR-15 F6 一线): 1. 首推 F5(ClaudeB 1.5d · 后端 + 文件系统 · 当前 ClaudeB 余量充足 · 解锁 F6) 2. 次推 F1(ClaudeA 0.8d · 模式命名 · 最短独立 · 价值高 · ClaudeA 队尾追加) 3. 三推 F4(ClaudeA 2.0d · grid layout · 用户最痛点 · 独立可起手) 4. 四推 F2(ClaudeA 1.5d · 编译预览 · 需 F3 配合) 5. 五推 F3(ClaudeA 0.5d · 与 F2 同步起 · 0.5d 极短) 6. 六推 F6(ClaudeA 1.2d · 等 F5 zombie 后) 7. 七推 F7(ClaudeC 1.5d · 等 F1-F6 全 zombie · ADR-24 fulfilled 🏆)


5. 风险与缓解

风险 缓解
F1 模式命名修改影响测试基线 ≥ 5 spec 派发前 grep 列全部命中 spec 路径 · 单 commit 同步 search-replace · vitest 跑全过守护
F4 grid 算法与 ADR-20 已 zombie 7 fork 数据不兼容 F4 §D.2 Step 1 强制 fallback 现有 type 分桶(无 layout 字段时)· vitest 加专门"老数据零回归"case · 真值断言 ADR-20 demo fixture 渲染不变
F5 后端 libraryType 字段引入 schema 不兼容 老数据自动 fallback libraryType='user' · DB migration 一次性 + admin 手动 mark system · 测试覆盖双向往返
ClaudeA 队列已 9.5d → 加 6.0d = 15.5d 过长 F1+F4 优先(2.8d 高价值前置)· F2+F3+F6 排到下周 · 必要时分 2 个 sprint · 与用户协商优先级
开发者模式开关绕过(localStorage 注入) E.4 + E.5 双层兜底:VITE_DEV_MODE_VISIBLE 构建 flag + 文件系统物理 read-only · 即便 UI 绕过仍写不了系统库
编译预览 floatingMode 容器宽度 480px 不够代表性 测试机型矩阵 + 用户反馈调整 · 必要时加用户自定义宽度参数(留 ADR-25 候选)
widget 二维布局 12 列栅格用户认知负担 UX 加视觉辅助线(grid 网格背景)+ 拖拽时显示当前 row/column 提示 + 文档示例
F1 移除 3 处映射函数破坏第三方扩展 短期 6 个月 alias def.mode = def.kind === 'legacy' ? 'runtime' : 'design'(deprecated) + 文档迁移指南

6. 决议者签名

角色 签名 时间
用户 待 accept
Cline-AIOS proposed 2026-06-13 20:10

7. 状态流转

时间 事件 版本
2026-06-13 20:10 proposed by Cline-AIOS · 草案 v0.1 · 含 5 议题代码真值核查附录 v0.1

8. 关联文档

  • ADR-AIOS-04-xiforge-architecture · accepted v1.0 · 父 ADR(L1/L2/L3 三层 + UID + codegen + xml-tuning · 本 ADR 仅升级 UX · 三层架构零修改)
  • ADR-AIOS-20-xiforge-completion · accepted+impl v0.1 · 7 fork 全 zombie · XiForge 30%→80% · 本 ADR 站在其上做 UX 完善 · 老数据零回归
  • ADR-AIOS-05-xistudio-workspace · 工作区持久化(本 ADR 不动 · 仅 ModuleDef 加 libraryType 字段时引用 workspace 路径约定)
  • ADR-AIOS-21/22/23 · 完全文件正交(P1-xilink / P4-xitest realtime / mini-node UX · 不交叉)
  • ADR-AIOS-07 · 三层分工(L3 前端零数学 · 本 ADR 严守 · widget 渲染纯 UI · 不做信号处理)

9. 教训沉淀

  • 2026-06-13 20:10:用户实测 ADR-04+ADR-20 闭环后,发现 5 个 UX/功能盲区。教训 ①:UI 标签层引入中文别名("design/runtime")时 · 必须与 schema 层统一命名(否则 3 处映射函数+维护负担+用户混淆)· 后续 ADR 在拍板时 §"实施清单"必须问"是否引入 UI 别名 · 是否 schema 层同步 rename"。
  • 教训 ②:Schema 表达力不足时 · 渲染器算法天然只能按 type 分桶(WidgetDef 缺 row/column · WidgetGroupLayout 自然按 type 桶横排)· 后续 widget 类 ADR 必须在 §"业务契约 ① 输入输出"显式列 layout 字段 · 不能仅含数据字段。
  • 教训 ③:架构 ADR(ADR-04 三层 + ADR-20 codegen)就位 ≠ UX 完整闭环(同 ADR-23 §9 教训复用 · 跨 stage 共性)。后续 ADR 拍板时必须问"是否含 UX 入口 fork · 是否含编辑器 UX fork · 是否含权限 / 上下文选项 fork"避免再现盲区。
  • 代码真值核查方法论:Cline-AIOS 在起 ADR 前必须做 search_files / grep 真值核查(2026-06-13 19:50 已落地 · 4 subagent 并行 + grep 列出 3 处映射函数 + WidgetDef 字段缺失 + 渲染器算法 type 分桶根因)· 不能仅凭文档推断 · 文档 ≠ 代码真相。