Terminus 2 手册

录制后处理(外部)

stage-6Recording Post-process (External)7 个函数

这个 Stage 在解决什么问题?它负责把运行过程中记下来的 asciinema marker(录屏里的时间标签)真正写回 .cast 录屏文件里,让回放不只是“有画面”,还“有关键节点”。之所以单独放在这里,是因为录屏文件要等 tmux(可远程控制的终端)会话结束后才算完整,才能安全地做合并。所以它不是 Agent run() 内的一段,而是框架在收尾时做的外部后处理。

主流程

  1. 先看这次运行有没有留下 marker

    这些 marker 不是这里产生的,而是前面运行时陆续记进 reg-asciinema-markers。如果一个都没有,这个 Stage 基本就没有事做;录屏文件保持原样即可。

  2. 等录屏真正结束,再做合并

    触发点是 TmuxSession.stop()(关闭 tmux 会话并收尾),不是 Agent 主循环。这样设计是因为 asciinema 的 .cast 文件在录制中还在持续追加,太早改,文件可能还没写完。

  3. 按时间把 marker 插进录屏流里

    AsciinemaHandler.merge_markers()(把内存里的 marker 合并进 .cast 文件)会保留原文件头,然后一行一行看录屏帧。核心规则很简单:当 marker 时间 <= 当前帧时间时,先把这些 marker 写出去,再写这一帧。 这样做的意义是:回放时,标签会出现在它应该出现的位置之前,不会晚于对应画面。

  4. 处理两种边角情况
    • 如果某些 marker 的时间比所有录屏帧都晚,就在最后统一补上。这个收尾由 _write_remaining_markers()(把还没写出的 marker 追加到末尾)负责。
    • 如果录屏里几乎没帧,或者文件很短,marker 也不会丢;它们仍然会在末尾被写出。
  5. 产出一个“带标签的最终录屏”

    这一步不改变 Agent 的决策,不回流给 LLM。它的价值主要在外部:给人看回放、做调试、对齐运行中的关键事件。 也正因为如此,它适合做“事后合并”,而不是在运行中实时往录屏里插事件——后者更容易和录制过程互相干扰,也更难保证文件顺序正确。

状态流动

与前后 Stage 的衔接

上游真正交给它的不是 stage-5,而是更早的 stage-4.7:那里把运行中的关键时间点写进 reg-asciinema-markers。本 Stage 在 Agent 结束之后、框架关闭 tmux 会话时读取这些标记,产出最终的带 marker 的 .cast 文件;它本质上是一次终态后处理,没有明确的 Agent 内部下游。

函数细节7

TmuxSession.stoptmux_session.py:496–508 ↗

会话收尾时的录屏标记合并关口

stage 上下文: 该片段位于 stage-6,是录屏后处理真正落地的入口分支:在 Harbor 框架调用 TmuxSession.stop() 之后,决定是否把运行期积累的 marker 写回 .cast 录屏文件。它本身不负责生成 marker,只消费先前阶段累积在 self._markers 中的数据,并把实际合并工作委托给 AsciinemaHandler.merge_markers()。由于这是本 stage 的首个已翻译单元,它承担了“是否进入后处理”的门控角色。

这段代码在干什么

这段代码检查 self._markersself._local_asciinema_recording_path 是否同时存在;若两者齐备,就把已积累的标记合并进本地 asciinema 录屏文件。输入来自会话对象保存的 marker 列表和本地录屏路径,输出去向是该 .cast 文件本身,主要副作用是触发一次外部文件改写并记录调试日志。若任一条件不满足,则明确跳过合并,仅写出“无需合并”的调试信息。

接口 · 参数 / IO

(self) -> None

  • 参数: self: ? — TmuxSession 实例;提供 marker 累积状态、录屏文件路径与日志器
  • 读状态: self._markers, self._local_asciinema_recording_path, self._logger
  • 返回: 返回 None;真正产出是按条件将 marker 合并进本地 asciinema 录屏文件,或记录一次跳过合并的调试日志。
  • 副作用: 调用 self._logger.debug(...) 记录合并前后或跳过的信息; 构造 AsciinemaHandler(self._markers, self._local_asciinema_recording_path); 调用 handler.merge_markers() 改写 self._local_asciinema_recording_path 指向的录屏文件

