录制后处理(外部)
这个 Stage 在解决什么问题?它负责把运行过程中记下来的 asciinema marker(录屏里的时间标签)真正写回 .cast 录屏文件里,让回放不只是“有画面”,还“有关键节点”。之所以单独放在这里,是因为录屏文件要等 tmux(可远程控制的终端)会话结束后才算完整,才能安全地做合并。所以它不是 Agent run() 内的一段,而是框架在收尾时做的外部后处理。
主流程
- 先看这次运行有没有留下 marker
这些 marker 不是这里产生的,而是前面运行时陆续记进
reg-asciinema-markers。如果一个都没有,这个 Stage 基本就没有事做;录屏文件保持原样即可。 - 等录屏真正结束,再做合并
触发点是
TmuxSession.stop()(关闭 tmux 会话并收尾),不是 Agent 主循环。这样设计是因为 asciinema 的.cast文件在录制中还在持续追加,太早改,文件可能还没写完。 - 按时间把 marker 插进录屏流里
AsciinemaHandler.merge_markers()(把内存里的 marker 合并进.cast文件)会保留原文件头,然后一行一行看录屏帧。核心规则很简单:当 marker 时间<=当前帧时间时,先把这些 marker 写出去,再写这一帧。 这样做的意义是:回放时,标签会出现在它应该出现的位置之前,不会晚于对应画面。 - 处理两种边角情况
- 如果某些 marker 的时间比所有录屏帧都晚,就在最后统一补上。这个收尾由
_write_remaining_markers()(把还没写出的 marker 追加到末尾)负责。 - 如果录屏里几乎没帧,或者文件很短,marker 也不会丢;它们仍然会在末尾被写出。
- 如果某些 marker 的时间比所有录屏帧都晚,就在最后统一补上。这个收尾由
- 产出一个“带标签的最终录屏”
这一步不改变 Agent 的决策,不回流给 LLM。它的价值主要在外部:给人看回放、做调试、对齐运行中的关键事件。 也正因为如此,它适合做“事后合并”,而不是在运行中实时往录屏里插事件——后者更容易和录制过程互相干扰,也更难保证文件顺序正确。
状态流动
- 读:
reg-asciinema-markers— 在 tmux 会话停止、录屏文件已完整后读取;按时间顺序把运行中积累的 marker 合并进.cast - 清:
reg-asciinema-markers— 本 Stage 不负责清;它只消费这些 marker,用于生成外部录屏文件
与前后 Stage 的衔接
上游真正交给它的不是 stage-5,而是更早的 stage-4.7:那里把运行中的关键时间点写进 reg-asciinema-markers。本 Stage 在 Agent 结束之后、框架关闭 tmux 会话时读取这些标记,产出最终的带 marker 的 .cast 文件;它本质上是一次终态后处理,没有明确的 Agent 内部下游。
TmuxSession.stoptmux_session.py:496–508 ↗
会话收尾时的录屏标记合并关口
stage 上下文: 该片段位于 stage-6,是录屏后处理真正落地的入口分支:在 Harbor 框架调用
TmuxSession.stop()之后,决定是否把运行期积累的 marker 写回.cast录屏文件。它本身不负责生成 marker,只消费先前阶段累积在self._markers中的数据,并把实际合并工作委托给AsciinemaHandler.merge_markers()。由于这是本 stage 的首个已翻译单元,它承担了“是否进入后处理”的门控角色。
这段代码在干什么
这段代码检查 self._markers 与 self._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 指向的录屏文件
执行流
- 先联合检查
self._markers与self._local_asciinema_recording_path;只有 marker 列表非空且本地录屏路径可用时,才进入后续合并分支。 - 进入合并分支后,先用
self._logger.debug记录待合并的 marker 数量,即len(self._markers),用于说明即将执行的后处理工作量。 - 随后以
self._markers和self._local_asciinema_recording_path为输入构造AsciinemaHandler,把具体文件后处理职责交给专门组件。 - 调用
handler.merge_markers()执行实际合并;成功返回后,再记录一条 debug 日志,明确目标文件就是self._local_asciinema_recording_path。 - 若前置条件不满足,则不实例化处理器、不触碰录屏文件,只记录
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()及其内部行处理逻辑;本片段只负责决定是否进入这些兄弟处理步骤。
reg-asciinema-markers— 停止会话时消费已累积 marker
AsciinemaHandler.__init__asciinema_handler.py:11–20 ↗
初始化录屏处理器内部字段
stage 上下文: 该单元是
AsciinemaHandler的构造函数,仅负责把构造参数落入实例状态。就当前源码可见范围而言,它不执行文件处理,也不触发任何外部流程;与同 stage 中真正执行合并的兄弟函数相比,这里只完成初始化准备。
这段代码在干什么
该函数接收 markers 与 recording_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._markers与self._recording_path的赋值。 - 副作用: 写入
self._markers; 写入self._recording_path
执行流
- 检查参数
markers的真值:若为真,则执行sorted(markers, key=lambda x: x[0]),并将结果写入self._markers;若为假,则将空列表[]写入self._markers。 - 将传入的
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__相比,后者只负责保存markers与recording_path,本函数则基于这些实例状态进入“直接返回”或“生成临时文件并替换原路径”两条路径。它自身不实现逐行合并逻辑,而是把实际内容生成委托给_write_merged_recording。
这段代码在干什么
函数读取 self._markers 与 self._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)替换原录屏路径
执行流
- 先检查
not self._markers or not self._recording_path.exists();只要待合并标记为空,或目标录屏路径当前不存在,就立即return,本次调用结束。 - 若继续执行,则从
self._recording_path派生temp_path = self._recording_path.with_suffix(".tmp"),把输出目标切换到同一路径基名下的.tmp文件。 - 调用
self._write_merged_recording(temp_path),把“如何根据现有录屏与标记写出合并结果”的工作委托给辅助方法完成。 - 在辅助方法返回后,调用
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__:负责把
markers与recording_path保存到实例状态,本函数消费这些状态决定是否合并; TmuxSession.stop:兄弟条目说明其会在条件满足时触发录屏标记合并;但这一调用关系不由本函数源码自身证明
reg-asciinema-markers— 经self._markers间接读取已积累标记
AsciinemaHandler._write_merged_recordingasciinema_handler.py:41–60 ↗
将标记写入新录屏文件的合并写出器
stage 上下文: 该方法位于 stage-6 的录屏后处理环节,职责是把已有录屏与已累积的 marker 数据写成一个新的
.cast输出文件。它是AsciinemaHandler.merge_markers()的内部写出步骤;相较同 stage 的merge_markers负责整体替换流程,本函数只负责生成合并后的文件内容。与兄弟函数AsciinemaHandler.__init__相比,这里不初始化状态,只读取self._recording_path与self._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
执行流
- 初始化局部变量
marker_index = 0,作为当前已消费 marker 的位置。 - 同时打开
self._recording_path作为输入文件、打开output_path作为输出文件,其中输出端显式使用写模式"w"。 - 先调用
input_file.readline()读取原文件首行,并立即写入output_file,因此后续for line in input_file处理的是除头部之外的剩余各行。 - 对输入文件剩余每一行,调用
self._process_recording_line(line, output_file, marker_index),并把其返回值重新赋给局部marker_index。 - 当逐行处理结束后,取
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__:负责把
markers与recording_path存入实例,本函数仅读取这些状态; TmuxSession.stop:兄弟条目中负责决定是否触发 marker 合并;本函数不做该判断
reg-asciinema-markers— 经由self._markers[marker_index:]只读消费剩余标记
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
执行流
- 首先检查
line.startswith("["):若当前行看起来不是 asciinema 事件数组行,则不做解析,直接把原行写入output_file,并原样返回传入的marker_index。 - 若该行以
[开头,则进入try分支,使用json.loads(line)解析该行,并从data[0]读取事件时间,再用float(...)统一成可比较的timestamp。 - 在成功取得
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递增。 - 若解析过程中出现
json.JSONDecodeError、ValueError或IndexError,异常被捕获后不终止处理,也不改写该行内容;函数仅放弃本行的 marker 插入判断。 - 无论本行是否成功解析、是否插入了 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 合并流程。
reg-asciinema-markers— 逐行合并时按游标读取标记
AsciinemaHandler._write_remaining_markersasciinema_handler.py:98–103 ↗
录屏尾部标记补写助手
stage 上下文: 该函数位于 stage-6 的 asciinema 后处理子流程中,角色是把一组尚待处理的 marker 逐个交给统一的 marker 写出逻辑。它本身不判断录屏状态、也不处理录屏行内容;与同 stage 的兄弟函数相比,这里只负责尾部补写这一件事。其触发时机与上层调用路径属于外部上下文,本函数源码自身未体现。
这段代码在干什么
函数接收 output_file 与 markers 两个参数,对 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_marker对output_file产生零次或多次写入
执行流
- 进入函数后不做前置判断,直接对参数
markers执行for marker in markers迭代。 - 每取得一个
marker,就调用一次self._write_marker(output_file, marker),把输出流与当前 marker 一并传入。 - 当
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_file 与 marker 两个参数,其中 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"
执行流
- 将参数
marker解包为marker_time, marker_label。 - 基于这两个值构造列表
marker_data = [marker_time, "m", marker_label]。 - 对
marker_data调用json.dumps(...)生成 JSON 字符串。 - 把该字符串与换行符
"\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_time与marker_label; 参数output_file提供写出目标 - 结果去向:
output_file中写入的单行 JSON 文本; 上层录屏改写流程消费该写出结果 - 同类 sibling: AsciinemaHandler._process_recording_line:负责决定何时调用本函数写出 marker。; AsciinemaHandler._write_remaining_markers:负责对多个 marker 逐个调用本函数。; AsciinemaHandler._write_merged_recording:负责整体文件读写与尾部补写。