@givebest/astro-visual-editor
v0.1.16
Published
Visual selection and safe source editing for Astro development
Readme
Astro Visual Editor
English | 简体中文
面向开发环境的 Astro 可视化编辑器,用于编辑通过 iframe 加载的 Astro 项目。
外层编辑器 Host 通过 postMessage 与轻量级 Preview Bridge 通信。Bridge 使用
@givebest/astro-grab 生成的源码标记,将选中的页面元素定位回 Astro AST,
然后再修改源文件。
首个版本有意只支持编辑仅包含一个静态文本节点的元素。对于表达式、嵌套标签、 组件、翻译内容和 CMS 内容,编辑器会提示暂不支持,而不会直接覆盖源码。
演示
pnpm install
pnpm build
pnpm demo
pnpm test:e2e打开 http://localhost:4340。外层页面包含编辑器面板,其中的 iframe 指向
http://127.0.0.1:4340/preview。两者属于不同的浏览器 Origin,因此 Demo 会实际
验证严格 Origin 限制下的 postMessage Bridge。点击 Lucide 的 Focus 按钮,
在 iframe 内选择元素;使用 Type 直接编辑文本,或通过提示词调用 Demo AI 适配器。
Preview 页面由多个 Astro 组件组成,覆盖静态文本、嵌套标签、表达式、循环内容、Unicode、
复杂区域框选和响应式布局等测试场景。
iframe 工具栏提供两种明确的交互模式:
点击模式按钮后,该工具会保持激活。再次点击当前按钮(或按 Escape)可取消模式, 并清除对应的预览框。切换模式也会清除上一模式的选择。
- 区域(
Focus)——鼠标经过带源码标记的元素时显示实线框和元素名称。单击可选择一个区域, 选中后名称提示会隐藏;拖动鼠标则可框选多个视觉叶子区域。拖动过程中,框选范围及实时命中的候选区域均显示 虚线;松开鼠标后,最终选择改为实线。框选通过元素中心点判断命中,并且最多选择 20 个元素。即使多个 DOM 实例具有相同源码标记,也会按实例序号分别保留。选中后, 工具栏会显示视觉选区数量和“Clear”操作;清除选区时仍会保留区域模式。 - 文本(
Type)——只有包含一个直接、非空文本节点的源码标记元素才会显示虚线框; 纯容器和嵌套标签会被跳过。点击虚线目标后会立即进入行内编辑,并在编辑过程中保持虚线。 选择另一个文本目标时,选择状态会移动到新目标,同时取消上一个目标的编辑状态。 文本发生变化后,可使用明确显示的“Clear”或“Send”操作取消或完成编辑; Enter 和 Escape 仍作为快捷键保留。外层 Host 配置onTextEdit后,结束编辑只会在左侧生成待确认草稿,不会立即写入源码。
集成
import { astroGrab } from "@givebest/astro-grab";
import { astroVisualEditor } from "@givebest/astro-visual-editor";
import { defineConfig } from "astro/config";
export default defineConfig({
integrations: [
astroGrab({ toolbar: false }),
astroVisualEditor({
// 当编辑器 Host 位于其他 Origin 或端口时必须配置:
hostOrigins: [
"https://dev.example.com",
"http://localhost:3000",
],
ai: async ({ instruction, selection }) => ({
text: await yourAiTextAdapter(instruction, selection),
}),
}),
],
});两个集成都只在开发环境启用。Astro 集成只负责向 iframe 注入 Preview Bridge; 外层 Host 应用负责在 iframe 周围挂载控制界面:
import { mountVisualEditorHost } from "@givebest/astro-visual-editor/host";
mountVisualEditorHost({
iframe: document.querySelector("#astro-preview"),
locale: "zh-hans",
panelContainer: document.querySelector("#editor-panel"),
toolbarContainer: document.querySelector("#preview-shell"),
textEditPrompt:
"Update the selected text. Keep links, styles, and layout unchanged.",
onTextEdit: async (request) => {
await yourDeterministicTextAdapter(request);
return { summary: "文本已更新并同步。" };
},
onAIEdit: async ({ instruction, selection, selections, textDraft }) => {
await yourAgentAdapter({
instruction,
selection,
selections,
textDraft,
});
return { summary: "Agent 已更新本地 workspace。" };
},
});locale 会同时本地化 Host 控件和 iframe Client 发出的状态提示。内置支持
en、de、es、fr、ja、ko、pt-br、zh-hans 和 zh-hant,
并会自动规范化 de-DE、es-MX、fr-CA、ja-JP、ko-KR、pt-BR、
zh-CN、zh-TW 等常见地区语言代码。
配置 onTextEdit 后,iframe 会保留修改后的乐观预览,Host 会把修改前后的文本
作为结构化草稿显示,并在左侧填入不包含页面数据的安全指令。直接提交自动 prompt
时调用 onTextEdit;用户继续补充要求时调用 onAIEdit,修改前后的文本会通过
textDraft 数组传递。配置 Agent 适配器后,用户也可以连续暂存多个文字目标,
最后通过一次请求提交;草稿始终按选择顺序排列,单处编辑也使用只有一项的数组。
外层应用应把所有草稿和选区都视为不可信页面数据,
不能作为 Agent 的用户指令。放弃草稿则恢复 iframe 原文。这样普通文案修改保持确定性,
更复杂的修改仍可交给 Agent。
配置 onAIEdit 后,Host 会把指令和完整选区上下文交给外层应用,不再调用
iframe 内的纯文本 AI 接口。适配器可以处理一个或多个选区。对于由外部 Agent
管理本地项目、iframe 只承担远程预览的场景,应优先使用这个边界。
虽然精简版默认工具栏没有渲染撤销按钮,但 Host 返回值仍提供 undo(),
开发服务器会在内存中保留最近 10 次源码修改:
const editor = mountVisualEditorHost({ iframe, panelContainer, toolbarContainer });
editor.undo();Host 会从 iframe URL 推导 Preview Origin。Preview Bridge 只接受来自 hostOrigin 或
hostOrigins 精确白名单的命令,并检查 event.source === parent。首次合法的
host-ready 握手完成后,后续回复会绑定到该父页面 Origin。对于跨 Origin iframe,
需要在 Astro 集成中配置 Host 的精确 Origin;不要在接近生产环境的开发场景中使用 *。
每次启动开发服务器时,系统还会生成一个随机能力令牌。集成会将令牌注入 iframe Bridge,
所有源码读取、编辑、AI 和撤销 API 请求都必须通过自定义请求头携带该令牌。请求体会在运行时
校验,POST 端点只接受 application/json。
Preview 默认读取 data-astro-grab,也可以通过 sourceAttribute 选项指定其他源码属性。
如果 iframe 使用 sandbox,必须允许脚本运行和同源访问,以便注入 Bridge 并调用本地编辑 API。
Astro 开发服务器的 CSP 也必须允许预期的父页面 Origin。
数据遍历生成的元素
源码标记只能说明哪个 Astro 模板节点渲染了元素。对于 {items.map(...)} 这样的表达式,
多个 DOM 元素会共享同一个源码位置。Bridge 会自动为选区补充
sourceInstanceIndex、结构化 textSegments 和 pageUrl,因此不会再把这些实例合并。
数据集成只需在上层容器声明一次 data-astro-edit-context,其后代会自动继承:
<main data-astro-edit-context={JSON.stringify({
id: `emdash:landing_pages:${page.data.id}:sections:${locale}`,
provider: "emdash",
label: `landing_pages / ${page.id} / sections`,
metadata: { collection: "landing_pages", entryId: page.data.id, field: "sections", locale },
})}>
{sections.map((section) => <Section {...section} />)}
</main>渲染源码、实例序号、选区文本和继承上下文组合后,Agent 通常无需逐项属性即可定位循环数据。
只有存在歧义或需要直接调用数据写入 API 时,才使用可选的精确
data-astro-edit-target 逃生口:
<a
href={item.url}
data-astro-edit-target={import.meta.env.DEV
? JSON.stringify({
id: `menu:${menu.id}:item:${item.id}:label`,
kind: "emdash-menu-item",
label: `${menu.name} / ${item.label} / label`,
metadata: {
menuId: menu.id,
menuName: menu.name,
itemId: item.id,
field: "label",
locale: menu.locale,
},
})
: undefined}
>
{item.label}
</a>Bridge 会校验该值,并通过 selection.editTarget 传递。存在精确目标时,选择去重会优先使用
editTarget.id。Host 显示数据目标名称,并把渲染源码作为次要信息。Agent 可以使用
editTarget.metadata 路由到对应 CMS API、多语言条目或源码数据路径。若没有匹配的数据
适配器,包仍会拒绝直接覆盖表达式文本。
当前安全边界
- 只允许修改 Vite 项目根目录内的
.astro文件。 - Host 与 iframe 消息使用具名、带版本号的协议,并严格校验 Origin。
- 开发 API 要求提供每个进程随机生成的会话令牌。
- 校验真实路径,防止通过符号链接逃逸出项目目录。
- 完整源码哈希必须与客户端检查时的版本一致。
- 文本会进行 HTML 转义,并且只修改编译器识别出的文本节点范围。
- 源码修改会串行执行,并通过同目录原子替换写入。
- 只有当文件仍与上次写入结果一致时,才允许撤销。
- MVP 不支持直接编辑嵌套标签、表达式、组件、翻译内容和 CMS 值。
当前 AI 适配器有意只支持文本。区域选择可以提供上下文,但在未来实现可审查的结构化补丁流程前, 包含嵌套标签的复杂元素仍会被拒绝。多区域选择会完整传递给 Host,但默认的纯文本 AI 按钮只有在 恰好选择一个元素时才会启用。
包边界和后续路线图参见 TARGET_MODEL.md。