执行流

  1. 先联合检查 self._markersself._local_asciinema_recording_path;只有 marker 列表非空且本地录屏路径可用时,才进入后续合并分支。
  2. 进入合并分支后,先用 self._logger.debug 记录待合并的 marker 数量,即 len(self._markers),用于说明即将执行的后处理工作量。
  3. 随后以 self._markersself._local_asciinema_recording_path 为输入构造 AsciinemaHandler,把具体文件后处理职责交给专门组件。
  4. 调用 handler.merge_markers() 执行实际合并;成功返回后,再记录一条 debug 日志,明确目标文件就是 self._local_asciinema_recording_path
  5. 若前置条件不满足,则不实例化处理器、不触碰录屏文件,只记录 No markers to merge 的调试信息。

源码

            if self._markers and self._local_asciinema_recording_path:
                self._logger.debug(
                    f"Merging {len(self._markers)} markers into recording"
                )
                handler = AsciinemaHandler(
                    self._markers, self._local_asciinema_recording_path
                )
                handler.merge_markers()
                self._logger.debug(
                    f"Successfully merged markers into {self._local_asciinema_recording_path}"
                )
            else:
                self._logger.debug("No markers to merge")

Non-obvious 设计决策

  • 这里采用“双条件门控”而非仅检查 self._markers:代码要求 marker 存在且 self._local_asciinema_recording_path 也可用,避免出现“有逻辑事件但没有本地录屏文件”时仍尝试后处理的无效或错误操作。
  • 合并逻辑没有内联在 TmuxSession.stop 中,而是委托给 AsciinemaHandler.merge_markers();这体现了会话停止与 cast 文件重写的职责分离,减少 stop 流程对 asciinema 文件格式细节的耦合。
  • 未满足条件时选择安静跳过并写 debug 日志,而不是抛错;这表明“没有 marker”或“没有本地录屏路径”在此处被视为合法的无工作场景,而非异常路径。

上下游关系

  • 调用方: Harbor 框架在 agent 返回后调用的 TmuxSession.stop() 收尾流程; stage-6 外部录屏后处理入口
  • 核心被调用: AsciinemaHandler(...); AsciinemaHandler.merge_markers(); self._logger.debug(...)
  • 配置/状态来源: self._markers:运行期间累积的 marker 列表; self._local_asciinema_recording_path:本地下载/保存的 asciinema 录屏路径; self._logger:会话级日志输出器
  • 结果去向: self._local_asciinema_recording_path 指向的 .cast 录屏文件; 调试日志输出; stage-6 的录屏后处理完成状态
  • 同类 sibling: 同 stage 的后续/相关单元应位于 AsciinemaHandler.merge_markers() 及其内部行处理逻辑;本片段只负责决定是否进入这些兄弟处理步骤。
寄存器交互
AsciinemaHandler.__init__asciinema_handler.py:11–20 ↗

初始化录屏处理器内部字段

stage 上下文: 该单元是 AsciinemaHandler 的构造函数,仅负责把构造参数落入实例状态。就当前源码可见范围而言,它不执行文件处理,也不触发任何外部流程;与同 stage 中真正执行合并的兄弟函数相比,这里只完成初始化准备。

这段代码在干什么

该函数接收 markersrecording_path 两个参数,并把它们写入实例属性。对于 markers,代码在其为真值时按时间戳 x[0] 排序后写入 self._markers,否则写入空列表 [];对于 recording_path,则直接赋给 self._recording_path。函数本身无返回值,副作用仅限于实例内部状态初始化。

接口 · 参数 / IO

(self, markers: list[tuple[float, str]], recording_path: Path) -> None

  • 参数: markers: list[tuple[float, str]] — 用于初始化 self._markers 的输入列表;若为真值则先按元组第 0 项排序; recording_path: Path — 用于初始化 self._recording_path 的路径对象
  • 返回: None;真正产出是对 self._markersself._recording_path 的赋值。
  • 副作用: 写入 self._markers; 写入 self._recording_path

执行流

  1. 检查参数 markers 的真值:若为真,则执行 sorted(markers, key=lambda x: x[0]),并将结果写入 self._markers;若为假,则将空列表 [] 写入 self._markers
  2. 将传入的 recording_path 原样赋给 self._recording_path

源码

    def __init__(self, markers: list[tuple[float, str]], recording_path: Path):
        """
        Initialize the AsciinemaHandler.

        Args:
            markers: List of tuples containing (timestamp, label) for each marker
            recording_path: Path to the asciinema recording file
        """
        self._markers = sorted(markers, key=lambda x: x[0]) if markers else []
        self._recording_path = recording_path

