Web 工作台与 HTTP API(ui)¶
devconfig-gen ui 启动一个零构建、零前端依赖的本地单页应用:HTML/CSS/JS
全部内嵌在 src/devconfig_gen/web_ui.py 中,由 Python 标准库
ThreadingHTTPServer 提供,不需要 npm / node_modules。
devconfig-gen ui
devconfig-gen ui --host 127.0.0.1 --port 8848 --workspace ~/projects/my-app --no-browser
| 参数 | 默认 | 说明 |
|---|---|---|
--host |
127.0.0.1 |
监听地址 |
--port |
8848 |
监听端口 |
--no-browser |
否 | 不自动打开浏览器 |
--workspace |
当前工作目录 | 允许 /api/export 写入的根目录 |
启动后终端打印本地地址、导出工作空间和停止方式;按 Ctrl+C 停止并返回
0。浏览器默认打开 http://127.0.0.1:8848。
界面功能¶
工作台是一个左右双栏的单页应用:左侧按 Provider 的声明式 steps 编辑,
右侧实时预览序列化产物。自 1.1.0 起,字段渲染完全查表驱动,界面也做了
一次视觉刷新(更大的圆角卡片、胶囊按钮、更柔和的阴影与间距)。
- 中英双语:右上角
中 / EN一键切换,偏好保存在localStorage["dcg_lang"];步骤/字段文案优先使用 Provider 的i18n翻译(内置 Provider 提供中文)。 - 亮 / 暗主题:页面根据
prefers-color-scheme自动切换配色,也可由 系统主题即时驱动,无需手动设置。 - 分步表单向导:按 Provider 的
steps渲染步骤导航,逐字段编辑;上一步/下一步在步骤间移动,最后一步的按钮显示为完成 ✓。 - 实时预览:左侧编辑,右侧实时显示序列化后的产物;输入防抖 150ms。
- 内联校验:预览区底部显示“配置有效”或“N 个校验问题”,并把诊断 挂到对应字段上。
- 表驱动的字段 Widget:内置六种字段类型各对应一个默认 Widget——
string(文本输入)、integer(数字输入)、boolean(复选框)、mapping(键值对表格)、document(拖拽/内联文本编辑器)、tree(递归树编辑器)。choices字段渲染为下拉选择框。 - 树编辑器(
tree):结构模式支持任意层级增删字段/项、切换类型 string/number/boolean/object/array/null、嵌套(往里)与批量添加; 文本模式可直接输入 JSON/YAML。批量添加是根节点工具栏里的批量添加开关,展开后出现输入栏,Enter或确认提交,✕取消;逗号(中英文) 分隔多个key或key=value,值会自动识别 number/boolean/null。 - 文档编辑器(
document):拖拽/点击上传.json/.yaml/.yml文件, 或直接编辑文本(载入示例、格式化、清空)。 - 导入文件:顶部按钮上传文档,通过
/api/parse反向解析并回填表单。 - 加载预设:为
custom/json/env提供内置示例数据。 - 全部清空:重置当前 Provider 的表单内容。
- 草稿保存:每次编辑写入
localStorage["devconfig_draft_<provider>"], 切换 Provider 或刷新后自动恢复。若草稿是空上下文({}或仅含一个空document),会被视为“无草稿”,从而回退显示 Provider 的起始示例数据, 不会被陈旧的清空状态遮住。 - 保存到磁盘:弹出目标目录输入框,通过
/api/export写入并返回保存 的文件列表。 - 复制:把预览内容复制到剪贴板。
- 快捷键:
Cmd/Ctrl + Enter下一步(最后一步显示完成提示),Cmd/Ctrl + S保存到磁盘。 - 输出格式开关:YAML / JSON。XML 按钮是预留入口,点击只弹出 “即将支持”提示,不会切换格式。
- 汉堡侧边栏:左上角
☰(三条线)打开侧边栏,提供 GitHub 仓库、 文档站点、问题反馈与邮箱的快捷链接;点击遮罩、✕或按Esc收起, 链接均以新标签页打开。
安全边界¶
Web 工作台按“仅本机”设计:
- 仅回环:
Host头不是127.0.0.1、localhost、::1(或空)的请求 直接返回403 {"error": "forbidden host"},用于缓解 DNS rebinding; - 导出沙箱:
/api/export的目标目录必须等于或位于workspace_root(默认当前目录)之内,否则返回400; - 不缓存页面:
/与/index.html返回Cache-Control: no-store, ..., JSON 接口返回Cache-Control: no-store; - 无外部资源:页面不引用任何 CDN 或远程资源。
HTTP API¶
所有接口仅接受本机请求。请求体必须是 UTF-8 编码的 JSON 对象;错误统一返回
{"error": "<message>"} 和 400(未知路由返回 404)。
GET /api/providers¶
返回当前注册表中的 Provider 名称列表。
{ "providers": ["custom", "env", "json"] }
GET /api/schema¶
返回 [step.as_dict() ...]。默认 Provider 为 custom;未知 Provider 返回
400。
curl 'http://127.0.0.1:8848/api/schema?provider=env'
GET /api/widgets¶
返回 Provider 通过可选方法 web_ui_widgets() 声明的自定义 WebUI Widget。
默认 Provider 为 custom;未知 Provider 返回 400。未声明 Widget 的
Provider 返回空对象(行为与之前完全一致)。
{
"provider": "singbox",
"widgets": {
"node-editor": "(ctx) => { const el = document.createElement('div'); /* ... */ return el; }"
}
}
前端在加载 schema 前先调用该接口,把返回的工厂函数注册进同一张 Widget 表; 求值失败或返回非函数时回退到该字段类型的内置 Widget。
POST /api/validate¶
请求:
{ "provider": "env", "context": {} }
响应:
{
"valid": false,
"diagnostics": [
{ "field": "variables", "message": "variables must not be empty", "severity": "error" }
]
}
provider 默认 custom,context 默认 {};未知 Provider 返回 400。
POST /api/generate¶
请求:
{ "provider": "custom", "context": { "document": { "app": { "name": "web" } } }, "format": "yaml" }
响应:
{
"artifacts": [
{
"name": "custom.yaml",
"content": "app:\n name: web\n",
"media_type": "application/yaml"
}
]
}
format默认yaml;字符串产物(如.env)原样返回,结构化产物按媒体 类型序列化为字符串;- 校验失败返回
400; - 该接口不写文件,仅返回内容用于预览。
POST /api/export¶
与 /api/generate 相同的请求字段,外加 output_dir:
{
"provider": "custom",
"context": { "document": { "a": 1 } },
"format": "json",
"output_dir": "."
}
成功响应:
{ "success": true, "saved": ["/abs/path/custom.json"] }
output_dir 相对 workspace_root 解析;越界返回 400。
POST /api/parse¶
把 JSON/YAML 文本解析为上下文,用于文件上传和文本模式编辑:
{ "content": "app:\n name: parsed-svc\n" }
{ "context": { "app": { "name": "parsed-svc" } } }
content 必须是字符串;解析失败返回 400。
Provider 自定义 Widget¶
字段渲染是查表驱动的:内置 string / integer / boolean / mapping /
document / tree 六种类型各对应一个默认 Widget,注册在
DEFAULT_WIDGET_FACTORIES,启动时由 registerDefaultWidgets() 写入
WidgetRegistry。Provider 可实现可选方法 web_ui_widgets(),把某个
field.type 映射到一段 JavaScript 工厂源码,从而在不改动前端文件的前提下
引入全新字段类型(或覆盖内置类型)。
class MyProvider:
name = "mydomain"
def web_ui_widgets(self):
return {
"node-editor": (
"(ctx) => {"
" const el = document.createElement('div');"
" el.className = 'node-editor';"
" return el;"
"}"
)
}
工厂上下文 ctx¶
工厂接收一个 ctx 参数,返回一个 DOM 元素(即该字段的 .form-group
内容):
ctx.field:当前ProviderField(含name、type、default、choices、i18n等);ctx.fid:字段名转义后的 id 片段;ctx.grp:容器元素;ctx.provider:当前 Provider 名称;ctx.existing:字段当前值;ctx.setValue(v)/ctx.setFormData(next):写回并触发草稿保存与实时预览;ctx.getFormData()/ctx.rerender():读取整份表单 / 重绘当前步骤。
回退与隔离¶
- 只接受同源
/api/widgets返回的代码;Widget 代码由 Provider 作者负责, 加载失败会在控制台报错并回退到内置 Widget; - 某个
field.type未注册任何 Widget 时,统一回退到string的默认 Widget; - 切换 Provider 时会先恢复默认 Widget 表(
resetWidgets()),再注册新 Provider 的 Widget; - 未实现
web_ui_widgets()的 Provider 行为完全不变。
WebUIWidgets 协议(devconfig_gen.models,并已从包根导出)仅作文档性
声明,不强制继承。
Python 入口¶
from devconfig_gen import run_web_ui
run_web_ui(
host="127.0.0.1",
port=8848,
open_browser=False,
workspace_root=".",
)
run_web_ui 阻塞运行,直到 Ctrl+C;registry 参数可注入自定义
ProviderRegistry。测试可以直接实例化
devconfig_gen.web_ui.WebUIRequestHandler,通过类属性 registry 和
workspace_root 注入自定义注册表与沙箱目录(tests/test_web_ui.py 即采用
这种方式)。