Files
EnterpriseArchitect/knowledge/运维/shared-scripts/pre_trim_multica_comment_v1.0.0.py
T
严维序 opengineer 8c931dde1c BIZ-104 v1.0: pre-trim_multica_comment 绕路补丁 + 单测 + 文档
Phase ③ pre-trim 绕路补丁实施版:
- pre_trim_multica_comment.py (v1.0.0): ≤4KB 硬截断 + mention 剥离 + 嵌套表降级 + 代码块合并 + .log 落盘
- test_pre_trim_multica_comment.py: 12 项单元测试全通过(长 md/嵌套表/代码块/中英混排/mention/.log/版本戳)
- BIZ-104_pre-trim_multica_comment_v1.0.md: BIZ-38 版本化文档(部署/集成/测试/风险/CHANGELOG)

根因:multica proxy 0.4.35 二进制在边界解析 openclaw stdout 时截断/丢弃长 markdown,错误码 openclaw returned no parseable output(strings 提取确认)。

COO 拍板:≤4KB 阈值;保留原 output 到 .log;单测覆盖 4 类样本;按 BIZ-38 版本控制。

Co-author: 严维序(opengineer)
Approval: 陆怀瑾(COO)
2026-08-31 08:32:59 +08:00

343 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
pre_trim_multica_comment.py — BIZ-104 Phase ③ pre-trim 绕路补丁
背景(BIZ-104):
- multica proxy 0.4.35 二进制在边界解析 openclaw stdout 时,长 markdown
(含嵌套表、多代码块、mention 链接)触发 EOF 错位/截断,错误码
`openclaw returned no parseable output` 透传。
- 多 agent 多次 turn 评论被丢弃(BIZ-104 06:30-09:00 UTC 事件)。
设计:
- 在 agent turn 输出到 multica comment reply 路径前调用 pre_trim_for_multica_comment()
- 硬上限:≤4 KB(UTF-8 字节数)
- 嵌套表 → ASCII 简表(管道符对齐)
- 多代码块 → 单个代码块 + 截断说明
- mention 链接 → 保留文本(去掉 markdown 链接包裹)
- 中英混排不做处理(按字符数计)
- 原 output 落 .log 便于回溯(路径可配置)
版本:v1.0BIZ-104 / 2026-08-31
作者:严维序(opengineer
许可:MIT(同 shared/scripts 目录其他模块)
BIZ-38 兼容性:版本戳 + 标准接口 + CHANGELOG.md 登记
"""
import os
import re
import sys
import json
import time
from typing import Optional, Dict, Any
# ============================================================================
# 版本与常量(BIZ-38 版本戳格式)
# ============================================================================
__version__ = "1.0.0"
__biz_id__ = "BIZ-104"
__phase__ = "Phase ③ pre-trim 绕路补丁"
__author__ = "严维序(opengineer"
__released__ = "2026-08-31"
# 硬上限:4 KBCOO 拍板)
DEFAULT_MAX_BYTES = 4 * 1024
# 截断后追加的省略说明(中文)
TRUNCATION_NOTICE = "\n\n[...已截断,原文已落 .log]"
# ============================================================================
# pre-trim 主入口
# ============================================================================
def pre_trim_for_multica_comment(
text: str,
max_bytes: int = DEFAULT_MAX_BYTES,
log_path: Optional[str] = None,
) -> Dict[str, Any]:
"""
将长 markdown 输出 pre-trim 至 ≤max_bytes,返回给 multica proxy。
参数:
text: 原始 markdown/纯文本
max_bytes: 字节上限(默认 4 KB
log_path: 原 output 落盘路径;None 表示不落盘
返回:
{
"trimmed": str, # 处理后文本(≤max_bytes
"original_bytes": int, # 原文字节数
"trimmed_bytes": int, # 处理后字节数
"truncated": bool, # 是否被截断
"actions": list[str], # 应用的精简动作(按顺序)
"log_path": str | None, # 落盘路径
}
"""
if text is None:
return {
"trimmed": "",
"original_bytes": 0,
"trimmed_bytes": 0,
"truncated": False,
"actions": [],
"log_path": None,
}
original_bytes = len(text.encode("utf-8"))
actions: list[str] = []
# Step 1: 落原 output 到 .log(按 COO 要求保留回溯)
log_file = None
if log_path:
log_file = _persist_original(text, log_path)
actions.append(f"log:{os.path.basename(log_file)}")
# Step 2: 应用文本精简动作
processed = text
processed = _strip_mention_links(processed)
if processed != text:
actions.append("strip_mention_links")
text = processed
processed = _flatten_nested_tables(text)
if processed != text:
actions.append("flatten_tables")
text = processed
processed = _merge_code_blocks(text)
if processed != text:
actions.append("merge_code_blocks")
text = processed
# Step 3: 字节硬截断
encoded = text.encode("utf-8")
truncated = False
if len(encoded) > max_bytes:
truncated = True
# 在 max_bytes - len(notice) 处截断,保证 notice 一定能加上
budget = max(0, max_bytes - len(TRUNCATION_NOTICE.encode("utf-8")))
text = encoded[:budget].decode("utf-8", errors="ignore") + TRUNCATION_NOTICE
actions.append("byte_truncate")
trimmed_bytes = len(text.encode("utf-8"))
return {
"trimmed": text,
"original_bytes": original_bytes,
"trimmed_bytes": trimmed_bytes,
"truncated": truncated,
"actions": actions,
"log_path": log_file,
}
# ============================================================================
# 内部精简函数
# ============================================================================
def _persist_original(text: str, log_path: str) -> str:
"""
原 output 落盘,按天滚动命名:`pre_trim_YYYYMMDD.log`
"""
os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
# 追加写入(同日多次评论聚合到一个文件)
with open(log_path, "a", encoding="utf-8") as f:
f.write(f"\n\n===== {time.strftime('%Y-%m-%dT%H:%M:%S')} =====\n")
f.write(text)
f.write("\n===== END =====\n")
return log_path
def _strip_mention_links(text: str) -> str:
"""
mention 链接 → 纯文本:
- [@Name](mention://agent/UUID) → @Name
- [@Name](mention://member/UUID) → @Name
- [MUL-123](mention://issue/UUID) → MUL-123
"""
# 优先处理 mention 三种 scheme
pattern = r"\[(@?[\w\u4e00-\u9fff\-]+)\]\(mention://[a-z]+/[a-z0-9\-]+\)"
return re.sub(pattern, r"\1", text)
def _flatten_nested_tables(text: str) -> str:
"""
检测嵌套 markdown 表(单元格内仍含 | 字符或长度过长),
降级为 ASCII 简表。
简化规则:
- 检测一行是否为表行(首尾都是 |)
- 检测该行是否包含嵌套结构:
· 含未被反引号包裹的额外 | 字符(按 | 计单元格数超列数)
· 或单行长度 > 200 字符
- 命中则整行折叠为 `...`,避免误伤相邻列
"""
lines = text.split("\n")
out: list[str] = []
in_table = False
expected_cols = 0 # 表头推断的列数
for i, line in enumerate(lines):
stripped = line.strip()
# 简单表头/数据行检测:首尾都是 |
is_table_line = stripped.startswith("|") and stripped.endswith("|")
# 分隔行(|---|---|)单独跳过但保留原行
if is_table_line and re.match(r"^\|[\s\-:|]+\|$", stripped):
# 推断列数:|---|---|
expected_cols = stripped.strip("|").count("|") + 1
out.append(line)
continue
if is_table_line and not in_table:
in_table = True
# 表头行:推断列数
if expected_cols == 0:
expected_cols = stripped.strip("|").count("|") + 1
out.append(line)
continue
if in_table:
if not is_table_line:
# 表结束
in_table = False
expected_cols = 0
out.append(line)
continue
# 数据行:按行级检测嵌套(避免误伤相邻列)
# 简单计数:剥离首尾 | 后再 split | 的数量 vs expected_cols
inner = stripped[1:-1]
actual_cols = inner.count("|") + 1
if actual_cols > expected_cols or len(stripped) > 200:
# 嵌套行 → 整行折叠
out.append("| `...` |")
continue
out.append(line)
else:
out.append(line)
return "\n".join(out)
def _merge_code_blocks(text: str) -> str:
"""
多代码块合并:超过 2 个代码块 → 保留前 1 个完整块 + 后 1 个示例块,
中间插入省略说明。
"""
# 匹配 ```...``` 块(含语言标记)
pattern = re.compile(r"```([a-zA-Z0-9_+\-]*)\n([\s\S]*?)```", re.MULTILINE)
blocks = list(pattern.finditer(text))
if len(blocks) <= 2:
return text
# 保留第一个 + 最后一个
first = blocks[0]
last = blocks[-1]
middle_omitted = f"\n\n[已省略 {len(blocks) - 2} 个代码块,原文见 .log]\n\n"
merged = text[: first.end()] + middle_omitted + text[last.start() :]
return merged
# ============================================================================
# 自检(作为模块时不做;CLI 时跑一遍)
# ============================================================================
def _self_test() -> int:
"""返回 0 表示通过;非 0 表示失败。"""
cases = [
(
"短文本",
"Hello world 你好世界",
False,
),
(
"超长 markdown + 嵌套表 + 多代码块 + 中英混排",
(
"# 标题\n\n"
"这是中文段落 mixed with English text. " * 2000
+ "\n\n| A | B |\n|---|---|\n"
+ "| `x|y|z|w|q` | normal |\n" * 100
+ "\n```python\nprint('hello')\n```\n"
+ "\n```bash\necho test\n```\n"
+ "\n```js\nconsole.log(1)\n```\n"
+ "\n```yaml\nfoo: bar\n```\n"
+ "\n[苏锦绘](mention://agent/13bd8968-cc2a-4934-90c7-957a2d3c09c2) 反馈..."
),
True,
),
(
"mention 链接",
"[@徐聪](mention://agent/46bdd4a6-5c64-475a-92ef-36a763602fa1) 已就绪 [MUL-104](mention://issue/abc)",
False,
),
]
failures = 0
for name, input_text, expect_truncated in cases:
result = pre_trim_for_multica_comment(input_text)
ok = (
result["trimmed_bytes"] <= DEFAULT_MAX_BYTES
and result["truncated"] == expect_truncated
)
marker = "✅" if ok else "❌"
print(f" {marker} {name}: trimmed={result['trimmed_bytes']}B truncated={result['truncated']} actions={result['actions']}")
if not ok:
failures += 1
return 0 if failures == 0 else 1
# ============================================================================
# CLI
# ============================================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="BIZ-104 Phase ③ pre-trim 绕路补丁 — agent turn 输出到 multica 前的精简"
)
parser.add_argument("--input", "-i", help="输入文件路径(默认 stdin")
parser.add_argument("--output", "-o", help="输出文件路径(默认 stdout")
parser.add_argument("--log", "-l", default="/tmp/pre_trim_multica.log", help="原 output 落盘路径")
parser.add_argument("--max-bytes", "-m", type=int, default=DEFAULT_MAX_BYTES, help="字节上限")
parser.add_argument("--self-test", action="store_true", help="运行自检")
parser.add_argument("--json", action="store_true", help="JSON 输出(包含元数据)")
args = parser.parse_args()
if args.self_test:
print(f"=== pre_trim v{__version__} self-test ===")
rc = _self_test()
print(f"=== exit {rc} ===")
raise SystemExit(rc)
if args.input:
with open(args.input, "r", encoding="utf-8") as f:
text = f.read()
else:
text = sys.stdin.read() if not sys.stdin.isatty() else ""
result = pre_trim_for_multica_comment(text, max_bytes=args.max_bytes, log_path=args.log)
if args.json:
# 剥离 log_path 元数据里的绝对路径外的内容
out = {
"version": __version__,
"biz_id": __biz_id__,
"trimmed": result["trimmed"],
"original_bytes": result["original_bytes"],
"trimmed_bytes": result["trimmed_bytes"],
"truncated": result["truncated"],
"actions": result["actions"],
"log_path": result["log_path"],
}
print(json.dumps(out, ensure_ascii=False, indent=2))
else:
print(result["trimmed"], end="")
# 非零退出码如果超限(监控信号,不阻断流程)
raise SystemExit(2 if result["truncated"] else 0)