Non-obvious 设计决策

  • markers 采用真值分支 sorted(...) if markers else [],这是显式的输入规范化:空输入不会保留原值,而会统一变成空列表。
  • 排序键固定为 lambda x: x[0],说明该构造函数只依据每个二元组的第 0 项决定顺序,而不关心第 1 项内容。
  • recording_path 未做复制、校验或变换,而是直接赋值给实例属性;这体现了此处仅承担保存输入的职责。

上下游关系

  • 调用方: AsciinemaHandler(...) 构造调用点
  • 核心被调用: sorted
  • 配置/状态来源: 构造参数 markers; 构造参数 recording_path
  • 结果去向: self._markers; self._recording_path
  • 同类 sibling: TmuxSession.stop:兄弟单元负责在条件满足时触发后续处理;本函数仅完成其所需处理器实例的字段初始化。
AsciinemaHandler.merge_markersasciinema_handler.py:22–39 ↗

录屏标记合并入口函数

stage 上下文: 该函数处于录屏后处理环节,职责是决定是否对现有 asciinema 录屏文件执行标记合并。与同 stage 的 AsciinemaHandler.__init__ 相比,后者只负责保存 markersrecording_path,本函数则基于这些实例状态进入“直接返回”或“生成临时文件并替换原路径”两条路径。它自身不实现逐行合并逻辑,而是把实际内容生成委托给 _write_merged_recording

这段代码在干什么

函数读取 self._markersself._recording_path,先判断是否存在待合并标记以及目标录屏文件是否存在。若任一条件不满足,则立即返回、不做任何文件改写;否则基于录屏路径派生一个 .tmp 路径,调用 _write_merged_recording(temp_path) 生成合并结果,并再以 temp_path.replace(self._recording_path) 用临时文件替换原录屏路径。函数返回 None,真正产出体现在文件系统上的“无操作”或“替换原录屏文件”两种结果。

接口 · 参数 / IO

(self) -> None

  • 参数: self: ? — AsciinemaHandler 实例;提供待合并标记、录屏路径与实际写出辅助方法
  • 读状态: self._markers, self._recording_path, self._write_merged_recording
  • 返回: 返回 None;结果分两种:若 self._markers 为空或 self._recording_path.exists() 为假,则无副作用直接结束;否则先写出临时合并文件,再调用 replace 以该临时路径替换原录屏路径。
  • 副作用: 在满足条件时创建 self._recording_path.with_suffix(".tmp") 对应的合并输出文件; 在满足条件时调用 _write_merged_recording(temp_path) 产生合并后的录屏内容; 在满足条件时调用 temp_path.replace(self._recording_path) 替换原录屏路径

执行流

  1. 先检查 not self._markers or not self._recording_path.exists();只要待合并标记为空,或目标录屏路径当前不存在,就立即 return,本次调用结束。
  2. 若继续执行,则从 self._recording_path 派生 temp_path = self._recording_path.with_suffix(".tmp"),把输出目标切换到同一路径基名下的 .tmp 文件。
  3. 调用 self._write_merged_recording(temp_path),把“如何根据现有录屏与标记写出合并结果”的工作委托给辅助方法完成。
  4. 在辅助方法返回后,调用 temp_path.replace(self._recording_path),用临时路径对应的文件替换原录屏路径。

源码

    def merge_markers(self) -> None:
        """
        Merge asciinema markers into a recording.

        Inserts marker events into an asciinema recording file at specified timestamps.
        Markers are added as special events with type 'm' in the recording format.
        The original recording is preserved until the merge is successful.

        In the future Asciinema might support adding markers via RCP:
        https://discourse.asciinema.org/t/add-markers-with-title-from-cli/861
        """
        if not self._markers or not self._recording_path.exists():
            return

        # Create a temporary file in the same directory as the recording
        temp_path = self._recording_path.with_suffix(".tmp")
        self._write_merged_recording(temp_path)
        temp_path.replace(self._recording_path)

Non-obvious 设计决策

  • 函数把“是否需要合并”的判断前置为单个早退条件:not self._markers or not self._recording_path.exists()。这样在无标记或无目标文件时直接结束,避免进入后续文件写出流程。
  • 该函数不直接在原录屏路径上写内容,而是先构造 with_suffix(".tmp") 得到临时输出路径,再在末尾调用 replace(...)。从可见代码看,这是一种“先产出完整合并结果、后替换原路径”的两阶段写法。
  • 实际合并内容的生成被下沉到 _write_merged_recording(temp_path),使本函数只保留入口判定与文件路径切换职责;这让控制流保持很短,但也意味着具体合并细节必须到被调辅助方法中查看。

