Skip to content

章节梗概 (3/9):ChaptersProcessor gate 安全加固 r1–r14 - #17

Merged
zj1123581321 merged 36 commits into
mainfrom
stack/chapters-03-gate-fixes-1
Jul 21, 2026
Merged

zj1123581321 merged 36 commits into
mainfrom
stack/chapters-03-gate-fixes-1

Conversation

@zj1123581321

Copy link
Copy Markdown
Collaborator

范围

Stacked PR 系列第 3 个(依赖 #16)。ChaptersProcessor 与字幕时间轴的 gate 评审加固第 1 批(r1–r14,16 个修复提交),全部为正确性/可靠性/安全修复:

  • 时间解析严格化:inf/nan/溢出拒绝、UnicodeDecodeError 兜底、OverflowError 防御、负值/倒挂区间清洗
  • 章节指纹防碰撞:不可见分隔符拼接、规范化 JSON 序列化、纳入时间戳与 speaker 字段
  • SRT 解析不变式:正文纯数字行需 lookahead 才当索引、cue 时间轴缺失时正文不丢、宽松 "-->" 误判收紧
  • 输入伪造防护:_build_segment_lines 压平 text/speaker 内换行,杜绝伪造编号行绕过门控
  • 门控补齐:有效 segment 数 < 2 跳过 LLM 调用、非 dict 条目不崩溃、title 超长软截断

配置/Schema 触点

LLMConfig 章节字段位置调整(移至可选位置参数末尾,避免移位既有参数)。

验证证据

  • 每个修复提交均先补复现测试再修(TDD),uv run pytest tests/unit 全绿(本地边界点已预验证)
  • 评审记录见 docs/sessions/260719-0513-chapters/ 下 gate 日志

Stacked 结构

  1. 章节梗概 (1/9):统一 segments 带时间片段读取适配器(T3) #15 T3 适配器 / 2. 章节梗概 (2/9):字幕时间戳保留(T2)+ ChaptersProcessor 核心(T5) #16 T2+T5 / 3. 本 PR(stack-02 ← stack-03)/ 4. gate 加固 r15–r28 / 5. 缓存层 / 6. 接线 / 7. T8 / 8. T9+T10 / 9. T11

zj1123581321 and others added 30 commits July 20, 2026 07:22
SRT/XML/transcript-api 三条字幕解析路径此前都存在同一类问题:单条 cue 的时间
轴损坏时,要么整条 cue(连同文本)被静默跳过不进 segments(SRT 路径),要么
一条脏数据直接让整批 segments 降级为 None(XML、transcript-api 路径的批量
try/except)。下游若按 segments 消费文本会静默丢字。

统一修复为按条目 (per-item) 容错:时间轴/属性损坏只让该条的 start_time /
end_time 置为 None,text 原样保留并留在 segments 里;只有整体没有任何可识别
cue(或全部文本为空)时 segments 才整体为 None。同步更新 SubtitleResult 的
docstring 声明这条不变式。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…成真实时间戳

_to_seconds 此前对数值型/字符串型(含 "inf" 这种 float() 能悄悄解析的字符串)
的非有限值不做过滤,会当成真实秒数往下传;_format_timestamp 对 inf/nan 调用
int() 分别抛 OverflowError/ValueError。现在两处都用 math.isfinite 兜底,双重
防御(即使调用方绕过 _to_seconds 直接调 _format_timestamp 也不崩)。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_validate_and_normalize_start_segs 之前对 title/gist 用 str(x or "").strip()
硬转成空串,LLM 漏填、返回非字符串(如数字)或空白字符串都会照样成章。现在与
start_seg 校验对齐:缺失/非 str/strip 后为空一律判定语义校验失败,拼错误提示
重试一次,仍不合法则 FAILED。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
原逻辑只在 segments 为 None/空时跳过;非空 segments 但每条 start_time 都是
None(或解析不出)也会照常调 LLM 并可能返回 GENERATED,而章节功能的核心价值
就是时间范围,完全没有时间轴生成出来的章节没有意义。新增门控:至少一条
segment 能解析出 start_time 才继续,部分 None 仍容忍(下游 _derive_times 本
就支持单条 None)。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…,不再把 None 传给 LLM 客户端

LLMConfig.from_dict() 会把未配置的 chapters_model 默认成 calibrate_model,但
直接用 LLMConfig(...) 构造(跳过 from_dict,例如某些测试或调用方)时
chapters_model 仍是 None,处理器会拿 model=None 去调 LLM 客户端。现在在
process() 选模型之后补一条运行时兜底链:chapters_model -> summary_model ->
calibrate_model,与 from_dict 的默认语义对齐,同时保留"已配置值优先"。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
原结构校验只在合并之前检查章节数量在 [2, 100] 区间,LLM 返回两个相邻同名章节
时能通过该检查(数量=2),但随后的 _merge_adjacent_same_title 会把它们合并成
1 章,最终仍以 GENERATED 返回,突破了"至少 2 章"这条不变式。现在合并后补一次
下限重查(合并只会减少数量,不需要重查上限),跌破则 FAILED,error 里带上
"合并后不足两章"的中文说明方便排查。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
本分支新增的 chapters_model/chapters_reasoning_effort/min_chapters_threshold/
max_chapters_input_chars 之前插在 dataclass 字段列表中部(speaker_model 之后、
质量配置区),会挤占其后既有可选字段的位置序号——老代码若按位置传参(例如
speaker 相关配置)会静默写进这些新字段。现在把这 4 个字段整体挪到字段列表最
末尾(total_timeout 之后),from_dict 用的是关键字参数构造,逻辑不变。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_looks_like_timeline_attempt 此前只要一行含 "-->" 就当作时间轴声明,导致
正文里偶然出现的 "-->"(如 "Settings --> Privacy" 这类操作指引文本)被误判
成下一条 cue 的时间轴,把该行从当前 cue 文本中错误地切走。现在只有紧跟在
纯数字索引行之后(预期时间轴位置)的 "-->" 行才会被当作(哪怕已损坏的)
时间轴声明,正文收集过程中遇到的 "-->" 行一律当普通文本处理。R1 引入的
"损坏时间轴仍保留 cue 文本" 行为不变,parse_srt_to_text 输出逐字节不变。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fingerprint 此前直接 "".join() 拼接各 segment 文本再 sha1,["ab","c"] 与
["a","bc"] 会拼出同一个 "abc",指纹碰撞导致上层误判"原文未变化"。改用
"\x1f"(ASCII Unit Separator)分隔各 segment 文本再取 sha1;full_text 仍
用无分隔符拼接(供门控 3 长度判断),不受影响。fingerprint 尚未在任何生产
路径持久化,改变取值方式是安全的。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
transcriber/segments.py 的 parse_time_to_seconds 与 chapters_processor.py
的 _to_seconds 都声明"绝不抛异常",但当输入是 JSON 合法的超大整数(如
10**400,反序列化后是 Python int)时,int/float 分支里的 float(value) 会
抛 OverflowError(区别于字符串路径的 float() 会静默变成 inf)。两处均补上
try/except OverflowError,按非法时间处理返回 None,行为与已有的
非有限值/负数兜底一致。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prompt 声明章节 title 不超过 20 字,但本地此前未做任何长度约束,纯靠 LLM
自觉遵守不可靠。超长标题本身不是语义错误,不值得为它触发重试或判定
FAILED——strip 后长度 > 24 字时本地截断为前 23 字 + "…",只记 warning 日志,
不影响 GENERATED 状态。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
四段冒号时间(如 "00:00:00:41")曾被 chapters_processor 私有 _to_seconds 的
手写解析逻辑误当成合法秒数(41s),因为它没有像 transcriber/segments.py 的
parse_time_to_seconds 那样校验 split 段数必须是 2 或 3。根因级修复:删除私有
重复解析逻辑,直接委托共享实现(transcriber 包不依赖 llm 包,无循环导入),
仅保留一层"调共享函数 + 记 warning 日志"的薄包装。共享实现已具备的全部防御
(非有限值/负数/OverflowError/段数校验)随之自动生效。

同步删除处理器侧因此重复的防御性测试(负数/非有限值/OverflowError 等,已被
test_segments_adapter.py::TestParseTimeToSeconds 覆盖),保留委托行为断言;
并给共享测试补上此前遗漏的 bool 输入用例。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
segments 时间乱序或坏数据会导致某章反查/合并出的 end_time 早于 start_time,
此前仍判 GENERATED 并原样产出这个非法区间。修复:新增 _sanitize_end_time,
在合并相邻同名章节之后、构造最终 Chapter 之前统一校验——若 start_time 与
end_time 均非 None 且 end_time < start_time,丢弃 end_time(置 None)并记
warning,状态照常 GENERATED;不重排 segments,不改变 start_seg/end_seg 的
顺序语义。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- get_subtitle_result() 的有效性判定由「仅看 text」改为「text 非空 或
  segments 非空」:API Server 路径的 SRT 若正文全是数字,旧的逐字节兼容
  文本提取会把纯数字行误判为序号行而跳过(text=""),但新的 segments
  提取用 lookahead 正确保留了这些内容;只看 text 会把整条结果(含时间轴)
  一并当作"无字幕"丢弃。本地分支的同类判定点同步统一口径;get_subtitle()
  (旧 str 入口)逐字节行为不变,未触碰。
- 新增共享 helper subtitle_types.sanitize_time_pair(),统一负 start / 负
  duration(体现为负 end 或 end<start)/ 区间倒挂 三类不合法时间戳的诚实
  降级规则(一律置 None,绝不影响文本保留),并在三条字幕解析路径
  (youtube-transcript-api 分段、TikHub XML、SRT)复用,避免各自重复实现。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sanitize_time_pair 的权威实现从 downloaders/subtitle_types.py 挪到
transcriber/segments.py(时间工具统一家,parse_time_to_seconds 已在此,
chapters_processor 也从这里导入),subtitle_types.py 改为 re-export 保持
既有 import 路径可用。normalize_segments 逐条应用该校验:此前
{start_time:10, end_time:5} 这类倒挂区间会原样放行给下游,而三条字幕解析
路径早已做了同款清洗,唯独中央适配器漏了。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolver 返回字符串 "1e309" 时 float() 静默溢出成 inf(不像超大整数 int 会抛
OverflowError),现有 try/except 完全拦不住,VideoMetadata.duration 会携带
非有限值暴露给下游;JSON 数值 1e309 反序列化后同样已是 inf。float() 转换
成功后补一道 math.isfinite 校验,非有限值按转换失败同等处理;顺带把负
duration(时长不可能为负)也纳入无效分支。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_parse_youtube_subtitle_xml 的 _safe_start 排序 key 只捕获了 float()
的 ValueError/TypeError,但 float("nan") 不会抛异常——一旦某条 XML
条目的 start="nan",sort() 会拿到字面量 nan 作为 key。NaN 与任何数
比较恒为 False,会破坏其它合法条目之间本应正确的相对顺序(如
[5, nan, 1] 排序后仍可能是乱序),而不只是这条脏数据自己排错位置。
改为 isfinite 校验后统一按 0 处理,语义与"不可解析字符串"一致。

via [HAPI](https://hapi.run)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: HAPI <noreply@hapi.run>
zj1123581321 and others added 6 commits July 20, 2026 07:22
_build_subtitle_result_from_snippets(youtube-transcript-api 路径)与
_parse_youtube_subtitle_xml(TikHub XML 路径)此前都是先用未清洗的
start 算出 end = start + duration,最后才统一调用 sanitize_time_pair
清洗负 start。start=-5、duration=10 这类输入会先算出 end=5(非负,
能骗过 sanitize_time_pair 的"end 为负"规则),最终泄漏 (None, 5) ——
这个 end 本身毫无意义,因为它是从一个已知非法的起点派生出来的。

改为先清洗 start(负数置 None),end_time 只从已验证合法的 start
派生;start 无效则 end 直接 None,不再计算。两条路径同步修复。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er=0 被吞

speaker=0 是合法的说话人 ID(FunASR 说话人分离结果里第一位说话人的
标签),但 `if speaker:` 把 0 当假值处理,导致该说话人的前缀在拼进
LLM prompt 时被静默省略。改为 `is not None`,与指纹计算侧
(`_compute_fingerprint`)已经在用的判断口径保持一致;str 强转后的
空串仍按"无说话人"省略,语义不变。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
新增三个测试锁定 P2 发现:segments 列表混入 None/字符串等非 dict 条目时
process() 崩溃、以及内部异常未被兜底捕获会直接向外抛出。此提交下这些
测试处于红灯状态,下一个提交实现修复使其转绿。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
两层修复:① 入口过滤非 dict 条目(记 warning 含过滤数量),语义与
transcriber/segments.py::normalize_segments 跳过非 dict 条目一致;过滤后
为空则走既有 SKIPPED_NO_TIMELINE 门控。② process() 主体拆到 _process_impl,
外层包一层 try/except Exception 兜底返回 FAILED(error 带异常信息,日志
带堆栈),作为接线批完成前防未知异常的最后一道防线;已知分支仍各自返回
对应诚实状态,不依赖这层兜底代劳。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Reviewed commit: 7650226687894df270fc77e34700940678e01b8c

🤖 Review 门禁 — 判定: pass(主审: claude-glm)

本 PR 是章节梗概系列第 3 个 stacked PR,对 ChaptersProcessor 与三条 YouTube 字幕时间轴解析路径做了 r1–r14 共 16 个修复提交。经逐文件审查与边界追踪,所有核心逻辑正确且匹配设计意图:

  1. 时间解析严格化(inf/nan/OverflowError/负值/倒挂区间清洗)— 在 parse_time_to_seconds、sanitize_time_pair、三条字幕路径的 per-item 容错中均正确实现,且测试矩阵覆盖完整。
  2. 章节指纹防碰撞 — _compute_fingerprint 改用规范化 JSON 序列化(sort_keys + 紧凑分隔符),彻底消除了手工分隔符拼接的碰撞风险;speaker 缺失 vs 空串 vs "\x00" 的区分测试齐全。
  3. SRT 解析不变式 — _looks_like_timeline_attempt + 孤儿文本缓冲 + digit 行 lookahead 逻辑经多组测试用例追踪验证正确,text 与 segments 的一致性符合预期设计。
  4. 门控补齐 — 有效 segment < 2 跳过 LLM、非 dict 过滤、title/gist 非空校验、合并后重新校验下限、title 软截断时序(截断在合并之后)均正确。
  5. LLMConfig 字段移至末尾 — 所有既有调用方均使用关键字参数,位置参数兼容性不受影响。

发现 3 个非阻断问题(2 minor + 1 nit),均不影响正确性或安全性。

覆盖:single;1 个审查单元;3595 行 diff。

  • minor · design · design.build_segment_lines_newline_not_flattened src/video_transcript_api/llm/processors/chapters_processor.py:268 — PR intent 与实现不一致:声称的输入伪造防护(压平内嵌换行)未落地。虽然当前威胁模型下(单人维护、输入来自可信上游)实际风险极低,但 PR 描述把一个不存在的加固列为了已完成项,后续维护者可能误以为该防护已生效。
    • 触发:PR intent 声称「_build_segment_lines 压平 text/speaker 内换行,杜绝伪造编号行绕过门控」,但实际代码未实现该压平逻辑
    • 证据:第 268 行 text = (seg.get("text") or "").strip() 仅去除首尾空白,不含 .replace("\n", ...) 或等价操作;第 282 行 str(speaker) 同样不做换行替换。若 segment text 内嵌换行(如 "hello\n42"),prompt 输出会在单条 segment 行后多出无 [i] 前缀的裸行。TestBuildSegmentLines 测试类中也没有任何换行压平的用例。
    • 验收:在 _build_segment_lines 中对 text 和 speaker 值做换行压平(如 text = (seg.get("text") or "").strip().replace("\n", " ")),并补一条对应单元测试(segment text 含内嵌换行时输出为单行);或修正 PR intent 描述,移除此条声称。
  • minor · correctness · correctness.density_warning_before_sanitize src/video_transcript_api/llm/processors/chapters_processor.py:789 — 密度告警基于未清洗的时间数据计算,在倒挂区间存在时可能产生误导性 warning。这是一个逻辑一致性问题:同一份时间数据在密度检查时用原始值、在最终输出时用清洗值,两处口径不一致。
    • 触发:_warn_if_density_out_of_range(merged) 在第 789 行调用,早于第 807 行的 _sanitize_end_time;密度计算使用的是未经倒挂清洗的原始 end_time
    • 证据:第 488–492 行 durations 列表过滤条件为 start_time is not None and end_time is not None,但不排除 end_time < start_time 的倒挂条目。当某章因 segments 时间乱序出现 end_time=100、start_time=500 时,该章贡献 duration=-400 到平均值,可能把原本正常的平均时长拉低到 _MIN_CHAPTER_SECONDS 以下,触发误导性的「average duration is short」warning。最终结果不受影响(步骤 7 的 _sanitize_end_time 会正确地将倒挂 end_time 置 None),但日志中的密度告警会失真。
    • 验收:将 _warn_if_density_out_of_range 调用移到 _sanitize_end_time 之后(对 merged 做一次批量清洗再传给密度检查),或在 _warn_if_density_out_of_range 内部过滤掉 end_time < start_time 的条目(durations 推导式追加 end_time >= start_time 条件)。
  • nit · reliability · reliability.duplicate_to_seconds_warnings src/video_transcript_api/llm/processors/chapters_processor.py:637 — 重复日志噪声:同一份坏数据在三个处理阶段各产生一遍 warning,不影响正确性但增加日志排查时的干扰。
    • 触发:_to_seconds 对同一批 segment 时间值在 _compute_fingerprint(第 637 行)、has_any_start_time 门控(第 643–645 行)、_derive_times(第 768 行)中被重复调用
    • 证据:当 segment 时间为不可解析的非 None 值(如 "not-a-time")时,_to_seconds 每次调用都会记录一条 [CHAPTERS] Unparseable time value warning。同一批 N 个脏时间值在最坏情况下会产生 3N 条重复 warning(指纹 + 门控 + 时间推导各一遍)。
    • 验收:可选优化:在 _compute_fingerprint 中直接调用 parse_time_to_seconds(无日志包装版本),仅保留 has_any_start_time / _derive_times 处的 warning;或将解析结果缓存到 segment dict 上避免重复解析。此项为 nit,不阻断合入。

@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

📒 Review ledger state

  • Commit: 7650226687894df270fc77e34700940678e01b8c
  • Round: 2
  • Status / findings: pass / 3
  • Comparison: same_head_rerun; stable/missing/appeared = 0/0/3

完整数据保存在 codex-review-ledger artifact;此 sticky comment 仅保存跨 rerun 的连续游标。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant