在 Hex 中捕获¶
状态:第 2 步和第 3 步,以及第 4 步的输入部分(配合 Scrawl 面板),已作为一项实验性功能构建在 Scrawl 的真实引擎上,位于构建选项后面(所有公开构建中均关闭);第 4 步的那一半在分支 scrawl-in-hex 上。该实验使用了导入到 crates/capture 的 scrawl-capture crate(见 当前已构建内容);目前没有发布任何内容,Kit 之后决定是否进行这次合并。这是 ROADMAP 中“将 Scrawl 并入 Hex”的 Hex 一侧:Hex 如何托管 Scrawl 的采集引擎而不削弱听写功能。
当前已构建内容¶
第 2 步:crates/capture 是 kitlangton/scrawl 仓库 e351ddb(分支 engine/split)中 Scrawl 的 crates/capture,无历史记录地复制而来,保留包名 scrawl-capture,私有测试值替换为合成值,并执行 cargo fmt 和不改变行为的 clippy 修复。Hex 通过路径依赖它。它位于 [workspace] exclude 中,因此 cargo build、cargo test 和 --workspace 永远不会构建它;它仅作为 capture 功能的 macOS 依赖进行编译,并与根目录的 Cargo.lock 和 target/ 共享。在 macOS 上,scripts/check 和 CI 通过名称(-p "$(cargo pkgid)" -p scrawl-capture --features capture)对其进行 lint 和测试。Scrawl 自己的改动现在通过手工方式迁移过来。
第 3 步:该 crate 上的 capture_host:
hex capture start|stop|cancel|status // `--features capture`; POST /control `scrawl-capture`
└── listener thread -> capture_host::CaptureHost (exists only with the experiment on)
├── start -> owner-only <captures folder>/<unix ms>.scrawl -> reply at once
│ ├── DictationAudio::open_capture_sink(bounded 512 chunks, since = t0)
│ └── capture-audio thread -> Engine::start(Start { t0_boot_ns, ActiveDisplay,
│ AudioIn::External, bundle_dir }) (blocks: target lookup, ScreenCaptureKit)
├── capture-audio thread -> Timeline: 16 kHz, gaps padded, start offset kept
├── start while two stopped captures are still writing -> busy
├── stop -> close_capture_sink(through) (≤ 100 ms wait) -> reply with the bundle path
│ ├── capture-audio thread -> Capture::stop: screen, window log, and input end now
│ └── capture-finish thread, one at a time, ≤ 2 waiting ->
│ SealedCapture::finish(Some(RecordedAudio { start_offset_ms, gaps }))
│ ├── HexTranscriber -> parakeet::transcribe_with_words, 30 s chunks,
│ │ each a foreground job on the dictation engine's loaded model
│ ├── Scrawl writes recording.mp4, audio.wav, frames/, scrawl.json, summary.md
│ │ (its own files 0600) -> Finished { words, gaps, missing_transcript }
│ └── Hex makes every file 0600 and folder 0700 (recording.mp4, folders)
└── cancel -> Capture::cancel, remove the bundle
- Hex 的接缝是
src/capture_host.rs中的三个小 trait:CaptureEngine、Recording和Sealed,它们为scrawl_capture::capture::{Engine, Capture, SealedCapture}实现,因此宿主测试无需屏幕即可运行。Hex 实现了该 crate 的Host(有界try_send事件,日志和指标进入 tracing)、ConfigSource和Transcriber。hex capture status显示上一次捕获的词、音频间隙,以及它为何没有转写文本。 ConfigSource是引擎唯一的设置来源;Hex 的路径上没有任何代码读取~/.config/scrawl/config.json(该 crate 只在调用enable_scrawl_host_defaults或enable_file_log后才读取 Scrawl 的配置并记录日志,而 Hex 从不调用这些)。Hex 还没有捕获设置,因此HexConfig有意采用 Scrawl 的默认值:自动裁剪开启(适用于屏幕捕获),agentContext摘要格式关闭。- 引擎是
Engine::new(..).with_resources(SharedResources::none());Hex 从不调用install_shared_resources,因此没有摄取服务器(47777)、投递重试或全局快捷键。当没有设置 sink 时,Engine::new将该 crate 的进程级日志路由到Host::log,因此每一行都会到达 Hex 的 tracing 一次,并且不会有任何内容写入~/Library/Logs/Scrawl。Engine::new和Engine::start不调用 TIS/TSM(只有install_shared_resources才会解析布局感知的快捷键)。 - 同一时间只有一次捕获在录制。
stop会将其封存,capture-audio 线程立即调用Capture::stop,结束 ScreenCaptureKit、窗口日志和输入;随后SealedCapture等待该单一写入者,最多同时等待两个。使用SharedResources::none()时,每次捕获都有自己的输入遥测,因此一次捕获在录制时,即使较早的一次捕获仍在写入,两者之间也不会共享任何内容。 - 数据包写入
<captures folder>/<unix ms>.scrawl,即 Scrawl 自己的文件夹,除非capture.folder指定了其他目录(第 3 步使用<support>/captures):采用 Scrawl 自己的scrawl.json格式,词以毫秒为单位,基于捕获自身的时钟。 hex dev capture-replay <wav>…在一个全新的转写引擎上以无头模式运行同一个CaptureHost,用 WAV 文件代替麦克风输入;它也会录制屏幕(仅限 debug 构建,与所有hex dev命令一样)。- 视图读取单个
CaptureSnapshot(capture_host::snapshot,由capture_host::watch唤醒):正在录制的那次捕获(数据包、开始时间、屏幕是否已启动)、处于引擎最新阶段的已停止捕获,以及最后一个已定稿的捕获(完成、失败、取消,包含词和间隙)。宿主在每次变化时发布该快照;引擎进度会推进一次捕获,但绝不会将其定稿。胶囊、路由器和hex capture list订阅的是此处,而不是宿主。 - 路由和投递(第 5 步)、有线连接的 iPhone、胶囊、屏幕上的选择器,以及墨水和激光叠加层均已构建;请参阅相关章节和
docs/features/capture.md。尚未构建:iPhone 摄取(ScrawlKit 配对)、移动摄取令牌,以及用于打开列表的监听器按键路径(⌥⇧V、输入、方向键、Return)。
第 4 步的前半部分(快捷键、输入、Escape),连同第 5 步中的面板、导入和共存:
settings.json capture.* (CaptureSettings, cfg hex_capture; other builds keep the raw key)
├── enabled (off) · hotkey (⌥⇧S) · transcriber (parakeet|apple) · folder (None = Scrawl's)
│ · media_days / bundle_days (Scrawl's Week preset: 7 / 30)
├── every key in `hex settings` (control/settings.rs CAPTURE_KEYS)
└── load -> import once (migration `scrawl-capture-settings`, not in MIGRATIONS):
~/.config/scrawl/config.json + Scrawl's shortcut file, read-only
recognition loop, every iteration
├── CaptureHost::reconcile -> start/stop the host as capture_on() changes
│ (experiment && capture.enabled); watch_scrawl polls Scrawl.app every 2 s
├── runtime_hotkeys() drops capture while Scrawl.app runs
├── fresh ⌥⇧S press (CaptureShortcut) -> CaptureHost::toggle: start | stop
│ (a press within SHORTCUT_DEBOUNCE, 400 ms, of the start is ignored)
└── Esc, nothing else recording, a capture recording -> cancel it first
suppression event tap (the one session tap)
├── observation mask + mouse down/up/drag/scroll when capture_available()
├── wants_input() (a capture records) -> report_capture_input -> TAP_INPUT
│ (bounded 1 024, drops counted) -> LiveInput::forward -> HostInput
│ (Moved / ModifiersChanged stand in for Scrawl's 8 ms poller)
└── capture-only events end there; keys, flags, and downs continue to the listener
crates/capture hosted_input.rs: Scrawl's ink and click rules as pure functions
(is_pin_click, drew_before, is_real_ink, on_own_window); Scrawl's own key tap
calls the same key_input / key_name. after_shortcut(): Shift must come up once.
Settings › Experiments › Scrawl (rune_ui/scrawl.rs; PlatformState.scrawl is
Some while capture_available(), so the pane exists only then)
├── Capture with Hex · Shortcut · Screen Recording · Transcriber
├── Captures folder (Reveal | choose) · Keep (retention presets)
├── "Scrawl is running" + Use Hex for captures -> quit_scrawl (NSRunningApplication)
└── "Hex copied Scrawl's settings once…" when the import read a file
capture-retention thread -> retention::run_pass(capture.* policy): 90 s, then hourly;
skipped while a capture is unfinished or Scrawl.app runs
- 与 Scrawl 的差异:快捷键在按下时生效(Scrawl 的 Carbon 热键也是在按下时启动,但 Hex 没有“松开时启动”的待处理状态);启动后 400 ms 内的按压会被忽略,而不是停止刚刚开始的捕获;指针位置来自点击事件,而非 8 ms 轮询器。
- 该窗格仅存在于
cfg(hex_capture)构建中:公开构建永远不会编译该模块、其字符串或其场景;特性构建只有在实验开启时才显示它。它的场景(mac-scrawl*)仅在特性构建中渲染,因此它们没有 golden 基准图(golden 基准图在 Linux 上运行)。 - iPhone 配对被省略,而不是显示为禁用:它需要摄取服务器,而 Hex 尚未运行该服务器。有线连接的 iPhone 不需要这一行。
特性与门控¶
- Hex 的 Cargo 特性
capture(默认关闭)会启用仅限 macOS 的可选依赖scrawl-capture(default-features = false, features = ["screen", "helper"])和anyhow(该 crate 的 trait 返回anyhow::Result)。 build.rs在开启该特性的 macOS 构建(debug 或 release)中设置cfg(hex_capture),所有捕获代码行都受其保护。未启用该特性的构建(所有公开发布、Linux、Nix、Windows)永远不会编译该 crate 或其 Swift helper,并会以unsupported拒绝scrawl-capture。- Release、测试构建和 Nix 构建永远不会传递该特性。
scripts/release-app.sh会拒绝HEX_CAPTURE,scripts/validate-app.sh(在每个 macOS release 产物上运行)会拒绝包含Contents/MacOS/scrawl-helper或含捕获主机启动日志行的hex的 bundle。 scripts/check和 macOS CI 使用--features capture对 Hex 和该 crate 执行 lint 和测试(该 crate 的约 260 个测试只需数秒即可运行)。Linux、Windows 和 Nix 构建永远不会选择它:该依赖仅限 macOS,且该 crate 位于工作区之外。- 关闭:
phone(摄取服务器、配对)、mic(引擎自身的 cpal 流)、input(rdev 点击事件)、hex-client、parakeet、cli。helper保持开启,用于scrawl-helper,它在完成时将recording.mp4解码为全质量关键帧(没有它,bundle 会保留 480 px 的采样帧)。
Helper 与权限¶
scrawl-helper由该 crate 的build.rs构建在二进制文件旁边(target/<profile>,位于OUT_DIR上方三级);引擎会在其自身可执行文件旁查找它(helper::path);$SCRAWL_HELPER可以覆盖。如果缺失,完成时会保留 480 px 的候选帧作为关键帧。Hex 的捕获路径使用它进行影片解码(不需要 TCC)、devices(USB 注册表,不需要 TCC)以及mirror --no-microphone(有线 iPhone 的捕获,需要 Camera)。- Camera 属于 Hex,即 helper 的负责进程;因此只有
HEX_CAPTURE=1构建会将NSCameraUsageDescription添加到 Hex.app 的 Info.plist(以及 helper 内嵌的那份);scripts/validate-app.sh会拒绝包含它的发布 bundle。 HEX_CAPTURE=1 scripts/build-app.sh使用--features capture和SCRAWL_HELPER_PLIST=packaging/macos/scrawl-helper-Info.plist(内嵌 bundle IDanomaly.hex.scrawl-helper,名称为 Hex)构建,将 helper 复制到Hex.app/Contents/MacOS/scrawl-helper,并在应用之前用应用的标识、hardened runtime、时间戳、标识符<bundle id>.scrawl-helper对其签名,且不附带 entitlements。- 屏幕录制权限归属于运行 ScreenCaptureKit 的进程:即 Hex 本身(或运行开发构建的终端)。helper 是 Hex 的子进程,因此它接触到的任何内容都归属于 Hex(其负责进程)。
结构¶
Hex.app (one process, macOS only for captures)
├─ hex (root package of a new workspace)
│ ├─ capture_host ← new: the experiment's one owner
│ │ ├─ Host impl logs → tracing, metrics, own windows, clipboard
│ │ ├─ Transcriber impl Parakeet words on the transcription engine
│ │ ├─ ConfigSource impl settings.json `capture.*` + one-time import
│ │ └─ capsule + ink overlay (Rune overlays, storybook scenes)
│ ├─ suppression + capture shortcut, + pointer events while capturing
│ ├─ dictation_audio + bounded capture sink on the same timeline
│ └─ transcription_engine + foreground capture clip (after queued dictation)
└─ crates/capture (package `scrawl-capture`, imported without history)
├─ features on in Hex: screen, helper (phone later, for iPhone ingest)
└─ features off in Hex: input, mic, hex-client, parakeet, cli
该引擎是 Scrawl 的:屏幕采样、关键帧、草图与摘要、渲染、自动裁剪、bundle、投递、路由(与 Jev 协同)、保留、恢复,以及 iPhone 摄取。Hex 提供麦克风、输入、转录、界面和设置。该引擎在 Hex 内部永远不会自行打开麦克风、安装事件点击、请求输入监控或粘贴。
工作区与构建¶
- 根目录下的
Cargo.toml将crates/capture列入[workspace] exclude(会被构建)。根目录是hex包。 - Hex 仅在
[target.'cfg(target_os = "macos")'.dependencies]下依赖scrawl-capture,目前为default-features = false, features = ["screen", "helper"](phone将随 iPhone 摄取功能加入)。Linux 和 Windows 构建永远不会编译它,因此它们的 CI 和包不变。 crates/capture/Cargo.toml列出显式版本(没有workspace = true),以便 Scrawl 应用在停用之前可以通过 Git 修订版依赖它。- macOS CI 使用
--features capture对-p scrawl-capture执行 lint 和测试(会被构建)。 crates/capture/build.rs编译引擎的 Swift helper。Hex 使用 Hex 身份(SCRAWL_HELPER_PLIST)构建它,scripts/build-app.sh会将其复制并签名到Contents/MacOS/(为HEX_CAPTURE=1构建)。
实验门控¶
实验 capture。当它关闭时,以下一切都不会启动:不注册任何快捷键,不存在捕获汇,不绑定摄取端口,不运行保留或投递重试 worker,也永远不会启动 helper。该门控位于工作开始的位置(capture_host worker),而不仅仅是在窗口中。
输入¶
- 捕获快捷键(默认
⌥⇧S)是suppression中除听写和 Voice Action 之外的第三个目标,其捕获和冲突规则与其他绑定相同。它拒绝与听写重叠的绑定。 - 当捕获正在录制时(且仅在此期间),会话 tap 还会报告鼠标按下、抬起、拖动和滚动,以及 Shift 状态。
capture_host将这些信息提供给Capture::input()。Scrawl 的墨迹和点击规则(Shift 墨迹、固定和墨迹阈值、忽略对 Hex 自身窗口的点击)作为这些事件上的纯函数移入引擎,因此 Hex 不会运行第二个 tap。 - Escape 会先取消进行中的捕获,然后才取消听写工作。捕获从不使用按住阈值或松手后的尾部。
- 已构建:参见目前已构建中的第 4 步。
麦克风¶
dictation_audio有一个有界的捕获汇(open_capture_sink、close_capture_sink):捕获接收到与听写相同的带时间戳样本。永远不会出现第二个cpal流。- 听写在捕获期间继续工作。两者是同一时间线的投影,因此在捕获中途按住听写快捷键仍会粘贴,同时捕获仍会将那段语音记录为叙述。
- 启用“空闲时释放麦克风”时,捕获会在其持续期间保持流打开,就像录音一样。
- 汇压力会丢弃捕获音频并标记一个间隙;它绝不会阻塞时间线或听写。
转录¶
capture_host使用转录引擎已加载的模型和词时间戳(parakeet::transcribe_with_words)实现引擎的Transcriber;听写仍然丢弃其词。- 已完成的捕获片段是前台工作:它在已排队的听写之后、会议块之前运行。它绝不会加载第二个模型。它按 30 秒块发送到引擎,每块一个任务,因此同时发送的听写最多只会等待一个块。当听写正在捕获或转录时,块会等待;如果某个块在实时流期间到达,引擎会一直持有它直到流结束,因此它绝不会打断实时流。
- Scrawl 的口语提示需要经过测量的词时间。与 Apple 的 SpeechAnalyzer 在 36 个真实捕获(10 月 25 日)上的对比显示,Parakeet Unified English 的词时间偏晚:开始时间中位数晚 280 ms(暂停后约 520 ms),结束时间晚 180 ms,在 30 秒接缝处没有漂移也没有开销;原始数据中,53% 的提示词晚于 300 ms。
HexTranscriber会减去基于一半捕获拟合的滞后(capture_host中的WORD_LAGS:开始时间 280 ms,或者当与上一个词结束之间的间隔至少为 250 ms 时为 520 ms;结束时间 195 ms),这使得另一半捕获中 96% 的提示词与 Apple 的差距在 300 ms 以内。该滞后与固定的模型文件绑定:更换模型或重新固定都会保留原始时间,直到再次测量。Apple 辅助程序路径仍可选择(hex settings set capture.transcriber apple)。
界面¶
- 胶囊(录制中、处理中、完成、路由提示、选择器)以及墨迹和激光叠加层都是可点击穿透的 Rune 叠加层,与会话胶囊一样。它们的窗口 ID 通过
Host::own_windows传给引擎,因此屏幕捕获会将其排除在外,点击也会穿透。 - 每个状态都有一个
--snap场景和一个 storybook 条目。 - Scrawl 的切换标签(
docs/capture.md):由快捷键启动的录制会打开到“区域 · 窗口 · 屏幕”并持续 3.5 秒(悬停时从不收起),或直到第一次点击、滚动或笔画。标签是重启而不是重新定向:Window 和 Area 会丢弃录制并将胶囊变成选择器。宿主端已构建:通过RecognitionControl::CaptureCommand传递的CaptureTarget、CaptureCommand::{Start, Discard, Switch}、RecordingCapture::{target, worked}、SettledCapture::error,以及用于选择器打开时的 ⌥⇧S 和 Esc 的begin_picking。该快捷键在修饰键释放时启动,和 Scrawl 一样。iPhone 标签的宿主端也已构建:CaptureTarget::Phone、TargetKind::Phone、CaptureSnapshot.phone(来自每次启动以及CaptureCommand::FindPhone时的capture-phone查找所得到的接线手机名称)。镜像以--no-microphone运行;叙述是 Hex 的时间线音频,移至影片的时钟上(RecordedAudio::since),并由 Hex 的转录器转录。自行结束的镜像(Event::PhoneEnded)会在监听器下一次收割时结束捕获。 - 完成胶囊的路由标签读取
SettledCapture.route(先读取其答案,然后只读取胶囊自身无法知晓的投递项),并发送CaptureCommand::{Send, Undo, Redirect}。已构建;offer不会被绘制。 - Experiments 下有一个由功能模块所有(D-5)的 Scrawl 面板:可选启用、快捷键、屏幕录制、转录器、捕获文件夹、保留策略,以及在 Scrawl 运行时显示“使用 Hex 进行捕获”(已构建;iPhone 随 ingest 加入)。
- 屏幕录制权限在第一次捕获时或从面板请求,而不是在核心引导流程中。
路由与投递¶
- 已构建(
capture_host/routing.rs):Scrawl 的路由器(crates/router)在 Hex 的进程中运行,使用capture.routing(从 Scrawl 导入一次;绝不是 Jev 键)和 Scrawl 自己的路由器文件夹,因此scrawl router undo、规则建议和评估会将 Hex 的发送视为 Scrawl 的发送。一个capture-look线程在停止时读取标签页、终端和会话;唯一的capture-route线程(队列 16)在包写入后做出决定,等待规则的 3 秒撤销期结束,发送,并按顺序对新捕获执行胶囊的 Send、Undo 和 Redirect。每次更改最终都会进入last.route。 - 投递使用引擎的投递路径。排队的发送每 15 秒重试一次,但测试中、Scrawl.app 运行时,或任何进程在 ingest 端口上提供服务时除外(重试由该进程负责)。Hex 本身从不存储 OpenCode 服务器密码;引擎会像 Scrawl 那样读取它。
- 未构建:用于路由的
live.ndjson事件(id、分档、分数、模型、阶段和结果,绝不包含转录文本、摘要或窗口文本)。
与 Scrawl 共存¶
- 同一时间只有一个所有者。当 Scrawl(
dev.kitlangton.scrawl)运行时,Hex 不会注册捕获快捷键、不会运行保留策略、不会绑定 ingest 端口(47777),也不会运行投递重试。设置中会显示“使用 Hex 进行捕获”,但只有点击该选项时才会退出 Scrawl(已构建,不含 ingest)。 - 配置:将 Scrawl 的设置一次性、只读地导入
settings.json的capture.*键,每个都有对应的hex settings键(已构建)。ingest 令牌会移到 Hex 支持文件夹中仅所有者可访问的文件,以便已配对的手机继续工作(未构建)。 - 捕获继续留在 Scrawl 的捕获文件夹中;Hex 在其中写入并清理它们(已构建)。
要添加的不变量¶
- Capture 从不打开麦克风、安装事件监听(event tap),也不会加载自己的模型,并且从不阻塞或丢弃听写音频。
- Capture 队列是有界的:捕获接收器(capture sink)、引擎的无障碍(accessibility)和保留(retention)队列(目前在 Scrawl 中是无界的;导入前请为它们设置边界),以及帧候选缓冲区(低于 Scrawl 的 384 MB)。
- 每个进程只有一个引擎(引擎保持进程全局状态)。
- 实验关闭意味着完全不进行任何捕获工作。
CLI¶
与其他一切功能一样,通过 POST /control 提供,并受构建选项控制(已构建;docs/cli.md):hex capture start|stop|cancel|status、hex capture list [--limit N]|show <capture>、hex capture send <capture> [--to <session>] 和 hex capture route <capture> [--ask-jev]。未构建:调试构建中 hex dev capture 下的引擎工具(autocrop 评估、捕获模拟、重新处理)。
分支上的顺序¶
- Scrawl 在自己的探索分支上准备引擎(其清单中的重构:
lib.rs、从main.rs中拆出的管道、事件、配置、权限、剪贴板和转录器接缝、一个Capture类型、Cargo 特性)。 - 在此导入
crates/capture,包含仅限 macOS 的依赖和 CI。已构建:从e351ddb无历史复制。 capture_host带有实验门控、捕获接收器、Parakeet 词和生成 bundle 的无头hex capture start|stop。已构建。- 输入与 Escape,然后是 Rune 胶囊、选择器和带有场景与运动轨迹的墨迹叠加层。已构建。
- 进程内路由器、Scrawl 面板、导入和共存。已构建。
- 在录制的捕获上比较 Parakeet 和 SpeechAnalyzer 的词时间。已完成(10月25日):Parakeet 的延迟已在
capture_host中修正。
10月18日的 experiment/scrawl 分支是一个已漂移的副本;feat/capture 取代了它的捕获接收器和词转录器,因此可以删除。
对 Scrawl API 的请求¶
由 engine/split 回答:Start::t0_boot_ns、带有 start_offset_ms 和 gaps 的 RecordedAudio、相对于第一个样本的词,以及默认的 SharedResources::none()。
然后在 86d7d6c 回答:Capture::stop 返回 SealedCapture(屏幕在 seal 时停止,finish 只负责写入)、无全局快捷键的逐捕获遥测、Engine::new 中不包含 TIS/TSM,以及来自 ConfigSource 的 autocrop 和 agentContext。
然后在 e351ddb(导入的提交)回答:当未设置接收器时,Engine::new 将 crate 的日志桥接到 Host::log,并且库仅在被要求时才写入 Scrawl 的日志文件或读取其配置;Engine::start 的文档说明它会阻塞;Scrawl 写入的 bundle 文件权限为 0600;Finished 报告 gaps 和 missing_transcript。
托管真实引擎后遗留的问题:
- 没有纯音频目标:每次捕获都会录制屏幕,因此需要屏幕录制权限。Hex 的纯旁白测试使用假引擎。
Engine::start在目标发现和 ScreenCaptureKit 上阻塞;Hex 在监听线程之外运行它。finish启动scrawl-helper来解码影片,因此库宿主需要附带并签名第二个可执行文件(Hex 通过HEX_CAPTURE=1这样做),否则会丢失全质量关键帧。进程内解码则可以省去第二个可执行文件。phone、routing、delivery和 ingest HTTP 代码无论特性如何都会编译,并且该 crate 会为其 Windows 目标将第二个windows(0.61)拉入Cargo.lock。- 公共 trait 中的
anyhow使得color_eyre宿主必须转换每个错误。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 hex19/hex