上下游关系

  • 调用方: 未在此源码片段中出现;外部持有 AsciinemaHandler 实例的代码可调用该方法以触发合并
  • 核心被调用: self._recording_path.exists; self._recording_path.with_suffix; self._write_merged_recording; temp_path.replace
  • 配置/状态来源: self._markers:决定是否直接返回; self._recording_path:决定目标文件是否存在,并提供临时路径与替换目标
  • 结果去向: 若早退:无结果写出到外部; 若执行合并:结果先写到 temp_path; 若执行合并:随后由 temp_path.replace(self._recording_path) 反映到原录屏路径; 调用方仅收到 None,可观察结果主要在文件系统
  • 同类 sibling: AsciinemaHandler.__init__:负责把 markersrecording_path 保存到实例状态,本函数消费这些状态决定是否合并; TmuxSession.stop:兄弟条目说明其会在条件满足时触发录屏标记合并;但这一调用关系不由本函数源码自身证明
寄存器交互
AsciinemaHandler._write_merged_recordingasciinema_handler.py:41–60 ↗

将标记写入新录屏文件的合并写出器

stage 上下文: 该方法位于 stage-6 的录屏后处理环节,职责是把已有录屏与已累积的 marker 数据写成一个新的 .cast 输出文件。它是 AsciinemaHandler.merge_markers() 的内部写出步骤;相较同 stage 的 merge_markers 负责整体替换流程,本函数只负责生成合并后的文件内容。与兄弟函数 AsciinemaHandler.__init__ 相比,这里不初始化状态,只读取 self._recording_pathself._markers

这段代码在干什么

函数接收 output_path 作为输出目标,读取 self._recording_path 指向的原始录屏文件,并把合并结果写入新文件。它先单独保留原文件首行头部,再按后续各行依次调用 _process_recording_line(line, output_file, marker_index),并用其返回值更新局部 marker_index。循环结束后,函数把 self._markers[marker_index:] 这一尾部切片交给 _write_remaining_markers 补写到输出文件末尾;实例自身状态不被修改,真正产出是 output_path 上的文件内容。

接口 · 参数 / IO

(self, output_path: Path) -> None

  • 参数: output_path: Path — 新录屏文件的写出路径;以文本写模式 "w" 打开
  • 读状态: self._recording_path, self._markers
  • 返回: 返回 None;实际结果是向 output_path 写出合并后的录屏文本内容。
  • 副作用: 打开 self._recording_path 对应文件进行读取; 以文本写模式 "w" 打开并写入 output_path; 向输出文件写入原录屏头部、逐行处理后的内容,以及剩余 marker 记录; 不修改任何 self 属性,仅推进局部变量 marker_index

执行流

  1. 初始化局部变量 marker_index = 0,作为当前已消费 marker 的位置。
  2. 同时打开 self._recording_path 作为输入文件、打开 output_path 作为输出文件,其中输出端显式使用写模式 "w"
  3. 先调用 input_file.readline() 读取原文件首行,并立即写入 output_file,因此后续 for line in input_file 处理的是除头部之外的剩余各行。
  4. 对输入文件剩余每一行,调用 self._process_recording_line(line, output_file, marker_index),并把其返回值重新赋给局部 marker_index
  5. 当逐行处理结束后,取 self._markers[marker_index:] 这一尚未消费的 marker 切片,调用 self._write_remaining_markers(output_file, self._markers[marker_index:]) 追加到输出末尾。

源码

    def _write_merged_recording(self, output_path: Path) -> None:
        """
        Write a new recording file with markers merged in at the correct timestamps.
        """
        marker_index = 0

        with (
            open(self._recording_path) as input_file,
            open(output_path, "w") as output_file,
        ):
            # Preserve header
            output_file.write(input_file.readline())

            for line in input_file:
                marker_index = self._process_recording_line(
                    line, output_file, marker_index
                )

            # Add any remaining markers at the end
            self._write_remaining_markers(output_file, self._markers[marker_index:])

