(null);
<>
>;
```
---
## 内部架构
```
TaskArtifactHtml (入口)
├── type="document" → TaskHtmlDoc
│ ├── editable=false → Html (只读 iframe)
│ └── editable=true
│ └── PPTEditProvider (编辑状态上下文)
│ ├── PPTEditToolBar (toolbar-doc, 文本选区工具栏)
│ └── HtmlWithEditMode (基础编辑层)
│ ├── useIframeMode → HTMLEditor 实例
│ ├── useDiff → 同步编辑状态到 Context
│ └── Html (iframe 渲染)
│
└── type="web" → TaskHtmlWeb
├── editable=false → Html (只读 iframe)
└── editable=true
└── PPTEditProvider
├── PPTEditToolBar (toolbar-web, 元素工具栏)
└── HtmlWithEditMode (isDoc=false)
```
### 核心模块说明
#### `Html`(`components/html-render/task-html.tsx`)
底层 iframe 渲染组件,接受 HTML 字符串作为 `srcDoc` 注入 iframe。配置了 `sandbox="allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox"`,并自动拦截 iframe 内的 `_blank` 链接和 `window.open` 调用,转发至父窗口打开。
#### `HtmlWithEditMode`(`mode/baseEdit.tsx`)
编辑模式的基础封装层,负责:
- 调用 `useIframeMode` 初始化 `HTMLEditor` 编辑器实例
- 调用 `useDiff` 将编辑器状态同步到 `PPTEditContext`
- 监听历史记录变化,通过 `onStateChange` 向上传递 `ArtifactEditState`(canUndo/canRedo/undo/redo)
- 内容变更时自动防抖保存(1 秒),通过 `saveMarkdown` 接口持久化到服务端
#### `PPTEditProvider`(`context/index.tsx`)
编辑状态上下文 Provider,维护:
- `state` — 当前 `useIframeMode` 的返回值(编辑器实例、选中元素、位置等)
- `setState` — 由 `useDiff` hook 调用,将编辑状态同步至上下文
- `originalSlide` — 原始幻灯片数据的引用(PPT 场景)
工具栏组件通过 `usePPTEditContext()` 消费该上下文,获取编辑器实例和选中元素信息来渲染对应的编辑工具。
---
## Hooks
### `useLoadContent(taskArtifact: TaskArtifact): string`
通过 `useNovaKit()` 提供的 API 加载产物的远程 HTML 内容。先调用 `api.getArtifactUrl()` 获取签名 URL,再 `fetch` 获取文本内容。
### `useIframeMode(id, containerRef, options?, scale?): UseInjectModeReturn`
核心编辑 hook,负责:
- 创建 `HTMLEditor` 实例并注入到 iframe 或普通 DOM 容器
- 管理元素选中状态和位置计算
- 管理 undo/redo 历史记录
- 绑定 `Cmd+Z` / `Cmd+Shift+Z` 快捷键
返回值:
```ts
interface UseInjectModeReturn {
editor: HTMLEditor | null; // 编辑器实例(ref 值)
editorIns: HTMLEditor | null; // 编辑器实例(state 值,触发重渲染)
selectedElement: HTMLElement | null; // 当前选中的 DOM 元素
position: Position | null; // 选中元素在 iframe 内的位置
tipPosition: Position | null; // 经过缩放换算后用于定位工具栏的位置
injectScript: (target: HTMLElement) => Promise; // 手动注入编辑器
canUndo: boolean;
canRedo: boolean;
clearHistory: () => void;
loadSuccess: boolean; // iframe 内容是否加载/注入成功
}
```
### `useDiff(useIframeReturn, isDoc?)`
区分 document / web 两种模式的差异逻辑,将 `useIframeMode` 的状态同步到 `PPTEditContext`:
- **document 模式**:使用 `useToolPosition` 基于文本选区末尾计算工具栏位置
- **web 模式**:基于选中元素的 `getBoundingClientRect` 定位工具栏;失焦后清除状态
### `useToolPosition(editor, isDoc): Position | null`
仅在 document 模式下生效。监听编辑器内的 `mousedown` / `mouseup` / `scroll` / `resize` 事件,根据当前文本选区(`Selection`)末尾位置计算工具栏坐标。
---
## 服务端接口
### `saveMarkdown`(`server/index.ts`)
编辑模式下内容变更会自动防抖(1 秒)调用此函数保存到服务端:
```ts
POST / v1 / super_agent / chat / write_file;
Body: {
task_id: string;
path: string;
content: string;
}
```
---
## 目录结构
```
html-editor/
├── index.tsx # 入口组件 TaskArtifactHtml
├── README.md # 本文档
├── types/
│ └── index.ts # ArtifactEditState 等类型定义
├── server/
│ └── index.ts # saveMarkdown 保存接口
├── mode/
│ ├── baseEdit.tsx # 编辑模式基础层 HtmlWithEditMode
│ ├── html-doc.tsx # 文档模式 TaskHtmlDoc
│ └── html-web.tsx # 网页模式 TaskHtmlWeb
├── context/
│ └── index.tsx # PPTEditProvider / usePPTEditContext
├── hooks/
│ ├── useDiff.ts # doc/web 差异逻辑
│ ├── useIframeMode.ts # 核心编辑器 hook
│ ├── useLoadContent.ts # 远程内容加载
│ └── useToolPostion.ts # 文本选区工具栏定位
├── components/
│ ├── html-render/ # iframe 渲染组件
│ ├── toolbar-doc/ # 文档模式工具栏
│ └── toolbar-web/ # 网页模式工具栏
├── lib/ # HTMLEditor 核心库
│ ├── core/ # 编辑器引擎、历史记录管理器
│ ├── types/ # 内部类型定义
│ └── config/ # 编辑器配置
└── assets/ # 图标等静态资源
```