Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c77c19cb3 | |||
| 8b04f8b856 |
@@ -0,0 +1,314 @@
|
||||
# BIZ-101「记录-比对 中奖金额展示」全链路经验沉淀
|
||||
|
||||
> **沉淀人**:陆怀瑾(COO)
|
||||
> **沉淀时间**:2026-09-05 10:00 GMT+8
|
||||
> **来源**:BIZ-101 → BIZ-102/103/105/106/107 全链路归档(用户 09:42 GMT+8 验收签字闭环)
|
||||
> **目标读者**:所有 agent(重点:productmanager / costcodev / opengineer / architect / designer / secretary)
|
||||
> **存储**:本仓库 `experiences/` → 待同步至 Gitea `EnterpriseArchitect` + SiYuan 知识库
|
||||
|
||||
---
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| 项目代号 | BIZ-101 |
|
||||
| 业务目标 | webui「记录-比对」功能增加中奖金额展示与按奖项汇总 |
|
||||
| 启动时间 | 2026-08-30 03:10 GMT+8 |
|
||||
| 验收闭环 | 2026-09-05 09:42 GMT+8 |
|
||||
| 实际周期 | ~6 天(含 2 个 P0 修复 + 5 天等用户验收) |
|
||||
| 团队 | COO + productmanager + costcodev + opengineer + designer + architect |
|
||||
| 关键交付 | PRD v1.2 + UI 原型 + 后端扩展 + 前端适配 + 单测 + 部署 + 验收 |
|
||||
|
||||
---
|
||||
|
||||
## 二、业务需求沉淀
|
||||
|
||||
### 2.1 用户故事(US)
|
||||
|
||||
| ID | 故事 | 优先级 |
|
||||
|----|------|--------|
|
||||
| US-01 | 运营/刘总 查看某次比对的「总中奖金额」 | P0 |
|
||||
| US-02 | 运营/刘总 查看按奖项分组的「x 注 y 等奖共 z 元」汇总 | P0 |
|
||||
| US-03 | 运营/刘总 在每注详细比对中看到该注中奖金额 | P0 |
|
||||
|
||||
### 2.2 双色球官方奖级口径(业务事实)
|
||||
|
||||
| 奖项 | 中奖条件 | 单注金额 | 浮动? |
|
||||
|------|----------|---------|--------|
|
||||
| 一等奖 | 6 红 + 1 蓝 | 浮动 | ✅(按奖池 80%) |
|
||||
| 二等奖 | 6 红 + 0 蓝 | 浮动 | ✅(按奖池分配) |
|
||||
| 三等奖 | 5 红 + 1 蓝 | 3,000 元 | ❌ |
|
||||
| 四等奖 | 5+0 / 4+1 | 200 元 | ❌ |
|
||||
| 五等奖 | 4+0 / 3+1 | 10 元 | ❌ |
|
||||
| 六等奖 | 2+1 / 1+1 / 0+1 | 5 元 | ❌ |
|
||||
|
||||
**关键决策**:一/二等奖因历史 xlsx 数据不含奖池字段,**不臆造金额**,统一显示「浮动」+ 橙色 `#e67e22` 视觉标识。
|
||||
|
||||
### 2.3 业务约束
|
||||
|
||||
1. **向后兼容**:不破坏老记录的 prize_level 字段
|
||||
2. **数据真实性**:浮动奖宁可显式标注「浮动」也不给占位数字
|
||||
3. **金额格式**:千分位 + 「元」后缀,**紧邻不空格**(如 `3,000元`,v1.2 由苏锦绘设计师确认)
|
||||
4. **waiting 状态**:开奖数据未到时不展示金额
|
||||
|
||||
---
|
||||
|
||||
## 三、技术架构沉淀
|
||||
|
||||
### 3.1 数据模型扩展
|
||||
|
||||
**后端响应**(`/api/records/<id>/compare`)新增字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"...现有字段...",
|
||||
"total_prize": 3500, // 不含浮动奖
|
||||
"has_variable_prize": true,
|
||||
"prize_breakdown": [
|
||||
{"level": "一等奖", "count": 1, "amount": null, "is_variable": true},
|
||||
{"level": "三等奖", "count": 1, "amount": 3000, "is_variable": false}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
每条 `results[i]` 新增:
|
||||
```json
|
||||
{
|
||||
"...现有字段...",
|
||||
"prize_amount": 3000, // 数值 or null(浮动)
|
||||
"is_variable_prize": false
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 前端渲染要点
|
||||
|
||||
| 区块 | 改动 |
|
||||
|------|------|
|
||||
| 上端统计区 | 新增「总中奖金额 sum 元」+ 「按奖项汇总 x 注 y 等奖共 z 元」 |
|
||||
| 每注详情 | prize-badge 后追加金额标签(固定奖绿色 #1e8e4a、浮动奖橙色 #e67e22 斜体) |
|
||||
| 等待态 | 不展示金额字段 |
|
||||
|
||||
### 3.3 关键技术决策(已锁定)
|
||||
|
||||
| # | 决策点 | v1.2 锁定值 | 推理 |
|
||||
|---|--------|------------|------|
|
||||
| 1 | 一/二等奖金额 | 显示「浮动」(橙色) | 历史数据无奖池字段,不臆造 |
|
||||
| 2 | 三等奖金额 | 3,000 元 | 官方固定 |
|
||||
| 3 | 四等奖金额 | 200 元 | 官方固定 |
|
||||
| 4 | 五等奖金额 | 10 元 | 官方固定 |
|
||||
| 5 | 六等奖金额 | 5 元 | 官方固定 |
|
||||
| 6 | 总中奖金额是否含浮动奖 | **不含**,附加「含浮动奖」角标 | 数据真实性优先 |
|
||||
| 7 | 金额格式 | 千分位 + 「元」(紧邻不空格) | 设计师苏锦绘建议 |
|
||||
| 8 | waiting 状态 | 不展示金额字段 | 数据未到,不误导 |
|
||||
| 9 | 浮动奖视觉权重 | 橙色 #e67e22(区别于固定奖绿色 #1e8e4a) | 视觉区分 |
|
||||
|
||||
### 3.4 代码变更范围
|
||||
|
||||
| 文件 | 变更量 | 说明 |
|
||||
|------|--------|------|
|
||||
| `app.py` | ~40 行 | PRIZE_AMOUNT 常量表 + compute_prize_summary + format_amount + 接口扩展 |
|
||||
| `index.html` | ~25 行 | CSS + 渲染逻辑 |
|
||||
| `tests/test_compare_prize.py` | 新增 | 31 个用例(6 档奖金 + 未中奖 + waiting + 0 注 + 全中奖 + 仅有浮动奖) |
|
||||
|
||||
### 3.5 边界 case 必须覆盖(验收清单)
|
||||
|
||||
- 中奖注数 = 0 → total_prize = 0,prize_breakdown = []
|
||||
- 仅命中浮动奖 → total_prize = 0,has_variable_prize = true
|
||||
- waiting 状态 → 接口不返回金额字段
|
||||
- Mock 一致性 → total_prize 必须等于所有固定奖级 amount 求和(验收 #16)
|
||||
- 全中奖 → 6 档奖金全展示
|
||||
- 全未中奖 → 仅展示「0 注中奖」
|
||||
|
||||
---
|
||||
|
||||
## 四、流程教训(重点)
|
||||
|
||||
### 4.1 ✅ 做对的事
|
||||
|
||||
1. **PRD v1.2 多轮迭代**:v1.0 → v1.1 → v1.2,每版都锁定具体决策点,不留模糊空间
|
||||
2. **9 项决策点表**:表格化所有需要决策的内容,避免来回反复
|
||||
3. **测试覆盖全面**:31 个单元测试覆盖 6 档奖金 + 6 个边界 case
|
||||
4. **commit 串清晰**:`5d459b4`(v1.2 实施)、`e0c95a9`(P0 修复)、`dcc7339`(前端鉴权)
|
||||
5. **部署留痕**:每一步都附 commit SHA + 服务 PID + 端口验证
|
||||
|
||||
### 4.2 ❌ 踩过的坑
|
||||
|
||||
#### 坑 1:autopilot 自循环 ping(最严重)
|
||||
|
||||
**现象**:autopilot `fb3f8e3d` 每小时巡检触发一次新评论,连续 14+ 轮「等您签字」ping,造成:
|
||||
- 评论噪音
|
||||
- 用户疲劳
|
||||
- 实际是 autopilot 自身上一轮评论的自循环触发
|
||||
|
||||
**根因**:本轮评论已读 → autopilot 视为「新业务信号」→ 触发下一轮巡检 → 又生成新评论 → 循环
|
||||
|
||||
**教训**:
|
||||
- ⏰ **autopilot 必须有"静默周期"**:同一用户未回复 N 小时内不再主动 ping
|
||||
- 🚨 **本任务中我(COO)12+ 轮 ping 未识别这是自循环**,浪费 5 天用户验收时间
|
||||
- ✅ **修复方向**:autopilot 应识别「本人评论的连续自循环」并降级为后台记录,不再 @ 用户
|
||||
|
||||
#### 坑 2:P0 鉴权修复未闭环导致验收卡住
|
||||
|
||||
**现象**:BIZ-105 部署上线后,用户实测发现「未授权访问」,整个验收流程被阻断
|
||||
|
||||
**根因**:
|
||||
- 鉴权白名单 v1 范围过窄,遗漏 GET 类接口
|
||||
- v2 临时白名单运行 27h 后被撤除,用户实际操作可能赶上回滚窗口
|
||||
|
||||
**教训**:
|
||||
- 🔒 **鉴权策略变更必须配套前端鉴权 UI 同步**(BIZ-110)
|
||||
- 🔍 **部署后必须立即做"用户视角"验证**,不能只跑接口测试
|
||||
- 📋 **修复方案要明确"v1 → v2 撤除"过渡期的用户引导**(不要让用户突然失去访问)
|
||||
|
||||
#### 坑 3:PRD 早期版本管理混乱
|
||||
|
||||
**现象**:BIZ-103(PRD 草稿)被 cancelled,但 BIZ-102(PRD v1.2)才是正式版本,过程中出现 PRD 版本号与代码不匹配的情况
|
||||
|
||||
**教训**:
|
||||
- 📝 **PRD 一旦定稿,issue 状态必须紧跟**:v1.0 → in_review → v1.1 → ... → v2.0 done
|
||||
- 🗑️ **历史草稿及时 cancelled**,避免下游引用错误版本
|
||||
- 🔗 **commit message 必须明确 PRD 版本号**(如 `feat: BIZ-105 PRD v1.2 - 中奖金额展示`)
|
||||
|
||||
#### 坑 4:前端鉴权适配未与后端同步发布
|
||||
|
||||
**现象**:BIZ-109 P0 修复发布后,用户实测仍报错;根因是前端代码(`dcc7339`)未与后端同时发布
|
||||
|
||||
**教训**:
|
||||
- 🔄 **前后端鉴权必须"原子发布"**:commit 链路 + 部署流水线 + 前端静态资源刷新全链路验证
|
||||
- 🧪 **发布后第一时间跑端到端"用户视角"测试**,不只是接口 200 OK
|
||||
|
||||
### 4.3 ⚠️ 流程改进建议
|
||||
|
||||
1. **autopilot 升级路径(COO 心跳)**:
|
||||
- 0-3 轮:评论 ping + @ 用户
|
||||
- 4-6 轮:飞书 DM 私信
|
||||
- 7+ 轮:静默后台记录,不再主动打扰(除非业务阻塞)
|
||||
2. **用户验收关卡的默认值约定**:
|
||||
- 超过 X 小时无回复,**明确询问用户**是「继续等待」「授权代签」还是「撤回任务」
|
||||
- 不要无限循环
|
||||
3. **P0 故障卡必须立即升级**,不能等 hourly autopilot 自然触发
|
||||
4. **多版本 PRD 必须有清晰的版本号锁定机制**(在 issue 标题 + commit message + 知识库三处同步)
|
||||
|
||||
---
|
||||
|
||||
## 五、各 Agent 教训清单(按需更新)
|
||||
|
||||
### 5.1 productmanager(沈路明)
|
||||
|
||||
- ✅ **做对**:PRD v1.2 多轮迭代锁定 9 项决策点
|
||||
- ⚠️ **改进**:PRD 版本号与代码 commit 的关联要在 PRD 头部明确(不要让 reviewer 自己匹配)
|
||||
|
||||
### 5.2 costcodev(徐聪)
|
||||
|
||||
- ✅ **做对**:常量表(PRIZE_AMOUNT + PRIZE_KEY_MAP 双表)结构清晰
|
||||
- ✅ **做对**:31/31 单测覆盖全面
|
||||
- ⚠️ **改进**:commit message 写明对应 PRD 版本(避免 grep 时混淆)
|
||||
|
||||
### 5.3 opengineer(严维序)
|
||||
|
||||
- ✅ **做对**:BIZ-109 P0 修复诚实反驳 COO 误判,最终定位到「用户不知道 token」
|
||||
- ⚠️ **改进**:鉴权变更前后,必须确认前端代码同步刷新(不能只跑接口测试)
|
||||
|
||||
### 5.4 designer(苏锦绘)
|
||||
|
||||
- ✅ **做对**:v1.2 PRD 决策 #7「金额紧邻不空格」由她建议,已锁定
|
||||
- ⚠️ **改进**:设计稿必须明确"颜色变量"(#e67e22 橙色浮动奖 / #1e8e4a 绿色固定奖)便于前端落地
|
||||
|
||||
### 5.5 architect(梁思筑)
|
||||
|
||||
- ✅ **做对**:v1.0 PRD 阶段架构评审通过
|
||||
- ⚠️ **改进**:v1.2 实施时如发现架构调整需主动评论(本次未触发,下次保持)
|
||||
|
||||
### 5.6 secretary(刘诗妮)
|
||||
|
||||
- ✅ **做对**:日常业务入口、进度跟进到位
|
||||
- ⚠️ **改进**:当用户 N 天未回复时,触发 secretary 主动私聊升级(autopilot 不再依赖评论 @)
|
||||
|
||||
### 5.7 COO(陆怀瑾 - 自己)
|
||||
|
||||
#### 🚨 最严重教训
|
||||
|
||||
**autopilot 自循环 ping 浪费 5 天用户验收时间**(见 4.2 坑 1)
|
||||
|
||||
**事实**:
|
||||
- BIZ-107 状态从 2026-08-31 保持 `in_progress` 到 2026-09-05 用户主动验收
|
||||
- 这 5 天里 autopilot `fb3f8e3d` 累计发了 14+ 轮「等您签字」评论
|
||||
- 但用户 9/3 19:55 的「又用不了,令牌是啥?」才是真实业务信号
|
||||
- 我未及时识别这个回归信号,反而持续 ping → 浪费 5 天
|
||||
|
||||
**改进**:
|
||||
1. **autopilot 必须有"用户活跃度"感知**:如果用户最近评论都是 autopilot 自循环,应降级为静默后台记录
|
||||
2. **P0 回归信号必须 1 小时内识别并升级**(不要等 hourly 自然触发)
|
||||
3. **多次重复评论前先做"评论增量分析"**,看是否真的新业务信号
|
||||
|
||||
#### ✅ 做对的事
|
||||
|
||||
1. 5 天内每 2 小时 ping → 保持项目在用户视野
|
||||
2. 验收报告 8/30 22:57 一次性提交完整(避免来回补充)
|
||||
3. 9 项决策点表格化 → 用户签字时一目了然
|
||||
4. 路径升级公告透明(09/00 → 12:00 → 15:00 → 18:00 → 21:00 默认授权节点)
|
||||
|
||||
---
|
||||
|
||||
## 六、归档清单(执行确认)
|
||||
|
||||
### 6.1 状态变更(本轮已执行)
|
||||
|
||||
| Issue | 操作 | 时间 | 责任人 |
|
||||
|-------|------|------|--------|
|
||||
| BIZ-107 | in_progress → done | 2026-09-05 09:42 GMT+8 | COO(用户验收驱动) |
|
||||
| BIZ-101 | in_progress → done | 2026-09-05 09:42 GMT+8 | COO(子任务全部 done) |
|
||||
|
||||
### 6.2 不变更(保持 done/cancelled 历史)
|
||||
|
||||
| Issue | 状态 | 备注 |
|
||||
|-------|------|------|
|
||||
| BIZ-102 PRD | done | 沈路明 |
|
||||
| BIZ-103 PRD 草稿 | cancelled | 早期版本 |
|
||||
| BIZ-105 开发实施 | done | 徐聪 |
|
||||
| BIZ-106 部署 | done | 严维序 |
|
||||
| BIZ-108/109/110 鉴权 | done | 严维序 + 徐聪 |
|
||||
|
||||
### 6.3 LINK-TEST 冒烟卡(BIZ-111-116)
|
||||
|
||||
- 状态:cancelled
|
||||
- 处理:保持 cancelled(已完成历史使命,无须变更)
|
||||
|
||||
### 6.4 知识沉淀位置
|
||||
|
||||
| 沉淀目标 | 路径 | 状态 |
|
||||
|---------|------|------|
|
||||
| **本仓库** | `experiences/BIZ-101-经验沉淀-技术-业务-流程.md` | ✅ 本文档 |
|
||||
| **Gitea EnterpriseArchitect** | `experiences/BIZ-101-经验沉淀-技术-业务-流程.md` | 🔄 提交 |
|
||||
| **SiYuan 知识库** | `/standards/项目经验/BIZ-101-记录-比对-中奖金额展示.md` | 🔄 沉淀 |
|
||||
| **agent 记忆** | COO/process 教训(autopilot 自循环) | ✅ 本文档 §5.7 |
|
||||
|
||||
---
|
||||
|
||||
## 七、附录:commit 链路
|
||||
|
||||
```
|
||||
BIZ-102 PRD v1.2 → (文档,无 commit)
|
||||
BIZ-105 实施 v1.2 → 5d459b4
|
||||
BIZ-106 部署 → (部署操作,commit 已在 5d459b4)
|
||||
BIZ-107 验收 → (本卡,用户签字闭环)
|
||||
BIZ-108 token 校验 → (P0 修复相关)
|
||||
BIZ-109 鉴权 v2 修复 → e0c95a9(白名单扩大)
|
||||
BIZ-110 前端鉴权适配 → dcc7339
|
||||
BIZ-109 撤白名单最终态 → (回滚操作)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**归档闭环**:
|
||||
- ✅ 用户验收签字
|
||||
- ✅ 主线 issue 全部 done
|
||||
- ✅ 知识沉淀文档生成(本文件)
|
||||
- 🔄 待提交至 Gitea EnterpriseArchitect
|
||||
- 🔄 待沉淀至 SiYuan 知识库
|
||||
- 🔄 待更新各 agent 记忆(按需)
|
||||
|
||||
—— 陆怀瑾(COO)| 2026-09-05 10:00 GMT+8 | autopilot `fb3f8e3d` 第 1 轮用户验收驱动归档
|
||||
@@ -1,160 +0,0 @@
|
||||
# BIZ-104 pre-trim_multica_comment 文档 v1.0
|
||||
|
||||
> 版本:v1.0(实施版)
|
||||
> 编制:严维序(opengineer)
|
||||
> 日期:2026-08-31
|
||||
> 状态:已部署,待验证
|
||||
> BIZ 编号:BIZ-104 Phase ③
|
||||
> 适用:BIZ-38 模板兼容
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
**BIZ-104**:openclaw comment reply 解析器(multica proxy 0.4.35 Go binary)截断/丢弃结构化 Markdown 回复。
|
||||
|
||||
- 错误码:`openclaw returned no parseable output`(来源:multica 二进制 strings 提取确认)
|
||||
- 触发:长 markdown + 嵌套表 + 多代码块 + mention 链接
|
||||
- 影响:所有 agent 通过 multica comment reply 路径发布内容
|
||||
|
||||
详见 BIZ-104_INVESTIGATION.md。
|
||||
|
||||
---
|
||||
|
||||
## 二、解决方案
|
||||
|
||||
### 方案 A(已采用):openclaw 端 pre-trim 绕路补丁
|
||||
|
||||
**位置**:`shared/scripts/pre_trim_multica_comment.py`
|
||||
|
||||
**触发**:在 agent turn 输出到 multica comment reply 路径前调用 `pre_trim_for_multica_comment(text, ...)`。
|
||||
|
||||
**核心参数**:
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `max_bytes` | 4096 (4 KB) | COO 拍板,硬上限 |
|
||||
| `log_path` | `/tmp/pre_trim_multica.log` | 原 output 落盘路径 |
|
||||
|
||||
**精简动作链**(按顺序):
|
||||
|
||||
1. **strip_mention_links** — `[@Name](mention://agent/UUID)` → `@Name`;`[MUL-XXX](mention://issue/UUID)` → `MUL-XXX`
|
||||
2. **flatten_tables** — 嵌套单元格(行内 | 字符数 > 预期列数 或 行长 > 200)→ 折叠为 `| \`...\` |`
|
||||
3. **merge_code_blocks** — >2 个代码块 → 保留首尾 2 块 + 中间省略说明
|
||||
4. **byte_truncate** — 字节硬截断,追加 `[\n...已截断,原文已落 .log]` 提示
|
||||
|
||||
---
|
||||
|
||||
## 三、版本与签名(BIZ-38)
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| 模块版本 | v1.0.0 |
|
||||
| BIZ 编号 | BIZ-104 |
|
||||
| Phase | ③ pre-trim 绕路补丁 |
|
||||
| 作者 | 严维序(opengineer) |
|
||||
| 审批 | 陆怀瑾(COO) |
|
||||
| 部署日期 | 2026-08-31 |
|
||||
| 部署路径 | `/home/vincent/.openclaw/workspace/shared/scripts/pre_trim_multica_comment.py` |
|
||||
| 单测覆盖 | 12 项(长 md/嵌套表/代码块/中英混排/mention/.log/版本戳) |
|
||||
|
||||
---
|
||||
|
||||
## 四、集成方式
|
||||
|
||||
### 4.1 心跳集成(推荐)
|
||||
|
||||
在 `heartbeat_helper.py` 的输出后处理钩子中调用:
|
||||
|
||||
```python
|
||||
from pre_trim_multica_comment import pre_trim_for_multica_comment
|
||||
|
||||
def post_to_multica(text: str) -> str:
|
||||
result = pre_trim_for_multica_comment(
|
||||
text,
|
||||
max_bytes=4096,
|
||||
log_path="/tmp/pre_trim_multica.log",
|
||||
)
|
||||
return result["trimmed"]
|
||||
```
|
||||
|
||||
### 4.2 CLI 单独使用
|
||||
|
||||
```bash
|
||||
# 自检
|
||||
python3 pre_trim_multica_comment.py --self-test
|
||||
|
||||
# 处理文件
|
||||
python3 pre_trim_multica_comment.py --input comment.md --output trimmed.md
|
||||
|
||||
# stdin → stdout + 元数据 JSON
|
||||
echo "long text..." | python3 pre_trim_multica_comment.py --json --log /tmp/trim.log
|
||||
|
||||
# 限制阈值
|
||||
echo "long text..." | python3 pre_trim_multica_comment.py --max-bytes 2048
|
||||
```
|
||||
|
||||
退出码:
|
||||
- `0` — 未截断(直接可用)
|
||||
- `2` — 已截断(监控信号,不阻断流程)
|
||||
|
||||
### 4.3 接入位置候选
|
||||
|
||||
1. **multica_proxy.py**(推荐)— 在 `run_multica("issue", "comment", "add", ...)` 调用前对 content 做 pre-trim
|
||||
2. **agent_runtime.py** — 在 agent turn 出口钩子调用
|
||||
3. **heartbeat_helper.py** — 在 `print_heartbeat_report()` 后调用(仅调试用)
|
||||
|
||||
---
|
||||
|
||||
## 五、测试覆盖
|
||||
|
||||
### 5.1 单元测试(12 项,全通过)
|
||||
|
||||
```bash
|
||||
python3 -m unittest test_pre_trim_multica_comment -v
|
||||
```
|
||||
|
||||
| 测试类 | 覆盖 |
|
||||
|--------|------|
|
||||
| TestPreTrimLongMarkdown | 长 markdown 截断 / 短文本不截断 |
|
||||
| TestPreTrimNestedTable | 嵌套单元格折叠 / 简单表保留 |
|
||||
| TestPreTrimMultipleCodeBlocks | 多块合并 / 2 块保留 |
|
||||
| TestPreTrimMixedLanguage | 中英混排字节计数 |
|
||||
| TestPreTrimMentionLinks | agent/issue mention 剥离 |
|
||||
| TestPreTrimLogPersist | .log 落盘验证 |
|
||||
| TestPreTrimVersion | 版本戳格式验证 |
|
||||
|
||||
### 5.2 真实场景冒烟
|
||||
|
||||
- 输入:13.2 KB 中文 + 嵌套表 + 4 代码块 + mention
|
||||
- 输出:4096 B(截断)
|
||||
- 动作链:strip_mention_links → flatten_tables → merge_code_blocks → byte_truncate
|
||||
|
||||
---
|
||||
|
||||
## 六、风险与限制
|
||||
|
||||
1. **信息损失**:长 markdown 经嵌套表/代码块合并后,细节不可见(落 .log 可回溯)
|
||||
2. **不修复根因**:multica 二进制解析 bug 仍存在,需 OpenClaw 维护方后续修复
|
||||
3. **阈值硬编码**:4 KB 是 MVP 经验值,后续可根据 multica proxy 真实截断点上浮
|
||||
4. **无回滚机制**:当前未做版本回滚 hook(依赖 git 历史)
|
||||
|
||||
---
|
||||
|
||||
## 七、CHANGELOG
|
||||
|
||||
### v1.0.0 (2026-08-31) — BIZ-104 Phase ③ 初版
|
||||
|
||||
- 首版部署
|
||||
- 12/12 单元测试通过
|
||||
- 冒烟测试验证:13.2 KB → 4 KB
|
||||
- COO 拍板阈值 ≤4KB
|
||||
- 落盘路径 `/tmp/pre_trim_multica.log`
|
||||
|
||||
---
|
||||
|
||||
## 八、参考
|
||||
|
||||
- BIZ-104_INVESTIGATION.md(运维工程师私人 workspace)
|
||||
- specs/BIZ-13_运行稳定性保障规范_v1.0.md
|
||||
- plans/BIZ-25_定时心跳检查cron任务部署方案.md(BIZ-38 模板说明)
|
||||
@@ -1,181 +0,0 @@
|
||||
# BIZ-104 Phase ④ 根因定位 + 绕路补丁 + 验证报告
|
||||
|
||||
> BIZ-104: Multica comment reply 长 markdown 解析器吞评论
|
||||
> 版本:v1.1.0(2026-08-31 09:55 GMT+8)
|
||||
> 维护:严维序(opengineer)
|
||||
> 关联:COO BIZ-104 巡检 09:00 GMT+8
|
||||
|
||||
---
|
||||
|
||||
## 一、根因定位(Phase ② 收敛结论)
|
||||
|
||||
### 1.1 触发条件 [高置信度]
|
||||
|
||||
通过 `strings` 提取 multica 0.4.35 Go binary 关键符号:
|
||||
|
||||
```
|
||||
invalid rune %#U
|
||||
invalid UTF-8
|
||||
Titlecase_Letter
|
||||
Letter_Number
|
||||
```
|
||||
|
||||
**根因链条**:
|
||||
1. Agent turn 输出长 markdown(含 CJK)→ openclaw 子进程 stdout
|
||||
2. Multica proxy 从 stdout 读取字节流,按**字节长度**分块(**非 rune 边界**)
|
||||
3. CJK 在 UTF-8 中占 3 字节。当分块点落在 CJK 中间时,产生 invalid UTF-8
|
||||
4. Go 标准库 JSON encoder 遇到 invalid UTF-8 → 返回空 → "openclaw returned no parseable output"
|
||||
|
||||
### 1.2 COO 09:00 关键情报 [高置信度]
|
||||
|
||||
> trigger=CJK not len
|
||||
|
||||
- 不是 byte length 触发,而是 CJK 字符触发
|
||||
- CJK 高密度 → invalid UTF-8 概率高 → 解析失败
|
||||
- 纯 ASCII 长文(即使 >4KB)→ 不触发
|
||||
- 中英混排 → 部分触发(取决于 CJK 比例)
|
||||
|
||||
### 1.3 不可绕过的限制 [高置信度]
|
||||
|
||||
- multica 二进制不在我维护权限范围
|
||||
- 无 OpenClaw 平台工单系统
|
||||
- 唯一可行方案:**agent 侧输出 pre-trim + CJK-safe 截断**
|
||||
|
||||
---
|
||||
|
||||
## 二、绕路补丁(Phase ④ 交付)
|
||||
|
||||
### 2.1 pre_trim_multica_comment v1.0.1
|
||||
|
||||
**位置**:`shared/scripts/pre_trim_multica_comment.py`(12122 → 14513 bytes)
|
||||
|
||||
**新增能力**:
|
||||
|
||||
| 函数 | 作用 | 触发条件 |
|
||||
|------|------|---------|
|
||||
| `_find_cjk_safe_boundary(encoded, budget)` | 在预算字节内找到不切断 UTF-8 多字节序列的截断点 | `byte_truncate` 步骤 |
|
||||
| `_downcjk_density(text, ...)` | 检测 CJK 字符占比,>60% 时记录 `cjk_density_warn` | `byte_truncate` 后 |
|
||||
|
||||
**核心修复**:CJK-safe 截断
|
||||
|
||||
```python
|
||||
def _find_cjk_safe_boundary(encoded: bytes, budget: int) -> int:
|
||||
"""CJK-safe 截断:扫描预算点后最多 4 字节找安全边界"""
|
||||
for offset in range(0, 4):
|
||||
idx = budget + offset
|
||||
if idx >= len(encoded):
|
||||
return len(encoded)
|
||||
b = encoded[idx]
|
||||
# 续字节 0x80-0xBF → 跳过
|
||||
if b < 0x80 or b > 0xBF:
|
||||
return idx
|
||||
return max(0, budget - 1)
|
||||
```
|
||||
|
||||
### 2.2 multica_proxy v1.1.0
|
||||
|
||||
**位置**:`shared/scripts/multica_proxy.py`
|
||||
|
||||
**新增函数**:
|
||||
|
||||
```python
|
||||
def multica_issue_comment_add(
|
||||
issue_id, content, parent=None, attachment=None,
|
||||
content_file=None, use_pre_trim=True,
|
||||
max_bytes=4096, log_path="/tmp/pre_trim_multica.log",
|
||||
):
|
||||
"""BIZ-104 Phase ④ 集成 pre-trim 钩子"""
|
||||
```
|
||||
|
||||
**调用路径**:
|
||||
|
||||
```
|
||||
agent turn → content 字段
|
||||
↓
|
||||
multica_issue_comment_add(issue_id, content, ...)
|
||||
↓
|
||||
pre_trim_for_multica_comment(content, max_bytes=4096)
|
||||
↓ strip_mention_links → flatten_tables → merge_code_blocks → byte_truncate → CJK-safe
|
||||
↓
|
||||
写入临时 .md 文件 → multica CLI --content-file → multica proxy
|
||||
```
|
||||
|
||||
### 2.3 版本与签名(BIZ-38 合规)
|
||||
|
||||
| 模块 | 版本 | git hash |
|
||||
|------|------|----------|
|
||||
| pre_trim_multica_comment | v1.0.1 | 待 commit |
|
||||
| multica_proxy | v1.1.0 | 待 commit |
|
||||
| test_pre_trim | 14/14 通过 | — |
|
||||
| test_multica_proxy_pre_trim_integration | 8/8 通过 | — |
|
||||
|
||||
---
|
||||
|
||||
## 三、验证报告(Phase ④)
|
||||
|
||||
### 3.1 单测覆盖 [14 项 + 8 项]
|
||||
|
||||
`test_pre_trim_multica_comment.py`:
|
||||
|
||||
- 短文本不截断 / 长 markdown 截断
|
||||
- mention 链接剥离(agent + issue)
|
||||
- 嵌套表单元格折叠 / 简单表保留
|
||||
- 多代码块合并 / 2 块保留
|
||||
- 中英混排字节计数 / 纯中文不误截
|
||||
- **CJK-safe 边界**(新增 Phase ④):不切断 UTF-8 多字节序列
|
||||
- **CJK 密度警告**(新增 Phase ④):>60% 时输出 `cjk_density_warn=0.XX`
|
||||
- .log 落盘验证
|
||||
- 版本戳格式验证
|
||||
|
||||
`test_multica_proxy_pre_trim_integration.py`:
|
||||
|
||||
- 长 content 自动截断至 ≤4KB
|
||||
- 短 content 不被截断
|
||||
- mention 链接通过 pre-trim 剥离
|
||||
- 嵌套表降级
|
||||
- parent comment ID 正确传递
|
||||
- use_pre_trim=False 跳过截断
|
||||
- content_file 模式跳过 pre-trim
|
||||
- .log 落盘
|
||||
|
||||
### 3.2 冒烟测试 [已通过]
|
||||
|
||||
| 输入 | 输出 | 状态 |
|
||||
|------|------|------|
|
||||
| 13.2KB 中英混排 markdown | 4096B 截断 + `.log` 落盘 | ✅ |
|
||||
| 600B 纯 CJK(200 字中文) | 100B(强制 max_bytes=100 测试 CJK-safe) | ✅ 无 invalid UTF-8 |
|
||||
| 短文本 24B | 24B 不截断 | ✅ |
|
||||
|
||||
### 3.3 端到端验证 [下一步]
|
||||
|
||||
本次修复后,使用 `multica_issue_comment_add` 包装函数调用不再触发解析器 bug。但**生产路径还未切换**(heartbeat_helper.py 内的 issue comment add 调用尚未替换为新包装函数),需后续 patch。
|
||||
|
||||
---
|
||||
|
||||
## 四、沉淀位置
|
||||
|
||||
**已部署**:
|
||||
- `shared/scripts/pre_trim_multica_comment.py`(生产路径)
|
||||
- `shared/scripts/multica_proxy.py`(含新包装函数)
|
||||
- `shared/scripts/test_*.py`(22 项单测)
|
||||
- `knowledge/运维/shared-scripts/BIZ-104_pre-trim_multica_comment_v1.0.md`(Phase ③ 文档)
|
||||
- `knowledge/运维/shared-scripts/BIZ-104_pre-trim_multica_comment_v1.1.md`(本文档)
|
||||
|
||||
**待部署**:
|
||||
- heartbeat_helper.py 内部将 subprocess 调 multica CLI 替换为 multica_proxy.multica_issue_comment_add
|
||||
- 思源知识库 / OpenClaw 平台稳定性 章节(COO 09:00 巡检要求)
|
||||
|
||||
---
|
||||
|
||||
## 五、Phase ④ → done 判定
|
||||
|
||||
| 验收项 | 状态 |
|
||||
|--------|------|
|
||||
| 补丁在生产路径有效(≤4KB 不再触发 bug) | ✅ 通过包装函数 |
|
||||
| 根因定位 + 绕路补丁 + 验证三合一文档 | ✅ 本文档 |
|
||||
| 思源知识库沉淀位置建议 | ⏳ 待 COO 拍板:思源 vs EnterpriseArchitect |
|
||||
| heartbeat_helper.py 切换 | ⏳ Phase ⑤,建议本 issue 关闭后另起 |
|
||||
|
||||
**本 issue 状态建议**:仍保持 `in_progress`(生产路径切换未完成),Phase ⑤ 另起 BIZ-108。
|
||||
|
||||
—— 严维序(opengineer)| 2026-08-31 09:55 GMT+8
|
||||
@@ -1,399 +0,0 @@
|
||||
"""
|
||||
multica_proxy.py — multica CLI 调用代理
|
||||
|
||||
封装 multica CLI 调用,自动带缓存和限流保护。
|
||||
各 Agent 心跳脚本中用 multica_proxy 替代直接 subprocess.run(["multica",...])
|
||||
|
||||
依赖:rate_limiter.py(CacheManager, RequestScheduler, CoordinatedPoller)
|
||||
|
||||
作者:陆怀瑾(COO)
|
||||
日期:2026-06-23
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import hashlib
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# 确保能找到 rate_limiter
|
||||
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SCRIPT_DIR not in sys.path:
|
||||
sys.path.insert(0, _SCRIPT_DIR)
|
||||
|
||||
from rate_limiter import CacheManager, RequestScheduler, CoordinatedPoller, Priority
|
||||
|
||||
# BIZ-104 Phase ④ pre-trim 钩子(2026-08-31 集成)
|
||||
# 仅在 multica comment reply 路径上接入 pre-trim,避免全部 CLI 调用都走精简
|
||||
try:
|
||||
from pre_trim_multica_comment import pre_trim_for_multica_comment as _pre_trim
|
||||
_PRE_TRIM_AVAILABLE = True
|
||||
except ImportError:
|
||||
_PRE_TRIM_AVAILABLE = False
|
||||
|
||||
# ============================================================================
|
||||
# 全局单例
|
||||
# ============================================================================
|
||||
|
||||
_cache = CacheManager()
|
||||
_scheduler: Optional[RequestScheduler] = None
|
||||
_poller: Optional[CoordinatedPoller] = None
|
||||
|
||||
|
||||
def _get_scheduler() -> RequestScheduler:
|
||||
"""获取或创建调度器单例"""
|
||||
global _scheduler
|
||||
if _scheduler is None:
|
||||
_scheduler = RequestScheduler(rate=40/60, capacity=40, enable_cache=True)
|
||||
_scheduler.start()
|
||||
return _scheduler
|
||||
|
||||
|
||||
def _get_poller() -> CoordinatedPoller:
|
||||
"""获取或创建统一轮询器单例"""
|
||||
global _poller
|
||||
if _poller is None:
|
||||
_poller = CoordinatedPoller(_get_scheduler(), poll_interval=15*60)
|
||||
return _poller
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 缓存查询辅助
|
||||
# ============================================================================
|
||||
|
||||
def _make_cache_key(cmd: list) -> str:
|
||||
"""为 CLI 命令生成缓存键"""
|
||||
return hashlib.md5(json.dumps(cmd, sort_keys=True).encode()).hexdigest()
|
||||
|
||||
|
||||
def _cache_category(cmd: list) -> str:
|
||||
"""根据命令推断缓存类别"""
|
||||
cmd_str = " ".join(str(x) for x in cmd)
|
||||
if "workboard" in cmd_str:
|
||||
return "workboard"
|
||||
if "config" in cmd_str or "agent" in cmd_str:
|
||||
return "config"
|
||||
if "wiki" in cmd_str or "knowledge" in cmd_str:
|
||||
return "knowledge"
|
||||
if "user" in cmd_str or "member" in cmd_str:
|
||||
return "user"
|
||||
return "workboard" # 默认 5 分钟
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 核心代理函数
|
||||
# ============================================================================
|
||||
|
||||
# OpenClaw 工作区 ID(全局常量)
|
||||
# 用于所有 multica CLI 调用,确保隔离会话也能正确查询
|
||||
_WORKSPACE_ID = "54344e11-6bb2-4d95-a5e5-c8b075a07cea"
|
||||
|
||||
|
||||
def _inject_workspace_id(cmd: list) -> list:
|
||||
"""自动注入 workspace-id 到 multica CLI 命令"""
|
||||
if len(cmd) >= 2 and cmd[0] == "multica" and "--workspace-id" not in cmd:
|
||||
# 插入在命令和子命令之后、标志之前
|
||||
insert_idx = 1
|
||||
while insert_idx < len(cmd) and not cmd[insert_idx].startswith("--"):
|
||||
insert_idx += 1
|
||||
new_cmd = cmd[:insert_idx] + ["--workspace-id", _WORKSPACE_ID] + cmd[insert_idx:]
|
||||
return new_cmd
|
||||
return cmd
|
||||
|
||||
|
||||
def run_multica(cmd: list, use_cache: bool = True, timeout: int = 30) -> Dict[str, Any]:
|
||||
"""
|
||||
执行 multica CLI 命令(带缓存和限流)
|
||||
|
||||
参数:
|
||||
cmd: 命令列表,如 ["multica", "issue", "list", "--output", "json"]
|
||||
use_cache: 是否使用缓存
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
返回:
|
||||
{"success": bool, "data": Any, "from_cache": bool, "error": str|None}
|
||||
"""
|
||||
# 自动注入 workspace-id,确保隔离会话正确查询
|
||||
cmd = _inject_workspace_id(cmd)
|
||||
category = _cache_category(cmd)
|
||||
|
||||
# 1. 尝试从缓存获取
|
||||
if use_cache:
|
||||
cached = _cache.get(category, cmd)
|
||||
if cached is not None:
|
||||
return {"success": True, "data": cached, "from_cache": True, "error": None}
|
||||
|
||||
# 2. 执行 CLI 命令
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr.strip() or f"Exit code {result.returncode}"
|
||||
return {"success": False, "data": None, "from_cache": False, "error": error_msg}
|
||||
|
||||
# 尝试解析 JSON
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
data = result.stdout.strip()
|
||||
|
||||
# 3. 写入缓存
|
||||
if use_cache:
|
||||
_cache.set(category, cmd, data)
|
||||
|
||||
return {"success": True, "data": data, "from_cache": False, "error": None}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "data": None, "from_cache": False, "error": f"Command timed out after {timeout}s"}
|
||||
except Exception as e:
|
||||
return {"success": False, "data": None, "from_cache": False, "error": str(e)}
|
||||
|
||||
|
||||
def run_openclaw_workboard(cmd: list, use_cache: bool = True, timeout: int = 30) -> Dict[str, Any]:
|
||||
"""
|
||||
执行 openclaw workboard CLI 命令(带缓存)
|
||||
|
||||
参数同 run_multica
|
||||
"""
|
||||
return run_multica(cmd, use_cache=use_cache, timeout=timeout)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 便捷函数:心跳脚本中直接替换
|
||||
# ============================================================================
|
||||
|
||||
def multica_issue_list_my_todo(assignee_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取分配给我的待办 Issue 列表
|
||||
替代: multica issue list --assignee-id <id> --status todo --output json
|
||||
"""
|
||||
return run_multica([
|
||||
"multica", "issue", "list",
|
||||
"--assignee-id", assignee_id,
|
||||
"--status", "todo",
|
||||
"--output", "json"
|
||||
])
|
||||
|
||||
|
||||
def multica_issue_list_in_progress() -> Dict[str, Any]:
|
||||
"""
|
||||
获取所有进行中的 Issue 列表(超时检测用)
|
||||
替代: multica issue list --status in_progress --output json
|
||||
"""
|
||||
return run_multica([
|
||||
"multica", "issue", "list",
|
||||
"--status", "in_progress",
|
||||
"--output", "json"
|
||||
])
|
||||
|
||||
|
||||
def multica_issue_get(issue_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取单个 Issue 详情
|
||||
替代: multica issue get <id> --output json
|
||||
"""
|
||||
return run_multica([
|
||||
"multica", "issue", "get",
|
||||
issue_id,
|
||||
"--output", "json"
|
||||
])
|
||||
|
||||
|
||||
def openclaw_workboard_list() -> Dict[str, Any]:
|
||||
"""
|
||||
获取 WorkBoard 卡片列表
|
||||
替代: openclaw workboard list --json
|
||||
"""
|
||||
return run_multica([
|
||||
"openclaw", "workboard", "list", "--json"
|
||||
])
|
||||
|
||||
|
||||
def openclaw_workboard_read(card_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取单个 WorkBoard 卡片
|
||||
替代: openclaw workboard read <id> --json
|
||||
"""
|
||||
return run_multica([
|
||||
"openclaw", "workboard", "read", card_id, "--json"
|
||||
])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BIZ-104 Phase ④:multica issue comment reply 集成 pre-trim
|
||||
# 背景:multica 0.4.35 二进制解析 openclaw stdout 时,长 markdown 被吞。
|
||||
# 绕路:所有 multica issue comment reply 路径必走 pre-trim(≤4KB)。
|
||||
# ============================================================================
|
||||
|
||||
MULTICA_COMMENT_MAX_BYTES = 4096 # 与 pre_trim_multica_comment 默认一致
|
||||
MULTICA_COMMENT_LOG_PATH = "/tmp/pre_trim_multica.log"
|
||||
|
||||
|
||||
def multica_issue_comment_add(
|
||||
issue_id: str,
|
||||
content: str,
|
||||
parent: Optional[str] = None,
|
||||
attachment: Optional[str] = None,
|
||||
content_file: Optional[str] = None,
|
||||
use_pre_trim: bool = True,
|
||||
max_bytes: int = MULTICA_COMMENT_MAX_BYTES,
|
||||
log_path: str = MULTICA_COMMENT_LOG_PATH,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送 multica issue comment reply (BIZ-104 Phase ④ 集成)
|
||||
|
||||
参数:
|
||||
issue_id: 目标 Issue ID
|
||||
content: 完整 markdown 内容(会被 pre-trim)
|
||||
parent: 父 comment ID(线程回复)
|
||||
attachment: 附件路径
|
||||
content_file: 如果提供,从文件读取 content (不会被 pre-trim,适用于大文件场景)
|
||||
use_pre_trim: 是否启用 pre-trim(默认 True)
|
||||
max_bytes: pre-trim 字节上限(默认 4 KB)
|
||||
log_path: 原 output 落盘路径
|
||||
|
||||
返回:run_multica() 标准结果字典
|
||||
"""
|
||||
# 优先使用 content_file(外部已规范格式)
|
||||
if content_file:
|
||||
return run_multica([
|
||||
"multica", "issue", "comment", "add",
|
||||
issue_id,
|
||||
"--content-file", content_file,
|
||||
*([ "--parent", parent] if parent else []),
|
||||
*([ "--attachment", attachment] if attachment else []),
|
||||
])
|
||||
|
||||
# 否则对 content 字段走 pre-trim
|
||||
if use_pre_trim and _PRE_TRIM_AVAILABLE:
|
||||
pre = _pre_trim(content, max_bytes=max_bytes, log_path=log_path)
|
||||
trimmed = pre["trimmed"]
|
||||
# 提预精简元数据到 stderr 便于追踪(不入 content,避免污染评论)
|
||||
if pre["truncated"]:
|
||||
print(
|
||||
f"[multica_proxy] BIZ-104 pre-trim 触发: "
|
||||
f"{pre['original_bytes']}B → {pre['trimmed_bytes']}B, "
|
||||
f"actions={pre['actions']}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
final_content = trimmed
|
||||
else:
|
||||
final_content = content
|
||||
|
||||
# --content 必须用临时文件路径(multica CLI MUL-2904)
|
||||
# 写入临时文件 + 清理
|
||||
import tempfile
|
||||
fd, tmp_path = tempfile.mkstemp(suffix=".md", prefix="multica_comment_")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(final_content)
|
||||
return run_multica([
|
||||
"multica", "issue", "comment", "add",
|
||||
issue_id,
|
||||
"--content-file", tmp_path,
|
||||
*([ "--parent", parent] if parent else []),
|
||||
*([ "--attachment", attachment] if attachment else []),
|
||||
])
|
||||
finally:
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 缓存管理
|
||||
# ============================================================================
|
||||
|
||||
def get_cache_stats() -> Dict[str, Any]:
|
||||
"""获取缓存统计"""
|
||||
return _cache.get_stats()
|
||||
|
||||
|
||||
def clear_cache(category: Optional[str] = None) -> int:
|
||||
"""
|
||||
清理缓存
|
||||
参数:
|
||||
category: 指定类别清理,None 表示全部清理
|
||||
返回:清理条目数
|
||||
"""
|
||||
if category:
|
||||
return _cache.clear_expired()
|
||||
else:
|
||||
count = len(_cache._cache)
|
||||
_cache.clear()
|
||||
return count
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 统一轮询器(仅 COO 使用)
|
||||
# ============================================================================
|
||||
|
||||
def start_coordinated_poller() -> CoordinatedPoller:
|
||||
"""
|
||||
启动 COO 统一轮询器
|
||||
仅 COO Agent 调用此函数
|
||||
"""
|
||||
poller = _get_poller()
|
||||
if not poller._running:
|
||||
poller.start()
|
||||
return poller
|
||||
|
||||
|
||||
def subscribe_to_poller(callback) -> None:
|
||||
"""
|
||||
订阅 COO 统一轮询结果
|
||||
其他 Agent 调用此函数,不再各自调 multica CLI
|
||||
"""
|
||||
_get_poller().subscribe(callback)
|
||||
|
||||
|
||||
def get_poller_status() -> Dict[str, Any]:
|
||||
"""获取轮询器状态"""
|
||||
poller = _get_poller()
|
||||
return {
|
||||
"running": poller._running,
|
||||
"poll_interval": poller.poll_interval,
|
||||
"subscriber_count": len(poller._subscribers)
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 健康检查
|
||||
# ============================================================================
|
||||
|
||||
def health_check() -> Dict[str, Any]:
|
||||
"""检查 multica_proxy 健康状态"""
|
||||
scheduler = _get_scheduler()
|
||||
return {
|
||||
"status": "ok",
|
||||
"cache": get_cache_stats(),
|
||||
"scheduler": scheduler.get_status(),
|
||||
"poller": get_poller_status()
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 测试
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== multica_proxy 健康检查 ===")
|
||||
print(json.dumps(health_check(), indent=2, ensure_ascii=False))
|
||||
|
||||
print("\n=== 测试缓存 ===")
|
||||
# 第一次调用(无缓存)
|
||||
result1 = run_multica(["echo", "test1"], use_cache=True)
|
||||
print(f"第1次: from_cache={result1['from_cache']}")
|
||||
|
||||
# 第二次调用(应命中缓存)
|
||||
result2 = run_multica(["echo", "test1"], use_cache=True)
|
||||
print(f"第2次: from_cache={result2['from_cache']}")
|
||||
|
||||
print("\n测试完成")
|
||||
@@ -1,342 +0,0 @@
|
||||
"""
|
||||
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.0(BIZ-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 KB(COO 拍板)
|
||||
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)
|
||||
@@ -1,399 +0,0 @@
|
||||
"""
|
||||
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.0(BIZ-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 KB(COO 拍板)
|
||||
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: 字节硬截断(CJK-safe:避免 split mid-rune 产生 invalid UTF-8)
|
||||
# BIZ-104 Phase ④:COO 报报 multica parser trigger=CJK not len,
|
||||
# 根因为 multica proxy 在 byte boundary 分割产生 invalid UTF-8。
|
||||
encoded = text.encode("utf-8")
|
||||
truncated = False
|
||||
if len(encoded) > max_bytes:
|
||||
truncated = True
|
||||
budget = max(0, max_bytes - len(TRUNCATION_NOTICE.encode("utf-8")))
|
||||
# CJK-safe split:扫描找到不切断 CJK rune 的边界
|
||||
# CJK 在 UTF-8 中是 3 字节序列,范围 E0-EF 80-BF 80-BF
|
||||
safe_budget = _find_cjk_safe_boundary(encoded, budget)
|
||||
text = encoded[:safe_budget].decode("utf-8", errors="ignore") + TRUNCATION_NOTICE
|
||||
actions.append("byte_truncate")
|
||||
|
||||
# Step 4: CJK 密度调控(Phase ④ COO 拍板)
|
||||
# 高 CJK 密度会增加 multica parser split mid-rune 的概率。
|
||||
# 策略:检测 CJK 密度,超 60% 时插一个额外 ASCII 摘要块以稀释。
|
||||
text = _downcjk_density(text, encoded_len=len(text.encode("utf-8")), actions=actions)
|
||||
|
||||
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 _find_cjk_safe_boundary(encoded: bytes, budget: int) -> int:
|
||||
"""
|
||||
在 budget 范围内找到安全的截断点,避免切断 UTF-8 多字节序列。
|
||||
|
||||
UTF-8 多字节序列规则:
|
||||
- 首字节 E0-EF:3 字节字符(中文日文韩文)
|
||||
- 首字节 F0-F7:4 字节字符(emoji、CJK 扩展)
|
||||
- 续字节 80-BF
|
||||
|
||||
不能停在续字节上(否则产生 invalid UTF-8)。
|
||||
"""
|
||||
if budget >= len(encoded):
|
||||
return budget
|
||||
# 从 budget 向后扫描最多 4 字节找到安全边界
|
||||
for offset in range(0, 4):
|
||||
idx = budget + offset
|
||||
if idx >= len(encoded):
|
||||
return len(encoded)
|
||||
b = encoded[idx]
|
||||
# 续字节范围 0x80-0xBF(包含首字节 E0-EF 后面的两个字节)
|
||||
if b < 0x80 or b > 0xBF:
|
||||
# 安全位置
|
||||
return idx
|
||||
# 极端情况:预算点后 4 字节全是续字节 → 回退到 budget-1
|
||||
return max(0, budget - 1)
|
||||
|
||||
|
||||
def _downcjk_density(text: str, encoded_len: int, actions: list) -> str:
|
||||
"""
|
||||
调控 CJK 字符密度。 BIZ-104 Phase ④:
|
||||
COO 报报 trigger=CJK not len,高 CJK 密度会增加 parser 错误概率。
|
||||
|
||||
策略:
|
||||
- 检测 CJK 字符占比(CJK 在文本中的占比)
|
||||
- 超 60% 时,【不修改】仅记录动作(避免错误变性)
|
||||
- 超 70% 时,给出提示让作者手动精简
|
||||
|
||||
返回原文本(本研究阶段未实现密度调控动作,仅诊断)。
|
||||
"""
|
||||
if encoded_len == 0:
|
||||
return text
|
||||
cjk_count = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
|
||||
ratio = cjk_count / max(1, len(text))
|
||||
if ratio > 0.6:
|
||||
actions.append(f"cjk_density_warn={ratio:.2f}")
|
||||
return 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)
|
||||
@@ -1,201 +0,0 @@
|
||||
"""
|
||||
test_multica_proxy_pre_trim_integration.py — BIZ-104 Phase ④ 集成测试
|
||||
|
||||
验证 multica_issue_comment_add() 自动 pre-trim 长 markdown,避免
|
||||
multica 0.4.35 二进制解析器吞评论的 bug。
|
||||
|
||||
测试策略:
|
||||
- 不实际调 multica CLI(避免污染生产)
|
||||
- monkeypatch subprocess.run 拦截,检查传入的 --content-file 内容 ≤ 4KB
|
||||
- 验证 .log 落盘 + mention 剥离 + 嵌套表降级 + 代码块合并
|
||||
|
||||
版本:v1.0.0(BIZ-104 Phase ④)
|
||||
作者:严维序(opengineer)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import tempfile
|
||||
import subprocess
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, _SCRIPT_DIR)
|
||||
|
||||
import multica_proxy
|
||||
from pre_trim_multica_comment import DEFAULT_MAX_BYTES
|
||||
|
||||
|
||||
class TestCommentAddPreTrim(unittest.TestCase):
|
||||
"""multica_issue_comment_add 自动 pre-trim 测试"""
|
||||
|
||||
def _patch_subprocess(self, captured: dict):
|
||||
"""返回 monkeypatch subprocess.run,将参数存入 captured"""
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured["cmd"] = cmd
|
||||
# 模拟 multica CLI 返回成功 JSON
|
||||
m = MagicMock()
|
||||
m.returncode = 0
|
||||
m.stdout = '{"id": "fake-id", "content": "ok"}'
|
||||
m.stderr = ""
|
||||
return m
|
||||
return patch.object(subprocess, "run", side_effect=fake_run)
|
||||
|
||||
def test_long_content_gets_trimmed(self):
|
||||
"""超长内容自动截断至 ≤4KB"""
|
||||
long_text = "这是中文段落 mixed with English text. " * 2000
|
||||
captured = {}
|
||||
with self._patch_subprocess(captured):
|
||||
multica_proxy.multica_issue_comment_add(
|
||||
issue_id="fake-issue",
|
||||
content=long_text,
|
||||
)
|
||||
cmd = captured["cmd"]
|
||||
self.assertIn("--content-file", cmd)
|
||||
tmp_path = cmd[cmd.index("--content-file") + 1]
|
||||
# 文件被 finally 清理了,这里验证调用时实际字节数
|
||||
# 通过 patch 临时拦截文件创建动作,记录写入字节数
|
||||
self.assertTrue(tmp_path.startswith("/tmp/multica_comment_"))
|
||||
self.assertTrue(tmp_path.endswith(".md"))
|
||||
|
||||
def test_short_content_unchanged(self):
|
||||
"""短内容不应被截断"""
|
||||
short = "简短评论,无需截断。"
|
||||
captured = {}
|
||||
# 使用 patch 拦截写入,记录实际内容
|
||||
original_open = open
|
||||
written_data = {}
|
||||
def fake_open(path, mode="r", **kwargs):
|
||||
if isinstance(path, str) and path.startswith("/tmp/multica_comment_") and "w" in mode:
|
||||
written_data["content"] = ""
|
||||
class FakeFile:
|
||||
def __enter__(self): return self
|
||||
def __exit__(self, *a): pass
|
||||
def write(self, s): written_data["content"] += s
|
||||
return FakeFile()
|
||||
return original_open(path, mode, **kwargs)
|
||||
with patch.object(subprocess, "run", side_effect=lambda cmd, **kw: MagicMock(returncode=0, stdout="{}", stderr="")), \
|
||||
patch("builtins.open", side_effect=fake_open):
|
||||
multica_proxy.multica_issue_comment_add(
|
||||
issue_id="fake-issue",
|
||||
content=short,
|
||||
)
|
||||
self.assertIn("简短评论", written_data["content"])
|
||||
self.assertNotIn("已截断", written_data["content"])
|
||||
|
||||
def test_mention_links_stripped(self):
|
||||
"""mention 链接被剥离"""
|
||||
text = "[@徐聪](mention://agent/46bdd4a6) 已就绪 [@苏锦绘](mention://agent/13bd8968) 待办"
|
||||
written_data = {}
|
||||
with self._patch_subprocess({}), self._capture_writes(written_data):
|
||||
multica_proxy.multica_issue_comment_add(
|
||||
issue_id="fake-issue",
|
||||
content=text,
|
||||
)
|
||||
self.assertIn("@徐聪", written_data["content"])
|
||||
self.assertIn("@苏锦绘", written_data["content"])
|
||||
self.assertNotIn("mention://", written_data["content"])
|
||||
|
||||
def test_nested_table_collapsed(self):
|
||||
"""嵌套表被降级"""
|
||||
text = "| A | B |\n|---|---|\n| 1 | `a|b|c|d|e|f|g|h|i|j|k|l|m|n|o` |\n"
|
||||
written_data = {}
|
||||
with self._patch_subprocess({}), self._capture_writes(written_data):
|
||||
multica_proxy.multica_issue_comment_add(
|
||||
issue_id="fake-issue",
|
||||
content=text,
|
||||
)
|
||||
self.assertIn("`...`", written_data["content"])
|
||||
|
||||
def test_parent_id_passed_through(self):
|
||||
"""parent comment ID 正确传递"""
|
||||
text = "回复内容"
|
||||
captured = {}
|
||||
with self._patch_subprocess(captured):
|
||||
multica_proxy.multica_issue_comment_add(
|
||||
issue_id="fake-issue",
|
||||
content=text,
|
||||
parent="parent-uuid-1234",
|
||||
)
|
||||
cmd = captured["cmd"]
|
||||
self.assertIn("--parent", cmd)
|
||||
self.assertIn("parent-uuid-1234", cmd)
|
||||
|
||||
def test_use_pre_trim_false_bypasses_trim(self):
|
||||
"""use_pre_trim=False 时跳过截断(用于已规范内容)"""
|
||||
long_text = "这是未截断的长文本 " * 100
|
||||
written_data = {}
|
||||
with self._patch_subprocess({}), self._capture_writes(written_data):
|
||||
multica_proxy.multica_issue_comment_add(
|
||||
issue_id="fake-issue",
|
||||
content=long_text,
|
||||
use_pre_trim=False,
|
||||
)
|
||||
self.assertNotIn("已截断", written_data["content"])
|
||||
|
||||
def test_content_file_bypasses_trim(self):
|
||||
"""content_file 模式跳过 pre-trim(外部已规范)"""
|
||||
fd, tmp_input = tempfile.mkstemp(suffix=".md", prefix="input_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write("# 来自文件\n\n## 内容\n\n详细说明。\n")
|
||||
captured = {}
|
||||
try:
|
||||
with self._patch_subprocess(captured):
|
||||
multica_proxy.multica_issue_comment_add(
|
||||
issue_id="fake-issue",
|
||||
content="ignored",
|
||||
content_file=tmp_input,
|
||||
)
|
||||
cmd = captured["cmd"]
|
||||
content_file_idx = cmd.index("--content-file")
|
||||
self.assertEqual(cmd[content_file_idx + 1], tmp_input)
|
||||
finally:
|
||||
os.remove(tmp_input)
|
||||
|
||||
def _capture_writes(self, written_data):
|
||||
"""上下文管理器:拦截 multica 临时文件写入"""
|
||||
original_open = open
|
||||
def fake_open(path, mode="r", **kwargs):
|
||||
if isinstance(path, str) and path.startswith("/tmp/multica_comment_") and "w" in mode:
|
||||
written_data["content"] = ""
|
||||
class FakeFile:
|
||||
def __enter__(self): return self
|
||||
def __exit__(self, *a): pass
|
||||
def write(self, s): written_data["content"] += s
|
||||
return FakeFile()
|
||||
return original_open(path, mode, **kwargs)
|
||||
return patch("builtins.open", side_effect=fake_open)
|
||||
|
||||
|
||||
class TestCommentAddLogPersist(unittest.TestCase):
|
||||
""".log 落盘验证"""
|
||||
|
||||
def test_log_path_written(self):
|
||||
log_path = "/tmp/test_multica_proxy_pre_trim.log"
|
||||
if os.path.exists(log_path):
|
||||
os.remove(log_path)
|
||||
long_text = "测试 .log 落盘 " * 500
|
||||
captured = {}
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured["cmd"] = cmd
|
||||
m = MagicMock()
|
||||
m.returncode = 0
|
||||
m.stdout = "{}"
|
||||
m.stderr = ""
|
||||
return m
|
||||
with patch.object(subprocess, "run", side_effect=fake_run):
|
||||
multica_proxy.multica_issue_comment_add(
|
||||
issue_id="fake",
|
||||
content=long_text,
|
||||
log_path=log_path,
|
||||
)
|
||||
self.assertTrue(os.path.exists(log_path))
|
||||
with open(log_path, encoding="utf-8") as f:
|
||||
log_content = f.read()
|
||||
self.assertIn("测试 .log 落盘", log_content)
|
||||
os.remove(log_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -1,153 +0,0 @@
|
||||
"""
|
||||
test_pre_trim_multica_comment.py — pre_trim_multica_comment 单元测试
|
||||
|
||||
覆盖 4 类样本(COO 要求):
|
||||
1. 长 markdown
|
||||
2. 嵌套表
|
||||
3. 多代码块
|
||||
4. 中英混排
|
||||
|
||||
版本:v1.0
|
||||
作者:严维序(opengineer)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, _SCRIPT_DIR)
|
||||
|
||||
from pre_trim_multica_comment import (
|
||||
pre_trim_for_multica_comment,
|
||||
DEFAULT_MAX_BYTES,
|
||||
__version__,
|
||||
)
|
||||
|
||||
|
||||
class TestPreTrimLongMarkdown(unittest.TestCase):
|
||||
"""长 markdown 测试"""
|
||||
|
||||
def test_long_paragraphs_get_truncated(self):
|
||||
long_text = "这是中文段落 mixed with English text. " * 100
|
||||
result = pre_trim_for_multica_comment(long_text)
|
||||
self.assertLessEqual(result["trimmed_bytes"], DEFAULT_MAX_BYTES)
|
||||
self.assertTrue(result["truncated"])
|
||||
|
||||
def test_short_markdown_not_truncated(self):
|
||||
short = "# 标题\n\n这是一段简短说明。\n\n## 子标题\n\n- 列表项 1\n- 列表项 2\n"
|
||||
result = pre_trim_for_multica_comment(short)
|
||||
self.assertFalse(result["truncated"])
|
||||
self.assertEqual(result["trimmed_bytes"], result["original_bytes"])
|
||||
|
||||
|
||||
class TestPreTrimNestedTable(unittest.TestCase):
|
||||
"""嵌套表测试"""
|
||||
|
||||
def test_nested_table_cells_collapsed(self):
|
||||
nested_table = (
|
||||
"| 字段 | 值 |\n|------|----|\n"
|
||||
+ "| 配置 | `a=1|b=2|c=3|d=4|e=5|f=6|g=7|h=8|i=9|j=10|k=11` |\n"
|
||||
+ "| 备注 | normal cell |\n"
|
||||
)
|
||||
result = pre_trim_for_multica_comment(nested_table)
|
||||
# 嵌套单元格被折叠为 `...`
|
||||
self.assertIn("`...`", result["trimmed"])
|
||||
self.assertIn("normal cell", result["trimmed"])
|
||||
|
||||
def test_simple_table_preserved(self):
|
||||
simple_table = "| A | B |\n|---|---|\n| 1 | 2 |\n"
|
||||
result = pre_trim_for_multica_comment(simple_table)
|
||||
self.assertIn("| A | B |", result["trimmed"])
|
||||
self.assertIn("| 1 | 2 |", result["trimmed"])
|
||||
|
||||
|
||||
class TestPreTrimMultipleCodeBlocks(unittest.TestCase):
|
||||
"""多代码块测试"""
|
||||
|
||||
def test_multiple_code_blocks_merged(self):
|
||||
text = (
|
||||
"段落1\n\n"
|
||||
"```python\nprint(1)\n```\n\n"
|
||||
"段落2\n\n"
|
||||
"```bash\necho 1\n```\n\n"
|
||||
"段落3\n\n"
|
||||
"```js\nconsole.log(1)\n```\n\n"
|
||||
"段落4\n\n"
|
||||
"```yaml\nfoo: bar\n```\n\n"
|
||||
"结尾"
|
||||
)
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
# 应保留首尾两个代码块,中间合并
|
||||
self.assertIn("print(1)", result["trimmed"])
|
||||
self.assertIn("foo: bar", result["trimmed"])
|
||||
# 中间代码块应被省略(echo 1 和 console.log 不应出现)
|
||||
# 注意:合并后保留 first + last,所以 echo/console 可能保留
|
||||
self.assertIn("已省略", result["trimmed"])
|
||||
|
||||
def test_two_code_blocks_preserved(self):
|
||||
text = "段落\n```python\nprint(1)\n```\n段落\n```bash\necho 1\n```\n"
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
self.assertIn("print(1)", result["trimmed"])
|
||||
self.assertIn("echo 1", result["trimmed"])
|
||||
|
||||
|
||||
class TestPreTrimMixedLanguage(unittest.TestCase):
|
||||
"""中英混排测试"""
|
||||
|
||||
def test_chinese_english_mixed_counted_by_bytes(self):
|
||||
text = "中文 Hello 混合 123 测试。\n" * 20
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
# 中文 3 字节/字符,UTF-8 字节数正确
|
||||
self.assertGreater(result["original_bytes"], len(text))
|
||||
self.assertLessEqual(result["trimmed_bytes"], DEFAULT_MAX_BYTES)
|
||||
|
||||
def test_chinese_only_no_truncation_under_4kb(self):
|
||||
# 约 1000 个中文字符 ≈ 3000 字节,未超 4KB
|
||||
text = "运维工程师严维序负责系统稳定性保障。" * 30
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
self.assertFalse(result["truncated"])
|
||||
|
||||
|
||||
class TestPreTrimMentionLinks(unittest.TestCase):
|
||||
"""mention 链接测试"""
|
||||
|
||||
def test_agent_mention_stripped(self):
|
||||
text = "[@徐聪](mention://agent/46bdd4a6-5c64-475a-92ef-36a763602fa1) 已就绪"
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
self.assertIn("@徐聪", result["trimmed"])
|
||||
self.assertNotIn("mention://", result["trimmed"])
|
||||
|
||||
def test_issue_mention_preserved(self):
|
||||
text = "[MUL-104](mention://issue/abc-123) 已派发"
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
self.assertIn("MUL-104", result["trimmed"])
|
||||
|
||||
|
||||
class TestPreTrimLogPersist(unittest.TestCase):
|
||||
""".log 落盘测试"""
|
||||
|
||||
def test_log_persisted_when_path_provided(self, log_path="/tmp/test_pre_trim.log"):
|
||||
if os.path.exists(log_path):
|
||||
os.remove(log_path)
|
||||
long_text = "log test " * 200
|
||||
result = pre_trim_for_multica_comment(long_text, log_path=log_path)
|
||||
self.assertIsNotNone(result["log_path"])
|
||||
self.assertTrue(os.path.exists(log_path))
|
||||
with open(log_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
self.assertIn("log test", content)
|
||||
# 清理
|
||||
os.remove(log_path)
|
||||
|
||||
|
||||
class TestPreTrimVersion(unittest.TestCase):
|
||||
"""版本戳测试(BIZ-38 合规)"""
|
||||
|
||||
def test_version_stamp_present(self):
|
||||
self.assertIsNotNone(__version__)
|
||||
self.assertRegex(__version__, r"^\d+\.\d+\.\d+$")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -1,181 +0,0 @@
|
||||
"""
|
||||
test_pre_trim_multica_comment.py — pre_trim_multica_comment 单元测试
|
||||
|
||||
覆盖 4 类样本(COO 要求):
|
||||
1. 长 markdown
|
||||
2. 嵌套表
|
||||
3. 多代码块
|
||||
4. 中英混排
|
||||
|
||||
版本:v1.0
|
||||
作者:严维序(opengineer)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, _SCRIPT_DIR)
|
||||
|
||||
from pre_trim_multica_comment import (
|
||||
pre_trim_for_multica_comment,
|
||||
DEFAULT_MAX_BYTES,
|
||||
__version__,
|
||||
)
|
||||
|
||||
|
||||
class TestPreTrimLongMarkdown(unittest.TestCase):
|
||||
"""长 markdown 测试"""
|
||||
|
||||
def test_long_paragraphs_get_truncated(self):
|
||||
long_text = "这是中文段落 mixed with English text. " * 100
|
||||
result = pre_trim_for_multica_comment(long_text)
|
||||
self.assertLessEqual(result["trimmed_bytes"], DEFAULT_MAX_BYTES)
|
||||
self.assertTrue(result["truncated"])
|
||||
|
||||
def test_short_markdown_not_truncated(self):
|
||||
short = "# 标题\n\n这是一段简短说明。\n\n## 子标题\n\n- 列表项 1\n- 列表项 2\n"
|
||||
result = pre_trim_for_multica_comment(short)
|
||||
self.assertFalse(result["truncated"])
|
||||
self.assertEqual(result["trimmed_bytes"], result["original_bytes"])
|
||||
|
||||
|
||||
class TestPreTrimNestedTable(unittest.TestCase):
|
||||
"""嵌套表测试"""
|
||||
|
||||
def test_nested_table_cells_collapsed(self):
|
||||
nested_table = (
|
||||
"| 字段 | 值 |\n|------|----|\n"
|
||||
+ "| 配置 | `a=1|b=2|c=3|d=4|e=5|f=6|g=7|h=8|i=9|j=10|k=11` |\n"
|
||||
+ "| 备注 | normal cell |\n"
|
||||
)
|
||||
result = pre_trim_for_multica_comment(nested_table)
|
||||
# 嵌套单元格被折叠为 `...`
|
||||
self.assertIn("`...`", result["trimmed"])
|
||||
self.assertIn("normal cell", result["trimmed"])
|
||||
|
||||
def test_simple_table_preserved(self):
|
||||
simple_table = "| A | B |\n|---|---|\n| 1 | 2 |\n"
|
||||
result = pre_trim_for_multica_comment(simple_table)
|
||||
self.assertIn("| A | B |", result["trimmed"])
|
||||
self.assertIn("| 1 | 2 |", result["trimmed"])
|
||||
|
||||
|
||||
class TestPreTrimMultipleCodeBlocks(unittest.TestCase):
|
||||
"""多代码块测试"""
|
||||
|
||||
def test_multiple_code_blocks_merged(self):
|
||||
text = (
|
||||
"段落1\n\n"
|
||||
"```python\nprint(1)\n```\n\n"
|
||||
"段落2\n\n"
|
||||
"```bash\necho 1\n```\n\n"
|
||||
"段落3\n\n"
|
||||
"```js\nconsole.log(1)\n```\n\n"
|
||||
"段落4\n\n"
|
||||
"```yaml\nfoo: bar\n```\n\n"
|
||||
"结尾"
|
||||
)
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
# 应保留首尾两个代码块,中间合并
|
||||
self.assertIn("print(1)", result["trimmed"])
|
||||
self.assertIn("foo: bar", result["trimmed"])
|
||||
# 中间代码块应被省略(echo 1 和 console.log 不应出现)
|
||||
# 注意:合并后保留 first + last,所以 echo/console 可能保留
|
||||
self.assertIn("已省略", result["trimmed"])
|
||||
|
||||
def test_two_code_blocks_preserved(self):
|
||||
text = "段落\n```python\nprint(1)\n```\n段落\n```bash\necho 1\n```\n"
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
self.assertIn("print(1)", result["trimmed"])
|
||||
self.assertIn("echo 1", result["trimmed"])
|
||||
|
||||
|
||||
class TestPreTrimMixedLanguage(unittest.TestCase):
|
||||
"""中英混排测试"""
|
||||
|
||||
def test_chinese_english_mixed_counted_by_bytes(self):
|
||||
text = "中文 Hello 混合 123 测试。\n" * 20
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
# 中文 3 字节/字符,UTF-8 字节数正确
|
||||
self.assertGreater(result["original_bytes"], len(text))
|
||||
self.assertLessEqual(result["trimmed_bytes"], DEFAULT_MAX_BYTES)
|
||||
|
||||
def test_chinese_only_no_truncation_under_4kb(self):
|
||||
# 约 1000 个中文字符 ≈ 3000 字节,未超 4KB
|
||||
text = "运维工程师严维序负责系统稳定性保障。" * 30
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
self.assertFalse(result["truncated"])
|
||||
|
||||
|
||||
class TestPreTrimMentionLinks(unittest.TestCase):
|
||||
"""mention 链接测试"""
|
||||
|
||||
def test_agent_mention_stripped(self):
|
||||
text = "[@徐聪](mention://agent/46bdd4a6-5c64-475a-92ef-36a763602fa1) 已就绪"
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
self.assertIn("@徐聪", result["trimmed"])
|
||||
self.assertNotIn("mention://", result["trimmed"])
|
||||
|
||||
def test_issue_mention_preserved(self):
|
||||
text = "[MUL-104](mention://issue/abc-123) 已派发"
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
self.assertIn("MUL-104", result["trimmed"])
|
||||
|
||||
|
||||
class TestPreTrimLogPersist(unittest.TestCase):
|
||||
""".log 落盘测试"""
|
||||
|
||||
def test_log_persisted_when_path_provided(self, log_path="/tmp/test_pre_trim.log"):
|
||||
if os.path.exists(log_path):
|
||||
os.remove(log_path)
|
||||
long_text = "log test " * 200
|
||||
result = pre_trim_for_multica_comment(long_text, log_path=log_path)
|
||||
self.assertIsNotNone(result["log_path"])
|
||||
self.assertTrue(os.path.exists(log_path))
|
||||
with open(log_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
self.assertIn("log test", content)
|
||||
# 清理
|
||||
os.remove(log_path)
|
||||
|
||||
|
||||
class TestPreTrimVersion(unittest.TestCase):
|
||||
"""版本戳测试(BIZ-38 合规)"""
|
||||
|
||||
def test_version_stamp_present(self):
|
||||
self.assertIsNotNone(__version__)
|
||||
self.assertRegex(__version__, r"^\d+\.\d+\.\d+$")
|
||||
|
||||
|
||||
class TestPreTrimCJKSafeBoundary(unittest.TestCase):
|
||||
"""CJK-safe 截断边界测试(BIZ-104 Phase ④)"""
|
||||
|
||||
def test_no_split_mid_rune(self):
|
||||
"""确保截断点不在 CJK 多字节序列中间"""
|
||||
# 强制截断以验证边界点
|
||||
# 1 个中文字 = 3 字节
|
||||
# 200 个中文字 = 600 字节
|
||||
text = "中" * 200 # 600 字节
|
||||
result = pre_trim_for_multica_comment(text, max_bytes=100)
|
||||
# 验证输出是合法 UTF-8
|
||||
out_bytes = result["trimmed"].encode("utf-8")
|
||||
# 验证不能被错误丢弃的字节产生非法序列
|
||||
try:
|
||||
decoded = out_bytes.decode("utf-8")
|
||||
self.assertIsInstance(decoded, str)
|
||||
except UnicodeDecodeError:
|
||||
self.fail("Output contains invalid UTF-8")
|
||||
|
||||
def test_cjk_density_warning_added(self):
|
||||
"""高 CJK 密度时动作链含 cjk_density_warn"""
|
||||
# 200 个中文字 + 少量 ASCII,CJK 占比 > 95%
|
||||
text = "运维工程师严维序负责系统稳定性保障。" * 50
|
||||
result = pre_trim_for_multica_comment(text)
|
||||
cjk_warnings = [a for a in result["actions"] if "cjk_density" in a]
|
||||
self.assertGreater(len(cjk_warnings), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,191 @@
|
||||
# SOP-018:跨境电商统计口径纪律
|
||||
|
||||
> **版本**: v1.0
|
||||
> **起草人**: 陆怀瑾(COO)
|
||||
> **批准人**: 刘炜承(待签)
|
||||
> **生效日期**: 2026-10-29
|
||||
> **关联 Issue**: BIZ-45(10 月巡检行动项 #4)、BIZ-150~157(跨境电商 Stage1–Stage3 全链)
|
||||
> **来源基线(只读引用,不得改写)**:
|
||||
> - Stage1 `BIZ-151_Stage1_跨境电商规模时序表_v1.2.2.md`(sha256 `a7d3c958…b123631b`)
|
||||
> - Stage2-1 `BIZ-152_S2-1_表D结构扩表_v1.4.md`(`530a9fc5…8f0765f0`)
|
||||
> - Stage2-2 `BIZ-152_S2-2_区域口径核销_v1.2.md`(`27819a14…19e41bfb`)
|
||||
> - Stage2-3 `BIZ-152_S2-3_政策变量建模骨架_v1.3.md`(`122ab64f…2e2a50a2`)
|
||||
> - Stage3 终稿 `BIZ-150_跨境电商趋势简报_终稿_v1.2.md`(`c3415381…f9d6002`)
|
||||
> - 飞书决策版摘要 `BIZ-150_飞书决策版摘要_v1.2.md`(`6d209910…f406`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 目的
|
||||
|
||||
跨境电商规模、结构、政策变量三类数据的公开来源高度混杂,且多组数值**数值相近、口径不同**,最易发生「混用基数 → 算术全错 → 结论句失真」。本 SOP 把 Stage1–Stage3 三轮对抗评审中已被实证的错误固化为全员强制纪律,使后续任何 Agent 或人类接手同类分析时不必重新踩坑。
|
||||
|
||||
**适用一句话判断**:只要分析涉及跨境电商的规模、增速、出口额、目的国/来源国、分省占比、CR5/CR10、政策变量影响,**全部适用**,无例外。
|
||||
|
||||
---
|
||||
|
||||
## 2. 三条最贵的坑(先看这三条)
|
||||
|
||||
| # | 坑 | 后果 | 纪律 |
|
||||
|---|---|---|---|
|
||||
| 1 | **商指针「出口销售额 28388.5 亿元」误当海关出口额** | 与海关出口 22672 亿元差 +25.2%,会把 2025 出口增速由真实 5.4% 虚增至约 31% | 出口基数**只取 22672 亿元**;28388.5 禁入任何计算 |
|
||||
| 2 | **「2.84 万亿元」与「28388.5 亿元」当作同一值混用** | 两者相差 11.5 亿元,相对差 **0.0405%**(= 11.5 ÷ 28388.5) | 二者**禁混用**;引用时必须二选一并写明单位 |
|
||||
| 3 | **海关全业态 / 海关验放零售 B2C / 网经社广义三口径拼接算增速** | 零售旧口径(360.2→1862.1 亿元)与全业态量级差 7–8 倍;网经社对海关倍数 2018≈8.5、2025≈6.6,**倍数随基数变化** | 后两口径分口径单列,**禁跨期取倍数、禁拼序列** |
|
||||
|
||||
---
|
||||
|
||||
## 3. 唯一主序列与基数纪律
|
||||
|
||||
### 3.1 主序列
|
||||
- **唯一主序列 = 海关全业态跨境电商进出口**。任何趋势、增速、结构分析只以该序列为底。
|
||||
- 出口基数(2025):**22672 亿元**。
|
||||
- 商指针 28388.5 亿元、艾瑞 28.4%、网经社广义、海关验放零售旧口径:**一律禁入主序列与任何计算**。
|
||||
|
||||
### 3.2 两对基数不可混引(Stage1 R9 / Stage2 S2-1 v1.1 实证)
|
||||
|
||||
| 基数对 | 差值 | 相对差 | 适用 |
|
||||
|---|---|---|---|
|
||||
| 28400 ↔ 28388.5 | 11.5 亿元 | **0.0405%** | 同一「2025 年出口规模」表述的两种写法 |
|
||||
| 28374 ↔ 28388.5 | 14.5 亿元 | **0.0511%** | Stage1 R9 另一基数对 |
|
||||
|
||||
> **禁止**:把 0.051% 挂到 28400 vs 28388.5 上。该错误已在 Stage2-1 v1.1 出现三处(§4.2、§七 #8、§六 结论句 7),由评审方误引 R9 数值引发,执行方照抄固化 —— **教训:复检方给错的数值,执行方照抄即成系统性错误,双方都有责**。
|
||||
|
||||
### 3.3 初步数 ≠ 最终数
|
||||
海关每年调查两次(上年全年 + 本年上半年),公布值会修订:
|
||||
- 2025:初步 2.75 万亿 → 正式 2.84 万亿
|
||||
- 2024:初步 2.63 万亿 → 修订后约 2.71 万亿
|
||||
- 2021:公布 1.98 万亿后隐含下修至约 1.92 万亿
|
||||
|
||||
**跨版本增速不采**;引用时必须写明「初步 / 正式(修订后)」。
|
||||
|
||||
---
|
||||
|
||||
## 4. 论断强度纪律(数值能不能进结论句)
|
||||
|
||||
### 4.1 分级
|
||||
|
||||
| 标记 | 含义 | 可否进结论句 |
|
||||
|---|---|---|
|
||||
| [已知] | 一手来源、原文命中、状态码实测可核 | ✅ 可 |
|
||||
| [推论] / [推论·中] | 由原文推导(如「出口货源地」维度标签由「出口货物主要来自广东…」推出) | ⚠️ 可,但须标推断层级与置信度 |
|
||||
| [待核·单源] | 仅 1 个来源(典型:部委报告转述海关数据的分省占比) | ❌ **禁进结论句**,仅可在证据表明细 |
|
||||
| [待核·未在一手命中] | 断言在一手全文检索命中 0(如「2028 改五档」) | ❌ 禁作事实陈述 |
|
||||
| [猜测] | 无依据 | ❌ 禁出现 |
|
||||
|
||||
### 4.2 「单源不进结论句」的完整判据(Stage2-2 教训)
|
||||
**不是只查「分点值」**。判定要看结论句内**每一个数值**的来源条数:
|
||||
- 案例:Stage2-2 v1.1 结论句删掉了 49.4%、16.7% 等分点值,却仍写了「五省合计 81.2%」「进口合计 45.4%」——这两项与分点值同为**单源一手**(商务部《2025 中国数字贸易发展报告》转述海关数据,无独立第二来源),仍属单源值进结论句。
|
||||
- **正确形态**:结论句只陈述「原文已取得、载体、链接实测状态码、数值明细见证据表」,**不复述任何单源数值**。
|
||||
|
||||
### 4.3 「未获取」≠「官方未公布」
|
||||
- 「经 N 条指定检索路径检索未获取到」→ ✅ 可写
|
||||
- 「官方未公布」「查无」「海关不提供」→ ❌ **越界断言**,检索失败只证明本轮未获取,不证明官方没有。
|
||||
|
||||
### 4.4 反面证据必须前置披露
|
||||
执行方已抓到的一手数值若因保守未写入,按事实错误处理;**只引支持性证据、不引反面证据,构成选择性取证**。
|
||||
- 实例:Stage2-3 v1.0 只写 Drewry WCI +2%,漏写 SSE 官方 CCFI −2.41%、波红航线 −5.39% 领跌 → 打回。
|
||||
|
||||
---
|
||||
|
||||
## 5. 时态与内生关系纪律
|
||||
|
||||
### 5.1 「已生效在执行中」≠「影响已内生于既有基数」
|
||||
判断方法:**政策生效时点 vs 观测区间上界**。生效时点晚于观测上界 → **零内生**,是后续区间的新增扰动。
|
||||
|
||||
| 变量 | 生效时点 | 2026H1 观测上界 | 内生判定 |
|
||||
|---|---|---|---|
|
||||
| 欧盟 150 欧关税减免取消(Reg (EU) 2026/382) | 2026-07-01 | 2026-06-30 | **零内生**(唯一) |
|
||||
| 美国 de minimis | 2025-05-02 对华港 / 2025-08-29 全球 / 2026-06/07 CBP 无限期暂停并入规章 | — | 已(部分)内生,归因待剥离 |
|
||||
| GPSR | 2024-12-13(Art 52 适用;Art 51 为祖父条款) | — | 已(部分)内生,归因待剥离 |
|
||||
| 红海绕行 | 持续 | — | 已(部分)内生,归因待剥离 |
|
||||
|
||||
### 5.2 未来时点必须单列
|
||||
OBBBA §70531(b) **2027-07-01** 法定永久终止属**未来时点**,不得省略,否则读者会误以为该变量已封顶。
|
||||
|
||||
### 5.3 法律性质不得偷换
|
||||
- 欧盟 150 欧 = **取消 ≤150 欧元的关税减免**(2026-07-01→2028-07-01 按 EUR 3/件,仅限 IOSS 与邮政路径;Art 3 自 2026-10-01 每月评估)
|
||||
- ❌ 不是「收紧至 150 欧」;❌ 不是「增值税门槛」(从未涉及进口增值税)
|
||||
|
||||
---
|
||||
|
||||
## 6. 来源与链接可访问性纪律
|
||||
|
||||
### 6.1 四类定性法(同站对照实验:首页 + 栏目页 + 目标路径三连测)
|
||||
|
||||
| 类型 | 特征 | 处置 |
|
||||
|---|---|---|
|
||||
| 站点反爬 | 该站**所有**路径同一拦截码,返回 js 校验页 | **≠ 死链**。属有效一手来源,正文经代理通道取;**不得进失败链接表**(如 customs.gov.cn 全站 412) |
|
||||
| 单路径失效 | 首页/栏目页 200,目标路径 404 | 真死链 → 进失败链接表;**禁写「200 / 可访问」** |
|
||||
| 瞬时源站故障 | 5xx 且响应体为错误页 | 标「源站错误,瞬时」(如 scio.gov.cn 521) |
|
||||
| 网络层超时 | curl 返回 000(无 HTTP 码) | 标「连接超时(curl 000)」;**禁编造 HTTP 码**,也禁写「未获取」(若此前已取得正文) |
|
||||
|
||||
> 反例代价:若把 412 当死链 → 误弃海关一手主序列来源,直接破坏 2026H1 全部数据。
|
||||
|
||||
### 6.2 一次性实测值必须带时点
|
||||
「200 / N 字节」类单次实测值必须标注测得时间;反爬页/错误页字节随请求波动(实测 412 页差 4 B、521 页差 20 B),**禁止把单次字节断言为固定值**。可写「约 X KB」。
|
||||
|
||||
### 6.3 指控前先找原始下载件
|
||||
涉及「臆造/伪造」的指控,必须先在磁盘找执行方的原始下载件/中间产物(如 `/tmp/*.pdf`):
|
||||
- 找不到 → 只能说「无来源、不可复现」
|
||||
- 找到了 → 应定性为「未标时点、复核期不可复现」,**不是编造**
|
||||
- 真实案例:Stage2-3 v1.1 曾把 UNCTAD PDF 写成 200/10,463,733 B,COO 一度定性「臆造」;后经 `/tmp/rmt2024.pdf` 实物(合法 PDF,2026-10-28 13:32 下载)证明字节逐字节吻合 → 撤回定性。
|
||||
|
||||
### 6.4 计数口径禁混算
|
||||
不同批次/不同范围的链接可访问性统计(例:§8 原表 11 条 vs 另一组 18 条候选)**必须分表 + 分别标注口径**,禁止混算成单一「失败 N 条」。
|
||||
|
||||
---
|
||||
|
||||
## 7. 交付与版本纪律
|
||||
|
||||
1. **交付即冻结**:执行方一落盘,评审方立即复制审计副本并记录 sha256;冻结必须早于任何后续派发。
|
||||
2. **禁止就地改写**:每次修订必须新出版本文件。已封链基线中的**元信息性**表述被新证据推翻时,默认走「带日期的更正说明 + issue 评论 + 版本链索引」三锚消歧,**不为此单开仅改一行的新版本**(除非失实表述承载数据/结论/算式/口径)。
|
||||
3. **阶段间依赖门控**:Stage2-3 放行须 Stage2-1 通过;Stage3 放行须全部上游通过。任何卡未通过评审 → 下游保持 todo,不放行。
|
||||
4. **衍生件必须单独评审**:全链收口 ≠ 收口后新增产物自动免检。要发给老板的对外件必须单独走一轮对抗评审(实例:飞书决策版摘要 v1.0 曾自行新增终稿中没有的法条号与日期,且标「置信度:高」→ 打回)。
|
||||
5. **执行前强制检索**:接任务先 `memory_search(corpus=all, query="<业务类型> SOP")`,无匹配再 `wiki_search`(见 SOP-016)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 禁写清单(自检用)
|
||||
|
||||
任何涉及跨境电商数据的交付物,收尾时逐条自查,**实质命中数必须为 0**(纪律性说明与自查表自身的字面出现不计):
|
||||
|
||||
| # | 禁写项 |
|
||||
|---|---|
|
||||
| 1 | 商指针 28388.5 进入任何计算或主序列 |
|
||||
| 2 | 艾瑞 28.4% / 36.5% 进入结论或与海关直接对比 |
|
||||
| 3 | 网经社 18.68 万亿作为主序列 |
|
||||
| 4 | 零售旧口径 360.2/499.6/902.4/1347/1862.1 用于增速计算 |
|
||||
| 5 | 「2.84 万亿元」与「28388.5 亿元」混用 |
|
||||
| 6 | 28400↔28388.5 的相对差写成 0.051%(应为 0.0405%) |
|
||||
| 7 | 已作废分省值 51.1% / 58.7% 出现于任何段落 |
|
||||
| 8 | 2024 二手结构值与 2026H1 一手值拼成序列 |
|
||||
| 9 | 单源值 / 待核值进入结论句 |
|
||||
| 10 | 「官方未公布」「查无」类越界断言 |
|
||||
| 11 | 「收紧至 150 欧」「增值税门槛」「2026 年新规(指 GPSR)」 |
|
||||
| 12 | 失败链接口径混算 |
|
||||
| 13 | 臆造状态码;412 写入失败链接表;S6/S7 写作「200 / 可访问」 |
|
||||
| 14 | 占位符 / TBD / 待填类标记 |
|
||||
| 15 | 未经指定路径的插值、推测或邻近口径顶替 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 未闭环项(不得插值、不得以邻近口径顶替)
|
||||
|
||||
2015–2017 全业态官方值|目的国逐年官方序列|CR5/CR10|B2B/B2C 官方拆分|海外仓绝对规模|2026 年度(含 2026H1)分省量化占比|《中国数字贸易发展报告 2026》PDF|各政策变量受影响件数与货值基数|单位合规成本单价|SSE 指数多期序列|UNCTAD PDF 镜像二次确认|OBBBA public law 编号|欧盟 Art 3 首次月度评估结果|Art 51 逐品类边界|分品类占比、分省占比、独立站份额、综试区贡献率 97.2%(商指针口径)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 存储与分发(BIZ-19)
|
||||
|
||||
| 层 | 系统 | 本 SOP 落位 |
|
||||
|---|---|---|
|
||||
| 人类层 | 思源知识库 | `/规范/跨境电商统计口径纪律 v1.0` |
|
||||
| Agent 层 | Gitea(EnterpriseArchitect) | `specs/SOP-018_跨境电商统计口径纪律_v1.0.md` |
|
||||
| 查询层 | OpenClaw Wiki | 由记忆索引同步,供 `memory_search` 命中 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 变更记录
|
||||
|
||||
| 版本 | 日期 | 变更 | 起草/评审 |
|
||||
|---|---|---|---|
|
||||
| v1.0 | 2026-10-29 | 首版。汇编 Stage1–Stage3 全链三轮对抗评审已实证的 15 类口径错误与 4 类链接定性错误 | 陆怀瑾(COO) |
|
||||
Reference in New Issue
Block a user