Non-obvious 设计决策

  • 首行通过 readline() 单独读取并原样写出,而不是让统一的逐行循环处理头部;这一写法明确把头部保留逻辑与后续事件行处理分开。
  • 逐行调用 _process_recording_line(...),并以其返回值覆盖局部 marker_index;这表明 marker 消费进度由辅助函数回传,而不是在本方法内直接改写 self._markers
  • 结束后显式调用 _write_remaining_markers(..., self._markers[marker_index:]) 处理尾部切片;该补写步骤避免未在循环中处理到的剩余 marker 被遗漏。

上下游关系

  • 调用方: AsciinemaHandler.merge_markers
  • 核心被调用: open(self._recording_path); open(output_path, "w"); input_file.readline; AsciinemaHandler._process_recording_line; AsciinemaHandler._write_remaining_markers
  • 配置/状态来源: self._recording_path:提供原始录屏输入路径; self._markers:提供待补写 marker 数据,末尾通过 self._markers[marker_index:] 读取
  • 结果去向: output_path:接收新写出的合并录屏文件; AsciinemaHandler.merge_markers:使用本方法生成的临时/目标文件内容继续后续文件替换流程
  • 同类 sibling: AsciinemaHandler.merge_markers:负责判断是否执行合并并安排目标/临时路径,本函数只负责实际写出内容; AsciinemaHandler.__init__:负责把 markersrecording_path 存入实例,本函数仅读取这些状态; TmuxSession.stop:兄弟条目中负责决定是否触发 marker 合并;本函数不做该判断
寄存器交互
AsciinemaHandler._process_recording_lineasciinema_handler.py:62–90 ↗

录屏逐行合并中的单行处理器

stage 上下文: 该函数位于 stage-6 的录屏后处理流程中,负责在遍历 .cast 事件行时,对单行执行“必要时插入 marker,再写回原行”的局部决策。它不独立启动流程,而是由 AsciinemaHandler._write_merged_recording 在逐行复制原录屏文件时调用;更上层则由 AsciinemaHandler.merge_markers 与框架触发的 TmuxSession.stop() 串接起来。与同 stage 的兄弟函数相比,_write_merged_recording 负责整体文件扫描与尾部补写,本函数只处理当前这一行及 marker_index 的推进。

这段代码在干什么

函数接收一行录屏文本 line、输出流 output_file 与当前 marker 游标 marker_index,尝试把时间戳不晚于该行事件时间的 marker 先写入输出,再把原始录屏行写入合并结果。它读取 self._markers 中待合并的标记,并通过 self._write_marker(...) 产生 marker 行。返回值是更新后的 marker_index;实例状态本身不被修改,真正产出体现为对 output_file 的追加写入。

接口 · 参数 / IO

(self, line: str, output_file: TextIO, marker_index: int) -> int

  • 参数: line: str — 当前读取到的一行 asciinema 录屏文本,来自原 .cast 文件的逐行遍历; output_file: TextIO — 合并后录屏文件的写入目标;marker 行与原始行都写到这里; marker_index: int — 指向 self._markers 中下一个待写 marker 的位置,由外层循环传入并接收更新值
  • 读状态: self._markers
  • 返回: 返回更新后的 marker_index,供外层逐行处理时继续衔接后续 marker;真正可见结果是 output_file 中新增的 marker 行和原始录屏行。
  • 副作用: 向 output_file 写入零个或多个 marker 行; 向 output_file 写入原始 line

