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/runtime → Normal/legacy)
- A.1 UI 层统一:
McModulePropsPane.vue:27-28+ModuleCreator.vue:369-370下拉 optionvalue/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-184→mapModeToLabel删除 · 直接用def.kindModuleCreator.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.vueTab '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:
WidgetGroupLayout加floatingMode?: boolean·true时容器最大宽度 ≤ 480px(模拟悬浮窗尺寸)+ 紧凑 padding ·false时全宽 - B.5 实时编译:点击按钮触发 widget schema → 渲染 props 转换(纯前端 · 不调后端)· 修改 widget 后关闭弹窗重新打开即可看到最新
2.2.3 子决议 C · 主页 vs 悬浮窗双界面 schema 选项
- C.1 WidgetDef 加字段:
- 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 加字段(向后兼容):
- 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/runtime → native/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 步)
- 用户打开 ModuleCreator → 看到下拉 "Normal(全功能 · xilink 链路)" / "Legacy(兼容模式 · UI 直发 PID)"
- 用户选 "Legacy" → 模块属性面板更新 · canvas store
kind = 'legacy' - 用户保存 → JSON 文件含
"kind": "legacy"· 不含"mode"字段 - 用户重启应用 → 加载 module → 下拉默认显示 "Legacy"(从 kind 读)
- 用户加载老版 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/.legacyCSS 类生效 - ⑤ 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 步)
- 用户在 XiForge layout tab 编辑 widgets · 完成后点击 toolbar "🔍 编译预览"
- 弹窗打开 · 默认显示 Tab 1 "主页模式"(全宽布局 · displayContexts 含 'main' 的 widget)
- 用户切到 Tab 2 "悬浮窗模式" · 容器宽度 ≤ 480px · 仅显示含 'floating' 的 widget
- 用户关闭弹窗 · 回到 layout 编辑 · 修改 widget displayContexts(子决议 C)
- 用户再点击 "编译预览" → 弹窗内容立即反映最新修改
⑤ 端到端真值 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 步)
- 用户拖入 widget · 默认 displayContexts=['main', 'floating']
- 用户打开选项面板 · 看到 2 个 checkbox 默认全勾选
- 用户取消"主页显示" → checkbox 单选"悬浮窗" · widget 仅悬浮窗 tab 可见
- 用户编译预览 → 主页 tab 不见该 widget · 悬浮窗 tab 见
- 用户保存 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 步)
- 用户拖入 label / button / toggle 3 widget 到画布(分别拖到 row 0 / row 1 / row 2)
- 系统自动赋值 layout.row = 0/½ · column = 0
- 编译预览 / 实际渲染:3 widget 各自独立一行(不再"全部挤一行")
- 用户拖第 4 widget(checkbox)到 row 1 + column 1 · 与 button 同行
- 编译预览: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 步)
- 普通用户启动 → 设置面板看不到"开发者模式"开关(发布版 VITE_DEV_MODE_VISIBLE=false)
- 用户打开 DrawerWorkspace · 看到"📦 用户模块库"分组(系统库分组隐藏)· 可自由读写
- 内部开发用户启动 dev 版 → 设置面板有"开发者模式"toggle · 默认 OFF
- 内部用户打开 toggle → DrawerWorkspace 显示"📦 系统模块库"分组 + 顶部 ⚠️ warning · 可编辑系统库 widget
- 内部用户关闭 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 分桶根因)· 不能仅凭文档推断 · 文档 ≠ 代码真相。