执行流

  1. 首先检查 line.startswith("["):若当前行看起来不是 asciinema 事件数组行,则不做解析,直接把原行写入 output_file,并原样返回传入的 marker_index
  2. 若该行以 [ 开头,则进入 try 分支,使用 json.loads(line) 解析该行,并从 data[0] 读取事件时间,再用 float(...) 统一成可比较的 timestamp
  3. 在成功取得 timestamp 后,循环检查 marker_index < len(self._markers)self._markers[marker_index][0] <= timestamp;只要下一个 marker 的时间不晚于当前录屏事件,就调用 self._write_marker(output_file, self._markers[marker_index]) 先写出该 marker,并将 marker_index 递增。
  4. 若解析过程中出现 json.JSONDecodeErrorValueErrorIndexError,异常被捕获后不终止处理,也不改写该行内容;函数仅放弃本行的 marker 插入判断。
  5. 无论本行是否成功解析、是否插入了 marker,最后都会执行 output_file.write(line) 写回原始录屏行,并返回当前的 marker_index

源码

    def _process_recording_line(
        self,
        line: str,
        output_file: TextIO,
        marker_index: int,
    ) -> int:
        """Process a single line from the recording, inserting markers as needed."""
        if not line.startswith("["):
            output_file.write(line)
            return marker_index

        try:
            data = json.loads(line)
            timestamp = float(data[0])

            # Insert any markers that should appear before this timestamp
            while (
                marker_index < len(self._markers)
                and self._markers[marker_index][0] <= timestamp
            ):
                self._write_marker(output_file, self._markers[marker_index])
                marker_index += 1

        except (json.JSONDecodeError, ValueError, IndexError):
            # If we can't parse the line, preserve it as-is
            pass

        output_file.write(line)
        return marker_index

Non-obvious 设计决策

  • 函数把“无法识别的行”视为应当保留的原始内容:既包括一开始不以 [ 开头的行,也包括 json.loads、时间转换或索引提取失败的情况。这样做的取舍是优先保证录屏文件内容不丢失,即使某些行无法参与 marker 对齐,也不因为后处理而破坏原始记录。
  • marker 的插入条件采用 self._markers[marker_index][0] <= timestamp,意味着与当前事件时间相同的 marker 会写在该事件行之前。这一约定保证同一时间点的标记在输出顺序上“先标记、后事件”,与 stage-6 对“在时间戳达到时插入 marker”的整体目标一致。
  • 函数只返回推进后的 marker_index,而不修改 self._markers 本身。这让 marker 列表保持只读输入,跨行状态由外层 _write_merged_recording 显式持有,便于逐行遍历时清楚地区分‘源数据’与‘处理进度’。

上下游关系

  • 调用方: AsciinemaHandler._write_merged_recording
  • 核心被调用: json.loads; float; output_file.write; self._write_marker
  • 配置/状态来源: self._markers; line 参数中的录屏事件文本; marker_index 参数中的外层遍历进度; output_file 参数中的合并输出目标
  • 结果去向: 返回给 AsciinemaHandler._write_merged_recording 的更新后 marker_index; 写入到 output_file 的 marker 事件行; 写入到 output_file 的原始录屏行; 间接贡献于 AsciinemaHandler.merge_markers 生成的临时合并录屏文件
  • 同类 sibling: AsciinemaHandler._write_merged_recording:负责打开原始/目标文件、保留头部、逐行调用本函数并在末尾补写剩余 marker。; AsciinemaHandler.merge_markers:负责整体入口判定与临时文件替换,不处理单行内容。; TmuxSession.stop:在框架结束会话时决定是否触发整个 marker 合并流程。
寄存器交互
AsciinemaHandler._write_remaining_markersasciinema_handler.py:98–103 ↗

录屏尾部标记补写助手

stage 上下文: 该函数位于 stage-6 的 asciinema 后处理子流程中,角色是把一组尚待处理的 marker 逐个交给统一的 marker 写出逻辑。它本身不判断录屏状态、也不处理录屏行内容;与同 stage 的兄弟函数相比,这里只负责尾部补写这一件事。其触发时机与上层调用路径属于外部上下文,本函数源码自身未体现。

这段代码在干什么

函数接收 output_filemarkers 两个参数,对 markers 中的每个元组执行一次 self._write_marker(output_file, marker)。若 markers 为空,则循环体不会执行,函数直接结束。返回值为 None;真正产出是向 output_file 零次或多次委托写入 marker 记录,本函数不修改任何实例属性。

接口 · 参数 / IO

(self, output_file: TextIO, markers: list[tuple[float, str]]) -> None

  • 参数: output_file: TextIO — marker 写出目标流,原样传给 _write_marker; markers: list[tuple[float, str]] — 待处理的 marker 序列;函数按其迭代顺序逐个转交
  • 返回: 返回 None;实际效果是向 output_file 零个或多个 marker 的写出委托。
  • 副作用: 对 markers 中每个元素调用一次 self._write_marker(output_file, marker); 可能经由 _write_markeroutput_file 产生零次或多次写入

执行流

  1. 进入函数后不做前置判断,直接对参数 markers 执行 for marker in markers 迭代。
  2. 每取得一个 marker,就调用一次 self._write_marker(output_file, marker),把输出流与当前 marker 一并传入。
  3. markers 遍历完成后自然结束;若输入列表为空,则不会发生任何 _write_marker 调用。

源码

    def _write_remaining_markers(
        self, output_file: TextIO, markers: list[tuple[float, str]]
    ) -> None:
        """Write any remaining markers that come after all recorded events."""
        for marker in markers:
            self._write_marker(output_file, marker)

Non-obvious 设计决策

  • 函数把逐项写出的具体工作完全委托给 _write_marker,自身只承担遍历与转交职责;这种拆分使当前函数保持极小粒度,而 marker 的具体写法集中在单一入口。
  • 代码没有对 markers 为空做显式分支,而是依赖 for 循环的自然零次执行语义处理空输入;这样减少了额外控制流,同时明确允许“什么都不写”这一结果。

上下游关系

  • 调用方: 外部上层合并流程中的某个调用点(源码片段自身未显示具体调用者)
  • 核心被调用: AsciinemaHandler._write_marker
  • 配置/状态来源: 参数 markers:提供待处理 marker 序列; 参数 output_file:提供写出目标流; 实例方法 self._write_marker:提供逐项写出能力
  • 结果去向: 传入的 output_file; 调用方可观察到的副作用:_write_marker 被按迭代次数调用
  • 同类 sibling: AsciinemaHandler._process_recording_line:同属 marker 合并链路,但该函数处理单条录屏行并返回更新后的游标;本函数不处理录屏行、也不返回游标,只做剩余 marker 的逐项转交。; AsciinemaHandler._write_merged_recording:兄弟函数负责整体文件读写编排;本函数只覆盖其中“把一组 marker 逐个写出”的局部职责。
AsciinemaHandler._write_markerasciinema_handler.py:92–96 ↗

将单个标记写成 JSON 行

stage 上下文: 该函数是 stage-6 录屏后处理中的最小写出单元,职责仅限于把一个 marker 参数转换为一行文本并写入 output_file。与同 stage 的 _process_recording_line_write_remaining_markers 相比,它不处理遍历、排序或游标推进,只负责单条 marker 的具体落盘格式。

这段代码在干什么

函数接收 output_filemarker 两个参数,其中 marker 按代码要求可解包为 (marker_time, marker_label)。它构造列表 [marker_time, "m", marker_label],再用 json.dumps(...) 序列化,并把结果字符串加上换行符 "\n" 后写入 output_file。函数返回 None,可观察产出是一次对文件对象的 write(...) 调用。

接口 · 参数 / IO

(self, output_file: TextIO, marker: tuple[float, str]) -> None

  • 参数: output_file: TextIO — 接收序列化结果的文本输出流; marker: tuple[float, str] — 必须能被解包为 (marker_time, marker_label) 的二元组
  • 返回: 返回 None;真正产出是向 output_file 写入一行以换行结尾的 JSON 字符串。
  • 副作用: 调用 output_file.write(...) 写出 json.dumps(marker_data) + "\n"

执行流

  1. 将参数 marker 解包为 marker_time, marker_label
  2. 基于这两个值构造列表 marker_data = [marker_time, "m", marker_label]
  3. marker_data 调用 json.dumps(...) 生成 JSON 字符串。
  4. 把该字符串与换行符 "\n" 拼接后传给 output_file.write(...)

源码

    def _write_marker(self, output_file: TextIO, marker: tuple[float, str]) -> None:
        """Write a single marker event to the output file."""
        marker_time, marker_label = marker
        marker_data = [marker_time, "m", marker_label]
        output_file.write(json.dumps(marker_data) + "\n")

Non-obvious 设计决策

  • 输出结构固定为三元素列表 [marker_time, "m", marker_label],其中事件标记写死为字符串字面量 "m",而不是从参数或实例状态读取。
  • 写出前先经过 json.dumps(marker_data),说明输出采用 JSON 文本格式而非手工拼接字符串。
  • 写入内容末尾显式补上 "\n",因此单次调用生成的是一条换行结尾的独立文本记录。

上下游关系

  • 调用方: AsciinemaHandler._process_recording_line; AsciinemaHandler._write_remaining_markers
  • 核心被调用: json.dumps; output_file.write
  • 配置/状态来源: 参数 marker 提供 marker_timemarker_label; 参数 output_file 提供写出目标
  • 结果去向: output_file 中写入的单行 JSON 文本; 上层录屏改写流程消费该写出结果
  • 同类 sibling: AsciinemaHandler._process_recording_line:负责决定何时调用本函数写出 marker。; AsciinemaHandler._write_remaining_markers:负责对多个 marker 逐个调用本函数。; AsciinemaHandler._write_merged_recording:负责整体文件读写与尾部补写。