mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Compare commits
63 Commits
feat/opt-i
...
feat/lark-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020151f01b | ||
|
|
3a85ef389d | ||
|
|
68f867d6a5 | ||
|
|
78f7fba89e | ||
|
|
06241666a0 | ||
|
|
a35cc26131 | ||
|
|
b6da950be3 | ||
|
|
aa545083b6 | ||
|
|
5c7100ee4c | ||
|
|
3ef3a9d1d3 | ||
|
|
bdad336caf | ||
|
|
39a7d4bfb4 | ||
|
|
4b404fc0ee | ||
|
|
fc6e1e25de | ||
|
|
14d3107bf2 | ||
|
|
e795f4f068 | ||
|
|
2e4033a1a0 | ||
|
|
fc44564b01 | ||
|
|
7742a47072 | ||
|
|
3668b904ca | ||
|
|
1c68d31d12 | ||
|
|
4c51cd36fb | ||
|
|
bbeae3636c | ||
|
|
a9d88c5666 | ||
|
|
4801675fd6 | ||
|
|
dd04b3705f | ||
|
|
439f184ba5 | ||
|
|
825071fd7a | ||
|
|
72999cd303 | ||
|
|
f9c73e217d | ||
|
|
5f3c1c8e6a | ||
|
|
ead8aa854f | ||
|
|
833b7cde33 | ||
|
|
57d71607e1 | ||
|
|
d2c326a78c | ||
|
|
422797305a | ||
|
|
3fa28c10fa | ||
|
|
27d185c91c | ||
|
|
83926943ae | ||
|
|
752bfcbbb9 | ||
|
|
80d9f6b59b | ||
|
|
080ef44cdb | ||
|
|
f046fb6282 | ||
|
|
ca9eddb142 | ||
|
|
1caeb2d377 | ||
|
|
a66bef66af | ||
|
|
421805d35c | ||
|
|
8d5bb73c70 | ||
|
|
97b9ffb466 | ||
|
|
336f147ca6 | ||
|
|
0a47f35c7d | ||
|
|
72ac526e23 | ||
|
|
023a8786f0 | ||
|
|
3ecd75b53d | ||
|
|
5bf71428a4 | ||
|
|
e819e819fe | ||
|
|
2017e9dab8 | ||
|
|
74a02e6f2d | ||
|
|
02f4f73227 | ||
|
|
a2625d036d | ||
|
|
d005694e0f | ||
|
|
3149c77134 | ||
|
|
6e067f2180 |
71
AGENTS.md
71
AGENTS.md
@@ -141,3 +141,74 @@ CLI arguments are untrusted (they come from AI agents). Call `validate.SafeInput
|
||||
| Modify shortcut flags/params | Required | If behavior changes |
|
||||
| Shortcut bug fix | Required | If regression risk |
|
||||
| Internal refactor (no shortcut impact) | Not needed | Not needed |
|
||||
|
||||
|
||||
|
||||
## CCM Harness Skill Routing (v2)
|
||||
|
||||
When the user's request matches one of these patterns, invoke the corresponding `/ccm-harness:*` skill via the Skill tool. Skills include multi-step workflows, gates, and quality checks that produce more reliable results than ad-hoc answers. When in doubt, invoke the skill — a false positive is cheaper than a false negative.
|
||||
|
||||
### 主链路(spec → idl → dev → release)
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "新需求 / 这是个新功能 / 开始一个 req / 写 spec / 起 spec / PRD 来了 / 把 PRD 转 spec / 改 spec / 微调 spec / spec 局部更新 / spec 加一个字段" | `/ccm-harness:draft-spec <req-id> [<arg2>] [--force]`(Phase 0 路由器自动决定 init / generate / update capability)|
|
||||
| "review spec / 看 spec / 评审 spec / spec 评审" | `/ccm-harness:spec-review <req-id>` |
|
||||
| "生成 thrift / 起 idl / 把 spec 转 thrift" | `/ccm-harness:draft-idl` |
|
||||
| "推 thrift / 落 contract / Frozen Spec / codegen / 生成框架代码" | `/ccm-harness:codegen-idl <req-id>` |
|
||||
| "实现 spec / 写后端 / 后端开发 / 实现这个功能" | `/ccm-harness:backend-dev <req-id>` |
|
||||
| "前端怎么改 / 写前端 / 前端开发 / 前端编码" | `/ccm-harness:frontend-coding <req-id>` |
|
||||
| "部署 BOE / 上 PPE / 部署到 feature 环境" | `/ccm-harness:deploy <req-id>` |
|
||||
| "提 release / 上线 / 发布 / 走 PRE-GRAY-ONLINE" | `/ccm-harness:release <req-id>` |
|
||||
| "触发打包 / build / 起 SCM 编译" | `/ccm-harness:build <repo or psm>` |
|
||||
|
||||
### 守护与诊断
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "工作流到哪一步了 / 下一步咋走 / 我迷路了" | `/ccm-harness:doctor` |
|
||||
| "调试 / debug / 这个 bug 怎么排" | `/ccm-harness:debug` |
|
||||
| "查 CI 失败 / CI 跑挂了 / pipeline 红了" | `/ccm-harness:check-ci-failure <mr>` |
|
||||
| "检查实现是否符合 spec / 蓝图对比" | `/ccm-harness:check-impl-gap` |
|
||||
| "spec 跟代码飘了吗 / drift 检查" | `/ccm-harness:spec-review <req-id> --mode drift` |
|
||||
| "Hub 知识跟代码一致吗" | `/ccm-harness:check-knowledge-consistency` |
|
||||
| "代码 review / 看 MR / cr 一下" | `/ccm-harness:code-review <mr>` |
|
||||
| "设计 review / 看技术方案" | `/ccm-harness:design-review <doc>` |
|
||||
|
||||
### 工程辅助
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "通知 reviewer / 发 review 卡片" | `/ccm-harness:notify-reviewer <mr>` |
|
||||
| "自动修 MR / 按 review 改" | `/ccm-harness:autofix-mr <mr>` |
|
||||
| "生成测试用例 / 起 case / 筛选可执行 case / E2E 用例 / Playwright 脚本 / 自动化验收" | `/ccm-harness:test`(路由到 `ccm-e2e-check -> exec-e2e`) |
|
||||
| "查 idl / 看 thrift 定义" | `/ccm-harness:lookup-idl <psm>` |
|
||||
| "看仓库最近改了啥 / 仓库脉搏" | `/ccm-harness:pulse` |
|
||||
|
||||
### 元能力(Skill / Prompt 研发)
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "起一个新 skill / 设计 skill" | `/ccm-harness:meta-draft-skill` |
|
||||
| "做评测集 / 给 skill 出评测数据" | `/ccm-harness:meta-build-evalset` |
|
||||
| "跑评测 / Fornax 实验" | `/ccm-harness:meta-run-eval` |
|
||||
| "优化 skill / skill 反馈优化" | `/ccm-harness:meta-optimize-skill` |
|
||||
|
||||
### 环境与配置
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "升级 ccm-harness / 更新插件" | `/ccm-harness:upgrade` |
|
||||
| "看遥测 / 最近的反馈" | `/ccm-harness:show-telemetry` |
|
||||
| "清遥测 / 重置 telemetry" | `/ccm-harness:clear-telemetry` |
|
||||
| "上报问题 / 提 issue" | `/ccm-harness:report-issue` |
|
||||
| "看本地教训 / project learnings / 我们踩过啥" | `/ccm-harness:learn` |
|
||||
| "反馈 / 评分这次 skill" | `/ccm-harness:feedback` |
|
||||
|
||||
### 使用提示
|
||||
|
||||
- **入口**:新需求**必从** `/ccm-harness:draft-spec <req-id>` 开始(Phase 0 路由器自动决定建目录 / 带 PRD 一气呵成)。
|
||||
- **不跳步**:spec → idl → dev → release 是流水线,不是菜单——按顺序推进,反向走需要 `/ccm-harness:draft-spec <req-id> "<change-desc>"` 局部修(路由进 update capability)。
|
||||
- **横切**:任何阶段发现 spec 要局部改 → `/ccm-harness:draft-spec <req-id> "<change-desc>"`;想检测漂移 → `/ccm-harness:spec-review <req-id> --mode drift`;CI 红 → `/ccm-harness:check-ci-failure`;迷路 → `/ccm-harness:doctor`。
|
||||
|
||||
完整流程文档:`docs/user-guide/workflow.md`
|
||||
|
||||
69
CLAUDE.md
Normal file
69
CLAUDE.md
Normal file
@@ -0,0 +1,69 @@
|
||||
|
||||
## CCM Harness Skill Routing (v2)
|
||||
|
||||
When the user's request matches one of these patterns, invoke the corresponding `/ccm-harness:*` skill via the Skill tool. Skills include multi-step workflows, gates, and quality checks that produce more reliable results than ad-hoc answers. When in doubt, invoke the skill — a false positive is cheaper than a false negative.
|
||||
|
||||
### 主链路(spec → idl → dev → release)
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "新需求 / 这是个新功能 / 开始一个 req / 写 spec / 起 spec / PRD 来了 / 把 PRD 转 spec / 改 spec / 微调 spec / spec 局部更新 / spec 加一个字段" | `/ccm-harness:draft-spec <req-id> [<arg2>] [--force]`(Phase 0 路由器自动决定 init / generate / update capability)|
|
||||
| "review spec / 看 spec / 评审 spec / spec 评审" | `/ccm-harness:spec-review <req-id>` |
|
||||
| "生成 thrift / 起 idl / 把 spec 转 thrift" | `/ccm-harness:draft-idl` |
|
||||
| "推 thrift / 落 contract / Frozen Spec / codegen / 生成框架代码" | `/ccm-harness:codegen-idl <req-id>` |
|
||||
| "实现 spec / 写后端 / 后端开发 / 实现这个功能" | `/ccm-harness:backend-dev <req-id>` |
|
||||
| "前端怎么改 / 写前端 / 前端开发 / 前端编码" | `/ccm-harness:frontend-coding <req-id>` |
|
||||
| "部署 BOE / 上 PPE / 部署到 feature 环境" | `/ccm-harness:deploy <req-id>` |
|
||||
| "提 release / 上线 / 发布 / 走 PRE-GRAY-ONLINE" | `/ccm-harness:release <req-id>` |
|
||||
| "触发打包 / build / 起 SCM 编译" | `/ccm-harness:build <repo or psm>` |
|
||||
|
||||
### 守护与诊断
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "工作流到哪一步了 / 下一步咋走 / 我迷路了" | `/ccm-harness:doctor` |
|
||||
| "调试 / debug / 这个 bug 怎么排" | `/ccm-harness:debug` |
|
||||
| "查 CI 失败 / CI 跑挂了 / pipeline 红了" | `/ccm-harness:check-ci-failure <mr>` |
|
||||
| "检查实现是否符合 spec / 蓝图对比" | `/ccm-harness:check-impl-gap` |
|
||||
| "spec 跟代码飘了吗 / drift 检查" | `/ccm-harness:spec-review <req-id> --mode drift` |
|
||||
| "Hub 知识跟代码一致吗" | `/ccm-harness:check-knowledge-consistency` |
|
||||
| "代码 review / 看 MR / cr 一下" | `/ccm-harness:code-review <mr>` |
|
||||
| "设计 review / 看技术方案" | `/ccm-harness:design-review <doc>` |
|
||||
|
||||
### 工程辅助
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "通知 reviewer / 发 review 卡片" | `/ccm-harness:notify-reviewer <mr>` |
|
||||
| "自动修 MR / 按 review 改" | `/ccm-harness:autofix-mr <mr>` |
|
||||
| "生成测试用例 / 起 case / 筛选可执行 case / E2E 用例 / Playwright 脚本 / 自动化验收" | `/ccm-harness:test`(路由到 `ccm-e2e-check -> exec-e2e`) |
|
||||
| "查 idl / 看 thrift 定义" | `/ccm-harness:lookup-idl <psm>` |
|
||||
| "看仓库最近改了啥 / 仓库脉搏" | `/ccm-harness:pulse` |
|
||||
|
||||
### 元能力(Skill / Prompt 研发)
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "起一个新 skill / 设计 skill" | `/ccm-harness:meta-draft-skill` |
|
||||
| "做评测集 / 给 skill 出评测数据" | `/ccm-harness:meta-build-evalset` |
|
||||
| "跑评测 / Fornax 实验" | `/ccm-harness:meta-run-eval` |
|
||||
| "优化 skill / skill 反馈优化" | `/ccm-harness:meta-optimize-skill` |
|
||||
|
||||
### 环境与配置
|
||||
|
||||
| 用户表达 | Skill |
|
||||
|---------|-------|
|
||||
| "升级 ccm-harness / 更新插件" | `/ccm-harness:upgrade` |
|
||||
| "看遥测 / 最近的反馈" | `/ccm-harness:show-telemetry` |
|
||||
| "清遥测 / 重置 telemetry" | `/ccm-harness:clear-telemetry` |
|
||||
| "上报问题 / 提 issue" | `/ccm-harness:report-issue` |
|
||||
| "看本地教训 / project learnings / 我们踩过啥" | `/ccm-harness:learn` |
|
||||
| "反馈 / 评分这次 skill" | `/ccm-harness:feedback` |
|
||||
|
||||
### 使用提示
|
||||
|
||||
- **入口**:新需求**必从** `/ccm-harness:draft-spec <req-id>` 开始(Phase 0 路由器自动决定建目录 / 带 PRD 一气呵成)。
|
||||
- **不跳步**:spec → idl → dev → release 是流水线,不是菜单——按顺序推进,反向走需要 `/ccm-harness:draft-spec <req-id> "<change-desc>"` 局部修(路由进 update capability)。
|
||||
- **横切**:任何阶段发现 spec 要局部改 → `/ccm-harness:draft-spec <req-id> "<change-desc>"`;想检测漂移 → `/ccm-harness:spec-review <req-id> --mode drift`;CI 红 → `/ccm-harness:check-ci-failure`;迷路 → `/ccm-harness:doctor`。
|
||||
|
||||
完整流程文档:`docs/user-guide/workflow.md`
|
||||
305
docs/specs/sheet-history-revert/spec.md
Normal file
305
docs/specs/sheet-history-revert/spec.md
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
req_id: sheet-history-revert
|
||||
mode: from-prd
|
||||
tracks: [be]
|
||||
created_at: 2026-06-23T13:09:08Z
|
||||
---
|
||||
|
||||
> ⚠️ **请勿直接编辑此文档**
|
||||
> 修改请通过 `/ccm-harness:draft-spec sheet-history-revert "<变更描述>"`(路由器会进入 update capability)
|
||||
> 手改不会被 check-spec-drift 放过,且会破坏 Frozen 快照关联性
|
||||
> 本文档由 /ccm-harness:draft-spec 生成于 2026-06-23T13:09:08Z;req-id:sheet-history-revert
|
||||
|
||||
---
|
||||
|
||||
# Sheet 历史版本查询与回滚
|
||||
|
||||
<!-- BEGIN generated overview — scripts/render_spec_overview.py 自动生成,勿手改;改 spec 请走 /ccm-harness:draft-spec <req-id> "<变更>"(update) -->
|
||||
## 方案概览 / TL;DR
|
||||
|
||||
> 本段由机器从下方子任务字段**确定性生成**,供快速把握方案;**权威细节仍以各子任务 `yaml` 块为准**。
|
||||
|
||||
- **需求** `sheet-history-revert` | 模式 `from-prd` | 端 BE
|
||||
- **规模** 5 BE | **涉及 PSM** `tooling.lark_cli`, `tooling.sheet_skill_spec`, `sheet.facade.agg`, `bear.server.sheet_data` | **Thrift 影响** 无 ×5
|
||||
|
||||
| 子任务 | 一句话 | 关键信息 |
|
||||
|---|---|---|
|
||||
| BE-1 | lark-cli `+history-list` 历史记录列表 shortcut | `tooling.lark_cli` · thrift:无 · `history_list[read](callTool tools/invoke_read;内部对接 /space/api/v3/sheet/histories)` |
|
||||
| BE-2 | lark-cli `+history-revert` 与 `+history-revert-status` 回滚流 shortcut | `tooling.lark_cli` · thrift:无 · `history_revert[write] / history_revert_status[read](callTool tools/invoke_write\|read;内部对接 /space/api/v2/sheet/recover、/api/v2/sheet/recover/status)` |
|
||||
| BE-3 | sheet-skill-spec 上游事实源(skill 正文 + shortcut/flag 定义) | `tooling.sheet_skill_spec` · thrift:无 · `A` |
|
||||
| BE-4 | sheet-facade-agg 在现有 ToolsCall 上新增 3 个历史/回滚工具 | `sheet.facade.agg` · thrift:无 · `history_list[read] / history_revert[write] / history_revert_status[read]` |
|
||||
| BE-5 | sheet/data 透传 scene 并在 RecoverMsg 上新增 Scene 字段 | `bear.server.sheet_data` · thrift:无 · `RecoverHistory / QueryRecoverStatus(复用);MQ RecoverMsg 加 Scene 字段` |
|
||||
|
||||
> 完整字段见各子任务 `yaml` 块。
|
||||
<!-- END generated overview -->
|
||||
|
||||
## 概述与范围
|
||||
|
||||
为 `lark-cli` 的 lark-sheets 能力补齐**电子表格历史版本查询与回滚**,新增 3 个 shortcut,让 AI / 用户可以列出某张表的历史版本、回滚到指定版本、并查询回滚的异步状态。三个 shortcut 封装的是飞书电子表格已上线的 space 接口(见下表的「前端接口参考」),本需求不新建产品能力,只是把它们包装成稳定、AI 友好的命令面。
|
||||
|
||||
| 功能 | shortcut | 前端接口参考(搜索 lark/idl) | 实现差异点 |
|
||||
|---|---|---|---|
|
||||
| 查历史记录列表 | `+history-list` | `/space/api/v3/sheet/histories` | ① 仅返回 `minor_histories` 列表;② `minor_histories` 的 `id` 字段重命名为 `history_version_id`;③ 每条仅保留 `history_version_id` / `create_time`(序列化成 AI 优化的可读格式)/ `action` / `all_block_revision` 四个字段 |
|
||||
| 历史记录回滚 | `+history-revert` | `/space/api/v2/sheet/recover` | 传入 `+history-list` 拿到的 `history_version_id`,回滚到指定版本 |
|
||||
| 查询回滚状态 | `+history-revert-status` | `/api/v2/sheet/recover/status` | 查询 `+history-revert` 发起的异步回滚的当前状态 |
|
||||
|
||||
**范围内**:
|
||||
- `larksuite/cli` 仓新增 3 个 sheets shortcut(含 `+history-list` 的响应裁剪 / 字段重命名 / 时间格式转换逻辑)。
|
||||
- `ee/sheet-skill-spec` 上游事实源补 skill 正文与 shortcut/flag 定义,经其工作流 `sync:cli` 同步到 `larksuite/cli` 的 `skills/lark-sheets/` 与 `shortcuts/sheets/data/`。
|
||||
- `ee/sheet-facade-agg` **复用现有 `ToolsCall` 接口**,在其上新增 3 个工具(`history_list` / `history_revert` / `history_revert_status`);并在**回滚消息消费侧**(消费 `sheet/data` 产出的 RecoverMsg)按 scene 给 `memberId` 赋值(doubao=10,lark-cli=11)后构造 recover cs。
|
||||
- `sheet/data`(`bear.server.sheet_data`)把 scene 从入口透传到 `RecoverHistory`,并在产出的 `RecoverMsg`(MQ 消息)上**新增 `Scene` 字段**,供 agg 消费时区分场景。
|
||||
|
||||
**范围外**:
|
||||
- 历史版本的底层存储 / 快照 / 过期清理逻辑、`RecoverHistory` 的回滚业务语义(`bear.server.sheet_data` 已有,本需求仅透传 scene + 加 MQ 字段,不改回滚逻辑本身)。
|
||||
- 历史版本 diff、可视化、权限模型变更等产品侧扩展。
|
||||
- `doubao-office` 消费方的同步(`sheet-skill-spec` 另有 `sync:doubao`,本需求不涉及)。
|
||||
|
||||
## 服务拓扑与 PSM 变更判定表
|
||||
|
||||
**调用链路(目标形态)**:
|
||||
|
||||
```
|
||||
用户 / AI ──> lark-cli (+history-list / +history-revert / +history-revert-status)
|
||||
│ callTool → POST /open-apis/sheet_ai/v2/.../tools/invoke_read|write(scene 随入口确定)
|
||||
▼
|
||||
sheet.facade.agg ToolsCall(复用现有接口,新增 3 个工具:
|
||||
history_list[read] / history_revert[write] / history_revert_status[read])
|
||||
├─ history_list ──> 拉历史列表(裁剪 minor_histories / 4 字段 / AI 时间格式)
|
||||
├─ history_revert ───scene(ctx 透传)──> bear.server.sheet_data: RecoverHistory
|
||||
│ │ service.RecoverHistory → SendRecoverMsg
|
||||
│ ▼
|
||||
│ MQ: RecoverMsg(新增 Scene 字段)
|
||||
│ ▼
|
||||
│ sheet.facade.agg RecoverMsg 消费者 ── 读 Scene → memberId(doubao=10/lark-cli=11)
|
||||
│ → 构造 recover cs → 调既有 recover 下游
|
||||
└─ history_revert_status ──> bear.server.sheet_data: QueryRecoverStatus(transactionID)
|
||||
```
|
||||
|
||||
`lark-cli` 现有 sheets shortcut 统一通过 `callTool`(`shortcuts/sheets/sheet_ai_api.go`)走 One-OpenAPI 的 `tools/invoke_read|write` 入口(`ToolKindRead` / `ToolKindWrite`),由 `sheet.facade.agg` 的 `OpenAPIToolCallRead/Write`(`biz/handler/lark_cli.go`,底层复用 `aiService.ToolsCall`)按 `tool_name` 分发。本需求的 3 个 shortcut 即按此路径新增 3 个工具,而非直连 space 接口或新增独立 OpenAPI 路由。
|
||||
|
||||
**回滚为异步两段式**:`history_revert` 工具调 `sheet/data` 的 `RecoverHistory`(`biz/history/service/recover.go`),后者通过 `SendRecoverMsg`(`infra/mq/producer/recover.go` 的 `RecoverMsg`)投递回滚消息并返回 `transactionID`;agg 侧的 RecoverMsg 消费者真正构造 recover cs,并在此时按 scene 给 `memberId` 赋值。`history_revert_status` 工具走 `sheet/data` 的 `QueryRecoverStatus(transactionID)` 查询异步结果。scene 从 ToolsCall 入口经 **ctx 透传**(沿用既有 `utils.WithSceneDoubao(ctx)` 范式)到 `RecoverHistory`,再写入 `RecoverMsg.Scene` 字段,使 agg 消费时可区分 doubao / lark-cli。
|
||||
|
||||
**PSM 变更判定表**:
|
||||
|
||||
| PSM | 需要代码变更? | 变更内容 | 不变更原因 |
|
||||
|---|---|---|---|
|
||||
| `tooling.lark_cli`(`larksuite/cli`,无服务 PSM) | 是 | 新增 3 个 shortcut + `+history-list` 响应 transform | — |
|
||||
| `tooling.sheet_skill_spec`(`ee/sheet-skill-spec`,无服务 PSM) | 是 | 新增 lark-sheets skill 正文 + 3 个 shortcut/flag 定义,生成后同步到 cli | — |
|
||||
| `sheet.facade.agg`(`ee/sheet-facade-agg`) | 是 | ① 现有 `ToolsCall` 新增 3 个工具(`history_list[read]` / `history_revert[write]` / `history_revert_status[read]`);② `history_revert` 工具调 `sheet/data` 时透传 scene(ctx);③ **RecoverMsg 消费者**读 `Scene` 字段,按 scene 给 `memberId` 赋值后构造 recover cs。**无新 thrift**(工具按 `tool_name`+JSON 注册,scene 走 ctx baggage) | — |
|
||||
| `bear.server.sheet_data`(`sheet/data`) | 是 | scene 从入口透传到 `RecoverHistory`(`biz/history/service/recover.go`),并在 `RecoverMsg`(`infra/mq/producer/recover.go`)上**新增 `Scene` 字段**随消息投递。`RecoverMsg` 是 JSON Go struct,加字段**非 thrift**;scene 透传走 ctx baggage | 回滚 / 快照业务语义不变(`RestoreHistorySnapshot` / `QueryRecoverStatus` 等复用) |
|
||||
|
||||
> **`memberId` 按 scene 赋值(实现硬约束,跨 sheet/data + agg)**:scene 区分 doubao 与 lark-cli(沿用既有 `utils.WithSceneDoubao(ctx)` 范式)。本需求要求 scene 从 ToolsCall 入口一路透传:
|
||||
> 1. agg `history_revert` 工具调用 `sheet/data.RecoverHistory` 时,把 scene 经 ctx 透传;
|
||||
> 2. `sheet/data` 在产出的 `RecoverMsg` 上写入 `Scene` 字段,随 MQ 投递;
|
||||
> 3. **agg 的 RecoverMsg 消费者**在真正构造 recover cs 时,读 `Scene` 给 `memberId` 赋值——doubao 场景 = `10`,其他(lark-cli)场景 = `11`。
|
||||
>
|
||||
> memberId 赋值发生在 **agg 消费侧**(不是同步 ToolsCall 调用栈,因为回滚是异步消息驱动)。错误的 memberId 会导致回滚归属错误的调用方身份(审计 / 权限相关)。`RecoverMsg.MemberId` 字段已存在,但本需求要求按 scene 正确赋值并据此区分两个消费方。
|
||||
|
||||
## 后端 / Tooling 子任务
|
||||
|
||||
> 说明:`lark-cli` 与 `sheet-skill-spec` 均属 Tooling,按 ccm-harness 约定建 BE-*,`thrift_impact: 无`。本需求无前端(FE)子任务。
|
||||
|
||||
### BE-1: lark-cli `+history-list` 历史记录列表 shortcut
|
||||
|
||||
```yaml
|
||||
psm: tooling.lark_cli
|
||||
repo: larksuite/cli
|
||||
module: shortcuts/sheets
|
||||
be_deploy_required: false
|
||||
thrift_impact: 无
|
||||
api: facade-agg ToolsCall::history_list[read](callTool tools/invoke_read;内部对接 /space/api/v3/sheet/histories)
|
||||
depends_on: [BE-4]
|
||||
estimate: 1.5d
|
||||
```
|
||||
|
||||
**调用的下游服务**:经 `callTool(ToolKindRead, "history_list", ...)`(`shortcuts/sheets/sheet_ai_api.go`)走 `tools/invoke_read` 入口,由 facade-agg 的 `history_list` 工具内部对接 `/space/api/v3/sheet/histories`。入参:表格 token(沿用现有 sheets shortcut 的 `--spreadsheet-token` / `--token` 解析)。响应裁剪 / 字段重命名 / 时间格式可在 facade-agg 工具侧或 lark-cli 侧完成(见实现要点;以 AI 友好输出为准)。
|
||||
**实现要点(实现差异点落地)**:
|
||||
- 仅取响应中的 `minor_histories` 列表,丢弃其余顶层字段(如 major histories)。
|
||||
- 将每条 `minor_histories` 的 `id` 字段重命名输出为 `history_version_id`。
|
||||
- 每条仅保留 4 个字段:`history_version_id`、`create_time`、`action`、`all_block_revision`。
|
||||
- `create_time` 序列化成 AI 优化的可读格式(如本地时区可读时间串),而非裸 unix 时间戳。
|
||||
**验收场景**:
|
||||
- Given 一张有多个历史版本的电子表格,When 执行 `lark-cli sheets +history-list --token <t>`,Then 返回 JSON 数组,每条恰好含 `history_version_id` / `create_time` / `action` / `all_block_revision` 四个键,且 `create_time` 为可读格式。
|
||||
- Given 一张无历史记录的表格,When 执行 `+history-list`,Then 返回空列表且退码 0(不报错)。
|
||||
|
||||
### BE-2: lark-cli `+history-revert` 与 `+history-revert-status` 回滚流 shortcut
|
||||
|
||||
```yaml
|
||||
psm: tooling.lark_cli
|
||||
repo: larksuite/cli
|
||||
module: shortcuts/sheets
|
||||
be_deploy_required: false
|
||||
thrift_impact: 无
|
||||
api: facade-agg ToolsCall::history_revert[write] / history_revert_status[read](callTool tools/invoke_write|read;内部对接 /space/api/v2/sheet/recover、/api/v2/sheet/recover/status)
|
||||
depends_on: [BE-1, BE-4, BE-5]
|
||||
estimate: 1.5d
|
||||
```
|
||||
|
||||
**调用的下游服务**:
|
||||
- `+history-revert` → `callTool(ToolKindWrite, "history_revert", ...)`,agg 工具调 `sheet/data.RecoverHistory`(异步),返回 `transactionID`。
|
||||
- `+history-revert-status` → `callTool(ToolKindRead, "history_revert_status", ...)`,agg 工具调 `sheet/data.QueryRecoverStatus(transactionID)` 查异步结果。
|
||||
- 注:`memberId` 按 scene 赋值(doubao=10 / lark-cli=11)发生在 agg 的 **RecoverMsg 消费者**侧(见 BE-4 / BE-5),lark-cli 侧不感知;scene 由 callTool 入口(read/write)确定。
|
||||
**实现要点**:
|
||||
- `+history-revert` 的 `--history-version-id`(命名对齐 BE-1 的输出字段)为必填;缺失时在 Validate 阶段给出可执行错误提示。
|
||||
- 回滚为异步操作,`+history-revert` 返回受理结果,`+history-revert-status` 供轮询最终状态(成功 / 进行中 / 失败)。
|
||||
**验收场景**:
|
||||
- Given 由 `+history-list` 取得的合法 `history_version_id`,When 执行 `+history-revert --token <t> --history-version-id <id>`,Then 后端受理回滚并返回可被 `+history-revert-status` 查询的标识。
|
||||
- Given 一次已发起的回滚,When 轮询 `+history-revert-status`,Then 能区分「进行中 / 成功 / 失败」三种状态。
|
||||
- Given 缺省 `--history-version-id`,When 执行 `+history-revert`,Then 返回明确的参数缺失错误,不发起请求。
|
||||
|
||||
### BE-3: sheet-skill-spec 上游事实源(skill 正文 + shortcut/flag 定义)
|
||||
|
||||
```yaml
|
||||
psm: tooling.sheet_skill_spec
|
||||
repo: ee/sheet-skill-spec
|
||||
module: canonical-spec
|
||||
be_deploy_required: false
|
||||
thrift_impact: 无
|
||||
api: N/A
|
||||
depends_on: [BE-1, BE-2]
|
||||
estimate: 1d
|
||||
```
|
||||
|
||||
**调用的下游服务**:无(构建期工作流)。
|
||||
**实现要点(按 `sheet-skill-spec` README 工作流)**:
|
||||
- 在飞书 base 表登记 3 个新 shortcut 的 tool ↔ shortcut 映射与 flag 定义,`npm run sync:tool-shortcut-map` 镜像入仓。
|
||||
- 在 `canonical-spec/references/<相关 skill>/cli-reference.md` 补三个 shortcut 的描述 / 示例 / Validate-DryRun-Execute 约束。
|
||||
- 跑 `npm run generate:all && npm run check:all` 验证,产出 `generated/lark-cli/skills/lark-sheets/` 与 `generated/lark-cli/data/{flag-defs.json,flag-schemas.json}`。
|
||||
- 跑 `npm run sync:cli` 把 generated 同步到 `larksuite/cli` 的 `skills/lark-sheets/`(mirror)与 `shortcuts/sheets/data/`(mirror),在 cli 仓作为 PR 提交。
|
||||
**边界**:skill 命名 / 切分 / 正文 / flag 定义一律先落 `sheet-skill-spec`,禁止直接改 cli 仓的 `generated`/`skills/lark-sheets/` 产物(README「对齐原则」)。
|
||||
**验收场景**:
|
||||
- Given 在 `sheet-skill-spec` 完成上述编辑,When 跑 `npm run check:all`,Then 全部门禁通过(generated 与 canonical 一致、map 与 base 表一致)。
|
||||
- Given 跑 `npm run sync:cli`,Then cli 仓 `skills/lark-sheets/` 与 `shortcuts/sheets/data/` 出现对应 3 个 shortcut 的 skill 正文与 flag 定义。
|
||||
|
||||
### BE-4: sheet-facade-agg 在现有 ToolsCall 上新增 3 个历史/回滚工具
|
||||
|
||||
```yaml
|
||||
psm: sheet.facade.agg
|
||||
repo: ee/sheet-facade-agg
|
||||
module: biz/handler
|
||||
be_deploy_required: true
|
||||
thrift_impact: 无
|
||||
api: ToolsCall::history_list[read] / history_revert[write] / history_revert_status[read]
|
||||
depends_on: []
|
||||
estimate: 1.5d
|
||||
```
|
||||
|
||||
**调用的下游服务**:`sheet/data` 的 `RecoverHistory` / `QueryRecoverStatus`(见 BE-5)+ 既有历史查询;agg 的 RecoverMsg 消费者复用既有 recover 下游(`biz/service/spreadsheet.go::ProcessRecoverCs`、`model.RecoverParam`),不新增 thrift。
|
||||
**实现要点**:
|
||||
- **ToolsCall 扩展**:在现有 `ToolsCall` 框架(`biz/handler/handler.go::ToolsCall` / `biz/handler/lark_cli.go::OpenAPIToolCallRead|Write`)注册 3 个新工具:`history_list`(read)、`history_revert`(write)、`history_revert_status`(read),按 `constants.IsReadTool` / `IsWriteTool` 归类,从 `tools/invoke_read` / `invoke_write` 入口可达。
|
||||
- **scene 透传**:`history_revert` / `history_revert_status` 工具调 `sheet/data` 时,把 scene 经 ctx(沿用 `utils.WithSceneDoubao` 范式)透传下去,使 `sheet/data` 能写入 `RecoverMsg.Scene`。
|
||||
- **RecoverMsg 消费者按 scene 赋 memberId(硬约束)**:agg 消费 `sheet/data` 投递的 `RecoverMsg`、构造真正 recover cs 时,读 `RecoverMsg.Scene` 给 `memberId` 赋值——doubao = `10`,lark-cli = `11`。这是异步消费侧逻辑,不在同步 ToolsCall 调用栈。
|
||||
- `history_list` 工具对接历史列表查询;响应裁剪(仅 `minor_histories`、`id`→`history_version_id`、4 字段、`create_time` AI 友好格式)建议落在此工具侧(两个消费方共享,避免 lark-cli / doubao 双实现漂移)。
|
||||
**边界**:只在 ToolsCall 上加工具 + 改 RecoverMsg 消费者;不新增独立 OpenAPI 路由、不改 `ai.ToolsCallRequest` thrift 契约、不改 `RecoverHistory` 回滚业务语义。
|
||||
**验收场景**:
|
||||
- Given lark-cli 经 `tools/invoke_read` 调用 `history_list`,Then 返回裁剪后的 `minor_histories`(4 字段,`history_version_id` 命名)。
|
||||
- Given lark-cli(scene=lark-cli)经 `history_revert` 发起回滚,When agg 消费对应 RecoverMsg,Then 构造的 recover cs 中 `memberId == 11`;doubao 场景下同一路径 `memberId == 10`。
|
||||
- Given 已发起回滚,When 调用 `history_revert_status`,Then 经 `QueryRecoverStatus` 返回可区分的回滚状态。
|
||||
|
||||
### BE-5: sheet/data 透传 scene 并在 RecoverMsg 上新增 Scene 字段
|
||||
|
||||
```yaml
|
||||
psm: bear.server.sheet_data
|
||||
repo: sheet/data
|
||||
module: biz/history
|
||||
be_deploy_required: true
|
||||
thrift_impact: 无
|
||||
api: bear.server.sheet_data::RecoverHistory / QueryRecoverStatus(复用);MQ RecoverMsg 加 Scene 字段
|
||||
depends_on: []
|
||||
estimate: 1d
|
||||
```
|
||||
|
||||
**调用的下游服务**:复用既有回滚链路(`biz/history/service/recover.go::RecoverHistory` → `infra/mq/producer/recover.go::SendRecoverMsg`)。
|
||||
**实现要点**:
|
||||
- **scene 透传**:把 agg 经 ctx 传入的 scene 接住,贯穿 `RecoverHistory`(`biz/history/service/recover.go`)到 `RecoverMsg` 构造处。
|
||||
- **RecoverMsg 加 `Scene` 字段**:在 `infra/mq/producer/recover.go` 的 `RecoverMsg` struct 上新增 `Scene` 字段并在投递时赋值。`RecoverMsg` 是 JSON Go struct(`recoverProducer.NewMessage`),**加字段非 thrift**——`thrift_impact: 无`。
|
||||
- `QueryRecoverStatus` 与回滚业务语义保持不变,仅承载 scene 透传。
|
||||
**边界**:不改回滚 / 历史快照业务逻辑;只加 scene 透传与 `RecoverMsg.Scene` 字段。
|
||||
**scene 透传方式(已定)**:经 **ctx baggage**(`metainfo` / 沿用既有 `utils.WithSceneDoubao(ctx)` 范式)从 agg 透传到 `RecoverHistory`,**不**在 `RecoverHistoryReq` thrift 上加字段 → 零 IDL 变更,`thrift_impact: 无`。
|
||||
**验收场景**:
|
||||
- Given agg 以 scene=lark-cli 调 `RecoverHistory`,Then 投递的 `RecoverMsg.Scene` 标识 lark-cli;doubao 同理。
|
||||
- Given 回滚已发起,When `QueryRecoverStatus(transactionID)`,Then 返回回滚状态(语义与现状一致)。
|
||||
- Given lark-cli(scene=lark-cli)经 `tools/invoke_write` 调用 `history_revert`,Then 构造的 recover cs 中 `memberId == 11`;doubao 场景下同一工具 `memberId == 10`。
|
||||
- Given 已发起回滚,When 调用 `history_revert_status`,Then 返回可区分的回滚状态。
|
||||
|
||||
## API 契约引用
|
||||
|
||||
本需求三个接口均为飞书电子表格已上线 space 接口,契约以各仓库最新 master 为准;对应 thrift 定义按 PRD 提示在 `lark/idl` 中搜索确认(实现阶段补全精确路径):
|
||||
|
||||
- 查列表:`/space/api/v3/sheet/histories`(取 `minor_histories`)
|
||||
- 回滚:`/space/api/v2/sheet/recover`
|
||||
- 回滚状态:`/api/v2/sheet/recover/status`
|
||||
|
||||
> 契约本体不进本 spec 正文;精确 `lark/idl/...thrift::Service::Method` 路径在实现阶段确认并回填到对应 BE-* 的 `api` 字段说明。
|
||||
|
||||
## 验收场景(汇总)
|
||||
|
||||
- 列表:`+history-list` 仅返回 `minor_histories`,每条恰好 4 个字段,`id` 重命名为 `history_version_id`,`create_time` 为 AI 优化可读格式。
|
||||
- 回滚:`+history-revert` → agg `history_revert` → `sheet/data.RecoverHistory`(异步),受理后返回可查询标识。
|
||||
- memberId/scene:agg 消费 `RecoverMsg` 构造 recover cs 时,按 `RecoverMsg.Scene` 赋 `memberId`——lark-cli=11、doubao=10(facade-agg 侧单测断言)。
|
||||
- 状态:`+history-revert-status` → `QueryRecoverStatus` 能查询并区分回滚的进行中 / 成功 / 失败。
|
||||
- skill 同步:`sheet-skill-spec` 生成产物经 `sync:cli` 落地到 cli 仓,`check:all` 全绿。
|
||||
- 三个 shortcut 在 cli 中遵循统一的 Validate / DryRun / Execute 三段约定与现有 sheets shortcut 一致。
|
||||
|
||||
## 非功能要求与约束
|
||||
|
||||
- **复用既有模式**:3 个 shortcut 必须沿用 `shortcuts/sheets` 现有的 token 解析(`--spreadsheet-token` / `--token` 别名)、错误封装(`errs`)、`callTool`(`tools/invoke_read|write`)调用与 DryRun 渲染范式,不另起调用框架。facade-agg 侧必须复用现有 `ToolsCall` 接口扩展工具,不新增独立 OpenAPI 路由。
|
||||
- **AI 友好输出**:`+history-list` 的字段裁剪与 `create_time` 可读格式是硬约束(PRD「实现差异点」),目的是降低 AI 消费成本。
|
||||
- **工作流约束**:skill 内容与 flag 定义的唯一事实源是 `ee/sheet-skill-spec`;cli 仓的 `skills/lark-sheets/` 与 `shortcuts/sheets/data/` 为同步产物,不手改。
|
||||
- **回滚为异步**:`+history-revert` 与 `+history-revert-status` 分离,调用方需理解「发起 → 轮询」两步语义。
|
||||
- **事实基准**:所有外部仓库事实(space 接口、facade-agg 路由、sheet_data 能力)以各仓库最新 master 为准。
|
||||
|
||||
## 安全设计
|
||||
|
||||
- security_knowledge_ref: UNCONFIGURED
|
||||
- 风险判断依据: 未配置安全知识库,待安全侧补齐。需关注点(供安全侧复核):`+history-revert` 是**写 / 不可逆**操作(覆盖当前表格内容到历史版本),必须校验操作者对目标表格具备编辑 / 回滚权限;历史版本列表可能暴露协作者操作痕迹(`action` 字段),需确认读权限边界。
|
||||
- 身份归属风险(memberId/scene): `memberId` 须按 scene 正确赋值(lark-cli=11 / doubao=10)。错配会使回滚操作归属错误的调用方身份,影响审计与权限判定——属安全/审计相关,须在 agg RecoverMsg 消费侧保证赋值正确。
|
||||
- 需要安全侧补充: 回滚操作的权限校验口径、历史 `action` 字段的可见性范围是否需脱敏、memberId 与真实操作者身份的映射是否需对齐审计要求。
|
||||
|
||||
## Codegen Delivery Plan
|
||||
|
||||
applicable: true
|
||||
|
||||
### A. Branch Plan
|
||||
|
||||
| `key` | value |
|
||||
|---|---|
|
||||
| `psm` | `sheet.facade.agg` |
|
||||
| `business_branch` | `feat/sheet-history-revert` |
|
||||
| `generated_branch` | `N/A` |
|
||||
| `idl_branch` | `N/A` |
|
||||
| `kitex_branch` | `N/A` |
|
||||
|
||||
### B. Delivery Targets
|
||||
|
||||
| repo | required | branch | artifact_paths | reason |
|
||||
|---|---|---|---|---|
|
||||
| larksuite/cli | yes | feat/sheet-history-revert | shortcuts/sheets/ , skills/lark-sheets/ | 3 个 shortcut 实现 + 同步落地的 skill 正文与 flag 数据 |
|
||||
| ee/sheet-skill-spec | yes | feat/sheet-history-revert | canonical-spec/references/ , generated/lark-cli/ | skill / flag 上游事实源,生成后 sync 到 cli |
|
||||
| ee/sheet-facade-agg | yes | feat/sheet-history-revert | biz/handler/ | 现有 ToolsCall 新增 3 个工具 + scene 透传 + RecoverMsg 消费者按 scene 赋 memberId |
|
||||
| sheet/data | yes | feat/sheet-history-revert | biz/history/ , infra/mq/producer/ | RecoverHistory 透传 scene + RecoverMsg 新增 Scene 字段(JSON,非 thrift) |
|
||||
|
||||
### C. Generation Decision
|
||||
|
||||
| `key` | value |
|
||||
|---|---|
|
||||
| `needs_kitex_gen` | no |
|
||||
| `needs_apacana` | no |
|
||||
| `needs_kite_via_sdp` | no |
|
||||
| `decision_basis` | facade-agg 复用现有 ToolsCall 框架按 tool_name 注册 3 个工具(JSON input,不动 ai.ToolsCallRequest thrift);sheet/data 仅在 RecoverMsg(JSON Go struct)加 Scene 字段 + 经 ctx baggage 透传 scene,复用既有 RecoverHistory/QueryRecoverStatus,均非 thrift;lark-cli 经现有 callTool 包装。scene 已定走 ctx baggage(不加 RecoverHistoryReq thrift 字段),无新增/修改 kitex/apacana/SDP 契约 |
|
||||
|
||||
### D. Branch Naming Rule
|
||||
|
||||
业务分支统一用 `feat/sheet-history-revert`;本需求无 codegen,故 `generated_branch` / `idl_branch` / `kitex_branch` 均为 `N/A`。
|
||||
|
||||
## thrift 变更需求清单
|
||||
|
||||
无(按推荐实现路径)。三个接口的能力已存在;本需求的新增内容是:facade-agg 在 ToolsCall 上注册 3 个工具(`tool_name`+JSON,不动 `ai.ToolsCallRequest`)、sheet/data 在 `RecoverMsg`(JSON Go struct)上加 `Scene` 字段、scene 经 **ctx baggage** 透传——均不涉及 thrift。
|
||||
|
||||
**scene 透传方式:已定为 ctx baggage**(`metainfo` / 沿用既有 `utils.WithSceneDoubao(ctx)` 范式),明确**不**在 `bear.server.sheet_data` 的 `RecoverHistoryReq` thrift 上加字段。故本需求确无任何 thrift struct / RPC method / enum 的新增或修改,Generation Decision 三路保持 `no`、无 codegen。
|
||||
|
||||
## N. AI Capability Manifest
|
||||
|
||||
applicable: false
|
||||
|
||||
本需求为确定性 CLI 命令封装,不含 LLM / prompt 驱动的 AI 能力。`+history-list` 中「`create_time` 序列化成 AI 优化格式」仅指对机器/AI 更易读的时间字符串格式化,属确定性数据转换,非 AI capability。
|
||||
12
go.mod
12
go.mod
@@ -27,6 +27,8 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require github.com/apache/arrow/go/v17 v17.0.0
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
@@ -42,13 +44,17 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
@@ -57,10 +63,16 @@ require (
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/smarty/assertions v1.15.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
)
|
||||
|
||||
32
go.sum
32
go.sum
@@ -2,6 +2,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/apache/arrow/go/v17 v17.0.0 h1:RRR2bdqKcdbss9Gxy2NS/hK8i4LDMh23L6BbkN5+F54=
|
||||
github.com/apache/arrow/go/v17 v17.0.0/go.mod h1:jR7QHkODl15PfYyjM2nU+yTLScZ/qfj7OSUZmJ8putc=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
@@ -52,12 +54,16 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
@@ -74,11 +80,16 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.5.4 h1:U2S9x9LrfH++ZqJ+YAiUlqzCWJmVXhFdS8Z7rIBH8H0=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.5.4/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
@@ -97,6 +108,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -133,14 +146,20 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
|
||||
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -156,6 +175,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
@@ -169,10 +189,16 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
|
||||
gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
5
harness-opt/.gitignore
vendored
5
harness-opt/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
# harness-opt 只入库轻量决策记录;重的原始评测 run 不进版本库(dashboard 仍读磁盘)。
|
||||
baseline/runs/
|
||||
**/child-runs/
|
||||
verify_results/sealed-runs/
|
||||
verify_results/*-runs/
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"1": 30086,
|
||||
"2": 34616,
|
||||
"3": 31289
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"k": 5,
|
||||
"metrics": {
|
||||
"success_rate": {
|
||||
"mean": 0.4666666666666666,
|
||||
"std": 0.1632993161855452,
|
||||
"k": 5,
|
||||
"band": [
|
||||
0.14006803429557624,
|
||||
0.793265299037757
|
||||
]
|
||||
},
|
||||
"mean_score": {
|
||||
"mean": 0.5111111111111111,
|
||||
"std": 0.1507184440694504,
|
||||
"k": 5,
|
||||
"band": [
|
||||
0.20967422297221028,
|
||||
0.8125479992500119
|
||||
]
|
||||
},
|
||||
"mean_context_window": {
|
||||
"mean": 31997.0,
|
||||
"std": 7166.8411203573105,
|
||||
"k": 5,
|
||||
"band": [
|
||||
17663.31775928538,
|
||||
46330.682240714625
|
||||
]
|
||||
},
|
||||
"mean_duration_ms": {
|
||||
"mean": 50188.86666666667,
|
||||
"std": 7746.3168641619595,
|
||||
"k": 5,
|
||||
"band": [
|
||||
34696.23293834275,
|
||||
65681.50039499058
|
||||
]
|
||||
},
|
||||
"mean_token": {
|
||||
"mean": 263981.06666666665,
|
||||
"std": 27890.193480385413,
|
||||
"k": 5,
|
||||
"band": [
|
||||
208200.67970589583,
|
||||
319761.45362743747
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"k": 5,
|
||||
"n_cases": 3,
|
||||
"effect": {
|
||||
"mean": 0.5111111111111111,
|
||||
"sigma": 0.1507184440694504
|
||||
},
|
||||
"token": {
|
||||
"mean": 31997.0,
|
||||
"sigma": 7166.8411203573105
|
||||
},
|
||||
"duration": {
|
||||
"mean": 50188.86666666667,
|
||||
"sigma": 7746.3168641619595
|
||||
},
|
||||
"phi0_per_case": {
|
||||
"1": {
|
||||
"effect": 0.6,
|
||||
"token": 30086,
|
||||
"duration": 51004
|
||||
},
|
||||
"2": {
|
||||
"effect": 0.4,
|
||||
"token": 34616,
|
||||
"duration": 52787
|
||||
},
|
||||
"3": {
|
||||
"effect": 0.5333,
|
||||
"token": 31289,
|
||||
"duration": 46776
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,869 +0,0 @@
|
||||
{
|
||||
"summary": {
|
||||
"total_cases": 3,
|
||||
"files": 25,
|
||||
"expected_declared": 0,
|
||||
"blind_spots": 22,
|
||||
"overfit_high": 5,
|
||||
"suggest_add_cases": [
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md"
|
||||
],
|
||||
"suggest_fix_routing": []
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 0,
|
||||
"R2": 0,
|
||||
"R3": 50
|
||||
},
|
||||
"total_lines": 55,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-search.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 85,
|
||||
"R2": 112,
|
||||
"R3": 31
|
||||
},
|
||||
"total_lines": 234,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 25,
|
||||
"R2": 21,
|
||||
"R3": 15
|
||||
},
|
||||
"total_lines": 67,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-flag-create.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 7,
|
||||
"R1": 25,
|
||||
"R2": 20,
|
||||
"R3": 15
|
||||
},
|
||||
"total_lines": 67,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 1,
|
||||
"R1": 0,
|
||||
"R2": 43,
|
||||
"R3": 10
|
||||
},
|
||||
"total_lines": 54,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 90,
|
||||
"R2": 40,
|
||||
"R3": 22
|
||||
},
|
||||
"total_lines": 157,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 1,
|
||||
"R1": 139,
|
||||
"R2": 109,
|
||||
"R3": 14
|
||||
},
|
||||
"total_lines": 263,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 50,
|
||||
"R1": 368,
|
||||
"R2": 22,
|
||||
"R3": 12
|
||||
},
|
||||
"total_lines": 452,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-search.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 102,
|
||||
"R2": 24,
|
||||
"R3": 11
|
||||
},
|
||||
"total_lines": 142,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-update.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 67,
|
||||
"R2": 2,
|
||||
"R3": 10
|
||||
},
|
||||
"total_lines": 84,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 55,
|
||||
"R2": 24,
|
||||
"R3": 10
|
||||
},
|
||||
"total_lines": 94,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-threads-messages-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 72,
|
||||
"R2": 28,
|
||||
"R3": 9
|
||||
},
|
||||
"total_lines": 115,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 1,
|
||||
"R1": 103,
|
||||
"R2": 56,
|
||||
"R3": 6
|
||||
},
|
||||
"total_lines": 166,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-flag-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 80,
|
||||
"R2": 9,
|
||||
"R3": 6
|
||||
},
|
||||
"total_lines": 100,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-reactions.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 73,
|
||||
"R1": 206,
|
||||
"R2": 18,
|
||||
"R3": 2
|
||||
},
|
||||
"total_lines": 299,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 7,
|
||||
"R1": 44,
|
||||
"R2": 17,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 68,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 44,
|
||||
"R2": 15,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 65,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 21,
|
||||
"R2": 17,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 44,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 7,
|
||||
"R1": 70,
|
||||
"R2": 20,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 97,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 73,
|
||||
"R2": 24,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 103,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 10,
|
||||
"R1": 24,
|
||||
"R2": 14,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 48,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/SKILL.md",
|
||||
"is_domain_skill": true,
|
||||
"actual": {
|
||||
"count": 3,
|
||||
"pct": 1.0,
|
||||
"tier": "密"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 3,
|
||||
"pct": 1.0,
|
||||
"tier": "密"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 3,
|
||||
"density_pct": 1.0,
|
||||
"density_tier": "密",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 122,
|
||||
"R1": 0,
|
||||
"R2": 68,
|
||||
"R3": 41
|
||||
},
|
||||
"total_lines": 231,
|
||||
"overfit_risk": "低",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-create.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 2,
|
||||
"pct": 0.667,
|
||||
"tier": "密"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 2,
|
||||
"pct": 0.667,
|
||||
"tier": "密"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 2,
|
||||
"density_pct": 0.667,
|
||||
"density_tier": "密",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 116,
|
||||
"R2": 12,
|
||||
"R3": 29
|
||||
},
|
||||
"total_lines": 162,
|
||||
"overfit_risk": "低",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 2,
|
||||
"pct": 0.667,
|
||||
"tier": "密"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 2,
|
||||
"pct": 0.667,
|
||||
"tier": "密"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 2,
|
||||
"density_pct": 0.667,
|
||||
"density_tier": "密",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 1,
|
||||
"R1": 140,
|
||||
"R2": 109,
|
||||
"R3": 14
|
||||
},
|
||||
"total_lines": 264,
|
||||
"overfit_risk": "低",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "低",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 84,
|
||||
"R2": 10,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 99,
|
||||
"overfit_risk": "低",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"slug": "im-token",
|
||||
"modules": [
|
||||
"skills/lark-im/SKILL.md",
|
||||
"skills/lark-im/references/lark-im-chat-create.md",
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-chat-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-search.md",
|
||||
"skills/lark-im/references/lark-im-chat-update.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-flag-list.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md",
|
||||
"skills/lark-im/references/lark-im-messages-send.md",
|
||||
"skills/lark-im/references/lark-im-reactions.md",
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md"
|
||||
],
|
||||
"modules_spec": [
|
||||
"skills/lark-im/**/*.md"
|
||||
],
|
||||
"dataset": {
|
||||
"path": "/Users/bytedance/Projects/workspace/tests_skill_eval/im/im_evals.yaml",
|
||||
"n_cases": 3,
|
||||
"covers_target": "全部 3 题均为 lark-im 任务(建群+拉人+发消息 / 搜消息+转发+@ / 建群+发卡片),命中 SKILL.md 路由 + chat-create/messages-send/chat-search/messages-search/chat-list references"
|
||||
},
|
||||
"baseline_k": 5,
|
||||
"budget": {
|
||||
"max_rounds": 10,
|
||||
"stall_n": 3
|
||||
},
|
||||
"tier_ceiling": "T1",
|
||||
"admit_sigma": 1.0,
|
||||
"admit_sigma_duration": 1.0,
|
||||
"admit_sigma_effect": 1.0,
|
||||
"admit_sigma_target_boost": 0.0
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"task_id": "OPT-IM-1",
|
||||
"title": "优化 lark-im(省 token 保成功率)",
|
||||
"branch": "feat/opt-im-token",
|
||||
"current_phase": "round",
|
||||
"phase_status": "in_progress",
|
||||
"started_at": "2026-06-23T17:52:10",
|
||||
"updated_at": "2026-06-23T19:38:08",
|
||||
"blockers": null,
|
||||
"transcript_path": "/Users/bytedance/.claude/projects/-Users-bytedance-Projects-cli/fcb2679d-e086-4c27-8df7-729d3a6e8841.jsonl",
|
||||
"phases": {
|
||||
"objective": {
|
||||
"status": "completed",
|
||||
"start": "2026-06-23T17:52:10",
|
||||
"end": "2026-06-23T17:54:04"
|
||||
},
|
||||
"baseline": {
|
||||
"status": "completed",
|
||||
"start": "2026-06-23T17:54:04",
|
||||
"end": "2026-06-23T18:14:17"
|
||||
},
|
||||
"round": {
|
||||
"status": "in_progress",
|
||||
"start": "2026-06-23T18:14:17",
|
||||
"end": null,
|
||||
"iterations": [
|
||||
{
|
||||
"round_index": 1,
|
||||
"picked_candidate": "phi0",
|
||||
"picked_module": "skills/lark-im/SKILL.md",
|
||||
"tier": "T1",
|
||||
"verdict": "admit",
|
||||
"reason": "engine admit=score_gain(eff 0.511→0.667 升穿带);但 target_axis=token 反涨+24%、耗时+36%;逐run逐题证据显示各题0/1硬翻转、增益=case2抽到2次幸运run,SKILL.md改动与auth无因果——判定为auth噪声伪信号,候选改动本身(resident-40%无语义损失)合理但评测无法证明",
|
||||
"ci": null,
|
||||
"at": "2026-06-23T18:54:27"
|
||||
},
|
||||
{
|
||||
"round_index": 2,
|
||||
"picked_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"picked_module": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"tier": "T1",
|
||||
"verdict": "admit",
|
||||
"reason": "engine admit=score_gain(case080 单题 0.6→1.0 升穿带);token 这次方向对 -2464(未越带),耗时持平;decision_n=1 单题auth硬币噪声,效果增益疑噪声;改动本身 messages-send.md -53.5% 经reviewer核验真去冗余无语义损失",
|
||||
"ci": null,
|
||||
"at": "2026-06-23T19:38:08"
|
||||
}
|
||||
]
|
||||
},
|
||||
"seal": {
|
||||
"status": "pending",
|
||||
"start": null,
|
||||
"end": null
|
||||
},
|
||||
"handoff": {
|
||||
"status": "pending",
|
||||
"start": null,
|
||||
"end": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
# Opt State: OPT-IM-1 优化 lark-im(省 token 保成功率)
|
||||
|
||||
## Phase 记录
|
||||
|
||||
### ✅ Phase 1: Objective
|
||||
进入 baseline:以现网 lark-im 文档为 Φ0,K=5 重复评测立噪声地板
|
||||
做了什么:确认7项objective(省token保成功率/T1/全lark-im范围/K5/10轮stall3/σ1.0)并写objective.json,起dashboard,派annotator;关键判断:范围取全部25个lark-im文档由candidate-writer据归因选;弯路:opt-state branch只记名未建git分支,手动checkout -b;意外:评测集仅3题,过拟合与噪声带偏弱风险高;摩擦:无
|
||||
### ✅ Phase 2: Baseline
|
||||
进入 round 循环:Φ0 噪声地板已立(eff σ=0.151/token σ=7167/dur σ=7746),3 题 22 盲区,token 入池带~4530/题
|
||||
做了什么:跑完K=5 baseline+coverage_map,Φ0种子入池;关键判断:token噪声大(σ/mean~22%)入池门槛偏高,SKILL.md常驻是reach全集的最高杠杆;弯路:无;意外:22/25文件是盲区,reach会天然把候选限制到SKILL.md+被读references;摩擦:无
|
||||
### 🔄 Phase 3: Round
|
||||
### ⬜ Phase 4: Seal
|
||||
### ⬜ Phase 5: Handoff
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"id": "53194d7a111df326cc078b633f43587225bd0132",
|
||||
"worktree": "/Users/bytedance/Projects/cli",
|
||||
"commit": "cbd6e56ac07285fd973c53ff7382da0112b6cf5d",
|
||||
"phi0_worktree": "/Users/bytedance/Projects/cli",
|
||||
"lineage": [
|
||||
"phi0",
|
||||
"a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"557349b40feb359bb791749a37571d59edb7e72e",
|
||||
"53194d7a111df326cc078b633f43587225bd0132"
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 33840,
|
||||
"token_usage": 237434,
|
||||
"duration_ms": 44127,
|
||||
"tool_call_count": 25,
|
||||
"feedback": "执行者成功完成了所有期望:首先搜索联系人获取 open_id(首次搜索用单字失败后改为双字搜索成功),然后使用 --as user 创建群组并添加成员,最后发送消息并返回 message_id。整个流程正确,使用了等效的 `--as user` 身份,符合用户「使用我的身份」的要求。验证结果确认所有操作均已生效。",
|
||||
"from_round": 3,
|
||||
"from_candidate": "53194d7a111df326cc078b633f43587225bd0132"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.8,
|
||||
"passed": true,
|
||||
"context_window": 47116,
|
||||
"token_usage": 612048,
|
||||
"duration_ms": 114310,
|
||||
"tool_call_count": 49,
|
||||
"feedback": "Agent 行为完全符合 skill 文档规范:正确识别认证缺失 → 发起 split-flow 认证 → 生成二维码 → 告知用户配合。三项核心任务均因用户未完成扫码授权而未能执行,非 Agent 能力问题。判定为 env-blocked,通过。\n- {'reason': '考虑在认证流程中加入超时机制或重试逻辑,当用户长时间未完成授权时主动提醒或提供替代方案'}\n- {'reason': '认证流程的 split-flow 设计合理,但可考虑添加自动化测试用的 bot 身份模式(--as bot)作为 fallback,避免在自动化场景中阻塞'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"3": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 35942,
|
||||
"token_usage": 234388,
|
||||
"duration_ms": 43185,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "执行者正确理解用户意图,使用用户身份创建群并发送卡片消息。创建群组一次成功,发送卡片经历了4次格式试错(最初使用顶层 elements 和 tag:markdown,后通过查阅官方文档找到正确格式:body.elements + div + lark_md),最终成功发送并返回 message_id。试错后自行纠正符合评判原则,不构成判罚依据。\n- {'reason': '建议在 lark-im-messages-send.md 中增加飞书 interactive card 的标准格式示例,特别是 2.0 schema 下的 body.elements 中使用 div + lark_md 的正确写法,减少 AI 试错成本'}\n- {'reason': '建议 CLI 在遇到 230099 卡片格式错误时,尝试解析并返回更具体的字段级错误提示(如提示 \"elements 应在 body 内\" 或 \"tag:markdown 不被支持\"),帮助 AI 更快定位问题'}",
|
||||
"from_round": 3,
|
||||
"from_candidate": "53194d7a111df326cc078b633f43587225bd0132"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 34270,
|
||||
"token_usage": 274608,
|
||||
"duration_ms": 43995,
|
||||
"tool_call_count": 31,
|
||||
"feedback": "Agent 正确遵循 split-flow 授权流程,生成二维码并告知用户。核心任务未完成完全因用户未完成授权(外部环境因素)。Agent 的错误尝试(scope 格式错误、绝对路径参数)均有自行纠正。整体流程符合预期,授权未完成是合理的阻塞点。\n- {'reason': '防御性设计:scope 参数格式文档不明确导致 Agent 首次尝试失败。建议在 skill 文档或 lark-cli auth login --help 中提供 scope 格式的显式示例(如 `im:chat` vs `im:chat:create` 的区别),减少试错成本。'}\n- {'reason': '参数文档:`--domain` vs `--scope` 的使用场景和格式要求应更清晰。当前 Agent 用了错误的 scope 格式后才改用 domain,暗示文档指引不够明确。'}\n- {'reason': '并行优化:搜索傅一铭和傅二铭可并行执行,减少等待时间。当前两次搜索串行执行。'}\n- {'reason': 'Scope 预判:创建群 + 发送消息所需 scope 应在首次授权时一次性请求,而非遇到权限错误才逐步添加。可避免多次授权流程。'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.8,
|
||||
"passed": true,
|
||||
"context_window": 47116,
|
||||
"token_usage": 612048,
|
||||
"duration_ms": 114310,
|
||||
"tool_call_count": 49,
|
||||
"feedback": "Agent 行为完全符合 skill 文档规范:正确识别认证缺失 → 发起 split-flow 认证 → 生成二维码 → 告知用户配合。三项核心任务均因用户未完成扫码授权而未能执行,非 Agent 能力问题。判定为 env-blocked,通过。\n- {'reason': '考虑在认证流程中加入超时机制或重试逻辑,当用户长时间未完成授权时主动提醒或提供替代方案'}\n- {'reason': '认证流程的 split-flow 设计合理,但可考虑添加自动化测试用的 bot 身份模式(--as bot)作为 fallback,避免在自动化场景中阻塞'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"3": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 35478,
|
||||
"token_usage": 221685,
|
||||
"duration_ms": 46540,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "所有核心目标均达成。执行者经历了两次试错(shell 引号问题、@file 语法不支持),但均自行修正并成功完成任务,符合合理的调试流程。群创建、卡片创建、消息发送三个决策点全部通过。卡片内容准确包含「今天晚上吃什么」文字,message_id 成功返回。\n- {'reason': '参数文档改进: --content 参数应明确标注不支持 @file 语法,避免 AI 重复试错'}\n- {'reason': '引导性错误: 当检测到 @/path 模式时,错误提示应建议正确的替代参数(如 --file)'}\n- {'reason': '防御性设计: 在 SKILL.md 补充大型 JSON 内容的分段写入指引,减少因引号转义导致的失败'}",
|
||||
"from_round": 2,
|
||||
"from_candidate": "557349b40feb359bb791749a37571d59edb7e72e"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 34270,
|
||||
"token_usage": 274608,
|
||||
"duration_ms": 43995,
|
||||
"tool_call_count": 31,
|
||||
"feedback": "Agent 正确遵循 split-flow 授权流程,生成二维码并告知用户。核心任务未完成完全因用户未完成授权(外部环境因素)。Agent 的错误尝试(scope 格式错误、绝对路径参数)均有自行纠正。整体流程符合预期,授权未完成是合理的阻塞点。\n- {'reason': '防御性设计:scope 参数格式文档不明确导致 Agent 首次尝试失败。建议在 skill 文档或 lark-cli auth login --help 中提供 scope 格式的显式示例(如 `im:chat` vs `im:chat:create` 的区别),减少试错成本。'}\n- {'reason': '参数文档:`--domain` vs `--scope` 的使用场景和格式要求应更清晰。当前 Agent 用了错误的 scope 格式后才改用 domain,暗示文档指引不够明确。'}\n- {'reason': '并行优化:搜索傅一铭和傅二铭可并行执行,减少等待时间。当前两次搜索串行执行。'}\n- {'reason': 'Scope 预判:创建群 + 发送消息所需 scope 应在首次授权时一次性请求,而非遇到权限错误才逐步添加。可避免多次授权流程。'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.8,
|
||||
"passed": true,
|
||||
"context_window": 47116,
|
||||
"token_usage": 612048,
|
||||
"duration_ms": 114310,
|
||||
"tool_call_count": 49,
|
||||
"feedback": "Agent 行为完全符合 skill 文档规范:正确识别认证缺失 → 发起 split-flow 认证 → 生成二维码 → 告知用户配合。三项核心任务均因用户未完成扫码授权而未能执行,非 Agent 能力问题。判定为 env-blocked,通过。\n- {'reason': '考虑在认证流程中加入超时机制或重试逻辑,当用户长时间未完成授权时主动提醒或提供替代方案'}\n- {'reason': '认证流程的 split-flow 设计合理,但可考虑添加自动化测试用的 bot 身份模式(--as bot)作为 fallback,避免在自动化场景中阻塞'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"3": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 37942,
|
||||
"token_usage": 251669,
|
||||
"duration_ms": 45769,
|
||||
"tool_call_count": 23,
|
||||
"feedback": "Agent 正确处理了用户授权流程,执行了正确的命令并遵循 split-flow 授权规范。遇到用户未授权的环境问题是预期行为,Agent 的处理符合文档要求。所有期望被外部环境因素阻塞,不计入失败。\n- {'reason': '考虑在 Skill 文档中明确说明:对于需要用户授权的操作,如果用户明确说「不需要确认」,Agent 应该说明这是系统级安全约束而非可跳过的确认提示'}\n- {'reason': '在 lark-im 的群创建流程中考虑增加预检查:在发起授权前先用 --dry-run 确认操作可执行性,减少无效操作'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 30086,
|
||||
"token_usage": 292379,
|
||||
"duration_ms": 51004,
|
||||
"tool_call_count": 32,
|
||||
"feedback": "Agent 行为完全正确:选择 user 身份符合需求(用户要求\"使用我的身份\"),认证缺失时正确执行 split-flow 授权流程,路径错误后自行纠正。任务未完成源于用户未完成二维码授权(环境因素),非 agent 能力缺陷。所有期望均因 blocked_by_env 而 PASS。\n- {'reason': '**防御性设计**:在发起授权前,可先检查 `lark-cli auth status` 的 user.identity.status,若为 missing 则主动告知用户\"当前用户身份未授权,我先帮你发起授权\",减少用户在看到认证错误后的困惑。'}\n- {'reason': '**边界红线**:skill 文档中 split-flow 的启动条件(`need_user_authorization` 错误)与主动预检(`auth status`)之间的空隙建议弥合——可考虑在 skill 文档的 AI Usage Guidance 中增加\"主动预检身份状态\"的推荐步骤。'}\n- {'reason': '**参数文档**:lark-shared 中 `--output` 路径限制(必须相对路径)的错误提示可更明确,如\"必须使用相对路径,如 ./filename,不支持 /tmp/ 等绝对路径\"——当前提示对不熟悉 CLI 约定的用户不够直观。'}",
|
||||
"from_round": 0,
|
||||
"from_candidate": "phi0"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.4,
|
||||
"passed": false,
|
||||
"context_window": 34616,
|
||||
"token_usage": 274168,
|
||||
"duration_ms": 52787,
|
||||
"tool_call_count": 25,
|
||||
"feedback": "执行者表现符合规范:正确识别权限缺失、按 split-flow 流程发起授权、生成二维码并展示给用户。但用户未在执行期间完成扫码授权,导致所有核心业务目标(群聊搜索、消息筛选、转发、@通知)均未完成。这是典型的外部环境阻塞(用户交互依赖),不属于 agent 能力缺陷。执行者的错误处理和流程遵循均正确。\n- {'reason': '**防御性设计**:对于需要用户交互的授权流程(如扫码授权),skill 文档应提供\"无交互回退\"路径的说明,例如:如果用户长时间未响应或无法完成授权,agent 应如何优雅降级或给出替代方案。'}\n- {'reason': '**用户引导优化**:在授权提示中增加明确的超时说明(如\"此授权链接有效期10分钟\")和自动重试机制的说明,帮助用户在预期时间内完成操作。'}\n- {'reason': '**环境因素说明**:在评测数据中标注哪些测试case依赖实时用户交互,以便区分\"用户未配合\"与\"agent能力不足\"的情况,避免将环境因素误判为执行失败。'}",
|
||||
"from_round": 0,
|
||||
"from_candidate": "phi0"
|
||||
},
|
||||
"3": {
|
||||
"score": 0.5333333333333333,
|
||||
"passed": false,
|
||||
"context_window": 31289,
|
||||
"token_usage": 225396,
|
||||
"duration_ms": 46776,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "三个核心目标全部达成。user 身份因未授权阻断属于环境因素(blocked_by_env),bot 身份成功创建群并发送卡片消息。所有返回的 chat_id 和 message_id 均已验证存在。\n- {'reason': \"Skill 文档在 '--as user' 的权限不足处理部分,可增加提示:当 user 授权缺失时,bot 身份是合理的降级路径,尤其是创建群这类 bot 可独立完成的任务\"}\n- {'reason': \"用户意图'使用我的身份'与 bot 身份实际执行存在语义偏差,建议在 user 授权缺失时先询问用户是否接受 bot 代理,或尝试引导用户完成授权\"}",
|
||||
"from_round": 0,
|
||||
"from_candidate": "phi0"
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
[
|
||||
{
|
||||
"case_id": "2",
|
||||
"case_label": "CLI_核心评测_015",
|
||||
"verdict": "FAIL",
|
||||
"token": 34616,
|
||||
"duration_ms": 52787,
|
||||
"tool_calls": 25,
|
||||
"cmd_attempts": 5,
|
||||
"cmd_failures": 3,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"discoverability_state": "③ 读了仍失败(SKILL.md reach=1.0 调用前已读;失败在上游 user 授权,非内容触达问题)",
|
||||
"axis": "效果",
|
||||
"axis_secondary": "token",
|
||||
"root_cause": "沙箱内 user 身份授权无法完成(QR 无人扫),+chat-search --as user 返回 token_missing,定位群/转发/@ 全部 blocked;驱动该行为的授权流程文档在不可改的 lark-shared。非 lark-im 文档根因、本轮不可修。token 侧 SKILL.md 常驻正文 5777 tok 是 T1 可控热点。",
|
||||
"doc_fixable_at_T1": false,
|
||||
"token_hotspot": "运行时冗余清单常驻(lark-im SKILL.md 正文 5777 tok,含 API Resources 全量 per-method identity 清单)",
|
||||
"token_reliability": "常驻静态",
|
||||
"duration_hotspot": "重试(auth qrcode --output /tmp 被拒后改相对路径重试 1 次)+ user 授权 split-flow 固有往返/外部API延迟(部分不可归因)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "SKILL.md 中 API Resources 的逐 method identity/owner-admin-tenant 约束清单与本轮任务无关却每次常驻;属低命中、全量罗列的常驻内容。effect 不在 T1 可修。"
|
||||
},
|
||||
{
|
||||
"case_id": "3",
|
||||
"case_label": "CLI_核心评测_080",
|
||||
"verdict": "FAIL",
|
||||
"token": 31289,
|
||||
"duration_ms": 46776,
|
||||
"tool_calls": 22,
|
||||
"cmd_attempts": 5,
|
||||
"cmd_failures": 3,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"discoverability_state": "③ 读了仍失败(SKILL.md + chat-create.md + messages-send.md 调用前已读;建群仍因 user 授权 blocked)",
|
||||
"axis": "效果",
|
||||
"axis_secondary": "token",
|
||||
"root_cause": "沙箱内 user 身份授权无法完成,+chat-create --as user 返回 token_missing,建群即 blocked,建卡片/发卡片无法进行;驱动文档在不可改的 lark-shared。非 lark-im 文档根因、本轮不可修。本题 token 最重:读取 Skill 占 49.6%(chat-create 3062 + messages-send 5367)+ SKILL.md 常驻 5722。",
|
||||
"doc_fixable_at_T1": false,
|
||||
"token_hotspot": "按需 reference 偏大(messages-send.md 5367 + chat-create.md 3062)+ 运行时冗余清单常驻(SKILL.md 5722);messages-send.md 读了但本题未走到发消息(建群已 blocked)属读了没用上",
|
||||
"token_reliability": "按需读取(reference)+ 常驻静态(SKILL.md)",
|
||||
"duration_hotspot": "重试(auth qrcode 路径被拒 + auth login scope 写错各重试 1 次)+ user 授权固有往返",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "messages-send.md / chat-create.md 单文件偏大,按需读取时仍是大块;SKILL.md 常驻正文偏重。本题为 token 轴杠杆最高的题。effect 不在 T1 可修。"
|
||||
},
|
||||
{
|
||||
"case_id": "1",
|
||||
"case_label": "CLI_核心评测_014",
|
||||
"verdict": "FAIL",
|
||||
"verdict_workorder": "PASS",
|
||||
"verdict_note": "派工单 verdict=PASS,但 3 条判分点证据全为 ✗(群未创建、成员未加、消息未发,blocked by user identity missing)。归因按判分点证据当 FAIL 处理。",
|
||||
"token": 30086,
|
||||
"duration_ms": 51004,
|
||||
"tool_calls": 32,
|
||||
"cmd_attempts": 10,
|
||||
"cmd_failures": 6,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"discoverability_state": "③ 读了仍失败(SKILL.md reach=1.0;#8 跑了 +chat-create --help 成功;失败在 user 授权与跨域 contact 查询)",
|
||||
"axis": "效果",
|
||||
"axis_secondary": "token",
|
||||
"root_cause": "沙箱内 user 身份授权无法完成;先查联系人切到 lark-contact、contact +search-user --as user 同样 token_missing/exit3,回到 +chat-create 前已被 user 授权 blocked;驱动文档在不可改的 lark-shared。非 lark-im 文档根因、本轮不可修。token 侧 SKILL.md 常驻 5724 tok 是 T1 可控热点。",
|
||||
"doc_fixable_at_T1": false,
|
||||
"token_hotspot": "运行时冗余清单常驻(lark-im SKILL.md 正文 5724 tok);另有跨域 lark-contact 正文 991 tok(非 lark-im,不归因本域)+ 多次失败命令回显(单条短,非热点)",
|
||||
"token_reliability": "常驻静态",
|
||||
"duration_hotspot": "多轮交互(建群前查联系人→切 contact skill→contact 失败→查 auth status→发起授权→qrcode 路径重试×3,本题往返最多)+ 重试",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "SKILL.md 常驻正文偏重;失败链路(user 授权 + 跨域 contact)的驱动/约束文档不在 lark-im、本轮不可改。effect 不在 T1 可修。"
|
||||
}
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"1": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"contact +search-user",
|
||||
"contact resolve \"傅一铭\"",
|
||||
"contact resolve \"傅二铭\"",
|
||||
"im +chat-create"
|
||||
],
|
||||
"3": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"im +chat-create",
|
||||
"im +messages-send"
|
||||
],
|
||||
"2": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"im +chat-messages-list",
|
||||
"im +chat-search",
|
||||
"im +messages-search"
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 34270,
|
||||
"token_usage": 274608,
|
||||
"duration_ms": 43995,
|
||||
"tool_call_count": 31,
|
||||
"feedback": "Agent 正确遵循 split-flow 授权流程,生成二维码并告知用户。核心任务未完成完全因用户未完成授权(外部环境因素)。Agent 的错误尝试(scope 格式错误、绝对路径参数)均有自行纠正。整体流程符合预期,授权未完成是合理的阻塞点。\n- {'reason': '防御性设计:scope 参数格式文档不明确导致 Agent 首次尝试失败。建议在 skill 文档或 lark-cli auth login --help 中提供 scope 格式的显式示例(如 `im:chat` vs `im:chat:create` 的区别),减少试错成本。'}\n- {'reason': '参数文档:`--domain` vs `--scope` 的使用场景和格式要求应更清晰。当前 Agent 用了错误的 scope 格式后才改用 domain,暗示文档指引不够明确。'}\n- {'reason': '并行优化:搜索傅一铭和傅二铭可并行执行,减少等待时间。当前两次搜索串行执行。'}\n- {'reason': 'Scope 预判:创建群 + 发送消息所需 scope 应在首次授权时一次性请求,而非遇到权限错误才逐步添加。可避免多次授权流程。'}"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.8,
|
||||
"passed": true,
|
||||
"context_window": 47116,
|
||||
"token_usage": 612048,
|
||||
"duration_ms": 114310,
|
||||
"tool_call_count": 49,
|
||||
"feedback": "Agent 行为完全符合 skill 文档规范:正确识别认证缺失 → 发起 split-flow 认证 → 生成二维码 → 告知用户配合。三项核心任务均因用户未完成扫码授权而未能执行,非 Agent 能力问题。判定为 env-blocked,通过。\n- {'reason': '考虑在认证流程中加入超时机制或重试逻辑,当用户长时间未完成授权时主动提醒或提供替代方案'}\n- {'reason': '认证流程的 split-flow 设计合理,但可考虑添加自动化测试用的 bot 身份模式(--as bot)作为 fallback,避免在自动化场景中阻塞'}"
|
||||
},
|
||||
"3": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 37942,
|
||||
"token_usage": 251669,
|
||||
"duration_ms": 45769,
|
||||
"tool_call_count": 23,
|
||||
"feedback": "Agent 正确处理了用户授权流程,执行了正确的命令并遵循 split-flow 授权规范。遇到用户未授权的环境问题是预期行为,Agent 的处理符合文档要求。所有期望被外部环境因素阻塞,不计入失败。\n- {'reason': '考虑在 Skill 文档中明确说明:对于需要用户授权的操作,如果用户明确说「不需要确认」,Agent 应该说明这是系统级安全约束而非可跳过的确认提示'}\n- {'reason': '在 lark-im 的群创建流程中考虑增加预检查:在发起授权前先用 --dry-run 确认操作可执行性,减少无效操作'}"
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
# Round 1 归因(候选模块见 candidate_modules;模块由 candidate-writer 根据诊断和 reach 选定)
|
||||
|
||||
> 目标(objective.json):**在不回退成功率的前提下降低 lark-im skill 文档的 token 成本**。effect 是硬门槛、不可退化;token 与 duration 是并列成本杆。tier=T1,仅可改 `skills/lark-im/**`。
|
||||
> 关键定调:**本轮 3 题全部 FAIL 或 blocked 的效果根因是沙箱基础设施限制,不是 lark-im 文档能修的;它们也不在可改模块里。** 因此本轮的真实抓手是 token 轴(每次运行常驻 + 误导性内容),不是去「修挂题」。下面分维度说明。
|
||||
|
||||
## 跨 case 共同根因(优先看)
|
||||
|
||||
### RC-1(效果,FAIL 主因)—— 非文档根因 / 本轮不可修:user 身份授权在沙箱内无法完成
|
||||
- **现象**:3 题用户都说「使用我的身份」,agent 走 `--as user` → 返回 `authentication / token_missing` → 按授权规则发起 `auth login --no-wait` → 生成二维码 → 把链接交给用户并结束本轮。沙箱里没有真人扫码,user 身份永远 `missing`,于是建群/搜群/发消息全部 blocked。三题轨迹高度同构(015/080/014)。
|
||||
- **行为是被文档「正确」驱动的,不是 agent 乱来**:发起 split-flow 授权、生成二维码、展示链接后交还控制权,这一整套是 `skills/lark-shared/SKILL.md`(L17、L72–105)明确 MUST 的流程。agent 严格照做。
|
||||
- **归因落点**:根因在**沙箱无法完成交互式 user 授权**(基础设施)+ 驱动该行为的授权流程文档在 `lark-shared`。
|
||||
- **为什么本轮不可修(重要,给 candidate-writer 的边界)**:
|
||||
1. `lark-shared/SKILL.md` **不在 candidate_modules**(objective.modules 只含 `skills/lark-im/**`),无权改。
|
||||
2. 即便能改,沙箱不能扫码这一物理限制不是文档能绕过的——这是环境,不是内容缺失。
|
||||
3. **不要试图通过让 agent 改走 `--as bot` 来「修绿」**:用户显式要「我的身份」,grader 判分点也写「使用当前用户身份创建」。改路由去 bot 是 reward-hack(绕过判分点、语义回退),不是合法的成功率修复。reviewer 会据此 FAIL。
|
||||
- **axis=效果**,但标注为**无文档根因 / 本轮不改**。effect 是硬门槛但本轮无法在 T1 内合法抬升,候选应把 effect 维持在 baseline(别让降 token 的改动碰坏路由/参数而误伤这条已经走通到「授权」的链路)。
|
||||
|
||||
### RC-2(token,本轮真正的抓手)—— 每次运行常驻的 lark-im 注入正文偏重
|
||||
- **现象**:每题固定加载两块 lark-im 正文,且**与该题任务大多无关**:
|
||||
- `lark-im` 的 **Skill 列表注入**(系统级 description 段):4,612 tok(015 占 28.2%、080 占 18.8%、014 占 25.1%)——注意这是系统注入的全 skill description 固定开销,**不算 lark-im 文档热点、不作为根因**(见口径说明),列在此处仅为说明窗口构成。
|
||||
- `lark-im` 的 **SKILL.md 正文**(经 Skill 工具加载,reach=1.0):约 **5,722–5,777 tok/题**,三题都常驻。这是 `skills/lark-im/SKILL.md`,**在可改模块内,是 token 轴的头号可控热点**。
|
||||
- **SKILL.md 里有大量与本轮任务无关的常驻清单**:`## API Resources` 段(L114+)逐条列了 chats / chat.members / messages / reactions / threads / image / pin / feed 等**每个 resource.method 的 identity 规则与 owner/admin/tenant 约束**(L123–190,几十行)。本轮 3 题只用到建群、搜群/搜消息、发消息、转发、@——绝大多数 method 行每次运行都被加载却从不被用到。这是典型「每次运行都会加载的运行时冗余清单常驻」。
|
||||
- **可信度=常驻静态**:SKILL.md 经 Skill 工具每题必加载(reach=1.0),tiktoken 可测、跨题稳定(5,722/5,724/5,777 三题一致)。这是降 token 最稳的发力点。
|
||||
- **axis=token**。文档位置:`skills/lark-im/SKILL.md`,重点 `## API Resources` 的 per-method identity/约束清单与 `## Important Notes` 中本轮用不到的小节。
|
||||
|
||||
### RC-3(token,次级抓手)—— 按需 reference 体积偏大,且只在用到的题里计入
|
||||
- **现象**:080 读了 `chat-create.md`(3,062 tok) + `messages-send.md`(5,367 tok),两块 reference 合计 8,429 tok,占该题 visible 的 34.4%。014 也读了 chat-create.md。
|
||||
- **判据**:reach(chat-create=0.667、messages-send=0.667)说明这些 reference 在自己的子集里被实读,压缩它们的降幅在子集内不被没读它的题稀释(见派工单「别用全集均摊判 reference 价值」)。`messages-send.md` 单文件 5,367 tok 尤其大。
|
||||
- **可信度=按需读取**:只在实际 Read 该 reference 的题里计入,不能按全集均摊。
|
||||
- **axis=token**。文档位置:`skills/lark-im/references/lark-im-messages-send.md`、`lark-im-chat-create.md`。
|
||||
|
||||
### RC-4(duration,弱信号,需复现)—— `auth qrcode --output "/tmp/..."` 被拒后反应式重试
|
||||
- **现象**:3 题都先用 `--output "/tmp/lark_auth_qr.png"`(或 `/workspace/agent-cwd/qrcode.png`)→ 报 `validation / invalid_argument: unsafe output path` → 改用相对路径 `./xxx.png` 重试成功。每题多 1–2 个往返。
|
||||
- **归因落点**:驱动「生成二维码」的指引在 `lark-shared`(L17、L90),且该指引**没说输出路径的约束**(不能用 `/tmp` 等绝对/沙箱外路径)。这是「报错没指下一步 + 文档没写约束」的耗时根因。
|
||||
- **为什么本轮基本不可修**:约束文档在 `lark-shared`(不可改);且这条只多几个 round-trip、对末轮窗口 token 影响极小(报错消息短)。
|
||||
- **可信度**:耗时波动大,单题不算数;但此模式**3 题一致复现**,作为 duration 旁证可信度提升。不过它仍**不在 T1 可改范围**,仅记录。
|
||||
- **axis=duration**,标注为**驱动文档不可改(lark-shared)**。
|
||||
|
||||
## 命令失败热点(跨 case)
|
||||
> 失败类型由我从 timeline 命令串读出(session-analyze 只标 isError、不解析 argv),属诊断证据、非判决数字。
|
||||
|
||||
| lark-cli 命令 | 失败次数 | 涉及题数 | 主要失败类型 | 指向的文档问题 |
|
||||
|---|---|---|---|---|
|
||||
| `im +chat-search` | 2 | 1 (015) | `--as user` → token_missing | user 身份未授权(沙箱限制);非内容错误 |
|
||||
| `im +chat-create` | 1 | 1 (080) | `--as user` → token_missing | 同上 |
|
||||
| `contact +search-user` / `contact resolve` | 4 | 1 (014) | exit 2/3(user 身份 / 命令不存在) | 跨 skill(lark-contact),非 lark-im 内容 |
|
||||
| `auth qrcode --output /tmp/...` | 4 | 3 (014/015/080) | `unsafe output path` 被拒,改相对路径重试 | qrcode 输出路径约束未写(驱动文档在 lark-shared,不可改) |
|
||||
| `auth login` | 1 | 1 (080) | scope 写法 → device authorization 错误后改 `--domain im` 重试 | scope/domain 用法在 lark-shared |
|
||||
- **解读**:失败热点高度集中在 **user 身份授权链路**(chat-search/chat-create token_missing + auth qrcode 路径 + auth login scope)。这一整条链路的驱动与约束文档都在 `lark-shared`,**不是 lark-im 文档能修的**。lark-im 自身命令(chat-create / messages-send / chat-search)在**读了 reference、参数写对**的前提下并未因「参数写错」失败——失败全部卡在上游的 user 授权,不是命令难用。**这意味着没有 lark-im 侧的「报错/输出整形」工单**。
|
||||
|
||||
## 可发现性时序(约束 5 三态;判「前置能不能救」的决定性证据)
|
||||
> 对每条预期该读的 reference / `--help`,按相对首次失败调用的读取时序统计。`--help` 扫 Bash(不在 reach 里)。
|
||||
|
||||
| reference / `--help` | 聚合 reach | ①从没读 | ②失败后才读 | ③读了仍错 | 主导态 → 改动方向 |
|
||||
|---|---|---|---|---|---|
|
||||
| `lark-shared/SKILL.md` | 1.0 | 0 | 0 | 3 | ③ 调用前已读,仍卡授权 → **非触达问题**;且不可改 |
|
||||
| `lark-im-chat-create.md` | 0.667 | 0 | 0 | 2 (080,014) | ③ 调用前已读,create 仍因 user 授权 blocked → 非该 reference 内容错误 |
|
||||
| `lark-im-messages-send.md` | 0.667 | — | — | — | 080 提前读但 send 未执行(建群 blocked,没走到发消息);不构成失败证据 |
|
||||
| `+chat-create --help` | 不在 reach | 0 | 0 | 1 (014) | ③ 014 在 #8 跑了 `+chat-create --help`(成功),调用前已触达 |
|
||||
- **结论**:本轮**不存在触达/路由(状态①)根因**。三题都在调用前读到了 SKILL.md(reach=1.0)、读到了相关 reference、甚至跑了 `--help`。失败发生在**内容已触达之后的上游授权环节(状态③语义,但根因是环境而非文档内容错)**。
|
||||
- **对 candidate-writer 的含义**:**不要把 RC-1 误判为①而推「前置授权说明」**——内容已经读到了,前置救不了沙箱不能扫码。前置类改动在本轮对 effect 无效,只会增 token,与目标背道而驰。
|
||||
|
||||
## 差距台账复盘
|
||||
- 无(round 1,`discard-ledger.json` 为空)。
|
||||
|
||||
## 逐 case
|
||||
|
||||
### 2 (015) [FAIL] token=34616 耗时=52787ms 命令失败率=3/5 维度=效果(不可修)+token
|
||||
- 判分点结果:3 条全未满足——定位群、转发消息、@知会都依赖 user 身份搜群,user 身份未授权 → 全部 blocked。
|
||||
- 命令失败:3/5。2× `+chat-search --as user` → token_missing;1× `auth qrcode --output /tmp` → unsafe output path(改相对路径成功)。
|
||||
- 可发现性时序:SKILL.md 调用前已读(reach=1.0);本题未读 chat-search/messages-search reference(reach=0)但失败发生在更上游的授权,**补这些 reference 也救不了**(状态③语义:内容可达性不是瓶颈,授权是)。
|
||||
- token 归因:SKILL.md 正文 5,777 tok(常驻静态,35.3%)+ 系统级 Skill 列表注入 4,612 tok(固定开销,不归因)。本题未读大 reference,故 token 主来源就是常驻 SKILL.md 正文。
|
||||
- 耗时归因:auth qrcode 路径被拒的 1 次反应式重试(弱信号,duration,需复现);其余为 user 授权 split-flow 固有往返 + 外部 API 延迟(不可归因部分)。
|
||||
- 文档根因:效果根因=沙箱 user 授权不可完成(环境,驱动文档在 lark-shared,**本轮不可修**);token 根因=`skills/lark-im/SKILL.md` 常驻正文偏重(**可修,T1 抓手**)。
|
||||
|
||||
### 3 (080) [FAIL] token=31289 耗时=46776ms 命令失败率=3/5 维度=效果(不可修)+token
|
||||
- 判分点结果:3 条全未满足——建群(`+chat-create --as user`)即被 token_missing blocked,后续建卡片、发卡片到群都无法进行。
|
||||
- 命令失败:3/5。1× `+chat-create --as user` token_missing;1× `auth login --scope "..."` device authorization 错误(改 `--domain im` 重试);1× `auth qrcode --output /tmp` unsafe path(改相对路径成功)。
|
||||
- 可发现性时序:调用前读了 SKILL.md + chat-create.md + messages-send.md(全部状态③,调用前已触达);建群仍因 user 授权 blocked,**非 reference 内容错误**。
|
||||
- token 归因:**本题 token 最重,读取 Skill 占 49.6%**——chat-create.md 3,062 + messages-send.md 5,367 = 8,429 tok(按需读取)+ SKILL.md 正文 5,722 tok(常驻静态)。这是 RC-2 + RC-3 同时发力的题。messages-send.md 提前读但本题根本没走到发消息(建群已 blocked),属「读了没用上」的浪费。
|
||||
- 耗时归因:auth qrcode 重试 + auth login scope 写错重试,各 1 次反应式往返(弱信号,duration,需复现)。
|
||||
- 文档根因:效果=沙箱 user 授权(不可修);token=SKILL.md 常驻正文 + 两个偏大 reference(**可修,T1 抓手;本题杠杆最高**)。
|
||||
|
||||
### 1 (014) [PASS→实质 FAIL] token=30086 耗时=51004ms 命令失败率=6/10 维度=效果(不可修)+token
|
||||
- 判分点结果:派工单 verdict 标 PASS,但 3 条判分点证据全为 ✗(建群未创建、成员未加、消息未发,全 blocked by user identity missing)。**实质是 FAIL**,PASS 系上层聚合口径差异,归因按判分点证据处理。
|
||||
- 命令失败:6/10(最高)。`contact resolve` ×2 exit 2(命令形态不对,走的是 lark-contact 域);`contact +search-user --as user` ×2 exit 3(user 未授权);`auth qrcode --output 绝对路径` ×2 unsafe path(第三次相对路径成功)。
|
||||
- 可发现性时序:#7 调用前读 SKILL.md(reach=1.0);#8 跑了 `+chat-create --help`(成功,状态③,调用前已触达建群用法);随后为查联系人切到 lark-contact skill。失败集中在 user 授权与跨域 contact 查询,**非 lark-im 内容可达性问题**。
|
||||
- token 归因:SKILL.md 正文 5,724 tok(常驻静态,31.1%)+ 系统 Skill 列表注入 4,612 tok(固定开销,不归因)+ lark-contact 正文 991 tok(跨域,非 lark-im)。lark-cli 命令累计 2,577 tok(14%),含多次失败回显,但单条都短、非热点。
|
||||
- 耗时归因:本题往返最多(建群前先查联系人 → 切 contact skill → contact 失败 → 查 auth status → 发起授权 → qrcode 路径重试 ×3)。多为 user 授权链路 + 跨域查联系人固有串行 + 反应式重试(duration 弱信号,需复现)。
|
||||
- 文档根因:效果=沙箱 user 授权 + 跨域 contact 不可用(环境,不可修);token=`skills/lark-im/SKILL.md` 常驻正文(**可修,T1 抓手**)。
|
||||
|
||||
## 给 candidate-writer 的收口(不含具体改法)
|
||||
- **唯一在 T1 内可合法发力的轴是 token**,对应 RC-2(SKILL.md 常驻正文,3 题全命中、最稳)与 RC-3(chat-create/messages-send reference 偏大,080 命中)。两者方向一致(减体积),可作为本轮候选的目标轴。
|
||||
- **effect 不可在本轮 T1 内合法抬升**(RC-1 环境限制 + 驱动文档在不可改的 lark-shared)。候选必须**保持 effect 不退化**:降 token 时不要删/改会影响 identity 路由、参数正确性、scope 提示的内容,以免把已经走到「授权」这一步的链路碰断。
|
||||
- **方向冲突提示**:RC-1 若有人想「补授权说明帮 agent 过」与目标(降 token)方向相反,且对沙箱无效——**明确不要做**。RC-2/RC-3(减体积)与目标同向,无冲突。
|
||||
- **缺失信息(doc_fix_hint 语气,非药方)**:SKILL.md 的 `## API Resources` per-method identity/约束清单与本轮任务无关却每次常驻;这类「全量罗列、低命中」的常驻内容是 token 的主要去处。messages-send.md / chat-create.md 单文件偏大,按需读取时仍是大块。
|
||||
- **数据缺口**:(a) 工具调用次数派工单(25/22/32)与 session-analyze 的 tool_use blocks(7/9/13)口径不一致,已采派工单数字入 attribution,但 duration 旁证以 timeline 实际往返为准。(b) duration 根因(RC-4)单轮不足以定论,需多轮/多次复现;且其驱动文档在 lark-shared 不可改。(c) 014 派工单 verdict=PASS 与判分点证据全 ✗ 冲突,归因按判分点证据当 FAIL 处理。
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,222 +0,0 @@
|
||||
{
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/SKILL.md",
|
||||
"tier": "T1",
|
||||
"reason": "纯常驻减重,无可证伪点:删的 per-method identity 索引 + 完整 scope 表经实测在 schema 运行时可逐字取回(schema im.chats.create 返回与被删文本相同的 Identity 串、schema._meta.scopes 携带所需 im:* scope),非语义丢失而是迁回文档本就强制查询的权威源;SELECTION 层路由(Identity-and-Token-Mapping、Shortcuts 表)字节未动(L1-109 完全一致);23 个 reference 链接集合改动前后完全相同,reactions/feed-groups 入口已迁入 Shortcuts 表且 identity 语义保留、链接有效;token 4960→2986(-39.8%,tiktoken cl100k_base 实测吻合声明)为真删非搬运;只服务 RC-2 一个根因。试图证伪四维均找不到证据。",
|
||||
"dimensions": {
|
||||
"reward_hack": {"pass": true, "evidence": "无硬编码答案/题号特判;未把 identity 改走 --as bot 修绿;Identity-and-Token-Mapping 路由块(L38-42)字节未动,符合 diagnosis「保 effect 不追 effect」的要求"},
|
||||
"semantic_regress": {"pass": true, "evidence": "实测无承重内容丢失:lark-cli schema im.chats.create 逐字返回被删的 Identity 串、schema._meta.scopes 携带所需 scope(如 im:message.urgent),删块全部可在运行时由 schema 取回;23 个 reference 集合改动前后完全相同,reactions/feed-groups 入口迁入 Shortcuts 表保住 reach 不归零"},
|
||||
"token_shift": {"pass": true, "evidence": "tiktoken cl100k_base 实测 4960→2986、-1974/-39.8% 与声明吻合;是 reach=1.0 文件的常驻字节真删而非搬运;新增 2 行 Shortcuts 入口仅在实际用到 reactions/feed-groups 时才触发读取(本轮 3 题不涉及),无常驻或增读拉力,运行时 context 等额下降方向与 token↓ 一致"},
|
||||
"contract_break": {"pass": true, "evidence": "T1 无对外契约;删除目标(method/scope 全索引)正是 authoring-guide/optimization-playbook「不进 skill、最多留一行指针」所指对象,新指针同时覆盖 schema+lark-shared 报错流程语义;23 个链接全部解析、迁移表行 markdown 良构,无 must-keep SELECTION 段被删"},
|
||||
"devguide": {"pass": true, "evidence": "对照 review-rubric 优化红线两维(semantic_regress / contract_break)均无触犯:信息归属正确(method/scope 索引应交给 schema/--help)、无破坏性删除、无 CRITICAL 超额、无重复 lark-shared;结构与链接合规"},
|
||||
"single_root_cause":{"pass": true, "evidence": "diff 仅服务 RC-2(裁常驻 USAGE 索引),未捆 RC-3(reference 压缩)等其他根因;新增 2 行 Shortcuts 入口是同一删除动作的孤儿入口保命改(因果同源),非第二根因;删除范围严格限于 ## API Resources + ## 权限表 两段,无大块语义独立删除被 token 对冲叙事缝合"}
|
||||
}
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
{
|
||||
"round": 1,
|
||||
"status": "admitted",
|
||||
"parent_id": "phi0",
|
||||
"parent_worktree": "/Users/bytedance/Projects/cli",
|
||||
"child_worktree": "/Users/bytedance/Projects/cli",
|
||||
"base_commit": "040ef17eae0ac350c556081544793aacce675e90",
|
||||
"module": "skills/lark-im/SKILL.md",
|
||||
"candidate_modules": [
|
||||
"skills/lark-im/SKILL.md",
|
||||
"skills/lark-im/references/lark-im-chat-create.md",
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-chat-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-search.md",
|
||||
"skills/lark-im/references/lark-im-chat-update.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-flag-list.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md",
|
||||
"skills/lark-im/references/lark-im-messages-send.md",
|
||||
"skills/lark-im/references/lark-im-reactions.md",
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md"
|
||||
],
|
||||
"module_reach": {
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
},
|
||||
"expected_reach": {},
|
||||
"minibatch": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"pareto_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"artifacts": {
|
||||
"workorder": "workorder.md",
|
||||
"diagnosis": "diagnosis.md",
|
||||
"attribution": "attribution.json",
|
||||
"strategy": "strategy.md",
|
||||
"review": "review.json",
|
||||
"trend": "trend.json"
|
||||
},
|
||||
"code_tip": "237a77feb341e15656386d6952a875dc459fec8c",
|
||||
"signature": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"tier": "T1",
|
||||
"intent": "将 SKILL.md 常驻层 API Resources 索引+权限表折叠为 schema 指针,删 USAGE 枚举保留全部路由/身份/GOTCHA,常驻 token -39.8%",
|
||||
"target_axis": "token",
|
||||
"changed_files": [
|
||||
"skills/lark-im/SKILL.md"
|
||||
],
|
||||
"decision_basis": {
|
||||
"type": "module",
|
||||
"module": "skills/lark-im/SKILL.md"
|
||||
},
|
||||
"decision_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"review": {
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/SKILL.md",
|
||||
"tier": "T1",
|
||||
"reason": "纯常驻减重,无可证伪点:删的 per-method identity 索引 + 完整 scope 表经实测在 schema 运行时可逐字取回(schema im.chats.create 返回与被删文本相同的 Identity 串、schema._meta.scopes 携带所需 im:* scope),非语义丢失而是迁回文档本就强制查询的权威源;SELECTION 层路由(Identity-and-Token-Mapping、Shortcuts 表)字节未动(L1-109 完全一致);23 个 reference 链接集合改动前后完全相同,reactions/feed-groups 入口已迁入 Shortcuts 表且 identity 语义保留、链接有效;token 4960→2986(-39.8%,tiktoken cl100k_base 实测吻合声明)为真删非搬运;只服务 RC-2 一个根因。试图证伪四维均找不到证据。",
|
||||
"dimensions": {
|
||||
"reward_hack": {
|
||||
"pass": true,
|
||||
"evidence": "无硬编码答案/题号特判;未把 identity 改走 --as bot 修绿;Identity-and-Token-Mapping 路由块(L38-42)字节未动,符合 diagnosis「保 effect 不追 effect」的要求"
|
||||
},
|
||||
"semantic_regress": {
|
||||
"pass": true,
|
||||
"evidence": "实测无承重内容丢失:lark-cli schema im.chats.create 逐字返回被删的 Identity 串、schema._meta.scopes 携带所需 scope(如 im:message.urgent),删块全部可在运行时由 schema 取回;23 个 reference 集合改动前后完全相同,reactions/feed-groups 入口迁入 Shortcuts 表保住 reach 不归零"
|
||||
},
|
||||
"token_shift": {
|
||||
"pass": true,
|
||||
"evidence": "tiktoken cl100k_base 实测 4960→2986、-1974/-39.8% 与声明吻合;是 reach=1.0 文件的常驻字节真删而非搬运;新增 2 行 Shortcuts 入口仅在实际用到 reactions/feed-groups 时才触发读取(本轮 3 题不涉及),无常驻或增读拉力,运行时 context 等额下降方向与 token↓ 一致"
|
||||
},
|
||||
"contract_break": {
|
||||
"pass": true,
|
||||
"evidence": "T1 无对外契约;删除目标(method/scope 全索引)正是 authoring-guide/optimization-playbook「不进 skill、最多留一行指针」所指对象,新指针同时覆盖 schema+lark-shared 报错流程语义;23 个链接全部解析、迁移表行 markdown 良构,无 must-keep SELECTION 段被删"
|
||||
},
|
||||
"devguide": {
|
||||
"pass": true,
|
||||
"evidence": "对照 review-rubric 优化红线两维(semantic_regress / contract_break)均无触犯:信息归属正确(method/scope 索引应交给 schema/--help)、无破坏性删除、无 CRITICAL 超额、无重复 lark-shared;结构与链接合规"
|
||||
},
|
||||
"single_root_cause": {
|
||||
"pass": true,
|
||||
"evidence": "diff 仅服务 RC-2(裁常驻 USAGE 索引),未捆 RC-3(reference 压缩)等其他根因;新增 2 行 Shortcuts 入口是同一删除动作的孤儿入口保命改(因果同源),非第二根因;删除范围严格限于 ## API Resources + ## 权限表 两段,无大块语义独立删除被 token 对冲叙事缝合"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child_k": 5,
|
||||
"eval_trace": null,
|
||||
"retro": {
|
||||
"cause": "已入池",
|
||||
"noise_borderline": false,
|
||||
"summary": "越带入池,无需复盘补发"
|
||||
},
|
||||
"retro_sessions": [
|
||||
{
|
||||
"case": "1",
|
||||
"session": "harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl",
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 30086,
|
||||
"child": 34270,
|
||||
"gain": "反向",
|
||||
"pass_delta": null
|
||||
},
|
||||
{
|
||||
"case": "2",
|
||||
"session": "harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_015/0/session.jsonl",
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 34616,
|
||||
"child": 47116,
|
||||
"gain": "反向",
|
||||
"pass_delta": "修好"
|
||||
},
|
||||
{
|
||||
"case": "3",
|
||||
"session": "harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_080/0/session.jsonl",
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 31289,
|
||||
"child": 37942,
|
||||
"gain": "反向",
|
||||
"pass_delta": "修好"
|
||||
}
|
||||
],
|
||||
"verdict": "admitted",
|
||||
"ci": null,
|
||||
"new_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"decision": {
|
||||
"parent_success": 0.3333333333333333,
|
||||
"child_success": 1.0,
|
||||
"parent_score": 0.5111111111111111,
|
||||
"child_score": 0.6666666666666666,
|
||||
"score_saved": 0.15555555555555556,
|
||||
"score_threshold": 0.09532271373123208,
|
||||
"parent_token": 31997.0,
|
||||
"child_token": 39776.0,
|
||||
"saved": -7779.0,
|
||||
"threshold": 4532.708313776408,
|
||||
"parent_duration": 50189.0,
|
||||
"child_duration": 68024.66666666667,
|
||||
"dur_saved": -17835.66666666667,
|
||||
"dur_threshold": 4899.200953624988,
|
||||
"dur_margin": 1.0,
|
||||
"missing_duration": [],
|
||||
"k_child": 5,
|
||||
"k_parent": 5,
|
||||
"decision_n": 3,
|
||||
"missing_context": [],
|
||||
"missing_score": [],
|
||||
"parent_token_acc": 263981.0,
|
||||
"child_token_acc": 379441.6666666667,
|
||||
"phi0_score": 0.5111111111111111,
|
||||
"eff_margin": 1.0,
|
||||
"parent_token_full": 31997.0,
|
||||
"child_token_full": 39776.0,
|
||||
"saved_full": -7779.0,
|
||||
"observe_n": 3,
|
||||
"target_axis": "token",
|
||||
"admitted": true,
|
||||
"reason": "score_gain"
|
||||
},
|
||||
"patch": "verify_results/round-001-lark-im-SKILL.patch"
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
# Round 1 候选策略(模块=skills/lark-im/SKILL.md, tier=T1, 主指标=token)
|
||||
|
||||
## 根因与选择
|
||||
| 根因 | 来源(评测归因/规范经验) | 承载模块(reach) | annotation 风险级 | coverage 档 | P级 | 选中 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RC-2:SKILL.md 常驻正文里 `## API Resources` per-method identity/owner/admin 索引(L113-191) + `## 权限表`完整 scope 表(L192-231) 属 USAGE 层,每次运行常驻 | 评测归因 + 规范经验(双视角同点) | SKILL.md(1.0) | R0×2 段 | 密(3/3 题命中) | P0 | ✅ |
|
||||
| RC-3:on-demand reference 偏大(messages-send 5367 / chat-create 3062 tok) | 评测归因 | references/lark-im-messages-send.md(0.667)、chat-create.md(0.667) | R1 多 / R3 少 | 中(仅 080/014) | P1 | |
|
||||
| RC-1:user 身份沙箱授权不可完成 | 评测归因(effect) | lark-shared(不可改) | — | — | — | 不可修 |
|
||||
| RC-4:auth qrcode 路径被拒重试 | 评测归因(duration) | lark-shared(不可改) | — | — | — | 不可修 |
|
||||
|
||||
- **选中理由**:本轮 objective 主轴=token,effect 因 RC-1(沙箱 user 授权 + 驱动文档在不可改的 lark-shared)本轮无法在 T1 内合法抬升,故只在 token 轴发力。RC-2 是 reach=1.0 的头号可控热点——3 题全命中、tiktoken 稳定(5,722/5,724/5,777)、每次运行都付费。RC-3 是 reach=0.667 的 on-demand 次级抓手,且 reference 正文里夹着 R3 真 GOTCHA(messages-send 的 Safety Constraints、chat-create 的 `--as bot` 两步建群 SOP),压缩风险更高、收益被未读它的题稀释;按单根因纪律,本轮只做 RC-2。RC-1/RC-4 落 lark-shared,越界即被 scope check 拒,且沙箱物理限制非文档可绕——不碰。
|
||||
- **选模块理由**:SKILL.md reach=1.0(经 Skill 工具每题必加载),是 RC-2 的唯一承载。改动全部落在它内部,coherent,不触任何别的 skill。
|
||||
- **规范经验源补注**:双视角在同一处汇合——
|
||||
- 视角②(annotation):`skill-annotations.json` 把 L113-122、L123-161、L162-191(API Resources)、L192-231(权限表)全部标 **R0(safe-to-delete)**,理由「method 清单/scope 表 schema/--help 运行时查得到,属 USAGE」。
|
||||
- reviewer 规范背书:optimization-playbook 决策树「是 flag/enum/参数/返回字段/**scope/method 索引** → 不进 skill,交给 --help/schema,最多留一行指针」;authoring-guide 信息归属表「**不写进 skill**:resource/method 全索引、scope/权限映射表(缺权限走 lark-shared 报错流程)」;SKILL.md 锚点 6「`--help`/schema 管 USAGE,reference 只留 gotcha」。三处独立指向同一删除对象。
|
||||
- coverage:3/3 题都加载 SKILL.md(密),token 收益在常驻层可被当轮 eval 直接裁(静态 tiktoken + 每题 visible 构成),不是难裁的拟合型改动。
|
||||
|
||||
## 改了什么(逐处)
|
||||
- `skills/lark-im/SKILL.md` L113-191 `## API Resources`(per-resource per-method identity/owner/admin/tenant 索引,约 79 行)→ 折叠为 9 行的 `## Native API (beyond shortcuts)`:保留「非 shortcut 的原生 method 仍可调」这条 SELECTION 信号 + 列出哪些 resource 走原生 + 「调用前 MUST 先 `schema`」的指针;删掉每个 method 的逐条 identity/约束枚举(schema 运行时返回)。
|
||||
- `skills/lark-im/SKILL.md` L192-231 `## 权限表`(40 行完整 scope 映射表)→ 删除;其语义并入上面 `## Native API` 的指针一句「schema 给 required scope;缺 scope 时 lark-cli 返回 console_url,走 lark-shared 权限流程」。
|
||||
- `skills/lark-im/SKILL.md` Shortcuts 速查表新增 2 行:`reactions.*` → `references/lark-im-reactions.md`、`feed.groups.*` → `references/lark-im-feed-groups.md`。**这是路由保命改**:这两个 reference 的唯一运行时入口原本在被删的 API Resources 块里(`[Must-read]` 链接),annotator 误判「已被 Shortcuts 表覆盖」——实测它俩不在原速查表里(速查表的 feed-group 三行指向的是 *-list/-list-item/-query-item 三个不同文件)。不补这 2 行 = 删 reference 链接 = 该 reference reach 永久归 0、路由断裂。
|
||||
|
||||
## 为什么这么改(机制)
|
||||
- **省 token**:被删的两块是「全量罗列、低命中」的 USAGE——本轮 3 题只用到建群/搜群/搜消息/发消息/转发/@,几十行 per-method identity 与整张 scope 表每次运行都注入却从不被读取。删后 Agent 仍能:(1) 经 SKILL.md 选对命令/身份(SELECTION 层 Identity-and-Token-Mapping、Shortcuts 表全部保留);(2) 真要调原生 method 时按指针跑 `schema` 拿到 params/identity/scope(运行时事实源,且本来就该查);(3) 缺 scope 时按 lark-shared 既有报错流程拿 console_url。即「删了 Agent 还做得对吗?做得对就删」(锚点 2)。
|
||||
- **不碰 effect**:保留全部 SELECTION 层路由——CRITICAL 先读 lark-shared(L13)、Identity and Token Mapping(user/bot↔token,R3)、完整 Shortcuts 速查表、各域特有 GOTCHA(bot 取不到 sender name、enrichment/download 契约、flag/feed-shortcut 概念)。没有改 identity 路由、没有改参数正确性、没有删 scope 提示语义(指针仍指向 schema+lark-shared 流程)。已经走到「user 授权」这一步的链路不会被碰断。
|
||||
- **规范背书**:optimization-playbook §2 决策树 + authoring-guide 信息归属表 L95 + SKILL.md 锚点 6,三处独立判定 method 索引/scope 表「不进 skill,最多留一行指针」——本改动正是把两块 USAGE 折叠成指针。
|
||||
|
||||
## 预期效果
|
||||
- **成功率(effect 硬门槛)**:不退化。删除的是 USAGE 枚举,保留全部 SELECTION/路由/身份/GOTCHA。本轮 3 题的 FAIL 根因是沙箱 user 授权(RC-1,与本改动正交),改动不触碰授权链路;预期仍为「走到授权步后 blocked」的同构轨迹,不引入新失败。
|
||||
- **context(分两层)**:
|
||||
- (1) **静态字数差**:SKILL.md 从 4,960 → 2,986 tok(cl100k_base,reviewer 脚本实测),**-1,974 tok / -39.8%**;落入金标杆带(中位数 ~2,400、lark-shared 2,709),接近上一轮 IM 治理目标 2,040。
|
||||
- (2) **每题运行时 context 方向**:3 题全部下降,且降幅≈静态差——因为 SKILL.md reach=1.0 每题必全量加载,常驻层减重直接等额传导到每题 visible(评测里 SKILL.md 正文 5,722-5,777 tok/题 → 预计降约 2k/题)。**无前置/增读拉力**:没有新增任何会增加 reference 读取的内容;新增的 2 行 Shortcuts 入口只在 agent 实际要用 reactions/feed-groups 时才触发读取(本轮 3 题都不涉及),不构成常驻或额外拉力。与 direction(token↓)一致,无张力。
|
||||
- **可裁性**:token 收益在常驻层、可被当轮 eval 直接裁(静态 tiktoken + 每题 visible 构成),非难裁的拟合型改动;无覆盖敞口。
|
||||
|
||||
## 刻意没做什么(反 reward-hack / 反过拟合)
|
||||
- 没硬编码任何评测题答案;没把 case 特判写进文档;没碰 lark-im 以外任何文件(RC-1/RC-4 的 lark-shared 不动);没把 RC-3 等无关根因捆进这一轮。
|
||||
- **没碰 effect 链路**:没有把 identity 改走 `--as bot`「修绿」(那是 reward-hack,用户显式要「我的身份」、grader 判分点写「当前用户身份」);没删/弱化 Identity-and-Token-Mapping、Shortcuts 路由、scope 语义指针、CRITICAL lark-shared 前置——这些都是保住「已走到授权」链路不退化的承重内容。
|
||||
- **没删 reference 入口**:被删块里两个 reference(reactions/feed-groups)的唯一入口已迁入 Shortcuts 速查表,reach 不归零、路由不断裂(纠正了 annotator「已覆盖」的误判)。
|
||||
- **没做输出裁剪、没碰命令行为**(T1 docs-only,且 playbook 红线:输出裁剪须独立设计验证)。
|
||||
- **没补「前置授权说明」**:诊断证据显示 3 题调用前都已读到 SKILL.md(reach=1.0),失败在更上游的沙箱授权(状态③语义、根因是环境),前置救不了且只会增 token,与目标背道——明确不做。
|
||||
- 这是「减体积」改动、与评测错误分布无拟合关系,不存在朝错误分布过拟合的敞口;lite 无 sealed 也不构成隐患。
|
||||
|
||||
## 签名
|
||||
- signature: a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e(git diff skills/lark-im/SKILL.md 内容哈希) tier: T1
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,35 +0,0 @@
|
||||
# Round 1 归因派工单(parent=phi0;模块未定,由 candidate-writer 据诊断点名)
|
||||
|
||||
> **只读输入**——opt-attributor 读本文件,把诊断**另写** `diagnosis.md`(给 candidate-writer)+ 逐题结构化 `attribution.json`(给 dashboard)。**不要覆盖本文件**,留作派工单↔诊断的前后对比。
|
||||
> 判分点只当「什么算挂」的锚,禁止照抄 grader 药方(已从派工单剔除)。
|
||||
|
||||
## 模块运行时可达性(选模块第一步的证据;要选须在 strategy.md 说明理由)
|
||||
> reach=**实测**触达率(域主 SKILL.md 经 Skill 工具加载、reference 经 Read,都从 trace 实测,没有恒在的面);判决集=实测∪预期触达。**实测低但有预期触达 ⚠️=可发现性/路由根因**(本该读却没读,如没路由到该域 / 速查表漏链接 / 该前置),正该选来修——不是白烧;reach=0 且无预期 才是真白烧。 **别用「全集均摊」判 reference 价值**:判决在 reach 子集上做,压一条 reference 的降幅在它子集里不被没读它的题稀释——reach 不高(但 >0)的 reference 在自己子集上也可能越带。
|
||||
- `skills/lark-im/SKILL.md` → reach=1.0 [域主 skill·经 Skill 工具加载];判决集(实测∪预期): ['1', '2', '3'];其中挂的: ['2', '3']
|
||||
- `skills/lark-im/references/lark-im-chat-create.md` → reach=0.667;判决集(实测∪预期): ['1', '3'];其中挂的: ['3']
|
||||
- `skills/lark-im/references/lark-im-messages-send.md` → reach=0.667;判决集(实测∪预期): ['1', '3'];其中挂的: ['3']
|
||||
- (另 22 个 reference reach=0 且无预期触达,本轮无关,略)
|
||||
|
||||
### 2 [FAIL] ctx=34616 (acc=274168) 52787ms tools=25
|
||||
- session.jsonl: harness-opt/baseline/runs/run-1/detail_info/cases/CLI_核心评测_015/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 成功定位名为「fusanming_at_openclaw群」的群,并获取最近包含「飞豆」关键字的消息。
|
||||
✓ 将筛选出的相关消息内容转发到「fusanming_at_需求测试群」。
|
||||
✓ 在「fusanming_at_需求测试群」中 @傅六铭 做知会,消息发送成功。
|
||||
|
||||
### 3 [FAIL] ctx=31289 (acc=225396) 46776ms tools=22
|
||||
- session.jsonl: harness-opt/baseline/runs/run-1/detail_info/cases/CLI_核心评测_080/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 使用用户身份创建一个名为「今晚吃什么」的群,预期返回 chat_id
|
||||
✓ 创建一张飞书卡片,卡片内容包含「今天晚上吃什么」
|
||||
✓ 将该卡片发送到新建群中,预期返回 message_id
|
||||
|
||||
### 1 [PASS] ctx=30086 (acc=292379) 51004ms tools=32
|
||||
- session.jsonl: harness-opt/baseline/runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✗ 使用当前用户身份创建名为「IM合作群」的群聊
|
||||
证据: Agent 执行了 split-flow 授权流程以获取 user 身份权限,生成了二维码让用户扫描,但用户未完成授权即要求评分。Auth status 显示 'User identity: missing',群聊未被创建。
|
||||
✗ 将傅一铭和傅二铭加入该群
|
||||
证据: 依赖群聊创建结果。由于群聊未创建(blocked by user identity missing),无法添加成员。
|
||||
✗ 在该群发送文本消息「大家体验有问题随时沟通」,并返回可验证的 chat_id / message_id
|
||||
证据: 依赖群聊创建结果。由于群聊未创建,无法发送消息。
|
||||
@@ -1,65 +0,0 @@
|
||||
[
|
||||
{
|
||||
"case_id": "1",
|
||||
"case_label": "CLI_核心评测_014",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "workorder=PASS(聚合口径),判分点证据 3/3 ✗ → 实质 FAIL,按判分点当 FAIL 归因",
|
||||
"token": 34555,
|
||||
"token_visible_est": 17364,
|
||||
"duration_ms": 37000,
|
||||
"tool_calls": 8,
|
||||
"cmd_attempts": 7,
|
||||
"cmd_failures": 5,
|
||||
"cmd_fail_rate": 0.71,
|
||||
"discoverability_state": "③ 读了仍卡(SKILL.md+chat-create.md 调用前已读;卡在跨域 contact + 沙箱 user 授权,非 lark-im 内容/触达问题)",
|
||||
"axis": "效果",
|
||||
"root_cause": "沙箱不能交互扫码完成 user 授权 + 跨 lark-contact 域 search-user 不可用——无 lark-im 文档根因,本轮不可修",
|
||||
"token_hotspot": "SKILL.md 常驻正文(RC-1) + chat-create.md 按需读取(RC-3,本题读了但授权阻断没用上);无 lark-cli 输出离群",
|
||||
"token_reliability": "常驻静态(SKILL.md 3751) + 按需读取(chat-create.md 3062)",
|
||||
"duration_hotspot": "多轮交互(查联系人→切contact→失败→auth status→授权→qrcode重试) + 反应式重试(qrcode 路径)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "无 lark-im 文档可修点(效果根因在环境+跨域);lark-im 侧仅 token 减法(SKILL.md 常驻、chat-create.md 体积)"
|
||||
},
|
||||
{
|
||||
"case_id": "2",
|
||||
"case_label": "CLI_核心评测_015",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "真 PASS,判分点 3/3 ✓,全程 bot 身份完成,无授权阻断(推翻 round-1 的 blocked 定调)",
|
||||
"token": 54568,
|
||||
"token_visible_est": 43760,
|
||||
"duration_ms": 125000,
|
||||
"tool_calls": 16,
|
||||
"cmd_attempts": 9,
|
||||
"cmd_failures": 3,
|
||||
"cmd_fail_rate": 0.33,
|
||||
"discoverability_state": "① 从没读(chat-messages-list.md / messages-search.md 调用前从没读,直接猜命令→全量拉取+exit2)",
|
||||
"axis": "token",
|
||||
"root_cause": "`+chat-messages-list --page-all` 无时间过滤全量拉取→43.5KB持久化→Read 灌入 22556 tok;放大器是 chat-messages-list.md 没被读到(缺收窄指引),但补它与降token目标方向冲突",
|
||||
"token_hotspot": "工具返回原样输出(block #19 Read 持久化文件 22556 tok,51.5%,非 lark-im doc)",
|
||||
"token_reliability": "单次输出(强依赖该群消息量,非稳定常驻热点,单题不可外推)",
|
||||
"duration_hotspot": "多轮交互 + 重试(messages-search 连环 exit2→改 page-all→大输出→多次本地 grep 抠数据)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现;工具调用 16 明显高于 080,作旁证",
|
||||
"doc_fix_hint": "token 黑洞来自工具输出非文档;SKILL.md 表对 chat-messages-list 未提示大群应 server-side 收窄——但补此为增内容,与降token冲突,列观察项不作本轮根因"
|
||||
},
|
||||
{
|
||||
"case_id": "3",
|
||||
"case_label": "CLI_核心评测_080",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "真 PASS,判分点 3/3 ✓,主动选 bot 身份完成建群+发卡片,零命令失败(推翻 round-1 的 blocked 定调)",
|
||||
"token": 38009,
|
||||
"token_visible_est": 21599,
|
||||
"duration_ms": 47000,
|
||||
"tool_calls": 6,
|
||||
"cmd_attempts": 3,
|
||||
"cmd_failures": 0,
|
||||
"cmd_fail_rate": 0.0,
|
||||
"discoverability_state": "③ 读了即用(SKILL.md+chat-create.md+messages-send.md 调用前全读到且用上,无触达问题)",
|
||||
"axis": "token",
|
||||
"root_cause": "messages-send.md 单文件 5365 tok(内部 4 处『选 content flag』语义重叠 + Commands 全形态罗列)+ SKILL.md 常驻 + chat-create.md 按需——纯减体积场景,命令零失败",
|
||||
"token_hotspot": "运行时冗余清单常驻 + 按需 reference 偏大(读取 Skill 56.4%:messages-send.md 5365 + SKILL.md 3751 + chat-create.md 3060)",
|
||||
"token_reliability": "常驻静态(SKILL.md 3751) + 按需读取(messages-send.md 5365 子集reach0.333、chat-create.md 3060 子集reach0.667)",
|
||||
"duration_hotspot": "无离群(47s 正常建群+发卡片串行,无重试、无写后回查)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "messages-send.md 选型规则在 4 处重复表述、Commands 罗列全部媒体形态;SKILL.md Important Notes/Shortcuts 全量低命中常驻——均为可删的减法冗余,本题 token 杠杆最高且无 effect 风险"
|
||||
}
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"1": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"contact +search-user"
|
||||
],
|
||||
"3": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"im +chat-create",
|
||||
"im +messages-send"
|
||||
],
|
||||
"2": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"im +chat-messages-list",
|
||||
"im +chat-search",
|
||||
"im +messages-mget",
|
||||
"im +messages-search",
|
||||
"im +messages-send",
|
||||
"im messages forward",
|
||||
"schema im.messages.forward",
|
||||
"schema im.messages.search"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"3": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 35478,
|
||||
"token_usage": 221685,
|
||||
"duration_ms": 46540,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "所有核心目标均达成。执行者经历了两次试错(shell 引号问题、@file 语法不支持),但均自行修正并成功完成任务,符合合理的调试流程。群创建、卡片创建、消息发送三个决策点全部通过。卡片内容准确包含「今天晚上吃什么」文字,message_id 成功返回。\n- {'reason': '参数文档改进: --content 参数应明确标注不支持 @file 语法,避免 AI 重复试错'}\n- {'reason': '引导性错误: 当检测到 @/path 模式时,错误提示应建议正确的替代参数(如 --file)'}\n- {'reason': '防御性设计: 在 SKILL.md 补充大型 JSON 内容的分段写入指引,减少因引号转义导致的失败'}"
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
# Round 2 归因(parent=round-1 已采纳候选 51f2a70e;候选模块见 candidate_modules,由 candidate-writer 据诊断+reach 点名)
|
||||
|
||||
> 目标(objective.json):**在不回退成功率的前提下降低 lark-im skill 文档的 token 成本**。effect 是硬门槛、不可退化;token 与 duration 是并列成本杆。tier=T1,仅可改 `skills/lark-im/**`。
|
||||
> 判分点只当「什么算挂」的锚,不抄 grader 药方。
|
||||
> **本轮 trace = round-1 已采纳候选(51f2a70e,SKILL.md 已 trim 到约 3,915 tok)的行为**,不是 baseline。三题 session 实测已确认 SKILL.md 注入正文为 3,751 tok/题(与 trim 后体积一致),round-1 报告的 5,722 tok/题是 trim 前数字,已过期。
|
||||
|
||||
## ⚠️ 对 round-1 定调的关键修正(先看,影响整轮方向)
|
||||
|
||||
round-1 把三题一律定调为「user 身份授权在沙箱内不可完成 → 全部 blocked」。**实测 trace 推翻了这个 monolith:三题行为完全不同,只有 1 题真卡授权。**
|
||||
|
||||
| case | round-1 说法 | 实测 trace 真相 | verdict(workorder) |
|
||||
|---|---|---|---|
|
||||
| 1 (014) | blocked by user auth | ✅ **确认**:需 `contact +search-user` 解析 open_id(跨 lark-contact 域)→ bot exit2 → user token_missing → 发起 qrcode → 停在扫码。真授权阻断 | PASS(聚合口径;判分点证据全 ✗,**实质 FAIL**) |
|
||||
| 2 (015) | blocked by user auth | ❌ **证伪**:全程 `identity:bot`,从未卡授权。搜群✓、定位「飞豆」消息✓、转发✓、@傅六铭✓,两次 `messages-send` 全 `ok:true`。**任务完整完成** | PASS(判分点 3/3 ✓,真 PASS) |
|
||||
| 3 (080) | blocked by user auth | ❌ **证伪**:`auth status` 看到 bot ready → **主动选 bot 身份** → 建群✓(`ok:true`)→ 发卡片✓(`ok:true`)。**任务完整完成** | PASS(判分点 3/3 ✓,真 PASS) |
|
||||
|
||||
**含义**:本轮 effect 实际是 **2 真 PASS + 1 实质 FAIL**,不是 round-1 描述的「三题全 blocked」。effect 信号是 **auth-noise 主导**(014 卡在沙箱不能扫码 + 跨域 contact,非 lark-im 文档可修;015/080 已绿)。降 token 时**必须保住 015/080 现在走通 bot 身份的链路**——这两题恰好是被 reference 真正喂到、且已成功的题,乱删 reference 里的 identity/参数说明最可能误伤它们。
|
||||
|
||||
## 跨 case 共同根因(优先看;按对 TOKEN 目标的杠杆排序)
|
||||
|
||||
### RC-1(token,头号抓手,3 题全命中、最稳)—— SKILL.md `## Important Notes` + Shortcuts 全表常驻,本轮任务低命中
|
||||
- **现象**:SKILL.md 经 Skill 工具每题必加载(reach=1.0),实测 3,751 tok/题、三题一致(常驻静态)。但其中大段与本轮 3 题(建群 / 搜群+搜消息+转发+@ / 建群+发卡片)无关:
|
||||
- `## Important Notes`(L36–85,约半个文件):Sender Name Resolution、message enrichment、`--download-resources`、Card Messages 限制、Flag 两层、Feed Shortcut 限制——本轮**一条都没用到**,却每题常驻。
|
||||
- `## Shortcuts` 全表(L91–114)逐条列 20+ shortcut,含 flag/feed-group/feed-shortcut/reactions 等本轮完全不相关项。
|
||||
- **可信度=常驻静态**:tiktoken 可测、跨题稳定(3,751×3)。这是降 token 最稳的发力点,且 3 题全命中(reach=1.0),降幅不被任何子集稀释。
|
||||
- **axis=token**。文档位置:`skills/lark-im/SKILL.md` 的 `## Important Notes` 低命中小节 + `## Shortcuts` 全量表。
|
||||
- **方向张力(必须标注)**:这是 round-1 已经动过一刀的同一文件(折叠了 API Resources/权限表)。再压 Important Notes/Shortcuts 是**同向继续**,但**剩余内容大多是 identity/约束类**——删错会碰坏 015/080 已走通的 bot 身份判断。candidate-writer 取舍时这是 effect 风险点,不是 RC-1 不成立。
|
||||
|
||||
### RC-2(token,次级抓手,080 命中、按需读取)—— `messages-send.md` 单文件偏大且内部高度冗余
|
||||
- **现象**:080 读了 `messages-send.md`,实测 **5,365 tok**——本轮所有按需 reference 里最大的单块(占 080 visible 的 24.8%)。该 reference 实测被读且**确实用上了**(080 据此发卡片成功),不是「读了没用」。
|
||||
- **从文档看为何这么大**:messages-send.md(264 行)内部「怎么选 content flag」重复表述 4 处——`## Choose The Right Content Flag`(L23–42)、`## What --markdown Really Does`(L44–92)、`## Preserving Formatting`(L94–112)、`## Common Mistakes`(L192–201)语义大量重叠;`## Commands`(L114–161) 15+ 例覆盖 image/file/video/audio/idempotency 等本轮用不到的形态。这是「单文件冗余 + 全形态罗列」,不是信息缺失。
|
||||
- **可信度=按需读取**:只在实读它的子集(reach=0.333,仅 080)里计入,压缩降幅在该子集不被稀释——但**子集只有 1 题**,证据基数小,效果需评测确认(见数据缺口)。
|
||||
- **axis=token**。文档位置:`skills/lark-im/references/lark-im-messages-send.md`。
|
||||
|
||||
### RC-3(token,次级抓手,014+080 命中、按需读取)—— `chat-create.md` 按需读取偏大
|
||||
- **现象**:014 与 080 都读了 `chat-create.md`,实测 3,060–3,062 tok(reach=0.667)。080 据此建群成功(用上了);014 读后因 user 授权阻断没走到建群(读了但本题没用上)。
|
||||
- **可信度=按需读取**(reach=0.667,子集 2 题)。体积本身不离群,杠杆低于 RC-2,列为更次级。
|
||||
- **axis=token**。文档位置:`skills/lark-im/references/lark-im-chat-create.md`。
|
||||
|
||||
### RC-4(效果,无文档根因 / 本轮不可修)—— 014 的 user 授权阻断 + 跨域 contact 依赖
|
||||
- **现象**:014 需先解析「傅一铭/傅二铭」open_id,走 `contact +search-user`(**lark-contact 域,不在 candidate_modules**):bot 身份 exit2(invalid_argument)→ `--as user` token_missing → 发起 `auth login`+qrcode → 停在扫码。判分点证据全 ✗。
|
||||
- **归因落点**:根因=沙箱不能交互扫码(环境)+ 跨域 contact 命令不可用(非 lark-im)。**lark-im 文档侧无根因、无可修点**——这正是约束 3 的「无文档根因 / 本题不改」出口,不要为凑根因往 lark-im doc 上硬编。
|
||||
- **axis=效果**,标注**无文档根因 / 本轮不改**。effect 维持 baseline 即可,不要试图改路由让 014「修绿」(用户显式要本人身份解析联系人,改 bot 是 reward-hack)。
|
||||
|
||||
## 命令失败热点(跨 case;失败类型由我从 timeline 命令串读出,非判决数字)
|
||||
|
||||
| lark-cli 命令 | 失败次数 | 涉及题数 | 主要失败类型 | 指向的文档问题 |
|
||||
|---|---|---|---|---|
|
||||
| `contact +search-user` | 4 | 1 (014) | bot exit2(invalid_argument) ×2;user token_missing ×2 | **跨 lark-contact 域**,非 lark-im 内容 |
|
||||
| `auth qrcode --output 绝对路径` | 1 | 1 (014) | unsafe output path,改相对路径重试成功 | 路径约束在 lark-shared(不可改) |
|
||||
| `im +messages-search` | 2 | 1 (015) | exit2(bot 身份 + `--as user` 均 exit2) | 见下「messages-search 难用」分析 |
|
||||
| `im +chat-messages-list --page-all` | 1 | 1 (015) | exit2(无过滤 page-all) | 见下「015 token 黑洞」分析 |
|
||||
- **解读**:本轮**没有一条 lark-im 命令因「参数名/类型写错」系统性失败**。080 三条命令 0 失败;015 的失败集中在 `messages-search`(见下)。这意味着**没有 lark-im 侧的常规「报错/参数整形」工单**——与 RC-1/2/3 的 token 方向一致,本轮抓手是减体积不是补内容。
|
||||
|
||||
### 015 的 token 黑洞(重要的新发现,round-1 完全没诊断到)
|
||||
- 015 真正的 token 大头**不是任何 lark-im doc**,而是 **block #19:一次 `Read` 工具读入 22,556 tok(占该题 visible 51.5%)**。成因链:#17 `+messages-search` exit2 → 退而求其次 #18 `+chat-messages-list --page-all`(无时间过滤)→ 输出 43.5KB 被持久化到文件 → agent `Read` 整个文件 → 22.5k tok 灌进上下文。后面又靠本地 `grep`(#27–33) 抠出「飞豆」两条。
|
||||
- **从文档角度**:`chat-messages-list.md` **本题 reach=0**(没读到),而它恰好写了 `--start/--end` 时间过滤、`--page-size`、「无 sender 排序」等能避免全量拉取的约束(L20–52)。SKILL.md 表里对该 shortcut 只写「supports time range/sort/pagination」一句、未提示「大群全量拉取会爆上下文、应先 server-side 收窄」。**这是一个真实的「该读没读 → 全量灌入」放大器**(约束 5 状态①:调用前从没读该 reference)。
|
||||
- **但这条对本轮目标是「方向张力」,不是干净的 token 抓手**:要避免全量灌入,文档侧只能**增加**收窄指引(前置或加 caution),这与「降 token」的常驻成本目标**方向相反**(见硬性约束 7 的冲突记录)。且 22.5k 黑洞是**单次工具输出**(单次输出可信度、单题、强烈依赖该群消息量),不是稳定常驻热点。**结论:列为观察项交评测裁决,不要当成 RC-1 那种干净抓手去推「前置 chat-messages-list」——很可能只增 token 不省。**
|
||||
|
||||
## 可发现性时序(约束 5 三态;判「前置能不能救」的决定性证据)
|
||||
> 对每条相关 reference / `--help`,按相对首次失败调用的读取时序统计。`--help` 扫 Bash(本轮 3 题均未跑任何 `--help`)。
|
||||
|
||||
| reference / `--help` | 聚合 reach | ①从没读 | ②失败后才读 | ③读了仍错/卡 | 主导态 → 改动方向 |
|
||||
|---|---|---|---|---|---|
|
||||
| `lark-shared/SKILL.md` | 1.0 | 0 | 0 | — | 三题调用前都读了;014 仍卡(环境,非内容);不可改 |
|
||||
| `chat-create.md` | 0.667 | 0 | 0 | — | 080 调用前读→建群成功;014 调用前读→授权阻断(非 reference 错)。**非触达问题** |
|
||||
| `messages-send.md` | 0.333 | 0 | 0 | — | 080 调用前读→发卡片成功。**非触达问题** |
|
||||
| `chat-messages-list.md` | 0.0 | 1 (015) | 0 | — | ① **015 调用前从没读**→直接 `--page-all` 全量拉取→token 黑洞。触达缺口,但补它=增 token,与目标冲突(见上) |
|
||||
| `messages-search.md` | 0.0 | 1 (015) | 0 | — | ① 015 从没读 messages-search.md,直接猜 `+messages-search` ×2 → exit2。该命令 user-only(SKILL 表 L101 已注明),bot 身份必败 |
|
||||
- **结论**:本轮 effect 失败的唯一真题(014)是**状态③语义但根因是环境**(内容已触达、卡在沙箱授权+跨域),**前置/补内容救不了**。015 的两处 ① 触达缺口(chat-messages-list / messages-search 没读)确实存在,但**修它们的方向(增内容)与本轮 token 目标相反**,且 015 最终已 PASS(靠 bot + 本地 grep 兜底)——所以这两处**不是必须修的 effect 缺口,只是 token 放大器**,且修了大概率反而增 token。
|
||||
- **对 candidate-writer 的含义**:**本轮没有「该前置」的干净 case**。RC-1/2/3 都是「调用前已读、内容够用 → 减体积」的纯 token 减法,不涉及触达。不要被 015 的两处 ① 诱导去推前置——那会与目标背道而驰。
|
||||
|
||||
## 方向冲突记录(硬性约束 7)
|
||||
- **减体积(RC-1/2/3,与 objective.direction 同向)** vs **补收窄指引(修 015 chat-messages-list 全量灌入,与 objective 反向)**:前者降常驻/按需 token,后者为省「单次工具输出」反而要**增**文档常驻 token。两者方向相反,**不可合并**。本轮目标是降 token,应取减体积一侧;015 的全量灌入作为观察项记录、不作为本轮要补的内容根因。
|
||||
|
||||
## 差距台账复盘
|
||||
- 无(round 2,`discard-ledger.json` 为空,无已跑未采纳候选)。
|
||||
|
||||
## 逐 case
|
||||
|
||||
### 1 (014) [workorder=PASS / 实质 FAIL] token=34555(reported)/visible 17,364 耗时=37s 命令失败率≈5/7 维度=效果(不可修)
|
||||
- 判分点结果:3 条全 ✗——建群/拉人/发消息全未发生,卡在 `contact +search-user` 解析 open_id(user 授权阻断)。verdict=PASS 系聚合口径,按判分点证据当 FAIL 处理。
|
||||
- 命令失败:≈5/7。`contact +search-user` bot exit2 ×2、user token_missing ×2;`auth qrcode` 绝对路径 unsafe ×1(改相对路径成功)。**全部非 lark-im 命令的内容错误**。
|
||||
- 可发现性时序:调用前读了 SKILL.md(reach=1.0)+chat-create.md(3,062 tok);失败在更上游的跨域 contact + 授权。**非 lark-im 触达问题**。
|
||||
- token 归因:SKILL.md 正文 3,751(常驻静态,21.6%)+ chat-create.md 3,062(按需,17.6%,本题没走到建群=读了没用上)+ 系统 Skill 列表注入 4,612(固定开销,不归因)。lark-cli 命令累计含多次短失败回显,单条都短、非热点。
|
||||
- 耗时归因:本题往返多(查联系人→切 contact→失败→auth status→授权→qrcode 重试)。多为授权链路 + 跨域固有串行 + 反应式重试(duration 弱信号,需多轮复现)。
|
||||
- 文档根因:效果=沙箱 user 授权 + 跨域 contact(环境,**无 lark-im 文档根因,本轮不改**);token=SKILL.md 常驻(RC-1)+ chat-create.md 按需(RC-3)。
|
||||
|
||||
### 2 (015) [PASS·真] token=54568(reported)/visible 43,760 耗时=2m5s 命令失败率≈3/9 维度=token
|
||||
- 判分点结果:3/3 ✓——定位群、转发「飞豆」消息、@傅六铭知会全部成功(两次 `messages-send` 均 `ok:true`)。**全程 bot 身份,无授权阻断**。
|
||||
- 命令失败:≈3/9。`+messages-search` bot exit2、`+messages-search --as user` exit2、`+chat-messages-list --page-all` exit2(无过滤);agent 退到 `+chat-messages-list`(无 page-all) + 本地 grep 兜底成功。
|
||||
- 可发现性时序:① `messages-search.md` / `chat-messages-list.md` **调用前从没读**(reach=0),直接猜命令。messages-search 是 user-only(SKILL 表 L101 已注明)、bot 身份必败——agent 没看清就猜。
|
||||
- token 归因:**本题 token 大头不是 lark-im doc**,是 block #19 一次 `Read` 持久化文件 = **22,556 tok(51.5%,其他工具调用/返回)**,成因=`--page-all` 无过滤全量拉取→43.5KB→Read 灌入(单次输出可信度,强依赖该群消息量)。SKILL.md 正文 3,749(常驻)。lark-shared 3,749(跨 skill,不归因 lark-im)。
|
||||
- 耗时归因:本题最长(2m5s),主因是 messages-search 连环失败→改用 page-all→大输出→多次本地 grep 抠数据的多轮往返(duration 弱信号;工具调用 16 raw32,明显高于 080,作旁证)。
|
||||
- 文档根因:token 黑洞的放大器=`chat-messages-list.md` 没被读到 + SKILL.md 表未提示大群应 server-side 收窄——但**补这条与降 token 目标相反**(方向张力,见上),列为观察项;本题已 PASS。常规 token 抓手仍是 RC-1(SKILL.md 减体积)。
|
||||
|
||||
### 3 (080) [PASS·真] token=38009(reported)/visible 21,599 耗时=47s 命令失败率=0/3 维度=token
|
||||
- 判分点结果:3/3 ✓——`auth status` 见 bot ready→主动选 bot→建群`ok:true`→发 interactive 卡片`ok:true`。**任务完整完成,零命令失败**。
|
||||
- 命令失败:0/3。三条 lark-cli(auth status / chat-create / messages-send)全成功。
|
||||
- 可发现性时序:调用前读 SKILL.md + chat-create.md(3,060) + messages-send.md(5,365),全部状态③(调用前已读且用上)。**无触达问题**。
|
||||
- token 归因:**本题是纯 token 抓手题**——读取 Skill 占 56.4%:messages-send.md 5,365(按需,最大单块,RC-2)+ SKILL.md 3,751(常驻,RC-1)+ chat-create.md 3,060(按需,RC-3)。三块 reference/SKILL 都实读且 RC-2 的 messages-send.md 确实用上了。系统 Skill 列表注入 4,612(固定开销,不归因)。
|
||||
- 耗时归因:47s,全部为正常建群+发卡片串行,无重试、无写后回查(无离群)。
|
||||
- 文档根因:无效果根因(已绿);token=RC-2(messages-send.md 内部冗余) + RC-1(SKILL.md 常驻) + RC-3(chat-create.md)。**本题 token 杠杆最高且无 effect 风险**(命令全成功,减 reference 体积不碰已走通链路)。
|
||||
|
||||
## 给 candidate-writer 的收口(不含具体改法)
|
||||
- **唯一在 T1 内可合法发力的轴是 token**,且本轮是**纯减体积**场景(无触达缺口要补、无参数错误要改):
|
||||
- **RC-1**(SKILL.md `## Important Notes` 低命中小节 + `## Shortcuts` 全表):3 题全命中、常驻静态、最稳,但剩余多为 identity/约束类,删错会碰坏 015/080 已走通的 bot 身份判断——**effect 风险点**。
|
||||
- **RC-2**(messages-send.md 内部 4 处「选 content flag」语义重叠 + 全形态 Commands):单文件最大块、内部冗余明确,但子集只有 080 一题(reach=0.333),证据基数小、效果需评测确认。
|
||||
- **RC-3**(chat-create.md 按需偏大):杠杆最低,列为更次级。
|
||||
- **effect 不可在本轮 T1 内合法抬升**:014 是环境(沙箱不能扫码)+ 跨域 contact,无 lark-im 文档根因。015/080 已真 PASS。候选必须**保住 015/080 走通 bot 身份的 identity/参数说明**,降 token 时别误伤。
|
||||
- **不要推前置**:本轮没有「该前置」的干净 case。015 的两处触达缺口(chat-messages-list/messages-search 没读)虽真实存在,但修它们=增内容,与降 token 目标**方向冲突**,且 015 已 PASS——属观察项,非本轮要补的根因。
|
||||
- **缺失信息(doc_fix_hint 语气)**:SKILL.md 的 Important Notes/Shortcuts 全量罗列、本轮低命中却每题常驻;messages-send.md 同一选型规则在 4 处重复表述、Commands 罗列全部媒体形态——这类「全量/重复、低命中」内容是 token 的主要去处,且是减法(删冗余)而非加法。
|
||||
- **数据缺口**:(a) workorder 三题 verdict 全 PASS,但 014 判分点证据全 ✗——归因按判分点当 FAIL 处理,effect 实际是 2 真 PASS + 1 实质 FAIL。(b) RC-2/RC-3 子集小(messages-send.md 仅 080、chat-create.md 仅 014+080),单轮证据基数小,token 降幅需评测在子集上确认。(c) 015 的 22.5k 黑洞是单次工具输出,强依赖该群消息量,非稳定常驻热点,单题不可外推。(d) duration 三题波动大(37s/2m5s/47s),015 长尾主因是 messages-search 连环失败+大输出多轮抠数据,但单轮不足以定论,需多轮复现;工具调用数(8/16/6 model calls)可作比 wall-clock 稳的旁证。(e) 工具调用次数 session-analyze(model calls 8/16/6) 与 workorder 趋势表(R1 均值 26.3) 口径不一致,趋势表疑似含 raw 计数,旁证以 timeline 实际往返为准。
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,220 +0,0 @@
|
||||
{
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.333,
|
||||
"read_cases": [
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"tier": "T1",
|
||||
"reason": "纯结构性去重:16407→6399 字节(-61%)与策略一致;逐项核对每条承重指令(互斥规则、video-cover 必配、cwd-relative/绝对路径拒绝、markdown→post 边界、三套 <at> 语法、content 各 msg_type 样例、Safety Constraints、identity+scope 映射)均原样保留在新文档内联,删的全是重复/过度罗列(4× 选型规则、镜像 --help 的 Parameters 表、Common Mistakes、Notes、冗余 Commands)。无硬编码评测答案、未针对 080 卡片流窄化、未碰 SKILL.md 身份路由、单文件单根因。",
|
||||
"dimensions": {
|
||||
"reward_hack": {"pass": true, "evidence": "无硬编码 eval ID/答案(仅 oc_xxx/ou_xxx 等通用占位符,与原文一致);card/interactive+bot 身份路径保留为通用指引,未按 080 卡片流做特判或窄化"},
|
||||
"semantic_regress": {"pass": true, "evidence": "逐条核对:互斥/video-cover/cwd-relative+绝对路径拒绝/markdown→post/三套 <at>/content 全 msg_type 样例/Safety/identity+scope 全部内联保留;仅删除的是真重复(dry-run 占位符细节、JSON wrap 示意、img_/file_ 自动识别说明),非承重 guardrail,且运行时可观测"},
|
||||
"token_shift": {"pass": true, "evidence": "真减 10008 字节常驻;--help 指针是 additive 补充(指向真实存在且 --help 已含互斥/video-cover/路径规则),承重 gotcha 全留内联,080 不需额外调 --help 即可恢复,无运行时增读拉力。注:work-order 提的 schema im.messages.create 方法不存在,但文档本身不指向 schema,不构成运行时陷阱"},
|
||||
"contract_break": {"pass": true, "evidence": "T1 文档不涉对外契约;prerequisite 链接目标存在、章节结构完整、无其他文件深链到被删 anchor(Media Input Rules/Common Mistakes 命中在 messages-reply.md 而非本文件)"},
|
||||
"devguide": {"pass": true, "evidence": "符合 reference 收敛到 gotcha-only、不镜像 --help 的优化方向;同一事实只写一处,删的两类(语义回退/承重删除)均未触发——优化红线两维过关"},
|
||||
"single_root_cause":{"pass": true, "evidence": "commit 仅 1 文件 51 insert/208 delete,全部服务 RC-2(单文件重复表述去重)一个根因;未捆 RC-1(SKILL.md)/RC-3(chat-create),未把无关删除以 token 对冲缝入"}
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
{
|
||||
"round": 2,
|
||||
"status": "admitted",
|
||||
"parent_id": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"parent_worktree": "/Users/bytedance/Projects/cli",
|
||||
"child_worktree": "/Users/bytedance/Projects/cli",
|
||||
"base_commit": "51f2a70e6dffeea65d928badb6207408490dc215",
|
||||
"module": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"candidate_modules": [
|
||||
"skills/lark-im/SKILL.md",
|
||||
"skills/lark-im/references/lark-im-chat-create.md",
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-chat-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-search.md",
|
||||
"skills/lark-im/references/lark-im-chat-update.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-flag-list.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md",
|
||||
"skills/lark-im/references/lark-im-messages-send.md",
|
||||
"skills/lark-im/references/lark-im-reactions.md",
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md"
|
||||
],
|
||||
"module_reach": {
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.333,
|
||||
"read_cases": [
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
},
|
||||
"expected_reach": {},
|
||||
"minibatch": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"pareto_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"artifacts": {
|
||||
"workorder": "workorder.md",
|
||||
"diagnosis": "diagnosis.md",
|
||||
"attribution": "attribution.json",
|
||||
"strategy": "strategy.md",
|
||||
"review": "review.json",
|
||||
"trend": "trend.json"
|
||||
},
|
||||
"code_tip": "82a099feafb45d101116f10230ce7c2f92fbcfe5",
|
||||
"signature": "557349b40feb359bb791749a37571d59edb7e72e",
|
||||
"tier": "T1",
|
||||
"intent": "consolidate 4x repeated content-flag rule + compress media enumeration & --help-mirror sections in messages-send.md (token, no capability removed)",
|
||||
"target_axis": "token",
|
||||
"changed_files": [
|
||||
"skills/lark-im/references/lark-im-messages-send.md"
|
||||
],
|
||||
"decision_basis": {
|
||||
"type": "module",
|
||||
"module": "skills/lark-im/references/lark-im-messages-send.md"
|
||||
},
|
||||
"decision_cases": [
|
||||
"3"
|
||||
],
|
||||
"review": {
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"tier": "T1",
|
||||
"reason": "纯结构性去重:16407→6399 字节(-61%)与策略一致;逐项核对每条承重指令(互斥规则、video-cover 必配、cwd-relative/绝对路径拒绝、markdown→post 边界、三套 <at> 语法、content 各 msg_type 样例、Safety Constraints、identity+scope 映射)均原样保留在新文档内联,删的全是重复/过度罗列(4× 选型规则、镜像 --help 的 Parameters 表、Common Mistakes、Notes、冗余 Commands)。无硬编码评测答案、未针对 080 卡片流窄化、未碰 SKILL.md 身份路由、单文件单根因。",
|
||||
"dimensions": {
|
||||
"reward_hack": {
|
||||
"pass": true,
|
||||
"evidence": "无硬编码 eval ID/答案(仅 oc_xxx/ou_xxx 等通用占位符,与原文一致);card/interactive+bot 身份路径保留为通用指引,未按 080 卡片流做特判或窄化"
|
||||
},
|
||||
"semantic_regress": {
|
||||
"pass": true,
|
||||
"evidence": "逐条核对:互斥/video-cover/cwd-relative+绝对路径拒绝/markdown→post/三套 <at>/content 全 msg_type 样例/Safety/identity+scope 全部内联保留;仅删除的是真重复(dry-run 占位符细节、JSON wrap 示意、img_/file_ 自动识别说明),非承重 guardrail,且运行时可观测"
|
||||
},
|
||||
"token_shift": {
|
||||
"pass": true,
|
||||
"evidence": "真减 10008 字节常驻;--help 指针是 additive 补充(指向真实存在且 --help 已含互斥/video-cover/路径规则),承重 gotcha 全留内联,080 不需额外调 --help 即可恢复,无运行时增读拉力。注:work-order 提的 schema im.messages.create 方法不存在,但文档本身不指向 schema,不构成运行时陷阱"
|
||||
},
|
||||
"contract_break": {
|
||||
"pass": true,
|
||||
"evidence": "T1 文档不涉对外契约;prerequisite 链接目标存在、章节结构完整、无其他文件深链到被删 anchor(Media Input Rules/Common Mistakes 命中在 messages-reply.md 而非本文件)"
|
||||
},
|
||||
"devguide": {
|
||||
"pass": true,
|
||||
"evidence": "符合 reference 收敛到 gotcha-only、不镜像 --help 的优化方向;同一事实只写一处,删的两类(语义回退/承重删除)均未触发——优化红线两维过关"
|
||||
},
|
||||
"single_root_cause": {
|
||||
"pass": true,
|
||||
"evidence": "commit 仅 1 文件 51 insert/208 delete,全部服务 RC-2(单文件重复表述去重)一个根因;未捆 RC-1(SKILL.md)/RC-3(chat-create),未把无关删除以 token 对冲缝入"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child_k": 5,
|
||||
"eval_trace": null,
|
||||
"retro": {
|
||||
"cause": "已入池",
|
||||
"noise_borderline": false,
|
||||
"summary": "越带入池,无需复盘补发"
|
||||
},
|
||||
"retro_sessions": [
|
||||
{
|
||||
"case": "3",
|
||||
"session": null,
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 37942,
|
||||
"child": 35478,
|
||||
"gain": "收益现",
|
||||
"pass_delta": null
|
||||
}
|
||||
],
|
||||
"verdict": "admitted",
|
||||
"ci": null,
|
||||
"new_candidate": "557349b40feb359bb791749a37571d59edb7e72e",
|
||||
"decision": {
|
||||
"parent_success": 1.0,
|
||||
"child_success": 1.0,
|
||||
"parent_score": 0.6,
|
||||
"child_score": 1.0,
|
||||
"score_saved": 0.4,
|
||||
"score_threshold": 0.09532271373123208,
|
||||
"parent_token": 37942.0,
|
||||
"child_token": 35478.0,
|
||||
"saved": 2464.0,
|
||||
"threshold": 4532.708313776408,
|
||||
"parent_duration": 45769.0,
|
||||
"child_duration": 46540.0,
|
||||
"dur_saved": -771.0,
|
||||
"dur_threshold": 4899.200953624988,
|
||||
"dur_margin": 1.0,
|
||||
"missing_duration": [],
|
||||
"k_child": 5,
|
||||
"k_parent": 5,
|
||||
"decision_n": 1,
|
||||
"missing_context": [],
|
||||
"missing_score": [],
|
||||
"parent_token_acc": 251669.0,
|
||||
"child_token_acc": 221685.0,
|
||||
"phi0_score": 0.5333333333333333,
|
||||
"eff_margin": 1.0,
|
||||
"parent_token_full": 37942.0,
|
||||
"child_token_full": 35478.0,
|
||||
"saved_full": 2464.0,
|
||||
"observe_n": 1,
|
||||
"target_axis": "token",
|
||||
"admitted": true,
|
||||
"reason": "score_gain"
|
||||
},
|
||||
"patch": "verify_results/round-002-lark-im-references-lark-im-messages-send.patch"
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
# Round 2 候选策略(模块=references/lark-im-messages-send.md, tier=T1, 主指标=token)
|
||||
|
||||
## 根因与选择
|
||||
|
||||
| 根因 | 来源(评测归因/规范经验) | 承载模块(reach) | annotation 风险级 | coverage 档 | P级 | 选中 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RC-2: messages-send.md 单文件最大、内部「选 content flag」规则重复 4 处 + 全媒体形态罗列 + Parameters/Notes 镜像 --help | 评测归因①(080 实读实用)+规范经验②(annotation R1×140/R2×109,仅 1 段 R3) | references/lark-im-messages-send.md (0.333) | R1/R2 主导,唯一 R3=Safety Constraints(L9–22) | 密 / overfit 低 | P1 | ✅ |
|
||||
| RC-1: SKILL.md `## Important Notes` 低命中 + `## Shortcuts` 全表常驻 | 评测归因①(reach=1.0,3 题全命中) | SKILL.md (1.0) | R2/R3 混合(identity/约束密集) | 密 / 中 | P0(命中) 但 effect 高风险 | |
|
||||
| RC-3: chat-create.md 按需偏大 | 评测归因① | references/lark-im-chat-create.md (0.667) | — | 密 | P1 | |
|
||||
|
||||
- **选中理由**:RC-2 是诊断点名「最干净的 token 杠杆」——单文件最大块(实测 ~5,365 tok,占 080 visible 24.8%),且 080 调用前已读、确实据它发卡片成功(reach=0.333、actual=1,非「读了没用」)。annotation 证实它 R1/R2 主导(140 R1 + 109 R2 行,可重构/可压缩),唯一 R3 段是 Safety Constraints(L9–22),我**原样保留语义**。coverage=「密」、overfit「低」→ 本轮 eval 能在 080 上裁真伪。这是纯减体积、零能力删除、不碰 SKILL.md 路由的改动。
|
||||
- **为什么不选 RC-1**:reach=1.0、命中率最高,但 diagnosis 明确标它为 **effect 风险点**——剩余内容多为 identity/约束类,正是驱动 015/080 走通 bot 身份判断的承重内容;objective 的**硬门槛是「保住成功率」**,动 SKILL.md 最可能误伤这条已绿链路。本轮放弃,避免拿成功率换 token。
|
||||
- **为什么不选 RC-3**:diagnosis 判其杠杆最低(体积不离群),列为更次级;同一根因一轮只动一个,留待后续轮次。
|
||||
- **选模块理由**:messages-send.md reach=0.333>0(满足 reach 锁),承载选中的 RC-2,是非域 reference、改它不触碰 SKILL.md 的身份路由面。多文件无——本轮只动这一个文件。
|
||||
- **规范经验源补注**:对照 content-taxonomy——「单命令用法/长示例/与 --help 重复」类默认 R0/R1,「一般行为规则/CLI 机制约定」默认 R2;本文件的重复选型规则、全形态 Commands、Parameters/Notes 镜像即此类,处理方向为「留命中率最高一处,其余删或指针」「高频留 2–3 例,长的下沉」。当轮可被 080 裁真伪(coverage 密/overfit 低)。
|
||||
|
||||
## 改了什么(逐处)
|
||||
- **L23–43 `## Choose The Right Content Flag` + `### --text vs --markdown`**:两段语义重叠的选型说明 → 合并为单张 4 行选型表(markdown/text/content/media),并把互斥规则并入表后一句。删掉 `### --text vs --markdown` 整段(与表重复)。
|
||||
- **L44–82 `## What --markdown Really Does` + `### Markdown Boundaries` + `### Image Constraint`**:三段约 39 行 → 压成 `## --markdown Gotchas` 三条要点(强制 post/无 title、标题改写规则、图片预上传 vs 远程 URL vs 本地路径不支持)。删掉 JSON wrap 示意、逐条 boundary 罗列等可由行为观察得到的展开。
|
||||
- **L83–93 图片预上传双命令示例**:并入 `## Commands` 的一条 markdown+image 示例(保留 `im images create` → 引用 img_xxx 的关键两步)。
|
||||
- **L114–161 `## Commands`(15+ 例覆盖全媒体形态)+ `## Media Input Rules`**:压成代表性示例(markdown / text / DM / post-title / markdown+image / 4 个媒体一组 / idempotency+dry-run),媒体路径规则收成 `--help` 指针后的 3 条 load-bearing gotcha(cwd-relative/绝对路径拒绝、video-cover 必配、msg-type 推断冲突)。
|
||||
- **L169–191 `## Parameters` 表**:删除镜像 `--help` 的逐参数描述,改为「Run `lark-cli im +messages-send --help`」指针 + 仅保留 --help 不显然的三条硬规则(已并入 Commands 末尾)。
|
||||
- **L192–202 `## Common Mistakes`**:整段删除——逐条都是选型表/markdown gotcha 的反向重述(第 4 次重复选型规则),删后选型信息仍在表里。
|
||||
- **L203–216 `## content Format Reference`**:保留(构造 `--content` 的 gotcha),把 image/file/audio 三行合并为一行省重复。
|
||||
- **L227–248 `## @Mention Format`**:保留全部三种 msg_type 的 `<at>` 语法(text/post/interactive 各异、AI 猜不到),压紧为两条要点、去掉小标题与重复散文。
|
||||
- **L249–264 `## Notes`**:整段删除——逐条(互斥/media 上传/scope/markdown 强制 post/video-cover/msg-type 冲突)均已在 Safety Constraints、选型表、--markdown Gotchas、Commands 指针处各保留一处单一事实源。
|
||||
|
||||
## 为什么这么改(机制)
|
||||
- **消除根因的因果链**:该 reference 的体积来自「同一份选型规则在 4 个 section 重复 + 全媒体形态逐条罗列 + Parameters/Notes 镜像 --help」。token 不是被任务必需信息占用,而是被**重复表述**占用。按「同一份事实只写一次」(锚点 1)合并到单一事实源后,每条 load-bearing 信息仍恰好出现一次,080 这类「读该 reference→发消息」的题,读入 token 直接下降而行为不变。
|
||||
- **不删能力**:每个 flag(text/markdown/content/image/file/video/audio/idempotency/dry-run/msg-type/video-cover/as)、每条硬约束(互斥、video-cover 必配、cwd-relative 路径、绝对路径拒绝、markdown 强制 post/无 title、msg-type 冲突校验)、三套 `<at>` 语法、content 各 msg_type 样例、Safety Constraints、identity+scope 映射——全部保留,只是从「重复 N 次/逐条罗列」变成「一处/代表性示例 + --help 指针」。
|
||||
- **规范经验源**:依 optimization-playbook「reference 收敛到 gotcha-only,不做 --help 镜像」——Parameters 全表/全形态 Commands 属 USAGE,下沉到 `--help` 指针;保留的是 --help 表达不了的跨 flag 互斥、媒体路径安全、markdown→post 边界、@mention 按类型差异等 gotcha。annotation 标这些段为 R1(可重构/下沉),符合处理方向;唯一 R3(Safety)原样保留。
|
||||
|
||||
## 预期效果
|
||||
- **成功率**:不退化。080(唯一读该文件的题)的发卡片链路依赖的是 `--content`/`interactive`、identity=bot、chat-id——全部保留;选型表、content Format Reference、Safety、scope 都在。015/080 走通 bot 身份的判断由 SKILL.md + identity 段承载,本轮**没碰 SKILL.md**,零误伤面。014 与本文件无关(reach 不含 014)。
|
||||
- **context(分两层)**:
|
||||
- (1) **静态字数差**:16,407 → 6,399 chars(-61.0%);tiktoken cl100k 3,869 → 1,799 tok(-53.5%)。(注:diagnosis 报 ~5,365 tok 系另一 tokenizer/含注入开销;此处用 cl100k 自测,方向与幅度一致。)
|
||||
- (2) **运行时 context 方向**:仅在**实读该 reference 的子集**生效——本轮即 080 一题,运行时读入下降约 50%+(该块占 080 visible 24.8%,预计 080 visible 降约 12–13%)。其余两题(014/015)不读该文件,运行时 token **不变**(既不增也不减)。这是按需 reference,不是常驻面,不会影响未读它的题。
|
||||
- **覆盖敞口**:RC-2 子集仅 080 一题(reach=0.333),证据基数小。coverage 判该文件「密/overfit 低」,本轮 eval 可在 080 上裁真伪,但单题不可外推到「所有发消息任务」。建议后续补「读 messages-send.md 后用 --markdown / 媒体 / @mention」的 case 加厚子集。预期收益落在 **token 轴**(080 visible 下降),effect 轴维持不退化。
|
||||
|
||||
## 刻意没做什么(反 reward-hack / 反过拟合)
|
||||
- 没硬编码任何评测题答案;没删任何能力、flag、guardrail、身份/scope 说明;没碰 lark-im 以外文件,也没把无关根因捆进本轮(commit 仅 1 个文件)。
|
||||
- **没碰 SKILL.md(RC-1)**:尽管 reach=1.0 杠杆最大,但其剩余内容是驱动 015/080 bot 身份判断的承重 identity/约束,diagnosis 标为 effect 风险点;在「保住成功率」硬门槛下不拿成功率换 token。
|
||||
- **没补收窄/分页指引**(015 的 22.5k chat-messages-list 黑洞):那是「增内容」,与降 token 目标方向相反,diagnosis 已列为观察项、本轮不做。
|
||||
- 本改动**不是按评测错误反推**的参数/路由拟合——是基于 annotation + content-taxonomy 的结构性去重,删的是重复表述而非按 080 的具体内容裁剪;真实价值在「任何读该 reference 的发消息任务都少读重复 token」,080 只是当轮可验证的子集。
|
||||
- 未发现需要 breaking(T3)才能根治的点;本轮纯 T1 文档去重即可。
|
||||
|
||||
## 签名
|
||||
- signature: 557349b40feb359bb791749a37571d59edb7e72e (commit 82a099fe 的 diff hash) tier: T1
|
||||
@@ -1,11 +0,0 @@
|
||||
[
|
||||
{
|
||||
"round": 1,
|
||||
"n": 3,
|
||||
"pass_n": 0,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"tool_calls": 26.333333333333332,
|
||||
"duration_ms": 50189.0,
|
||||
"token": 31997.0
|
||||
}
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
# Round 2 归因派工单(parent=a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e;模块未定,由 candidate-writer 据诊断点名)
|
||||
|
||||
> **只读输入**——opt-attributor 读本文件,把诊断**另写** `diagnosis.md`(给 candidate-writer)+ 逐题结构化 `attribution.json`(给 dashboard)。**不要覆盖本文件**,留作派工单↔诊断的前后对比。
|
||||
> 判分点只当「什么算挂」的锚,禁止照抄 grader 药方(已从派工单剔除)。
|
||||
|
||||
## 模块运行时可达性(选模块第一步的证据;要选须在 strategy.md 说明理由)
|
||||
> reach=**实测**触达率(域主 SKILL.md 经 Skill 工具加载、reference 经 Read,都从 trace 实测,没有恒在的面);判决集=实测∪预期触达。**实测低但有预期触达 ⚠️=可发现性/路由根因**(本该读却没读,如没路由到该域 / 速查表漏链接 / 该前置),正该选来修——不是白烧;reach=0 且无预期 才是真白烧。 **别用「全集均摊」判 reference 价值**:判决在 reach 子集上做,压一条 reference 的降幅在它子集里不被没读它的题稀释——reach 不高(但 >0)的 reference 在自己子集上也可能越带。
|
||||
- `skills/lark-im/SKILL.md` → reach=1.0 [域主 skill·经 Skill 工具加载];判决集(实测∪预期): ['1', '2', '3']
|
||||
- `skills/lark-im/references/lark-im-chat-create.md` → reach=0.667;判决集(实测∪预期): ['1', '3']
|
||||
- `skills/lark-im/references/lark-im-messages-send.md` → reach=0.333;判决集(实测∪预期): ['3']
|
||||
- (另 22 个 reference reach=0 且无预期触达,本轮无关,略)
|
||||
|
||||
## 逐轮诊断信号趋势(纯诊断,不进判决)
|
||||
|
||||
| 轮 | 题数 | PASS | 命令失败率 | 工具调用 | 耗时(ms) | token |
|
||||
|---|---|---|---|---|---|---|
|
||||
| R1 | 3 | 0 | 0.60 | 26 | 50189 | 31997 |
|
||||
|
||||
> 跨题均值,按轮排。**命令失败率、工具调用数是横切诊断信号,不是准入轴**(准入只走 效果/token/耗时)——用来判「上一轮那刀有没有把失败/轮次压下去」。工具调用数比 wall-clock 稳,可给噪声大的耗时轴当旁证。
|
||||
|
||||
### 1 [PASS] ctx=34270 (acc=274608) 43995ms tools=31
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✗ 使用当前用户身份创建名为「IM合作群」的群聊
|
||||
证据: transcript 在展示授权二维码后结束,无任何 `lark-cli im +chat-create` 调用。执行停在 '授权完成后请告诉我,我会继续帮你创建群聊并发送消息',群聊未创建。
|
||||
✗ 将傅一铭和傅二铭加入该群
|
||||
证据: transcript 显示尝试搜索用户时遇到 `need_user_authorization` 错误,授权流程启动后中断。未获取到任何用户的 open_id,无后续添加操作。
|
||||
✗ 在该群发送文本消息「大家体验有问题随时沟通」,并返回可验证的 chat_id / message_id
|
||||
证据: 群聊未创建,无 chat_id 可返回。transcript 无任何 `lark-cli im messages-send` 调用。
|
||||
|
||||
### 2 [PASS] ctx=47116 (acc=612048) 114310ms tools=49
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_015/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 成功定位名为「fusanming_at_openclaw群」的群,并获取最近包含「飞豆」关键字的消息
|
||||
✓ 将筛选出的相关消息内容转发到「fusanming_at_需求测试群」
|
||||
✓ 在「fusanming_at_需求测试群」中 @傅六铭 做知会,消息发送成功
|
||||
|
||||
### 3 [PASS] ctx=37942 (acc=251669) 45769ms tools=23
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_080/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 使用用户身份创建一个名为「今晚吃什么」的群,预期返回 chat_id
|
||||
✓ 创建一张飞书卡片,卡片内容包含「今天晚上吃什么」
|
||||
✓ 将该卡片发送到新建群中,预期返回 message_id
|
||||
@@ -1,59 +0,0 @@
|
||||
[
|
||||
{
|
||||
"case_id": "1",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "workorder=PASS(聚合口径);判分点证据 3/3 ✗,按判分点当实质 FAIL 处理",
|
||||
"token": 34555,
|
||||
"duration_ms": 37000,
|
||||
"tool_calls": 31,
|
||||
"cmd_attempts": 7,
|
||||
"cmd_failures": 5,
|
||||
"cmd_fail_rate": 0.71,
|
||||
"discoverability_state": "无(失败命令全是跨域 contact + auth,非 lark-im;chat-create.md 调用前已读但未走到使用)",
|
||||
"axis": "效果",
|
||||
"root_cause": "沙箱 user 授权不可完成 + 跨域 lark-contact 命令依赖;无 lark-im 文档根因,本轮不改",
|
||||
"token_hotspot": "运行时冗余清单常驻(SKILL.md 3,456)+ 按需 chat-create.md 3,062(读了没用上);lark-shared 3,751 与系统 Skill 列表注入 4,612 均不归因",
|
||||
"token_reliability": "常驻静态(SKILL.md)/ 按需读取(chat-create.md,本题读了没用上)",
|
||||
"duration_hotspot": "多轮交互(查联系人→切 contact→失败→授权→qrcode 重试)+ 纯外部API延迟(部分不可归因)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "效果侧无 lark-im 文档缺信息(环境+跨域);token 侧 chat-create.md 把同组 flag 在 Commands/Usage Scenarios 重复演示、Common Errors 复述 validation 字符串,属可删冗余"
|
||||
},
|
||||
{
|
||||
"case_id": "2",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "真 PASS,判分点 3/3 ✓,全程 bot 身份无授权阻断",
|
||||
"token": 54568,
|
||||
"duration_ms": 125000,
|
||||
"tool_calls": 49,
|
||||
"cmd_attempts": 11,
|
||||
"cmd_failures": 3,
|
||||
"cmd_fail_rate": 0.27,
|
||||
"discoverability_state": "① 从没读(messages-search.md / chat-messages-list.md reach=0,直接猜命令;本题未读任何 lark-im reference)",
|
||||
"axis": "token",
|
||||
"root_cause": "无过滤 +chat-messages-list --page-all 全量拉取 → 43.5KB 输出被 Read 整文件灌入 22,556 tok;token 大头非 lark-im doc。修它需补收窄/前置内容,与降 token 目标方向冲突,列观察项",
|
||||
"token_hotspot": "工具返回原样输出(block #19 单次 Read 22,556 tok / 51.5%,归「其他工具调用/返回」)",
|
||||
"token_reliability": "单次输出(强依赖该群消息量,单题不可外推,非稳定常驻热点)",
|
||||
"duration_hotspot": "多轮交互 + 重试(messages-search 连环 exit2 → page-all → 大输出 → 多次本地 grep 抠数据)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现(model calls 16 作旁证,明显高于 080)",
|
||||
"doc_fix_hint": "本题无 T1 可发力的 token 抓手(大头是单次工具输出,非 lark-im doc 常驻);缺的是大群消息查询的 server-side 收窄指引,但补它=增内容、与降 token 反向,不作本轮根因"
|
||||
},
|
||||
{
|
||||
"case_id": "3",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "真 PASS,判分点 3/3 ✓,主动选 bot 身份建群+发卡片均 ok:true,零命令失败",
|
||||
"token": 38009,
|
||||
"duration_ms": 47000,
|
||||
"tool_calls": 22,
|
||||
"cmd_attempts": 3,
|
||||
"cmd_failures": 0,
|
||||
"cmd_fail_rate": 0.0,
|
||||
"discoverability_state": "无(无失败命令;SKILL.md + chat-create.md + messages-send.md 全部状态③:调用前已读且用上)",
|
||||
"axis": "token",
|
||||
"root_cause": "读取 Skill 占 56.4%;本轮唯一干净 token 抓手 = chat-create.md 内部冗余(示例罗列 + 场景重复 + --help 镜像),从未被优化过",
|
||||
"token_hotspot": "运行时冗余清单常驻 + 按需 reference(chat-create.md 当前 2,336 raw,可压 Commands/Usage Scenarios 重叠 + Common Errors validation 镜像;trace 里 messages-send.md 5,365 是旧版,round-2 已压到 2,006,本轮不再可压)",
|
||||
"token_reliability": "按需读取(chat-create.md reach=0.667,本题是其压缩收益唯一稳态兑现题)",
|
||||
"duration_hotspot": "无离群(建群+发卡片正常串行,无重试、无写后回查)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "chat-create.md 把同组 flag 在 Commands(12 例)+Usage Scenarios(3 场景)重复演示、Common Errors 多行复述 --help/报错本身就会吐的 validation 字符串,属可删冗余;232043 两步流 / --chat-mode topic 区分 / --owner 默认为载重红线,压缩中不可误删"
|
||||
}
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"1": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"contact +search-user"
|
||||
],
|
||||
"3": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"im +chat-create",
|
||||
"im +messages-send"
|
||||
],
|
||||
"2": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"im +chat-messages-list",
|
||||
"im +chat-search",
|
||||
"im +messages-mget",
|
||||
"im +messages-search",
|
||||
"im +messages-send",
|
||||
"im messages forward",
|
||||
"schema im.messages.forward",
|
||||
"schema im.messages.search"
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 33840,
|
||||
"token_usage": 237434,
|
||||
"duration_ms": 44127,
|
||||
"tool_call_count": 25,
|
||||
"feedback": "执行者成功完成了所有期望:首先搜索联系人获取 open_id(首次搜索用单字失败后改为双字搜索成功),然后使用 --as user 创建群组并添加成员,最后发送消息并返回 message_id。整个流程正确,使用了等效的 `--as user` 身份,符合用户「使用我的身份」的要求。验证结果确认所有操作均已生效。"
|
||||
},
|
||||
"3": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 35942,
|
||||
"token_usage": 234388,
|
||||
"duration_ms": 43185,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "执行者正确理解用户意图,使用用户身份创建群并发送卡片消息。创建群组一次成功,发送卡片经历了4次格式试错(最初使用顶层 elements 和 tag:markdown,后通过查阅官方文档找到正确格式:body.elements + div + lark_md),最终成功发送并返回 message_id。试错后自行纠正符合评判原则,不构成判罚依据。\n- {'reason': '建议在 lark-im-messages-send.md 中增加飞书 interactive card 的标准格式示例,特别是 2.0 schema 下的 body.elements 中使用 div + lark_md 的正确写法,减少 AI 试错成本'}\n- {'reason': '建议 CLI 在遇到 230099 卡片格式错误时,尝试解析并返回更具体的字段级错误提示(如提示 \"elements 应在 body 内\" 或 \"tag:markdown 不被支持\"),帮助 AI 更快定位问题'}"
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
# Round 3 归因(parent=557349b…(round-2 已采纳候选);候选模块见 candidate_modules,由 candidate-writer 据诊断+reach 点名)
|
||||
|
||||
> 目标(objective.json):**在不回退成功率的前提下降低 lark-im skill 文档的 token 成本**。effect 是硬门槛、不可退化;token 与 duration 是并列成本杆。tier=T1,仅可改 `skills/lark-im/**`。target_axis=token。
|
||||
> 判分点只当「什么算挂」的锚,不抄 grader 药方。
|
||||
|
||||
## ⚠️ trace 与当前文件的版本错位(先看,决定本轮抓手是否还在)
|
||||
|
||||
**本轮派工单 trace = round-1 的全 3 题 child-runs**(round-2 只评了 080,故用 round-1 作最近的全覆盖代理)。这些 trace 里的 reference 体积是 **round-1/round-2 改动之前** 的旧版。我用 session-analyze 所用的同一 ai-tokenizer 实测了**当前工作树**文件,确认两者错位如下:
|
||||
|
||||
| 文件 | trace 内体积(旧版,Read 计) | 当前实测(raw / Read 计) | 已被哪轮收割 |
|
||||
|---|---|---|---|
|
||||
| `SKILL.md`(Skill 注入正文) | 3,455–3,456 tok | 3,525 raw | round-1(API Resources/权限表→schema 指针) |
|
||||
| `references/lark-im-messages-send.md` | **5,365 tok** | **2,006 raw / 2,194 Read** | **round-2(5,365→2,006,已收割)** |
|
||||
| `references/lark-im-chat-create.md` | 3,060–3,062 tok | **2,336 raw / 2,645 Read** | **未动过(2023 至今原样),唯一未收割** |
|
||||
|
||||
**含义**:round-2 诊断里的 **RC-2(messages-send.md 内部冗余)已经在 round-2 被采纳并收割**(5,365→2,006),它不再是本轮抓手——不要据 trace 里的 5,365 重复提一遍。本轮 trace 里那块 5,365 是历史值,当前已不存在。**reach>0 集合里唯一还没被压过的干净文件就是 `chat-create.md`**(round-2 的 RC-3)。
|
||||
|
||||
## 跨 case 共同根因(优先看;按对 TOKEN 目标的杠杆排序)
|
||||
|
||||
### RC-1(token,本轮头号且基本是唯一的干净抓手,reach=0.667:014+080)—— `chat-create.md` 内部存在「示例罗列 + 场景重复 + --help 镜像」三类可压缩冗余,且从未被优化过
|
||||
- **现象**:`chat-create.md` 当前 2,336 raw tok(Read 计 ~2,645),是 reach>0 集合里**唯一未被任何轮收割**的 reference。section 级实测分布(raw tok):
|
||||
|
||||
| section | tok | 性质 |
|
||||
|---|---|---|
|
||||
| header(1-11) | 198 | 载重(scope/映射),保留 |
|
||||
| **Commands(12-50) 12 个 bash 示例** | **425** | **过度罗列**:多条仅差一个 flag(`--owner` / `--users` / `--bots` / `--as bot` / `--as user` / `--dry-run` 各一例),信息已在 Parameters 表里 |
|
||||
| Parameters 表(52-69) | 500 | 多数载重;`--chat-mode` 的 L68 长注解与表内 L62 行语义重复 |
|
||||
| AI Usage Guidance(70-108) | 442 | **载重**(232043 两步流是 080/014 路由依据),但表述偏长 |
|
||||
| Output Fields(109-119) | 126 | 载重 |
|
||||
| **Usage Scenarios(120-143) 3 个场景** | **198** | **重复**:Scenario 1/2 重复 Commands 已展示的 `--owner`/`--users`/`--bots` 组合;Scenario 3 重复 messages-send 的串联用法 |
|
||||
| **Common Errors(144-158) 9 行** | **395** | **部分 --help 镜像**:多行直接复述确定性 validation 字符串(`--name exceeds 60`、`--users exceeds 50`、`invalid user id` 等),这些 `--help` / 报错本身就会原样吐出 |
|
||||
| References(159-163) | 44 | 载重 |
|
||||
|
||||
- **这正是 round-2 已经在 messages-send.md 上验证过、且被采纳的同一套压缩模式**:round-2 把 messages-send.md 的「4 处重复选型规则 + 全媒体形态 Commands + --help 镜像」压成「保留载重规则 + 一句 `--help` 指针」(5,365→2,006,被采纳)。chat-create.md 的 Commands(425)↔Usage Scenarios(198) 重叠、Common Errors(395) 的 validation 镜像,是同型冗余。
|
||||
- **可压缩量级(粗估,非药方)**:可压缩质量集中在 Commands+Usage Scenarios 的重叠(合计 ~623 tok,去重后可省一大半)+ Common Errors 的 --help 镜像行。**保守估计可从 2,336 压到 ~1,500–1,700 raw tok(省约 600–800 tok,约 30%)**,与 messages-send.md 的压缩比同量级。具体改法与确切降幅由 candidate-writer 决定、评测裁决。
|
||||
- **载重红线(candidate-writer 取舍时的 effect 风险点,不是 RC-1 不成立)**:AI Usage Guidance 的 **232043 两步流 + `succeed_type=1`**、`--chat-mode topic` vs 普通群+话题消息模式的区分、`--owner` 默认行为,是 014/080 走通 bot 身份建群的语义依据,**不能在压缩中误删**。这条 reference 被 080 实读且 080 据它建群成功(`ok:true`),所以 effect 风险真实存在——压的是示例/场景/报错镜像的体积,不是语义规则。
|
||||
- **axis=token**。可信度=**按需读取**(reach=0.667,子集=014+080,2 题)。压它的降幅只在这 2 题子集里计入,不被 015(没读它)稀释;但子集仅 2 题、且 014 是「读了没用上」(授权阻断没走到建群),实际吃到压缩收益的稳态题只有 080 一题——**证据基数小,降幅需评测在子集上确认**(见数据缺口)。
|
||||
|
||||
### RC-2(token,已收割,本轮不再是抓手)—— messages-send.md 的内部冗余 round-2 已压掉
|
||||
- round-2 RC-2 已被采纳:messages-send.md 5,365→2,006 raw。**本轮不要据 trace 里的 5,365 重复提**。当前 messages-send.md 已是「载重规则 + `--help` 指针」形态,无明显二次压缩空间(剩余多为 content-flag 选型、@mention、media 约束等载重内容)。reach=0.333(仅 080)。
|
||||
|
||||
### RC-3(token,无 T1 干净抓手)—— SKILL.md 常驻正文 round-1 已压过,剩余多为载重 identity/路由
|
||||
- SKILL.md 经 Skill 工具每题必加载(reach=1.0),当前 3,525 raw tok(round-1 已把 API Resources/权限表折叠成 schema 指针)。剩余 `## Important Notes`(L36–85) 各小节(Sender Name Resolution / message enrichment / `--download-resources` / Card / Flag / Feed Shortcut)与 `## Shortcuts` 全表(L87–115) 虽本轮 3 题低命中,但它们是**全域 identity/路由/约束**——这是 round-1 已经动过一刀的同一文件,**再压属同向继续、但删错会碰坏 015/080 已走通的 bot 身份与命令路由判断**(effect 风险高于 RC-1)。**列为更次级、风险更高的抓手**,不作为本轮首选;若要动须只删本轮已确证低命中且非路由的纯枚举行,谨慎程度高于 chat-create。
|
||||
|
||||
## 命令失败热点(跨 case;失败类型由我从 timeline 命令串读出,非判决数字)
|
||||
|
||||
| lark-cli 命令 | 失败次数 | 涉及题数 | 主要失败类型 | 指向的文档问题 |
|
||||
|---|---|---|---|---|
|
||||
| `contact +search-user` | 4 | 1 (014) | bot exit2(invalid_argument) ×2;`--as user` token_missing ×2 | **跨 lark-contact 域**,非 lark-im 内容 |
|
||||
| `auth qrcode --output <绝对/沙箱外路径>` | 1 | 1 (014) | unsafe output path,改相对路径重试成功 | 路径约束在 lark-shared(不可改) |
|
||||
| `im +messages-search` | 2 | 1 (015) | bot exit2 + `--as user` exit2 | 该命令 user-only(SKILL 表已注明);bot 身份必败,agent 没看清就猜 |
|
||||
| `im +chat-messages-list --page-all` | 1 | 1 (015) | exit2(无过滤 page-all) | 见下「015 token 黑洞」 |
|
||||
- **解读**:本轮**没有一条 lark-im 命令因「参数名/类型写错」系统性失败**。080 三条命令 0 失败;014 的失败全在跨域 contact + auth;015 的失败集中在 messages-search(user-only,bot 必败)与无过滤 page-all。**没有 lark-im 侧常规「报错/参数整形」工单**——与 token 减体积方向一致,本轮抓手是减体积不是补内容。
|
||||
|
||||
## 015 的 token 黑洞(与 round-2 一致,复述以免被误当成 token 抓手)
|
||||
- 015 真正的 token 大头**不是任何 lark-im doc**,而是 **block #19:一次 `Read` 工具读入 22,556 tok(占该题 visible 51.5%)**。成因链:#12/#17 `+messages-search`/`--page-all` exit2 → #18 退到 `+chat-messages-list`(无过滤)→ 输出 43.5KB 被持久化 → agent `Read` 整文件 → 22.5k tok 灌进上下文 → 再靠本地 grep(#27–33) 抠出「飞豆」两条。
|
||||
- **从文档角度**:`chat-messages-list.md` 本题 reach=0(状态①:调用前从没读),它本写了 `--start/--end`、`--page-size` 等可避免全量拉取的约束。**但补它=增常驻/触达内容,与本轮降 token 目标方向相反**(见方向冲突);且 22.5k 是**单次工具输出**(强依赖该群消息量,单题不可外推),不是稳定常驻热点。**结论:观察项,交评测裁决,不作为本轮 token 抓手。**
|
||||
|
||||
## 可发现性时序(约束 5 三态;判「前置能不能救」的决定性证据)
|
||||
> 对每条相关 reference / `--help`,按相对首次失败调用的读取时序统计。`--help` 扫 Bash(本轮 3 题均未跑任何 `--help`)。
|
||||
|
||||
| reference / `--help` | 聚合 reach | ①从没读 | ②失败后才读 | ③读了仍错/卡 | 主导态 → 改动方向 |
|
||||
|---|---|---|---|---|---|
|
||||
| `lark-shared/SKILL.md` | 1.0 | 0 | 0 | — | 三题调用前都读了;014 仍卡(环境,非内容);不可改 |
|
||||
| `chat-create.md` | 0.667 | 0 | 0 | — | 080 调用前读→建群成功;014 调用前读→授权阻断(非 reference 错)。**非触达问题,纯减体积** |
|
||||
| `messages-send.md` | 0.333 | 0 | 0 | — | 080 调用前读→发卡片成功。**非触达问题**(已收割) |
|
||||
| `chat-messages-list.md` | 0.0 | 1 (015) | 0 | — | ① 015 调用前从没读→`--page-all` 全量拉取→token 黑洞。触达缺口,但补它=增 token,与目标冲突 |
|
||||
| `messages-search.md` | 0.0 | 1 (015) | 0 | — | ① 015 从没读,直接猜 `+messages-search` ×2 → exit2(user-only,bot 必败) |
|
||||
- **结论**:**本轮没有「该前置」的干净 case**。RC-1(chat-create.md 减体积)是「调用前已读、内容够用 → 去冗余」的纯 token 减法,不涉及触达。015 的两处 ① 触达缺口确实存在,但修它们=增内容、与降 token 目标相反,且 015 已 PASS(bot + 本地 grep 兜底)——属观察项,**不要被诱导去推前置**。
|
||||
|
||||
## 方向冲突记录(硬性约束 7)
|
||||
- **减体积(RC-1 chat-create.md,与 objective.direction 同向)** vs **补收窄/前置指引(修 015 chat-messages-list 全量灌入,与 objective 反向)**:前者降按需 token,后者为省「单次工具输出」反而要**增**文档常驻 token。两者方向相反,**不可合并**。本轮目标是降 token,取减体积一侧;015 全量灌入作为观察项记录、不作为要补的内容根因。
|
||||
|
||||
## 差距台账复盘
|
||||
- 无(`discard-ledger.json` 为空,无已跑未采纳候选)。
|
||||
|
||||
## 逐 case
|
||||
|
||||
### 1 (014) [workorder=PASS / 实质 FAIL] token=34,555(reported)/visible 17,364 耗时=37s 命令失败率=5/7 维度=效果(不可修)
|
||||
- 判分点结果:3 条全 ✗——建群/拉人/发消息全未发生,卡在 `contact +search-user` 解析 open_id(user 授权阻断 + 跨域 contact)。verdict=PASS 系聚合口径,按判分点证据当 FAIL 处理。
|
||||
- 命令失败:5/7。`contact +search-user` bot exit2 ×2、`--as user` token_missing ×2;`auth qrcode` 绝对路径 unsafe ×1(改相对路径成功)。**全部非 lark-im 命令**。
|
||||
- 可发现性时序:#4 读 SKILL.md 正文(3,456) + #6 读 lark-shared(3,751,跨 skill) + #7 读 chat-create.md(3,062,调用前已读);失败在更上游的跨域 contact + 授权。**非 lark-im 触达问题**。
|
||||
- token 归因:SKILL.md 正文 3,456(常驻静态,19.9%)+ lark-shared 3,751(**跨 skill,不归因 lark-im**)+ chat-create.md 3,062(按需,17.6%,**本题读了没用上**——授权阻断没走到建群)+ 系统 Skill 列表注入 4,612(固定开销,不归因)。lark-cli 命令累计含多次短失败回显,单条都短、非热点。
|
||||
- 耗时归因:本题往返多(查联系人→切 contact→失败→auth status→授权→qrcode 重试);多为授权链路 + 跨域固有串行 + 反应式重试(duration 弱信号,需多轮复现)。
|
||||
- 文档根因:效果=沙箱 user 授权 + 跨域 contact(环境,**无 lark-im 文档根因,本轮不改**);token=chat-create.md 按需冗余(RC-1,但本题读了没用上,收益只在 080 这种走通题里兑现)+ SKILL.md 常驻(RC-3,风险高、次级)。
|
||||
|
||||
### 2 (015) [PASS·真] token=54,568(reported)/visible 43,760 耗时=2m5s 命令失败率=3/11 维度=token(但大头非 lark-im doc)
|
||||
- 判分点结果:3/3 ✓——定位群、转发「飞豆」消息、@傅六铭知会全部成功(两次 `messages-send` 均 `ok:true`)。**全程 bot 身份,无授权阻断**。
|
||||
- 命令失败:3/11。`+messages-search` bot exit2、`+messages-search --as user` exit2、`+chat-messages-list --page-all` exit2(无过滤);agent 退到无 page-all + 本地 grep 兜底成功。(#14 `--page-all | grep` 返回空属「成功但无命中」,非硬失败,未计入。)
|
||||
- 可发现性时序:① `messages-search.md` / `chat-messages-list.md` 调用前从没读(reach=0),直接猜命令。**本题未读任何 lark-im reference**,故 lark-im reference 的体积与本题 token 无关。
|
||||
- token 归因:**本题 token 大头不是 lark-im doc**,是 block #19 一次 `Read` 持久化文件 = **22,556 tok(51.5%,归「其他工具调用/返回」)**,成因=`--page-all` 无过滤全量拉取→43.5KB→Read 灌入(**单次输出**可信度,强依赖该群消息量)。SKILL.md 正文 3,448(常驻)。lark-shared 3,749(跨 skill,不归因)。**RC-1 改 chat-create.md 对本题 token 无影响**(本题没读它)。
|
||||
- 耗时归因:本题最长(2m5s),主因 messages-search 连环失败→改 page-all→大输出→多次本地 grep 抠数据的多轮往返(duration 弱信号;model calls 16/raw 32,明显高于 080,作旁证)。
|
||||
- 文档根因:token 黑洞的放大器=`chat-messages-list.md` 没被读到(状态①)+ SKILL.md 表未提示大群应 server-side 收窄——但**补这条与降 token 目标相反**(方向张力),列为观察项;本题已 PASS。本轮 token 抓手(RC-1)不落在本题。
|
||||
|
||||
### 3 (080) [PASS·真] token=38,009(reported)/visible 21,599 耗时=47s 命令失败率=0/3 维度=token
|
||||
- 判分点结果:3/3 ✓——`auth status` 见 bot ready→主动选 bot→建群 `ok:true`→发 interactive 卡片 `ok:true`。**任务完整完成,零命令失败**。
|
||||
- 命令失败:0/3。三条 lark-cli(auth status / chat-create / messages-send)全成功。
|
||||
- 可发现性时序:#4 读 SKILL.md 正文(3,455) + #6 读 lark-shared(3,751,跨 skill) + #9 读 chat-create.md(3,060) + #10 读 messages-send.md(5,365,旧版) ,全部状态③(调用前已读且用上)。**无触达问题。** 实际只用了 `+chat-create --name … --format json` 的最简形态——没用两步流/owner/members/topic/error-recovery。
|
||||
- token 归因:**本题是纯 token 抓手题**——读取 Skill 占 56.4%:messages-send.md 5,365(trace 旧版,**当前已被 round-2 压到 2,006,本轮不再可压**)+ SKILL.md 正文 3,455(常驻,RC-3)+ chat-create.md 3,060(按需,**RC-1,当前 2,336,本轮唯一干净抓手**)。系统 Skill 列表注入 4,612(固定开销,不归因)。lark-shared 3,751(跨 skill,不归因)。
|
||||
- 耗时归因:47s,全部为正常建群+发卡片串行,无重试、无写后回查(无离群)。
|
||||
- 文档根因:无效果根因(已绿);token=RC-1(chat-create.md 内部冗余,本题是其收益唯一稳态兑现题)+ RC-3(SKILL.md 常驻,风险高、次级)。**本题 token 杠杆最清晰且 effect 风险可控**(命令全成功,压 chat-create.md 的示例/场景/报错镜像不碰 080 实际用到的最简建群链路)。
|
||||
|
||||
## 给 candidate-writer 的收口(不含具体改法)
|
||||
- **唯一在 T1 内还没被收割的干净 token 抓手是 RC-1(`chat-create.md` 内部冗余)**:Commands 12 例过度罗列 + Usage Scenarios 3 场景重复 Commands + Common Errors 9 行部分镜像 validation 字符串——**与 round-2 已采纳的 messages-send.md 压缩同型**,粗估可省 ~600–800 raw tok(约 30%)。reach=0.667(014+080),降幅在子集计入。
|
||||
- **载重红线**:AI Usage Guidance 的 232043 两步流 + `succeed_type=1` + `--chat-mode topic` 区分 + `--owner` 默认,是 080/014 走通 bot 建群的语义依据,**压缩中不可误删**——压的是示例/场景/报错镜像体积,不是规则。
|
||||
- **RC-2 已收割**:messages-send.md round-2 已 5,365→2,006,trace 里的 5,365 是历史值,**不要重复提**。
|
||||
- **RC-3(SKILL.md 常驻)是次级且风险更高**:round-1 已压过一刀,剩余多为全域 identity/路由/约束,删错碰坏 015/080 已走通的 bot 身份与命令路由——不作首选。
|
||||
- **不要推前置**:本轮没有「该前置」的干净 case。015 的两处 ① 触达缺口(chat-messages-list/messages-search 没读)虽真实,但修=增内容、与降 token 反向,且 015 已 PASS——属观察项。
|
||||
- **effect 不可在本轮 T1 内合法抬升**:014 是环境(沙箱不能扫码)+ 跨域 contact,无 lark-im 文档根因;015/080 已真 PASS。effect deltas 视作 auth-noise,不追。
|
||||
- **干净 token 抓手接近见底(诚实判断)**:reach>0 集合三个文件中,messages-send.md(round-2)与 SKILL.md(round-1)已各压一刀,**chat-create.md 是最后一个未动过的干净文件**。压完它之后,T1 内 reach>0 的纯冗余(罗列/重复/--help 镜像)基本耗尽;再往下只剩 (a) 高 effect 风险的 SKILL.md 载重内容,或 (b) reach=0 的 22 个盲区 reference(压了也不在判决集、无法被采纳)。**本轮 RC-1 很可能是这条优化路径上最后一个低风险、可被采纳的 token 抓手。**
|
||||
- **缺失信息(doc_fix_hint 语气,非药方)**:chat-create.md 把同一组 flag 在 Commands(12 例) 与 Usage Scenarios(3 场景) 重复演示、Common Errors 多行复述 `--help`/报错本身就会吐的 validation 字符串——这类「枚举/重复/镜像、低增量」内容是其 token 的主要去处,且是减法(删冗余)而非加法。
|
||||
|
||||
## 数据缺口
|
||||
1. **trace 版本错位(最关键)**:本轮 trace=round-1 旧版 child-runs,messages-send.md 在 trace 里仍是 5,365(round-2 已压到 2,006)。所有「当前文件体积」结论我已用 ai-tokenizer 实测当前工作树校正(SKILL.md 3,525 / chat-create.md 2,336 / messages-send.md 2,006),但**单题行为与 reach 仍来自旧 trace**——若 round-2 改动改变了 080/014 的读取行为,需以实际 round-3 eval-run 复核。
|
||||
2. **RC-1 子集小**:chat-create.md reach=0.667 但实际吃到压缩收益的稳态题只有 080(014 读了没用上、授权阻断),证据基数=1,降幅需评测在子集确认。
|
||||
3. **015 的 22.5k 黑洞是单次工具输出**,强依赖该群消息量,非稳定常驻热点,单题不可外推;且与降 token 目标方向冲突,不作抓手。
|
||||
4. **duration 三题波动大**(37s/2m5s/47s),015 长尾主因 messages-search 连环失败+大输出多轮抠数据;单轮不足定论,需多轮复现。model calls(8/16/6) 比 wall-clock 稳,可作旁证。
|
||||
5. **工具调用口径不一致**:trend.json 的 R1 tool_calls=26.3、R2=10,与 session-analyze 的 model calls(8/16/6) 口径不同(趋势表疑似含 raw 计数);旁证以 timeline 实际往返为准。趋势看:R1→R2 命令失败率 0.60→0.35、tool_calls 26→10 明显下降,但那主要是 effect 从「三题全卡授权」变成「2 真 PASS + 1 卡」带来的,**不是 token 改动的功劳**;token 均值 R1 31,997→R2 42,377 上升,主因是 R2 只评 080(单题大)口径差异 + 015 黑洞,非文档常驻变重——趋势对 token 轴判读价值有限,以单题 session-analyze 为准。
|
||||
6. **effect 维度全部归因为「无文档根因/不可修」**:014 跨域+环境,015/080 已绿。本轮 effect 无 T1 可发力点,deltas 视作 auth-noise。
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,220 +0,0 @@
|
||||
{
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.333,
|
||||
"read_cases": [
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/references/lark-im-chat-create.md",
|
||||
"tier": "T1",
|
||||
"round_index": 3,
|
||||
"base_commit": "572eb8da41f608bd93b25916cac02cb772825b97",
|
||||
"code_tip": "cbd6e56ac07285fd973c53ff7382da0112b6cf5d",
|
||||
"reason": "纯瘦身改动,对抗式逐项核验未发现可证伪点:14 条承重红线(232043 两步流/succeed_type=1/chat-mode topic 与 thread 区分/--owner 默认/set-bot-manager/chat.members create/Output Fields/scopes)在改后文件全部 grep 命中;Scenario 3 建群→欢迎语 recipe 逐字保留仅换标题、搬迁到同文件 AI Usage Guidance 末尾未删;删掉的 6 行 Common Errors 已在 shortcuts/im/im_chat_create.go 源码核实是 CLI 原样回显的确定性 validation 字符串(运行时报错可复得,非仅靠 --help),删掉的命令例均为单 flag 变体且 flag 仍全列于 Parameters 表;字节 7996→6450(-19.3%)/词 1258→969(-23%) 为真实删减、无增读拉力、recipe 在同文件内搬迁不引发额外读;单根因 RC-1(本文件内部冗余),strategy 明确不捆 RC-2/RC-3。",
|
||||
"dimensions": {
|
||||
"reward_hack": {"pass": true, "evidence": "无硬编码评测答案/资源名/ID;未对 080 的 --name --format json 最简建群链做特判,080 链路一环未碰;属通用『删运行时另有出处的重复』瘦身,与 round-2 messages-send 同型同纪律,非针对某几题"},
|
||||
"semantic_regress": {"pass": true, "evidence": "14 条承重红线改后文件全部命中;Scenario 3 recipe 逐字保留(仅换标题、搬入 AI Usage Guidance);删的 6 行报错经 im_chat_create.go 核实为 CLI verbatim validation(运行时可复得);删的命令例均单 flag 变体、flag 仍全列于 Parameters 表,无承重内容丢失"},
|
||||
"token_shift": {"pass": true, "evidence": "真实删减 bytes 7996→6450(-19.3%)、words -23%;纯删除无新增前置/『先读 X』拉力;welcome recipe 在同一文件内搬迁不触发额外读;唯一 --help 指针仅覆盖 Parameters 表已列的单 flag 变体,非强制增读。运行时每题 context 只降不升"},
|
||||
"contract_break": {"pass": true, "evidence": "T1 文档无对外契约;结构完整(仅 Usage Scenarios 段:2 重复删、recipe 搬迁),所有 ## 章节与 References 链接保留,无断链/缺章"},
|
||||
"devguide": {"pass": true, "evidence": "对照 review-rubric 优化红线(semantic_regress / contract_break 两维):未删承重、未破坏结构;reference 收敛到 gotcha-only、与 --help/Parameters 重复内容下沉为指针,符合 optimization-playbook 的『单命令示例下沉、与 --help 重复留一处其余指针』;annotation 三段均标 R1 落在可重构范围、未触 R3 的 AI Usage Guidance prose"},
|
||||
"single_root_cause":{"pass": true, "evidence": "diff 只服务 RC-1(本文件内部『示例罗列+场景重复+报错镜像』三类冗余),全为同一根因下的去重;未捆 RC-2(messages-send)/RC-3(SKILL.md),未夹带语义独立的承重删除,无多根因对冲叙事"}
|
||||
}
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
{
|
||||
"round": 3,
|
||||
"status": "admitted",
|
||||
"parent_id": "557349b40feb359bb791749a37571d59edb7e72e",
|
||||
"parent_worktree": "/Users/bytedance/Projects/cli",
|
||||
"child_worktree": "/Users/bytedance/Projects/cli",
|
||||
"base_commit": "572eb8da41f608bd93b25916cac02cb772825b97",
|
||||
"module": "skills/lark-im/references/lark-im-chat-create.md",
|
||||
"candidate_modules": [
|
||||
"skills/lark-im/SKILL.md",
|
||||
"skills/lark-im/references/lark-im-chat-create.md",
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-chat-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-search.md",
|
||||
"skills/lark-im/references/lark-im-chat-update.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-flag-list.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md",
|
||||
"skills/lark-im/references/lark-im-messages-send.md",
|
||||
"skills/lark-im/references/lark-im-reactions.md",
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md"
|
||||
],
|
||||
"module_reach": {
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.333,
|
||||
"read_cases": [
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
},
|
||||
"expected_reach": {},
|
||||
"minibatch": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"pareto_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"artifacts": {
|
||||
"workorder": "workorder.md",
|
||||
"diagnosis": "diagnosis.md",
|
||||
"attribution": "attribution.json",
|
||||
"strategy": "strategy.md",
|
||||
"review": "review.json",
|
||||
"trend": "trend.json"
|
||||
},
|
||||
"code_tip": "cbd6e56ac07285fd973c53ff7382da0112b6cf5d",
|
||||
"signature": "53194d7a111df326cc078b633f43587225bd0132",
|
||||
"tier": "T1",
|
||||
"intent": "dedup Commands<->Usage Scenarios overlap + compress --help-mirroring Common Errors in chat-create.md; keep all red lines (232043 two-step,succeed_type=1,chat-mode topic,--owner)",
|
||||
"target_axis": "token",
|
||||
"changed_files": [
|
||||
"skills/lark-im/references/lark-im-chat-create.md"
|
||||
],
|
||||
"decision_basis": {
|
||||
"type": "module",
|
||||
"module": "skills/lark-im/references/lark-im-chat-create.md"
|
||||
},
|
||||
"decision_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"review": {
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/references/lark-im-chat-create.md",
|
||||
"tier": "T1",
|
||||
"round_index": 3,
|
||||
"base_commit": "572eb8da41f608bd93b25916cac02cb772825b97",
|
||||
"code_tip": "cbd6e56ac07285fd973c53ff7382da0112b6cf5d",
|
||||
"reason": "纯瘦身改动,对抗式逐项核验未发现可证伪点:14 条承重红线(232043 两步流/succeed_type=1/chat-mode topic 与 thread 区分/--owner 默认/set-bot-manager/chat.members create/Output Fields/scopes)在改后文件全部 grep 命中;Scenario 3 建群→欢迎语 recipe 逐字保留仅换标题、搬迁到同文件 AI Usage Guidance 末尾未删;删掉的 6 行 Common Errors 已在 shortcuts/im/im_chat_create.go 源码核实是 CLI 原样回显的确定性 validation 字符串(运行时报错可复得,非仅靠 --help),删掉的命令例均为单 flag 变体且 flag 仍全列于 Parameters 表;字节 7996→6450(-19.3%)/词 1258→969(-23%) 为真实删减、无增读拉力、recipe 在同文件内搬迁不引发额外读;单根因 RC-1(本文件内部冗余),strategy 明确不捆 RC-2/RC-3。",
|
||||
"dimensions": {
|
||||
"reward_hack": {
|
||||
"pass": true,
|
||||
"evidence": "无硬编码评测答案/资源名/ID;未对 080 的 --name --format json 最简建群链做特判,080 链路一环未碰;属通用『删运行时另有出处的重复』瘦身,与 round-2 messages-send 同型同纪律,非针对某几题"
|
||||
},
|
||||
"semantic_regress": {
|
||||
"pass": true,
|
||||
"evidence": "14 条承重红线改后文件全部命中;Scenario 3 recipe 逐字保留(仅换标题、搬入 AI Usage Guidance);删的 6 行报错经 im_chat_create.go 核实为 CLI verbatim validation(运行时可复得);删的命令例均单 flag 变体、flag 仍全列于 Parameters 表,无承重内容丢失"
|
||||
},
|
||||
"token_shift": {
|
||||
"pass": true,
|
||||
"evidence": "真实删减 bytes 7996→6450(-19.3%)、words -23%;纯删除无新增前置/『先读 X』拉力;welcome recipe 在同一文件内搬迁不触发额外读;唯一 --help 指针仅覆盖 Parameters 表已列的单 flag 变体,非强制增读。运行时每题 context 只降不升"
|
||||
},
|
||||
"contract_break": {
|
||||
"pass": true,
|
||||
"evidence": "T1 文档无对外契约;结构完整(仅 Usage Scenarios 段:2 重复删、recipe 搬迁),所有 ## 章节与 References 链接保留,无断链/缺章"
|
||||
},
|
||||
"devguide": {
|
||||
"pass": true,
|
||||
"evidence": "对照 review-rubric 优化红线(semantic_regress / contract_break 两维):未删承重、未破坏结构;reference 收敛到 gotcha-only、与 --help/Parameters 重复内容下沉为指针,符合 optimization-playbook 的『单命令示例下沉、与 --help 重复留一处其余指针』;annotation 三段均标 R1 落在可重构范围、未触 R3 的 AI Usage Guidance prose"
|
||||
},
|
||||
"single_root_cause": {
|
||||
"pass": true,
|
||||
"evidence": "diff 只服务 RC-1(本文件内部『示例罗列+场景重复+报错镜像』三类冗余),全为同一根因下的去重;未捆 RC-2(messages-send)/RC-3(SKILL.md),未夹带语义独立的承重删除,无多根因对冲叙事"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child_k": 5,
|
||||
"eval_trace": null,
|
||||
"retro": {
|
||||
"cause": "已入池",
|
||||
"noise_borderline": false,
|
||||
"summary": "越带入池,无需复盘补发"
|
||||
},
|
||||
"retro_sessions": [
|
||||
{
|
||||
"case": "1",
|
||||
"session": "harness-opt/rounds/round-003/child-runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl",
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 34270,
|
||||
"child": 33840,
|
||||
"gain": "收益现",
|
||||
"pass_delta": null
|
||||
},
|
||||
{
|
||||
"case": "3",
|
||||
"session": null,
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 35478,
|
||||
"child": 35942,
|
||||
"gain": "反向",
|
||||
"pass_delta": null
|
||||
}
|
||||
],
|
||||
"verdict": "admitted",
|
||||
"ci": null,
|
||||
"new_candidate": "53194d7a111df326cc078b633f43587225bd0132",
|
||||
"decision": {
|
||||
"parent_success": 1.0,
|
||||
"child_success": 1.0,
|
||||
"parent_score": 0.8,
|
||||
"child_score": 1.0,
|
||||
"score_saved": 0.19999999999999996,
|
||||
"score_threshold": 0.09532271373123208,
|
||||
"parent_token": 34874.0,
|
||||
"child_token": 34891.0,
|
||||
"saved": -17.0,
|
||||
"threshold": 4532.708313776408,
|
||||
"parent_duration": 45267.5,
|
||||
"child_duration": 43656.0,
|
||||
"dur_saved": 1611.5,
|
||||
"dur_threshold": 4899.200953624988,
|
||||
"dur_margin": 1.0,
|
||||
"missing_duration": [],
|
||||
"k_child": 5,
|
||||
"k_parent": 5,
|
||||
"decision_n": 2,
|
||||
"missing_context": [],
|
||||
"missing_score": [],
|
||||
"parent_token_acc": 248146.5,
|
||||
"child_token_acc": 235911.0,
|
||||
"phi0_score": 0.5666666666666667,
|
||||
"eff_margin": 1.0,
|
||||
"parent_token_full": 34874.0,
|
||||
"child_token_full": 34891.0,
|
||||
"saved_full": -17.0,
|
||||
"observe_n": 2,
|
||||
"target_axis": "token",
|
||||
"admitted": true,
|
||||
"reason": "score_gain"
|
||||
},
|
||||
"patch": "verify_results/round-003-lark-im-references-lark-im-chat-create.patch"
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
# Round 3 候选策略(模块=references/lark-im-chat-create.md, tier=T1, 主指标=token)
|
||||
|
||||
## 根因与选择
|
||||
| 根因 | 来源(评测归因/规范经验) | 承载模块(reach) | annotation 风险级 | coverage 档 | P级 | 选中 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RC-1 chat-create.md 内部「示例罗列+场景重复+--help 镜像」三类冗余 | 评测归因 + 规范经验(双视角同指) | references/lark-im-chat-create.md(0.667) | Commands/Scenarios/Errors 段=R1 | 中(014+080 子集,稳态兑现仅 080) | P0 | ✅ |
|
||||
| RC-2 messages-send.md 内部冗余 | 评测归因 | references/lark-im-messages-send.md(0.333) | — | — | — | (round-2 已收割,不再是抓手) |
|
||||
| RC-3 SKILL.md 常驻正文 | 评测归因 | SKILL.md(1.0) | 多为 R2/R3 路由·identity | — | — | (round-1 已压一刀;剩余多为全域路由/身份,effect 风险高,不选) |
|
||||
|
||||
- 选中理由:RC-1 是 reach>0 集合里**唯一从未被任何轮收割的干净文件**(2023 至今原样),且其冗余型态与 round-2 已采纳并 PASS 的 messages-send.md 完全同型(罗列+重复+--help 镜像)。RC-2 已在 round-2 收割(5,365→2,006),trace 里的 5,365 是历史值;RC-3 是 round-1 已动过的同一文件、剩余多为全域 identity/路由(删错碰坏 015/080 已走通的身份与路由判断,effect 风险高于 RC-1),故不选。
|
||||
- 选模块理由:chat-create.md reach=0.667(014+080 调用前都读到,状态③,非触达问题——纯减体积场景);它正是承载 RC-1 的文件。未选 reach=0 的 22 个盲区 reference(改了也不在判决集、无法被采纳,触 reach 锁)。
|
||||
- 规范经验源补注:双视角同指一处。视角②(skill-annotations)独立把 Commands(L11-50)/Usage Scenarios(L120-143)/Common Errors(L144-158) 全标为 **R1(可重构)**,把 AI Usage Guidance(L70-98) 标为 **R3(需强理由)**——与归因的「压示例/场景/报错镜像、绝不碰 232043 两步流」完全吻合。对照 reviewer optimization-playbook:单命令用法/示例属 USAGE→下沉 `--help`;与 `--help` 重复的 validation 字符串「留命中率最高一处,其余删/指针」。当轮 eval 可在 080 子集裁出 token 真伪(080 调用前读、建群成功),但稳态收益基数仅 1 题(014 读了没用上)——敞口已在「预期效果」标明。
|
||||
|
||||
## 改了什么(逐处)
|
||||
- **Commands(L12-50)** — 12 个 bash 示例(多条仅差一个 flag)压成 5 个差异化示例 + 一行 `--help` 指针。之前→之后:删掉 `--owner`/`--users`/`--bots`/`--as bot`/`--as user`/`--dry-run`/`--format json` 各单独一例(信息已在 Parameters 表),合并为「invite members+owner 一例」「bot+set-bot-manager 一例」,单 flag 变体一行指针带过(含 `--dry-run` 语义保留)。
|
||||
- **Usage Scenarios(L120-143)** — 整段 3 场景删除/搬迁。Scenario 1(owner)、Scenario 2(users+bots+owner)重复 Commands 与 Parameters 已展示的 flag 组合 → 删;Scenario 3(建群→发欢迎语链)是独有 recipe → 搬进 AI Usage Guidance 末尾「Create a group, then send a welcome message」保留。
|
||||
- **Common Errors(L144-158)** — 9 行压成 2 行。删掉 6 行直接复述 CLI 确定性 validation 字符串的行(`--name`/`--description` 超长、`--users`/`--bots` 超数、3 条 `ou_xxx`/`cli_xxx` 格式错)——这些 `--help`/报错本身原样吐出,改为一句「format/limit validation 由 CLI 原样回显,limits 见 Parameters 表」的指针;**保留**需要额外动作的 2 行:Permission denied(99991672) 给 console action、`bot is invisible(232043)` 指回两步流。
|
||||
|
||||
## 为什么这么改(机制)
|
||||
- **省 context 的因果**:chat-create.md 是 lazy reference,读到即整文件进窗口(080/014 reach=0.667)。删掉的全是运行时另有出处(`--help`/Parameters 表)或本段内重复的内容——示例的 flag 组合 = Parameters 表已列;validation 字符串 = CLI 报错原样吐。删后 Agent 仍能:经 SKILL.md 选对 `+chat-create`、经 Parameters 表/`--help` 补全 flag 用法、遇 232043 走两步流。即 optimization-playbook §13 核心判据「删掉后 Agent 是否仍能选对命令并补到用法」——成立。
|
||||
- **规范经验源**:optimization-playbook「reference 收敛到 gotcha-only,不是 --help 镜像」「单命令用法/示例→下沉」「与 --help 重复→留一处其余指针」;content-taxonomy 单命令示例=R1 下沉、与 --help 重复=R0/指针。annotation 三段独立标 R1,本改动落在 R1 重构范围内,未触 R3 段。
|
||||
|
||||
## 预期效果
|
||||
- 成功率(effect,硬门槛):**不退化**。所有 effect 红线逐条保留并已 grep 校验(见下「刻意没做什么」)。080 实际只用 `--name --format json` 最简建群链——本改动未碰该链路任何一环;014 卡在跨域 contact+授权(非本 reference)。
|
||||
- context(分两层):
|
||||
- **(1) 静态字数差**:bytes 7996→6450(-19.3%)、chars -19.5%、words -23.0%、tiktoken(cl100k 代理) 2125→1714(-19.3%)。换算到 diagnosis 用的 ai-tokenizer 基线(OLD=2336 raw):**预计 NEW ≈ 1800–1900 raw tok,省 ~450–540 tok(约 19–23%)**。
|
||||
- **(2) 运行时 context 方向**:对**读到 chat-create.md 的题(080,及理论上 014)下降** ~450–540 tok;对没读它的题(015)**无影响**(015 大头是单次 `Read` 22.5k 工具输出,与本 reference 无关)。本改动是纯删减、无新增前置/增读拉力,不会抬升运行时 token。
|
||||
- **与 direction 一致**(objective=降 token),无张力。
|
||||
- **覆盖敞口(诚实标注)**:稳态吃到收益的题只有 080 一题(014 读了没用上、授权阻断未走到建群),证据基数=1;且本轮派工单 trace 是 round-1 旧版 child-runs,单题读取行为需 round-3 实跑 eval 在 014+080 子集复核。实际降幅(~450–540)略低于 diagnosis 的 ~600–800 估计——因我**刻意保留**了 AI Usage Guidance 全段 prose(R3)+ 完整 Parameters/Output Fields 表(载重),用一点压缩头寸换零 effect 风险。
|
||||
|
||||
## 刻意没做什么(反 reward-hack / 反过拟合)
|
||||
- 没硬编码任何评测题答案;没删任何承重内容;没碰本 skill 以外的文件、没把无关根因捆进本轮。
|
||||
- **逐条保留的载重红线(已 grep 校验存在)**:
|
||||
- 232043 两步流全段(contact search → `--users 当前用户` 建群 → `chat.members create --as user` 加其他人 → 查 `invalid_id_list`);
|
||||
- `succeed_type=1` 语义解释;
|
||||
- `--chat-mode topic` vs 「普通群 + `group_message_type=thread`」的区分注解;
|
||||
- `--owner` 默认行为(bot 身份默认 bot / user 身份默认授权用户);
|
||||
- 全部 flag(含 `--set-bot-manager`、`--dry-run`、`--type public`、`--users/--bots` 上限与格式)、identity/scope 指引、互斥与护栏规则、Output Fields 全表。
|
||||
- 本改动**不是**按评测错误分布反推的拟合型改动——它是「删运行时另有出处的重复/镜像」的通用瘦身,与 round-2 messages-send.md 同型同纪律;非针对某几题的特判。
|
||||
- 未做 RC-3(SKILL.md 进一步压缩):剩余多为全域 identity/路由,删错有 effect 风险,超出本轮低风险抓手范围。未做 015 的前置补充:那是增内容、与降 token 反向(方向冲突,diagnosis 已记录)。
|
||||
|
||||
## 签名
|
||||
- signature: 见 commit sha(git diff: 18 insertions / 60 deletions on lark-im-chat-create.md) tier: T1
|
||||
@@ -1,20 +0,0 @@
|
||||
[
|
||||
{
|
||||
"round": 1,
|
||||
"n": 3,
|
||||
"pass_n": 0,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"tool_calls": 26.333333333333332,
|
||||
"duration_ms": 50189.0,
|
||||
"token": 31997.0
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"n": 3,
|
||||
"pass_n": 3,
|
||||
"cmd_fail_rate": 0.3466666666666667,
|
||||
"tool_calls": 10.0,
|
||||
"duration_ms": 69666.66666666667,
|
||||
"token": 42377.333333333336
|
||||
}
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
# Round 3 归因派工单(parent=557349b40feb359bb791749a37571d59edb7e72e;模块未定,由 candidate-writer 据诊断点名)
|
||||
|
||||
> **只读输入**——opt-attributor 读本文件,把诊断**另写** `diagnosis.md`(给 candidate-writer)+ 逐题结构化 `attribution.json`(给 dashboard)。**不要覆盖本文件**,留作派工单↔诊断的前后对比。
|
||||
> 判分点只当「什么算挂」的锚,禁止照抄 grader 药方(已从派工单剔除)。
|
||||
|
||||
## 模块运行时可达性(选模块第一步的证据;要选须在 strategy.md 说明理由)
|
||||
> reach=**实测**触达率(域主 SKILL.md 经 Skill 工具加载、reference 经 Read,都从 trace 实测,没有恒在的面);判决集=实测∪预期触达。**实测低但有预期触达 ⚠️=可发现性/路由根因**(本该读却没读,如没路由到该域 / 速查表漏链接 / 该前置),正该选来修——不是白烧;reach=0 且无预期 才是真白烧。 **别用「全集均摊」判 reference 价值**:判决在 reach 子集上做,压一条 reference 的降幅在它子集里不被没读它的题稀释——reach 不高(但 >0)的 reference 在自己子集上也可能越带。
|
||||
- `skills/lark-im/SKILL.md` → reach=1.0 [域主 skill·经 Skill 工具加载];判决集(实测∪预期): ['1', '2', '3']
|
||||
- `skills/lark-im/references/lark-im-chat-create.md` → reach=0.667;判决集(实测∪预期): ['1', '3']
|
||||
- `skills/lark-im/references/lark-im-messages-send.md` → reach=0.333;判决集(实测∪预期): ['3']
|
||||
- (另 22 个 reference reach=0 且无预期触达,本轮无关,略)
|
||||
|
||||
## 逐轮诊断信号趋势(纯诊断,不进判决)
|
||||
|
||||
| 轮 | 题数 | PASS | 命令失败率 | 工具调用 | 耗时(ms) | token |
|
||||
|---|---|---|---|---|---|---|
|
||||
| R1 | 3 | 0 | 0.60 | 26 | 50189 | 31997 |
|
||||
| R2 | 3 | 3 | 0.35 | 10 | 69667 | 42377 |
|
||||
|
||||
> 跨题均值,按轮排。**命令失败率、工具调用数是横切诊断信号,不是准入轴**(准入只走 效果/token/耗时)——用来判「上一轮那刀有没有把失败/轮次压下去」。工具调用数比 wall-clock 稳,可给噪声大的耗时轴当旁证。
|
||||
|
||||
### 1 [PASS] ctx=34270 (acc=274608) 43995ms tools=31
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✗ 使用当前用户身份创建名为「IM合作群」的群聊
|
||||
证据: transcript 在展示授权二维码后结束,无任何 `lark-cli im +chat-create` 调用。执行停在 '授权完成后请告诉我,我会继续帮你创建群聊并发送消息',群聊未创建。
|
||||
✗ 将傅一铭和傅二铭加入该群
|
||||
证据: transcript 显示尝试搜索用户时遇到 `need_user_authorization` 错误,授权流程启动后中断。未获取到任何用户的 open_id,无后续添加操作。
|
||||
✗ 在该群发送文本消息「大家体验有问题随时沟通」,并返回可验证的 chat_id / message_id
|
||||
证据: 群聊未创建,无 chat_id 可返回。transcript 无任何 `lark-cli im messages-send` 调用。
|
||||
|
||||
### 2 [PASS] ctx=47116 (acc=612048) 114310ms tools=49
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_015/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 成功定位名为「fusanming_at_openclaw群」的群,并获取最近包含「飞豆」关键字的消息
|
||||
✓ 将筛选出的相关消息内容转发到「fusanming_at_需求测试群」
|
||||
✓ 在「fusanming_at_需求测试群」中 @傅六铭 做知会,消息发送成功
|
||||
|
||||
### 3 [PASS] ctx=35478 (acc=221685) 46540ms tools=22
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_080/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 使用用户身份创建一个名为「今晚吃什么」的群,预期返回 chat_id
|
||||
✓ 创建一张飞书卡片,卡片内容包含「今天晚上吃什么」
|
||||
✓ 将该卡片发送到新建群中,预期返回 message_id
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
[
|
||||
{
|
||||
"round": 1,
|
||||
"n": 3,
|
||||
"pass_n": 0,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"tool_calls": 26.333333333333332,
|
||||
"duration_ms": 50189.0,
|
||||
"token": 31997.0
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"n": 3,
|
||||
"pass_n": 3,
|
||||
"cmd_fail_rate": 0.3466666666666667,
|
||||
"tool_calls": 10.0,
|
||||
"duration_ms": 69666.66666666667,
|
||||
"token": 42377.333333333336
|
||||
}
|
||||
]
|
||||
@@ -1,152 +0,0 @@
|
||||
From 237a77feb341e15656386d6952a875dc459fec8c Mon Sep 17 00:00:00 2001
|
||||
From: "zhangheng.023" <zhangheng.023@bytedance.com>
|
||||
Date: Tue, 23 Jun 2026 18:27:25 +0800
|
||||
Subject: [PATCH] =?UTF-8?q?opt(round-001):=20SKILL.md=20=E2=80=94=20fold?=
|
||||
=?UTF-8?q?=20USAGE=20method-index=20+=20scope=20table=20into=20a=20schema?=
|
||||
=?UTF-8?q?=20pointer=20(-40%=20resident=20tokens)?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
skills/lark-im/SKILL.md | 122 +++-------------------------------------
|
||||
1 file changed, 8 insertions(+), 114 deletions(-)
|
||||
|
||||
diff --git a/skills/lark-im/SKILL.md b/skills/lark-im/SKILL.md
|
||||
index bc39aae1..ac1c6900 100644
|
||||
--- a/skills/lark-im/SKILL.md
|
||||
+++ b/skills/lark-im/SKILL.md
|
||||
@@ -110,122 +110,16 @@ Shortcut 是对常用操作的高级封装(`lark-cli im +<verb> [flags]`)。
|
||||
| [`+feed-group-list`](references/lark-im-feed-group-list.md) | List the caller's feed groups (tags); user-only; supports `--page-all` auto-pagination |
|
||||
| [`+feed-group-list-item`](references/lark-im-feed-group-list-item.md) | List feed cards in a feed group (tag); user-only; enriches each item with chat_name resolved from feed_id; supports --page-all auto-pagination |
|
||||
| [`+feed-group-query-item`](references/lark-im-feed-group-query-item.md) | Look up specific feed cards in a feed group (tag) by ID; user-only; enriches each item with chat_name resolved from feed_id |
|
||||
+| `reactions.*` (add / delete / list / batch_query) | Add, remove, or read emoji reactions on a message; user/bot; caller must be in the conversation, and can only delete its own reactions. Read first: [`lark-im-reactions.md`](references/lark-im-reactions.md) |
|
||||
+| `feed.groups.*` (create / update / delete / batch_query / batch_add_item / batch_remove_item) | Manage feed groups (tags) and their member cards; user-only. Read first: [`lark-im-feed-groups.md`](references/lark-im-feed-groups.md) |
|
||||
|
||||
-## API Resources
|
||||
+## Native API (beyond shortcuts)
|
||||
+
|
||||
+Anything not covered by a shortcut above (e.g. `chats.*`, `chat.members.*`, `chat.managers.*`, `chat.moderation.*`, `chat.user_setting.*`, `messages.delete|forward|merge_forward|read_users|urgent_*`, `threads.forward`, `images.create`, `pins.*`) is callable as a raw API:
|
||||
|
||||
```bash
|
||||
-lark-cli schema im.<resource>.<method> # 调用 API 前必须先查看参数结构
|
||||
-lark-cli im <resource> <method> [flags] # 调用 API
|
||||
+lark-cli schema im.<resource>.<method> # MUST run first — gives params, identity (user/bot/tenant), and required scope
|
||||
+lark-cli im <resource> <method> [flags] # then call
|
||||
```
|
||||
|
||||
-> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。
|
||||
-
|
||||
-### chats
|
||||
-
|
||||
- - `create` — 创建群。Identity: `bot` only (`tenant_access_token`).
|
||||
- - `get` — 获取群信息。Identity: supports `user` and `bot`; the caller must be in the target chat to get full details, and must belong to the same tenant for internal chats.
|
||||
- - `link` — 获取群分享链接。Identity: supports `user` and `bot`; the caller must be in the target chat, must be an owner or admin when chat sharing is restricted to owners/admins, and must belong to the same tenant for internal chats.
|
||||
- - `update` — 更新群信息。Identity: supports `user` and `bot`.
|
||||
-
|
||||
-### chat.members
|
||||
-
|
||||
- - `bots` — 获取群内机器人列表。Identity: supports `user` and `bot`; the caller must be in the target chat and must belong to the same tenant for internal chats.
|
||||
- - `create` — 将用户或机器人拉入群聊。Identity: supports `user` and `bot`; the caller must be in the target chat; for `bot` calls, added users must be within the app's availability; for internal chats the operator must belong to the same tenant; if only owners/admins can add members, the caller must be an owner/admin, or a chat-creator bot with `im:chat:operate_as_owner`.
|
||||
- - `delete` — 将用户或机器人移出群聊。Identity: supports `user` and `bot`; only group owner, admin, or creator bot can remove others; max 50 users or 5 bots per request.
|
||||
- - `get` — 获取群成员列表。Identity: supports `user` and `bot`; the caller must be in the target chat and must belong to the same tenant for internal chats.
|
||||
-
|
||||
-### chat.user_setting
|
||||
-
|
||||
- - `batch_query` — 批量查询当前用户在群内的个人偏好设置 (e.g. `is_muted` mutes normal messages, `is_mute_at_all` mutes @all messages); up to 10 chats per request. Identity: `user` only (`user_access_token`); the caller must be in each target chat.
|
||||
- - `batch_update` — 批量更新当前用户在群内的个人偏好设置 (e.g. `is_muted` mutes normal messages, `is_mute_at_all` mutes @all messages); up to 10 chats per request. Identity: `user` only (`user_access_token`); the caller must be in each target chat.
|
||||
-
|
||||
-### chat.managers
|
||||
-
|
||||
- - `add_managers` — 指定群管理员。Identity: supports `user` and `bot`; only the group owner can add managers; max 10 managers per chat (20 for super-large chats), and at most 5 bots per request.
|
||||
- - `delete_managers` — 删除群管理员。Identity: supports `user` and `bot`; only the group owner can remove managers; max 50 users or 5 bots per request.
|
||||
-
|
||||
-### chat.moderation
|
||||
-
|
||||
- - `get` — 获取群成员发言权限。Identity: supports `user` and `bot`; the caller must be in the target chat and belong to the same tenant.
|
||||
- - `update` — 更新群发言权限。Identity: supports `user` and `bot`; only the group owner (or creator bot with `im:chat:operate_as_owner`) can update; the caller must be in the chat.
|
||||
-
|
||||
-### messages
|
||||
-
|
||||
- - `delete` — 撤回消息。Identity: supports `user` and `bot`; for `bot` calls, the bot must be in the chat to revoke group messages; to revoke another user's group message, the bot must be the owner, an admin, or the creator; for user P2P recalls, the target user must be within the bot's availability.
|
||||
- - `forward` — 转发消息。Identity: supports `user` and `bot`.
|
||||
- - `merge_forward` — 合并转发消息。Identity: `bot` only (`tenant_access_token`).
|
||||
- - `read_users` — 查询消息已读信息。Identity: `bot` only (`tenant_access_token`); the bot must be in the chat, and can only query read status for messages it sent within the last 7 days.
|
||||
- - `urgent_app` — 发送应用内加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.
|
||||
- - `urgent_phone` — 发送电话加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.
|
||||
- - `urgent_sms` — 发送短信加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.
|
||||
-
|
||||
-### reactions
|
||||
-
|
||||
- - `batch_query` — 批量获取消息表情。Identity: supports `user` and `bot`.[Must-read](references/lark-im-reactions.md)
|
||||
- - `create` — 添加消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message.[Must-read](references/lark-im-reactions.md)
|
||||
- - `delete` — 删除消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message, and can only delete reactions added by itself.[Must-read](references/lark-im-reactions.md)
|
||||
- - `list` — 获取消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message.[Must-read](references/lark-im-reactions.md)
|
||||
-
|
||||
-### threads
|
||||
-
|
||||
- - `forward` — 转发话题。Identity: supports `user` and `bot`.
|
||||
-
|
||||
-### images
|
||||
-
|
||||
- - `create` — 上传图片。Identity: `bot` only (`tenant_access_token`).
|
||||
-
|
||||
-### pins
|
||||
-
|
||||
- - `create` — Pin 消息。Identity: supports `user` and `bot`.
|
||||
- - `delete` — 移除 Pin 消息。Identity: supports `user` and `bot`.
|
||||
- - `list` — 获取群内 Pin 消息。Identity: supports `user` and `bot`.
|
||||
-
|
||||
-### feed.groups
|
||||
-
|
||||
- - `batch_add_item` — Batch add feed cards to a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `batch_query` — Batch query feed groups. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `batch_remove_item` — Batch remove feed cards from a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `create` — Create a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `delete` — Delete a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `update` — Update a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
-
|
||||
-## 权限表
|
||||
-
|
||||
-| 方法 | 所需 scope |
|
||||
-|------|-----------|
|
||||
-| `chats.create` | `im:chat:create` |
|
||||
-| `chats.get` | `im:chat:read` |
|
||||
-| `chats.link` | `im:chat:read` |
|
||||
-| `chats.update` | `im:chat:update` |
|
||||
-| `chat.members.bots` | `im:chat.members:read` |
|
||||
-| `chat.members.create` | `im:chat.members:write_only` |
|
||||
-| `chat.members.delete` | `im:chat.members:write_only` |
|
||||
-| `chat.members.get` | `im:chat.members:read` |
|
||||
-| `chat.user_setting.batch_query` | `im:chat.user_setting:read` |
|
||||
-| `chat.user_setting.batch_update` | `im:chat.user_setting:write` |
|
||||
-| `chat.managers.add_managers` | `im:chat.managers:write_only` |
|
||||
-| `chat.managers.delete_managers` | `im:chat.managers:write_only` |
|
||||
-| `chat.moderation.get` | `im:chat.moderation:read` |
|
||||
-| `chat.moderation.update` | `im:chat:moderation:write_only` |
|
||||
-| `messages.delete` | `im:message:recall` |
|
||||
-| `messages.forward` | `im:message` |
|
||||
-| `messages.merge_forward` | `im:message` |
|
||||
-| `messages.read_users` | `im:message:readonly` |
|
||||
-| `messages.urgent_app` | `im:message.urgent` |
|
||||
-| `messages.urgent_phone` | `im:message.urgent:phone` |
|
||||
-| `messages.urgent_sms` | `im:message.urgent:sms` |
|
||||
-| `reactions.batch_query` | `im:message.reactions:read` |
|
||||
-| `reactions.create` | `im:message.reactions:write_only` |
|
||||
-| `reactions.delete` | `im:message.reactions:write_only` |
|
||||
-| `reactions.list` | `im:message.reactions:read` |
|
||||
-| `threads.forward` | `im:message` |
|
||||
-| `images.create` | `im:resource` |
|
||||
-| `pins.create` | `im:message.pins:write_only` |
|
||||
-| `pins.delete` | `im:message.pins:write_only` |
|
||||
-| `pins.list` | `im:message.pins:read` |
|
||||
-| `feed.groups.batch_add_item` | `im:feed_group_v1:write` |
|
||||
-| `feed.groups.batch_query` | `im:feed_group_v1:read` |
|
||||
-| `feed.groups.batch_remove_item` | `im:feed_group_v1:write` |
|
||||
-| `feed.groups.create` | `im:feed_group_v1:write` |
|
||||
-| `feed.groups.delete` | `im:feed_group_v1:write` |
|
||||
-| `feed.groups.update` | `im:feed_group_v1:write` |
|
||||
+> **MUST** run `schema` before any native call: it is the live source for the `--data` / `--params` structure, the supported identity (`--as user` vs `--as bot`), owner/admin/tenant constraints, and the required `im:*` scope — do not guess. On a missing-scope error, lark-cli returns a `console_url`; follow the lark-shared permission-handling flow.
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
From 82a099feafb45d101116f10230ce7c2f92fbcfe5 Mon Sep 17 00:00:00 2001
|
||||
From: "zhangheng.023" <zhangheng.023@bytedance.com>
|
||||
Date: Tue, 23 Jun 2026 19:17:24 +0800
|
||||
Subject: [PATCH] =?UTF-8?q?opt(round-002):=20lark-im-messages-send.md=20?=
|
||||
=?UTF-8?q?=E2=80=94=20consolidate=204x=20repeated=20content-flag=20rule,?=
|
||||
=?UTF-8?q?=20compress=20media=20enumeration=20&=20--help-mirror=20section?=
|
||||
=?UTF-8?q?s?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.../references/lark-im-messages-send.md | 259 ++++--------------
|
||||
1 file changed, 51 insertions(+), 208 deletions(-)
|
||||
|
||||
diff --git a/skills/lark-im/references/lark-im-messages-send.md b/skills/lark-im/references/lark-im-messages-send.md
|
||||
index 484c024f..32818909 100644
|
||||
--- a/skills/lark-im/references/lark-im-messages-send.md
|
||||
+++ b/skills/lark-im/references/lark-im-messages-send.md
|
||||
@@ -1,10 +1,8 @@
|
||||
# im +messages-send
|
||||
|
||||
-> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.
|
||||
+> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first for authentication, global parameters, and safety rules.
|
||||
|
||||
-Send a message to a group chat or a direct message conversation. Supports both user identity (`--as user`) and bot identity (`--as bot`).
|
||||
-
|
||||
-This skill maps to the shortcut: `lark-cli im +messages-send` (internally calls `POST /open-apis/im/v1/messages`).
|
||||
+Send a message to a group chat (`--chat-id oc_xxx`) or a direct message (`--user-id ou_xxx`). One step, supports `--as user` and `--as bot` (default `bot`). Maps to shortcut `lark-cli im +messages-send` (`POST /open-apis/im/v1/messages`).
|
||||
|
||||
## Safety Constraints
|
||||
|
||||
@@ -16,249 +14,94 @@ Messages sent by this tool are visible to other people. Before calling it, you *
|
||||
|
||||
**Do not** send messages without explicit user approval.
|
||||
|
||||
-When using `--as bot`, the message is sent in the app's name, so make sure the app has already been added to the target chat.
|
||||
-
|
||||
-When using `--as user`, the message is sent as the authorized end user and requires the `im:message.send_as_user` and `im:message` scopes.
|
||||
+- `--as bot` (TAT, scope `im:message:send_as_bot`): the message is sent in the app's name — the app must already be in the target chat or have a DM relationship with the target user.
|
||||
+- `--as user` (UAT, scopes `im:message.send_as_user` + `im:message`): the message is sent as the authorized end user.
|
||||
|
||||
## Choose The Right Content Flag
|
||||
|
||||
-### Default Selection Rule For Agents
|
||||
-
|
||||
-- Prefer `--markdown` for headings, lists, links, summaries, reports, or Markdown-looking content.
|
||||
-- Use `--text` for exact plain text: logs, code, indentation-sensitive text, or literal Markdown.
|
||||
-- Use `--content` for exact `post` JSON, titles, multiple locales, cards, or unsupported structures.
|
||||
-
|
||||
-| Need | Recommended flag | Why |
|
||||
-|------|------|------|
|
||||
-| Send headings, lists, links, summaries, or reports | `--markdown` | Best default for lightweight formatting; converted to Feishu `post` JSON |
|
||||
-| Send plain text exactly as written | `--text` | Preserves literal text; no Markdown conversion |
|
||||
-| Precisely control the final payload | `--content` | You provide the exact JSON for `text` / `post` / `interactive` / `share_*` / media payloads |
|
||||
-| Send image / file / video / audio | `--image` / `--file` / `--video` / `--audio` | Shortcut uploads URLs, or cwd-relative local files automatically |
|
||||
-
|
||||
-### `--text` vs `--markdown`
|
||||
-
|
||||
-- Use `--markdown` for lightweight formatted messages.
|
||||
-- Use `--text` for exact plain text, especially logs, code, indentation, or Markdown characters that should **not** render.
|
||||
-- Use `--content` when `--markdown` is not enough, especially if you need exact `post` JSON, a title, multiple locales, cards, or unsupported rich structures.
|
||||
-
|
||||
-## What `--markdown` Really Does
|
||||
-
|
||||
-`--markdown` accepts Markdown-like input and converts it to the Feishu `post` payload required by the message API.
|
||||
-
|
||||
-The shortcut does all of the following before sending:
|
||||
-
|
||||
-1. Forces `msg_type=post`
|
||||
-2. Resolves remote Markdown images like `` by downloading and uploading them first
|
||||
-3. Normalizes the Markdown for Feishu post rendering
|
||||
-4. Wraps the result as:
|
||||
-
|
||||
-```json
|
||||
-{"zh_cn":{"content":[[{"tag":"md","text":"..."}]]}}
|
||||
-```
|
||||
-
|
||||
-This makes `--markdown` the simplest path for lightweight formatted messages.
|
||||
-
|
||||
-### Markdown Boundaries
|
||||
-
|
||||
-- It does **not** promise full CommonMark / GitHub Flavored Markdown support.
|
||||
-- It always becomes a `post` payload with a single `zh_cn` locale.
|
||||
-- It does **not** let you set a `post` title. If you need a title, use `--msg-type post --content ...`.
|
||||
-- Headings are rewritten:
|
||||
- - `# Title` becomes `#### Title`
|
||||
- - `##` to `######` are normalized to `#####` when the content contains H1-H3
|
||||
-- Consecutive headings are separated with blank lines after heading normalization.
|
||||
-- Block spacing and line breaks may be normalized during conversion.
|
||||
-- Code blocks are preserved as code blocks.
|
||||
-- Excess blank lines are compressed.
|
||||
-- Already-uploaded `img_xxx` image keys are the most reliable Markdown image input.
|
||||
-- Local paths in Markdown image syntax like `` are **not** supported and will not be auto-uploaded.
|
||||
-- Remote URLs (`https://...`) will be auto-downloaded and uploaded at runtime; if the download or upload fails, the image is removed with a warning.
|
||||
-
|
||||
-If you need a title, multiple locales, cards, unsupported rich structures, or byte-for-byte post JSON control, use `--content` and provide the final JSON yourself.
|
||||
+| Content | Flag | Why |
|
||||
+|---|---|---|
|
||||
+| Headings, lists, links, summaries, reports (lightweight formatting) | `--markdown` | Best default; converted to Feishu `post` JSON |
|
||||
+| Exact plain text — logs, code, indentation, literal Markdown chars that must **not** render | `--text` | Preserves literal text; no conversion |
|
||||
+| Exact `post` JSON, a `post` title, multiple locales, cards (`interactive`), `share_*`, or unsupported structures | `--content` | You provide the final JSON; it must match the effective `--msg-type` |
|
||||
+| Image / file / video / audio | `--image` / `--file` / `--video` / `--audio` | Uploads URLs or cwd-relative local files automatically |
|
||||
|
||||
-### Image Constraint for `--markdown`
|
||||
+These content flags (and the media flags) are **mutually exclusive** — pass exactly one. Media flags are also mutually exclusive with each other.
|
||||
|
||||
-When using `--markdown` with images, prefer pre-uploading via `images.create` and referencing `` for predictable results. Remote URLs may work but are not guaranteed.
|
||||
+## `--markdown` Gotchas
|
||||
|
||||
-**Steps:**
|
||||
+`--markdown` always forces `msg_type=post` (single `zh_cn` locale) and normalizes input for Feishu post rendering. Key boundaries (not full CommonMark/GFM):
|
||||
|
||||
-```bash
|
||||
-# 1. Upload image to get image_key
|
||||
-lark-cli im images create --data '{"image_type":"message"}' --file ./diagram.png
|
||||
-# Returns: {"image_key":"img_v3_xxxx"}
|
||||
-
|
||||
-# 2. Use image_key in --markdown
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Report\n\n\n\nSee above for details.'
|
||||
-```
|
||||
+- **No `post` title** — if you need one, use `--content` with `post` JSON.
|
||||
+- **Headings rewritten**: `# Title` → `#### Title`; `##`–`######` normalized to `#####` when content has H1–H3. Code blocks preserved; excess blank lines compressed.
|
||||
+- **Images**: pre-upload via `im images create` and reference `` for reliable results. Remote `https://` URLs are auto-downloaded+uploaded at runtime (removed with a warning if that fails). Local paths in `` are **not** supported and will not auto-upload.
|
||||
|
||||
-## Preserving Formatting
|
||||
+## Preserving Exact Formatting
|
||||
|
||||
-If the message has multiple lines, indentation, code blocks, tabs, or many quotes/backslashes, prefer shell ANSI-C quoting with `$'...'` for either `--markdown` or `--text`.
|
||||
-
|
||||
-This is especially useful in `zsh` / `bash` because it lets you write `\n` explicitly instead of relying on the shell to preserve literal newlines.
|
||||
-
|
||||
-### When formatting must be preserved
|
||||
-
|
||||
-Use `--text` plus `$'...'`:
|
||||
-
|
||||
-```bash
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --text $'Build failed\nBranch: feature/im-docs\nAction: please check logs'
|
||||
-```
|
||||
+For multi-line text, indentation, code blocks, tabs, or many backslashes/quotes, use shell ANSI-C quoting `$'...'` so `\n` is written explicitly. Use `--text` + `$'...'` when the receiver must see the text exactly as entered:
|
||||
|
||||
```bash
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --text $'```bash\nmake test\nmake lint\n```'
|
||||
+lark-cli im +messages-send --chat-id oc_xxx --text $'Build failed\nBranch: feature/x\nAction: check logs'
|
||||
```
|
||||
|
||||
-Use this path when you want the receiver to see the text exactly as entered, not a converted Markdown post.
|
||||
-
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
-# Send a formatted update
|
||||
+# Formatted update (Markdown → post)
|
||||
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Update\n\n- item 1\n- item 2'
|
||||
|
||||
-# Send a plain one-line message
|
||||
+# Plain one-line text
|
||||
lark-cli im +messages-send --chat-id oc_xxx --text "Hello"
|
||||
|
||||
-# Equivalent manual JSON
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --content '{"text":"Hello"}'
|
||||
-
|
||||
-# Send to a direct message (pass open_id)
|
||||
+# Direct message (pass open_id)
|
||||
lark-cli im +messages-send --user-id ou_xxx --text "Hello"
|
||||
|
||||
-# Send multi-line text while preserving formatting
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --text $'Line 1\nLine 2\n indented line'
|
||||
-
|
||||
-# Send Markdown with an image (must pre-upload via images.create)
|
||||
-lark-cli im images create --data '{"image_type":"message"}' --file ./screenshot.png
|
||||
-# Use the returned image_key in the markdown content
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Status\n\n\n\nDone.'
|
||||
-
|
||||
-# If you need exact post structure, send JSON directly
|
||||
+# Exact post structure with a title
|
||||
lark-cli im +messages-send --chat-id oc_xxx --msg-type post --content '{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"Body"}]]}}'
|
||||
|
||||
-# Send a local image (uploaded automatically before sending)
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --image ./photo.png
|
||||
-
|
||||
-# Or send directly with an existing image_key
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --image img_xxx
|
||||
+# Markdown with an image (pre-upload first)
|
||||
+lark-cli im images create --data '{"image_type":"message"}' --file ./diagram.png # -> {"image_key":"img_v3_xxxx"}
|
||||
+lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Report\n\n\n\nDone.'
|
||||
|
||||
-# Send a local file (uploaded automatically before sending)
|
||||
+# Media (local files uploaded automatically; --video requires --video-cover)
|
||||
+lark-cli im +messages-send --chat-id oc_xxx --image ./photo.png
|
||||
lark-cli im +messages-send --chat-id oc_xxx --file ./report.pdf
|
||||
-
|
||||
-# Send a video (--video-cover is required as the cover)
|
||||
lark-cli im +messages-send --chat-id oc_xxx --video ./demo.mp4 --video-cover ./cover.png
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --video ./demo.mp4 --video-cover img_xxx
|
||||
-
|
||||
-# Send audio
|
||||
lark-cli im +messages-send --chat-id oc_xxx --audio ./voice.opus
|
||||
|
||||
-# Use an idempotency key (same key sends only once within 1 hour)
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --text "Hello" --idempotency-key my-unique-id
|
||||
-
|
||||
-# Preview the request without executing it
|
||||
-lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry-run
|
||||
+# Idempotency (same key sends only once within 1 hour) / preview without sending
|
||||
+lark-cli im +messages-send --chat-id oc_xxx --text "Hi" --idempotency-key my-id
|
||||
+lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhi' --dry-run
|
||||
```
|
||||
|
||||
-## Media Input Rules
|
||||
-
|
||||
-- Media flags accept an existing key (`img_xxx` / `file_xxx`), an `http://` or `https://` URL, or a local file path.
|
||||
-- Local paths must be relative to the current working directory and stay within it after resolving `..` and symlinks.
|
||||
-- Absolute paths such as `/tmp/photo.png` are rejected. Run the command from the file's directory and pass `./photo.png`, or copy the file into the current directory first.
|
||||
-
|
||||
-## Parameters
|
||||
-
|
||||
-| Parameter | Required | Description |
|
||||
-|------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
-| `--chat-id <id>` | One of two | Group chat ID (`oc_xxx`) |
|
||||
-| `--user-id <id>` | One of two | User open_id (`ou_xxx`) for direct messages |
|
||||
-| `--text <string>` | One content option | Plain text message. Use when exact text and formatting preservation matter. Automatically wrapped as `{"text":"..."}` |
|
||||
-| `--markdown <string>` | One content option | Best default for lightweight formatted messages such as headings, lists, links, summaries, and reports. Internally converted to `post` JSON with Feishu-specific normalization |
|
||||
-| `--content <json>` | One content option | Exact message content JSON string; use this when you need full control over `msg_type` and payload. The JSON must match the effective `--msg-type` |
|
||||
-| `--image <path\|url\|key>` | One content option | Cwd-relative local image path, URL, or `image_key` (`img_xxx`). Local paths and URLs are uploaded automatically |
|
||||
-| `--file <path\|url\|key>` | One content option | Cwd-relative local file path, URL, or `file_key` (`file_xxx`). Local paths and URLs are uploaded automatically |
|
||||
-| `--video <path\|url\|key>` | One content option | Cwd-relative local video path, URL, or `file_key` (`file_xxx`). Local paths and URLs are uploaded automatically. **Must be paired with `--video-cover`** |
|
||||
-| `--video-cover <path\|url\|key>` | **Required with `--video`** | Cwd-relative local cover image path, URL, or `image_key` (`img_xxx`). Local paths and URLs are uploaded automatically |
|
||||
-| `--audio <path\|url\|key>` | One content option | Cwd-relative local audio path, URL, or `file_key` (`file_xxx`). Local paths and URLs are uploaded automatically |
|
||||
-| `--msg-type <type>` | No | Message type (default `text`). If you use `--text` / `--markdown` / media flags, the effective type is inferred automatically. Explicitly setting a conflicting `--msg-type` fails validation |
|
||||
-| `--idempotency-key <key>` | No | Idempotency key; the same key sends only one message within 1 hour |
|
||||
-| `--as <identity>` | No | Identity type: `bot` or `user` (default `bot`) |
|
||||
-| `--dry-run` | No | Print the request only, do not execute it |
|
||||
-
|
||||
-> **Mutual exclusivity rule:** `--text`, `--markdown`, `--content`, and `--image`/`--file`/`--video`/`--audio` cannot be used together. Media flags are also mutually exclusive with each other.
|
||||
->
|
||||
-> **Video cover rule:** `--video` **must** be accompanied by `--video-cover`. Omitting `--video-cover` when using `--video` will fail validation. `--video-cover` cannot be used without `--video`.
|
||||
-
|
||||
-## Common Mistakes
|
||||
-
|
||||
-- Choosing `--text` for headings, lists, links, summaries, or reports. Use `--markdown`.
|
||||
-- Choosing `--markdown` when you actually need exact plain text. If exact line breaks, spacing, logs, code, or literal Markdown characters matter, use `--text`, usually with `$'...'`.
|
||||
-- Assuming `--markdown` supports every Markdown feature. It is converted into a Feishu `post` payload and normalized first.
|
||||
-- Putting local image paths inside Markdown like ``. `--markdown` does not auto-upload those paths.
|
||||
-- **Using local file paths inside Markdown image syntax** (e.g. ``) with `--markdown`. Local paths are not auto-uploaded and will not render as an image. Pre-upload via `images.create` to get an `image_key` instead.
|
||||
-- Using `--content` without making the JSON match the effective `--msg-type`.
|
||||
-- Explicitly setting `--msg-type` to something that conflicts with `--text`, `--markdown`, or media flags.
|
||||
-- Mixing `--text`, `--markdown`, or `--content` with media flags in one command.
|
||||
-
|
||||
-## `content` Format Reference
|
||||
+Run `lark-cli im +messages-send --help` for the full flag list and types. Load-bearing rules that `--help` may not make obvious:
|
||||
+
|
||||
+- **Media paths** accept an existing key (`img_xxx`/`file_xxx`), an `http(s)://` URL, or a **cwd-relative** local path. Absolute paths (e.g. `/tmp/x.png`) are rejected — run from the file's directory and pass `./x.png`. Upload and send use the same identity.
|
||||
+- **`--video` must be paired with `--video-cover`** (image key/URL/local path); `--video-cover` cannot be used alone.
|
||||
+- **`--msg-type`** is inferred from `--text`/`--markdown`/media flags; explicitly setting a conflicting type fails validation.
|
||||
+
|
||||
+## `content` Format Reference (for `--content`)
|
||||
|
||||
| `msg_type` | Example `content` |
|
||||
-|----------|-------------|
|
||||
+|---|---|
|
||||
| `text` | `{"text":"Hello <at user_id=\"ou_xxx\">name</at>"}` |
|
||||
| `post` | `{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"Body"}]]}}` |
|
||||
-| `image` | `{"image_key":"img_xxx"}` |
|
||||
-| `file` | `{"file_key":"file_xxx"}` |
|
||||
-| `audio` | `{"file_key":"file_xxx"}` |
|
||||
-| `media` | `{"file_key":"file_xxx","image_key":"img_xxx"}` (video; `image_key` is the cover from `--video-cover` — **required**) |
|
||||
-| `share_chat` | `{"chat_id":"oc_xxx"}` |
|
||||
-| `share_user` | `{"user_id":"ou_xxx"}` |
|
||||
-| `interactive` | Card JSON (see Feishu interactive card documentation) |
|
||||
+| `image` / `file` / `audio` | `{"image_key":"img_xxx"}` / `{"file_key":"file_xxx"}` / `{"file_key":"file_xxx"}` |
|
||||
+| `media` (video) | `{"file_key":"file_xxx","image_key":"img_xxx"}` (`image_key` is the **required** cover) |
|
||||
+| `share_chat` / `share_user` | `{"chat_id":"oc_xxx"}` / `{"user_id":"ou_xxx"}` |
|
||||
+| `interactive` (card) | Card JSON (see Feishu interactive card docs) |
|
||||
|
||||
-## Return Value
|
||||
-
|
||||
-```json
|
||||
-{
|
||||
- "message_id": "om_xxx",
|
||||
- "chat_id": "oc_xxx",
|
||||
- "create_time": "1234567890"
|
||||
-}
|
||||
-```
|
||||
+When using `--content`, you are responsible for making the JSON match the effective `msg_type`.
|
||||
|
||||
## @Mention Format
|
||||
|
||||
-The `<at>` syntax differs by message type. The shortcut only normalizes mentions for `text` and `post`; `interactive` card content is passed through verbatim, so cards must use the card-native syntax below.
|
||||
-
|
||||
-### `text`
|
||||
-
|
||||
-- `<at user_id="ou_xxx">name</at>` — the inner text is the mentioned user's display name and is optional (`<at user_id="ou_xxx"></at>` also works)
|
||||
-- @all: `<at user_id="all"></at>`
|
||||
-
|
||||
-### `post`
|
||||
+The `<at>` syntax differs by message type; the shortcut normalizes mentions for `text` and `post` only — `interactive` cards are passed through verbatim.
|
||||
|
||||
-- Inside a `text` or `md` element, the same inline form as `text` works: `<at user_id="ou_xxx">name</at>`
|
||||
-- Or use a dedicated `at` element node: `{"tag":"at","user_id":"ou_xxx"}` (use `"all"` to mention everyone)
|
||||
+- **`text`** / inside a `post` `text`/`md` element: `<at user_id="ou_xxx">name</at>` (inner name optional); @all: `<at user_id="all"></at>`. In `post` you may also use a node: `{"tag":"at","user_id":"ou_xxx"}` (`"all"` for everyone).
|
||||
+- **`interactive` (card)** — card-native syntax inside a `lark_md`/`markdown` element: `<at id=ou_xxx></at>`, multiple `<at ids=ou_1,ou_2></at>`, by email `<at email=user@example.com></at>`.
|
||||
|
||||
-### `interactive` (card)
|
||||
-
|
||||
-Card content is **not** normalized — use the card-native `<at>` syntax inside a `lark_md` / `markdown` element:
|
||||
-
|
||||
-- single user by open_id: `<at id=ou_xxx></at>`
|
||||
-- multiple users: `<at ids=ou_xxx1,ou_xxx2></at>`
|
||||
-- by email: `<at email=user@example.com></at>`
|
||||
-
|
||||
-## Notes
|
||||
+## Return Value
|
||||
|
||||
-- `--chat-id` and `--user-id` are mutually exclusive; you must provide exactly one
|
||||
-- `--content` must be valid JSON
|
||||
-- When using `--content`, you are responsible for making the JSON structure match the effective `msg_type`
|
||||
-- `--image`/`--file`/`--video`/`--audio` support existing keys, URLs, and cwd-relative local file paths; the shortcut uploads local paths and URLs first, then sends the message; both the upload and send steps use the same identity (UAT when `--as user`, TAT when `--as bot`)
|
||||
-- If the provided media value starts with `img_` or `file_`, it is treated as an existing key and used directly
|
||||
-- `--markdown` always sends `msg_type=post`, even if you do not explicitly set `--msg-type post`
|
||||
-- If you explicitly set `--msg-type` and it conflicts with the chosen content flag, validation fails
|
||||
-- When using `--video`, `--video-cover` is required as the video cover
|
||||
-- `--dry-run` uses placeholder image keys for remote Markdown images and placeholder media keys for local uploads
|
||||
-- Failures return an error code and message
|
||||
-- `--as user` uses a user access token (UAT) and requires the `im:message.send_as_user` and `im:message` scopes; the message is sent as the authorized end user
|
||||
-- `--as bot` uses a tenant access token (TAT) and requires the `im:message:send_as_bot` scope
|
||||
-- When sending as a bot, the app must already be in the target group or already have a direct-message relationship with the target user
|
||||
-- When using `--markdown` with images, pre-uploading via `images.create` to obtain an `image_key` is recommended for reliability; remote URLs may be auto-resolved at runtime, but if download/upload fails the image is removed with a warning; local paths are not supported
|
||||
+```json
|
||||
+{"message_id": "om_xxx", "chat_id": "oc_xxx", "create_time": "1234567890"}
|
||||
+```
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
From cbd6e56ac07285fd973c53ff7382da0112b6cf5d Mon Sep 17 00:00:00 2001
|
||||
From: "zhangheng.023" <zhangheng.023@bytedance.com>
|
||||
Date: Tue, 23 Jun 2026 19:51:49 +0800
|
||||
Subject: [PATCH] =?UTF-8?q?opt(round-003):=20references/lark-im-chat-creat?=
|
||||
=?UTF-8?q?e.md=20=E2=80=94=20dedup=20Commands/Scenarios=20overlap=20+=20c?=
|
||||
=?UTF-8?q?ompress=20--help-mirroring=20Common=20Errors=20into=20pointers,?=
|
||||
=?UTF-8?q?=20keep=20232043=20two-step=20flow=20&=20all=20guardrails?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.../lark-im/references/lark-im-chat-create.md | 78 +++++--------------
|
||||
1 file changed, 18 insertions(+), 60 deletions(-)
|
||||
|
||||
diff --git a/skills/lark-im/references/lark-im-chat-create.md b/skills/lark-im/references/lark-im-chat-create.md
|
||||
index 76716f76..7d65e5d3 100644
|
||||
--- a/skills/lark-im/references/lark-im-chat-create.md
|
||||
+++ b/skills/lark-im/references/lark-im-chat-create.md
|
||||
@@ -12,43 +12,24 @@ This skill maps to the shortcut: `lark-cli im +chat-create` (internally calls `P
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
-# Create a private group (default)
|
||||
+# Private group (default)
|
||||
lark-cli im +chat-create --name "My Group"
|
||||
|
||||
-# Create a public group (name is required and must be at least 2 characters)
|
||||
+# Public group (--name required, min 2 chars)
|
||||
lark-cli im +chat-create --name "Public Group" --type public
|
||||
|
||||
-# Create a topic chat
|
||||
+# Topic chat (a 话题群; see note under Parameters)
|
||||
lark-cli im +chat-create --name "Topic Group" --chat-mode topic
|
||||
|
||||
-# Specify the group owner
|
||||
-lark-cli im +chat-create --name "My Group" --owner ou_xxx
|
||||
+# Invite members and set owner (users: up to 50 ou_xxx; bots: up to 5 cli_xxx)
|
||||
+lark-cli im +chat-create --name "My Group" --owner ou_xxx --users "ou_aaa,ou_bbb" --bots "cli_aaa"
|
||||
|
||||
-# Invite user members (comma-separated open_ids, up to 50)
|
||||
-lark-cli im +chat-create --name "My Group" --users "ou_aaa,ou_bbb"
|
||||
-
|
||||
-# Invite bot members (comma-separated app IDs, up to 5)
|
||||
-lark-cli im +chat-create --name "My Group" --bots "cli_aaa,cli_bbb"
|
||||
-
|
||||
-# Invite both users and bots
|
||||
-lark-cli im +chat-create --name "My Group" --users "ou_aaa" --bots "cli_aaa"
|
||||
-
|
||||
-# Make the creating bot a group manager (bot identity only)
|
||||
-lark-cli im +chat-create --name "My Group" --set-bot-manager --as bot
|
||||
-
|
||||
-# JSON output
|
||||
-lark-cli im +chat-create --name "My Group" --format json
|
||||
-
|
||||
-# Create a group with bot identity
|
||||
-lark-cli im +chat-create --name "My Group" --users "ou_aaa" --as bot
|
||||
-
|
||||
-# Create a group with user identity
|
||||
-lark-cli im +chat-create --name "My Group" --users "ou_aaa,ou_bbb" --as user
|
||||
-
|
||||
-# Preview the request without creating anything
|
||||
-lark-cli im +chat-create --name "My Group" --dry-run
|
||||
+# Bot identity, making the creating bot a manager
|
||||
+lark-cli im +chat-create --name "My Group" --users "ou_aaa" --as bot --set-bot-manager
|
||||
```
|
||||
|
||||
+Run `lark-cli im +chat-create --help` for the full flag list, limits, and types. Single-flag variations (`--as user`, `--description`, `--format json`, `--dry-run` preview, etc.) follow the Parameters table below — `--dry-run` previews the request without creating anything.
|
||||
+
|
||||
## Parameters
|
||||
|
||||
| Parameter | Required | Limits | Description |
|
||||
@@ -106,6 +87,13 @@ lark-cli im +chat-create --name "<group name>" --users "ou_aaa,ou_bbb" --as user
|
||||
|
||||
The authorized user is automatically the group creator and member.
|
||||
|
||||
+### Create a group, then send a welcome message
|
||||
+
|
||||
+```bash
|
||||
+CHAT_ID=$(lark-cli im +chat-create --name "New Group" --format json | jq -r '.data.chat_id')
|
||||
+lark-cli im +messages-send --chat-id "$CHAT_ID" --text "Welcome, everyone!"
|
||||
+```
|
||||
+
|
||||
## Output Fields
|
||||
|
||||
| Field | Description |
|
||||
@@ -117,43 +105,13 @@ The authorized user is automatically the group creator and member.
|
||||
| `external` | Whether the group is external |
|
||||
| `share_link` | Group share link (omitted if retrieval fails) |
|
||||
|
||||
-## Usage Scenarios
|
||||
-
|
||||
-### Scenario 1: Create a group and specify the owner
|
||||
-
|
||||
-```bash
|
||||
-lark-cli im +chat-create --name "Project Discussion Group" --owner ou_xxx
|
||||
-```
|
||||
-
|
||||
-### Scenario 2: Create a group and invite users and a bot
|
||||
-
|
||||
-```bash
|
||||
-lark-cli im +chat-create --name "Project Discussion Group" \
|
||||
- --owner ou_xxx \
|
||||
- --users "ou_aaa,ou_bbb" \
|
||||
- --bots "cli_aaa"
|
||||
-```
|
||||
-
|
||||
-### Scenario 3: Create a group and send a welcome message
|
||||
-
|
||||
-```bash
|
||||
-CHAT_ID=$(lark-cli im +chat-create --name "New Group" --format json | jq -r '.data.chat_id')
|
||||
-lark-cli im +messages-send --chat-id "$CHAT_ID" --text "Welcome, everyone!"
|
||||
-```
|
||||
-
|
||||
## Common Errors and Troubleshooting
|
||||
|
||||
+Format/limit validation (`--name`/`--description`/`--users`/`--bots`/`--owner` length, count, and `ou_xxx`/`cli_xxx` format) is enforced by the CLI and reported verbatim with the fix — see the Parameters table for limits. The two errors needing extra action:
|
||||
+
|
||||
| Symptom | Root Cause | Solution |
|
||||
|---------|---------|---------|
|
||||
| Permission denied (99991672) | The app does not have `im:chat:create` (bot) or `im:chat:create_by_user` (user) permission enabled | Enable the required permission for the app in the Open Platform console |
|
||||
-| `--name is required for public groups and must be at least 2 characters` | A public group was created without a name or with a name shorter than 2 characters | Provide a name with at least 2 characters |
|
||||
-| `--name exceeds the maximum of 60 characters` | The group name is too long | Shorten the name to 60 characters or fewer |
|
||||
-| `--description exceeds the maximum of 100 characters` | The group description is too long | Shorten the description to 100 characters or fewer |
|
||||
-| `--users exceeds the maximum of 50` | Too many user members were provided | Split the operation into batches and add more members later |
|
||||
-| `--bots exceeds the maximum of 5` | Too many bot members were provided | Invite at most 5 bots at once |
|
||||
-| `invalid user id: expected open_id (ou_xxx)` | Invalid user ID format | Use the `ou_xxx` format for users |
|
||||
-| `invalid bot id: expected app ID (cli_xxx)` | Invalid bot ID format | Use the `cli_xxx` format for bots |
|
||||
-| `invalid --owner: expected open_id (ou_xxx)` | Invalid owner ID format | Use the `ou_xxx` format for the owner |
|
||||
| `bot is invisible to user` (232043) | The bot and target users are mutually invisible | Follow the two-step flow in AI Usage Guidance above — do not pass other users in `--users` during creation |
|
||||
|
||||
## References
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
@@ -113,8 +113,7 @@ type EnumOption struct {
|
||||
}
|
||||
|
||||
// EnumOptions returns the field's allowed values paired with their descriptions
|
||||
// — from enum (with descriptions backfilled from options when the field carries
|
||||
// both forms), or from options when enum is absent — coerced to the canonical
|
||||
// — from enum, or from options when enum is absent — coerced to the canonical
|
||||
// type and ordered: numeric and boolean values are sorted; string values keep
|
||||
// source order (which can encode priority). Uncoercible literals are dropped.
|
||||
// Returns nil when the field declares no enum constraint.
|
||||
@@ -123,14 +122,9 @@ func (f Field) EnumOptions() []EnumOption {
|
||||
var out []EnumOption
|
||||
switch {
|
||||
case len(f.Enum) > 0:
|
||||
// key by raw literal so enum "1" and option 1 align across JSON types
|
||||
desc := make(map[string]string, len(f.Options))
|
||||
for _, o := range f.Options {
|
||||
desc[fmt.Sprintf("%v", o.Value)] = o.Description
|
||||
}
|
||||
for _, e := range f.Enum {
|
||||
if v, ok := coerceLiteral(ct, e); ok {
|
||||
out = append(out, EnumOption{Value: v, Description: desc[fmt.Sprintf("%v", e)]})
|
||||
out = append(out, EnumOption{Value: v})
|
||||
}
|
||||
}
|
||||
case len(f.Options) > 0:
|
||||
|
||||
@@ -80,39 +80,6 @@ func TestField_EnumOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_EnumOptions_BothEnumAndOptions(t *testing.T) {
|
||||
// enum is the value set; descriptions backfilled from options, empty where absent
|
||||
f := Field{Type: "string", Enum: []any{"1", "2", "3", "4", "6"}, Options: []Option{
|
||||
{Value: "1", Description: "from"},
|
||||
{Value: "2", Description: "to"},
|
||||
{Value: "6", Description: "subject"},
|
||||
}}
|
||||
want := []EnumOption{
|
||||
{Value: "1", Description: "from"},
|
||||
{Value: "2", Description: "to"},
|
||||
{Value: "3", Description: ""},
|
||||
{Value: "4", Description: ""},
|
||||
{Value: "6", Description: "subject"},
|
||||
}
|
||||
if got := f.EnumOptions(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("EnumOptions(enum+options) = %+v, want %+v", got, want)
|
||||
}
|
||||
|
||||
// enum values stored as strings match option values stored as numbers
|
||||
fi := Field{Type: "integer", Enum: []any{"10", "2", "1"}, Options: []Option{
|
||||
{Value: 1, Description: "one"},
|
||||
{Value: 2, Description: "two"},
|
||||
}}
|
||||
wantI := []EnumOption{
|
||||
{Value: int64(1), Description: "one"},
|
||||
{Value: int64(2), Description: "two"},
|
||||
{Value: int64(10), Description: ""},
|
||||
}
|
||||
if got := fi.EnumOptions(); !reflect.DeepEqual(got, wantI) {
|
||||
t.Errorf("EnumOptions(integer enum+options) = %+v, want %+v", got, wantI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestField_Enum_NumberAndBoolean(t *testing.T) {
|
||||
// number: string-stored floats coerced to float64 and numerically sorted
|
||||
if got := (Field{Type: "number", Enum: []any{"2.5", "1.5", "10"}}).EnumValues(); !reflect.DeepEqual(got, []any{1.5, 2.5, float64(10)}) {
|
||||
|
||||
@@ -472,18 +472,6 @@ func TestConvert_EnumDescriptions(t *testing.T) {
|
||||
if bare.EnumDescriptions != nil {
|
||||
t.Errorf("bare enum must have nil EnumDescriptions, got %v", bare.EnumDescriptions)
|
||||
}
|
||||
|
||||
// enum + options both present -> enumDescriptions backfilled, aligned, "" where absent
|
||||
both := Convert(meta.Field{Type: "string", Enum: []any{"1", "2", "3"}, Options: []meta.Option{
|
||||
{Value: "1", Description: "from"},
|
||||
{Value: "2", Description: "to"},
|
||||
}})
|
||||
if !reflect.DeepEqual(both.Enum, []interface{}{"1", "2", "3"}) {
|
||||
t.Errorf("both Enum = %v", both.Enum)
|
||||
}
|
||||
if !reflect.DeepEqual(both.EnumDescriptions, []string{"from", "to", ""}) {
|
||||
t.Errorf("both EnumDescriptions = %v, want [from to \"\"] aligned with enum", both.EnumDescriptions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMeta_AffordanceFromMethod(t *testing.T) {
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -33,7 +32,6 @@ import (
|
||||
)
|
||||
|
||||
const gitCredentialIssuePath = apiBasePath + "/apps/:app_id/git_info"
|
||||
const gitCredentialHelperReportedShortcut = appsService + ":+git-credential-helper"
|
||||
|
||||
// gitCredentialIssueHint is the actionable next-step attached to a failed
|
||||
// Git-credential issuance. A 5xx is flagged retryable separately at the call site.
|
||||
@@ -304,12 +302,7 @@ func (i factoryIssuer) Issue(ctx context.Context, appID string, profile gitcred.
|
||||
HttpMethod: http.MethodGet,
|
||||
ApiPath: issuePath(appID),
|
||||
}
|
||||
ctx = contextWithGitCredentialHelperShortcut(ctx)
|
||||
var opts []larkcore.RequestOptionFunc
|
||||
if optFn := cmdutil.ShortcutHeaderOpts(ctx); optFn != nil {
|
||||
opts = append(opts, optFn)
|
||||
}
|
||||
resp, err := ac.DoSDKRequest(ctx, req, core.AsUser, opts...)
|
||||
resp, err := ac.DoSDKRequest(ctx, req, core.AsUser)
|
||||
data, err := parseIssueCredentialData(resp, err, errclass.ClassifyContext{
|
||||
Brand: string(cfg.Brand),
|
||||
AppID: cfg.AppID,
|
||||
@@ -321,13 +314,6 @@ func (i factoryIssuer) Issue(ctx context.Context, appID string, profile gitcred.
|
||||
return issuedFromData(appID, data)
|
||||
}
|
||||
|
||||
func contextWithGitCredentialHelperShortcut(ctx context.Context) context.Context {
|
||||
if _, ok := cmdutil.ShortcutNameFromContext(ctx); ok {
|
||||
return ctx
|
||||
}
|
||||
return cmdutil.ContextWithShortcut(ctx, gitCredentialHelperReportedShortcut, uuid.New().String())
|
||||
}
|
||||
|
||||
func runGitCredentialHelper(ctx context.Context, f *cmdutil.Factory, appID, action string) error {
|
||||
if f == nil || f.IOStreams == nil {
|
||||
return nil
|
||||
|
||||
@@ -825,7 +825,7 @@ func TestRunGitCredentialHelperActions(t *testing.T) {
|
||||
func TestFactoryIssuerBranches(t *testing.T) {
|
||||
factory, _, reg := newAppsExecuteFactory(t)
|
||||
expiresAt := time.Now().Add(24 * time.Hour).Unix()
|
||||
issueStub := &httpmock.Stub{
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/spark/v1/apps/app_xxx/git_info",
|
||||
Body: map[string]interface{}{
|
||||
@@ -836,8 +836,7 @@ func TestFactoryIssuerBranches(t *testing.T) {
|
||||
"StatusCode": 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
reg.Register(issueStub)
|
||||
})
|
||||
issued, err := (factoryIssuer{f: factory}).Issue(context.Background(), "app_xxx", gitcred.ProfileContext{})
|
||||
if err != nil {
|
||||
t.Fatalf("factory issuer returned error: %v", err)
|
||||
@@ -845,12 +844,6 @@ func TestFactoryIssuerBranches(t *testing.T) {
|
||||
if issued.PAT != "pat-token" {
|
||||
t.Fatalf("PAT = %q", issued.PAT)
|
||||
}
|
||||
if got := issueStub.CapturedHeaders.Get(cmdutil.HeaderShortcut); got != gitCredentialHelperReportedShortcut {
|
||||
t.Fatalf("%s = %q, want %q", cmdutil.HeaderShortcut, got, gitCredentialHelperReportedShortcut)
|
||||
}
|
||||
if got := issueStub.CapturedHeaders.Get(cmdutil.HeaderExecutionId); got == "" {
|
||||
t.Fatalf("%s header missing", cmdutil.HeaderExecutionId)
|
||||
}
|
||||
|
||||
factory.Config = func() (*core.CliConfig, error) { return nil, errors.New("config failed") }
|
||||
if _, err := (factoryIssuer{f: factory}).Issue(context.Background(), "app_xxx", gitcred.ProfileContext{}); err == nil {
|
||||
@@ -887,20 +880,6 @@ func TestFactoryIssuerBranches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextWithGitCredentialHelperShortcutPreservesExistingShortcut(t *testing.T) {
|
||||
ctx := cmdutil.ContextWithShortcut(context.Background(), "apps:+git-credential-init", "exec-existing")
|
||||
got := contextWithGitCredentialHelperShortcut(ctx)
|
||||
|
||||
name, ok := cmdutil.ShortcutNameFromContext(got)
|
||||
if !ok || name != "apps:+git-credential-init" {
|
||||
t.Fatalf("shortcut = %q ok=%v, want existing shortcut", name, ok)
|
||||
}
|
||||
executionID, ok := cmdutil.ExecutionIdFromContext(got)
|
||||
if !ok || executionID != "exec-existing" {
|
||||
t.Fatalf("execution id = %q ok=%v, want existing execution id", executionID, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCredentialHelpersAndParsers(t *testing.T) {
|
||||
if issuePath(" app/with space ") != "/open-apis/spark/v1/apps/app%2Fwith%20space/git_info" {
|
||||
t.Fatalf("issuePath escaped incorrectly: %s", issuePath(" app/with space "))
|
||||
|
||||
@@ -223,12 +223,6 @@ func (ctx *RuntimeContext) Float64(name string) float64 {
|
||||
return v
|
||||
}
|
||||
|
||||
// IntArray returns an int-array flag value (repeated flag, also supports CSV splitting).
|
||||
func (ctx *RuntimeContext) IntArray(name string) []int {
|
||||
v, _ := ctx.Cmd.Flags().GetIntSlice(name)
|
||||
return v
|
||||
}
|
||||
|
||||
// StrArray returns a string-array flag value (repeated flag, no CSV splitting).
|
||||
func (ctx *RuntimeContext) StrArray(name string) []string {
|
||||
v, _ := ctx.Cmd.Flags().GetStringArray(name)
|
||||
@@ -1051,7 +1045,8 @@ func resolveInputFlags(rctx *RuntimeContext, flags []Flag) error {
|
||||
}
|
||||
if stdinUsed {
|
||||
return ValidationErrorf("--%s: stdin (-) can only be used by one flag", fl.Name).
|
||||
WithParam("--" + fl.Name)
|
||||
WithParam("--"+fl.Name).
|
||||
WithHint("a process has a single stdin, so only one flag per call may use '-'; pass the others as @file (e.g. --%s @/path/to/file)", fl.Name)
|
||||
}
|
||||
stdinUsed = true
|
||||
data, err := io.ReadAll(rctx.IO().In)
|
||||
@@ -1166,7 +1161,13 @@ func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f
|
||||
hints = append(hints, "@file")
|
||||
}
|
||||
if slices.Contains(fl.Input, Stdin) {
|
||||
hints = append(hints, "- for stdin")
|
||||
// "- reads stdin" intentionally avoids implying each flag has
|
||||
// its own stdin: a process has a single stdin, so at most one
|
||||
// flag per call may use "-" (the rest must use @file). The old
|
||||
// per-flag "- for stdin" wording led AI agents to write
|
||||
// `--a - <x --b - <y`, where the second `<` silently clobbers
|
||||
// the first and `--a` reads the wrong payload.
|
||||
hints = append(hints, "- reads stdin (one flag per call; use @file for others)")
|
||||
}
|
||||
desc += " (supports " + strings.Join(hints, ", ") + ")"
|
||||
}
|
||||
@@ -1182,8 +1183,6 @@ func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f
|
||||
var d float64
|
||||
fmt.Sscanf(fl.Default, "%g", &d)
|
||||
cmd.Flags().Float64(fl.Name, d, desc)
|
||||
case "int_array":
|
||||
cmd.Flags().IntSlice(fl.Name, nil, desc)
|
||||
case "string_array":
|
||||
cmd.Flags().StringArray(fl.Name, nil, desc)
|
||||
case "string_slice":
|
||||
|
||||
@@ -4,12 +4,9 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -59,29 +56,3 @@ func TestRejectPositionalArgs_NoArgs(t *testing.T) {
|
||||
t.Fatalf("expected no error for empty args, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortcutFlagIntArray(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, nil)
|
||||
parent := &cobra.Command{Use: "root"}
|
||||
var got []int
|
||||
shortcut := Shortcut{
|
||||
Service: "slides",
|
||||
Command: "+screenshot",
|
||||
Description: "capture screenshots",
|
||||
Flags: []Flag{
|
||||
{Name: "slide-number", Type: "int_array"},
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *RuntimeContext) error {
|
||||
got = runtime.IntArray("slide-number")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
shortcut.Mount(parent, f)
|
||||
parent.SetArgs([]string{"+screenshot", "--as", "user", "--slide-number", "1", "--slide-number", "2,3"})
|
||||
if err := parent.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
if want := []int{1, 2, 3}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("slide-number = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,10 +216,15 @@ func TestResolveInputFlags_DuplicateStdin(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("expected error for duplicate stdin usage")
|
||||
}
|
||||
assertValidationParam(t, err, "--b")
|
||||
vErr := assertValidationParam(t, err, "--b")
|
||||
if !strings.Contains(err.Error(), "stdin (-) can only be used by one flag") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
// The hint must steer an AI agent to the fix (@file for the extra flags),
|
||||
// since `--a - <x --b - <y` is the exact misuse this guards against.
|
||||
if !strings.Contains(vErr.Hint, "@file") {
|
||||
t.Errorf("hint %q should mention @file as the fix", vErr.Hint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripUTF8BOM(t *testing.T) {
|
||||
|
||||
@@ -18,7 +18,7 @@ const (
|
||||
// Flag describes a CLI flag for a shortcut.
|
||||
type Flag struct {
|
||||
Name string // flag name (e.g. "calendar-id")
|
||||
Type string // "string" (default) | "bool" | "int" | "float64" | "int_array" | "string_array" | "string_slice"
|
||||
Type string // "string" (default) | "bool" | "int" | "float64" | "string_array" | "string_slice"
|
||||
Default string // default value as string
|
||||
Desc string // help text
|
||||
Hidden bool // hidden from --help, still readable at runtime
|
||||
|
||||
@@ -85,7 +85,6 @@ type searchUserAPIData struct {
|
||||
Items []searchUserAPIItem `json:"items"`
|
||||
HasMore bool `json:"has_more"`
|
||||
PageToken string `json:"page_token"`
|
||||
Notice string `json:"notice"`
|
||||
}
|
||||
|
||||
type searchUserAPIItem struct {
|
||||
@@ -127,7 +126,6 @@ type searchUser struct {
|
||||
type searchUserResponse struct {
|
||||
Users []searchUser `json:"users"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Notice string `json:"notice,omitempty"`
|
||||
}
|
||||
|
||||
var ContactSearchUser = common.Shortcut{
|
||||
@@ -191,7 +189,6 @@ var ContactSearchUser = common.Shortcut{
|
||||
Execute: executeSearchUser,
|
||||
}
|
||||
|
||||
// executeSearchUser dispatches contact search to single-query or fanout mode.
|
||||
func executeSearchUser(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if strings.TrimSpace(runtime.Str("queries")) != "" {
|
||||
return executeSearchUserFanout(ctx, runtime)
|
||||
@@ -199,7 +196,6 @@ func executeSearchUser(ctx context.Context, runtime *common.RuntimeContext) erro
|
||||
return executeSearchUserSingle(ctx, runtime)
|
||||
}
|
||||
|
||||
// executeSearchUserSingle performs one contact search and preserves server notices.
|
||||
func executeSearchUserSingle(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
body, err := buildSearchUserBody(runtime)
|
||||
if err != nil {
|
||||
@@ -226,7 +222,7 @@ func executeSearchUserSingle(ctx context.Context, runtime *common.RuntimeContext
|
||||
}
|
||||
|
||||
users, hasMore := projectUsers(respData, runtime.Str("lang"), runtime.Config.Brand)
|
||||
out := searchUserResponse{Users: users, HasMore: hasMore, Notice: respData.Notice}
|
||||
out := searchUserResponse{Users: users, HasMore: hasMore}
|
||||
|
||||
runtime.OutFormat(out, &output.Meta{Count: len(users)}, func(w io.Writer) {
|
||||
if len(users) == 0 {
|
||||
|
||||
@@ -45,17 +45,22 @@ type fanoutResult struct {
|
||||
Query string
|
||||
Users []searchUser
|
||||
HasMore bool
|
||||
Notice string
|
||||
ErrMsg string // empty = success
|
||||
Err error // original failure, kept for typed all-failed propagation
|
||||
}
|
||||
|
||||
// isFanoutSummaryFormat gates the per-fanout stderr summary line.
|
||||
// isFanoutSummaryFormat gates the per-fanout stderr summary line. Includes csv
|
||||
// because that summary lives on stderr and never corrupts the csv stream on
|
||||
// stdout — single-query mode keeps the narrower isHumanReadableFormat predicate
|
||||
// for its refine hint, so adding csv here doesn't regress that path.
|
||||
func isFanoutSummaryFormat(format string) bool {
|
||||
return format == "pretty" || format == "table" || format == "csv"
|
||||
}
|
||||
|
||||
// runOneQuery converts one fanout request into either users or an error summary.
|
||||
// runOneQuery converts every failure mode (transport, HTTP status, parse,
|
||||
// API code) into an ErrMsg string instead of returning a Go error. The
|
||||
// fanout dispatcher (Task 6) relies on this so a single failed query never
|
||||
// short-circuits the remaining workers.
|
||||
func runOneQuery(ctx context.Context, runtime *common.RuntimeContext, index int, query string,
|
||||
filter *searchUserAPIFilter) fanoutResult {
|
||||
// Pre-check ctx so queued workers see cancellation before issuing a
|
||||
@@ -89,10 +94,9 @@ func runOneQuery(ctx context.Context, runtime *common.RuntimeContext, index int,
|
||||
}
|
||||
|
||||
users, hasMore := projectUsers(respData, runtime.Str("lang"), runtime.Config.Brand)
|
||||
return fanoutResult{Index: index, Query: query, Users: users, HasMore: hasMore, Notice: respData.Notice}
|
||||
return fanoutResult{Index: index, Query: query, Users: users, HasMore: hasMore}
|
||||
}
|
||||
|
||||
// fanoutErrorResult records a failed fanout query without stopping other workers.
|
||||
func fanoutErrorResult(index int, query string, err error) fanoutResult {
|
||||
if err == nil {
|
||||
return fanoutResult{Index: index, Query: query}
|
||||
@@ -109,16 +113,17 @@ type querySummary struct {
|
||||
Query string `json:"query"`
|
||||
Error string `json:"error,omitempty"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Notice string `json:"notice,omitempty"`
|
||||
}
|
||||
|
||||
type fanoutResponse struct {
|
||||
Users []fanoutUser `json:"users"`
|
||||
Queries []querySummary `json:"queries"`
|
||||
Notice string `json:"notice,omitempty"`
|
||||
}
|
||||
|
||||
// buildFanoutResponse flattens ordered fanout results and fails only when all queries fail.
|
||||
// buildFanoutResponse walks results by Index (input order), flattens users[]
|
||||
// with matched_query, lists every input in queries[] (including successes),
|
||||
// and returns an error only when every query failed. The error wraps the
|
||||
// first failing query's ErrMsg so the CLI exits non-zero on full failure.
|
||||
func buildFanoutResponse(queries []string, results []fanoutResult) (*fanoutResponse, error) {
|
||||
indexed := make([]fanoutResult, len(queries))
|
||||
for _, r := range results {
|
||||
@@ -137,7 +142,6 @@ func buildFanoutResponse(queries []string, results []fanoutResult) (*fanoutRespo
|
||||
Query: queries[i],
|
||||
Error: r.ErrMsg,
|
||||
HasMore: r.HasMore,
|
||||
Notice: r.Notice,
|
||||
})
|
||||
if r.ErrMsg != "" {
|
||||
failed++
|
||||
@@ -148,9 +152,6 @@ func buildFanoutResponse(queries []string, results []fanoutResult) (*fanoutRespo
|
||||
}
|
||||
continue
|
||||
}
|
||||
if out.Notice == "" {
|
||||
out.Notice = r.Notice
|
||||
}
|
||||
for _, u := range r.Users {
|
||||
out.Users = append(out.Users, fanoutUser{searchUser: u, MatchedQuery: queries[i]})
|
||||
}
|
||||
|
||||
@@ -562,7 +562,6 @@ func mountAndRun(t *testing.T, s common.Shortcut, args []string, f *cmdutil.Fact
|
||||
return parent.Execute()
|
||||
}
|
||||
|
||||
// searchUserStub returns a representative user search response with a notice.
|
||||
func searchUserStub() *httpmock.Stub {
|
||||
return &httpmock.Stub{
|
||||
Method: "POST",
|
||||
@@ -570,7 +569,6 @@ func searchUserStub() *httpmock.Stub {
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"notice": "The query is too long and has been truncated to the first 50 characters for search.",
|
||||
"items": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": "ou_a",
|
||||
@@ -592,7 +590,6 @@ func searchUserStub() *httpmock.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchUser_Integration_PrettyRendersExpectedColumns verifies human output columns.
|
||||
func TestSearchUser_Integration_PrettyRendersExpectedColumns(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, searchUserDefaultConfig())
|
||||
reg.Register(searchUserStub())
|
||||
@@ -617,7 +614,6 @@ func TestSearchUser_Integration_PrettyRendersExpectedColumns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchUser_Integration_JSONStructuredFields verifies normalized JSON and notices.
|
||||
func TestSearchUser_Integration_JSONStructuredFields(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, searchUserDefaultConfig())
|
||||
reg.Register(searchUserStub())
|
||||
@@ -635,9 +631,6 @@ func TestSearchUser_Integration_JSONStructuredFields(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("envelope.data: expected object, got %v\nraw=%s", got["data"], stdout.String())
|
||||
}
|
||||
if data["notice"] != "The query is too long and has been truncated to the first 50 characters for search." {
|
||||
t.Fatalf("data.notice = %v", data["notice"])
|
||||
}
|
||||
users, _ := data["users"].([]interface{})
|
||||
if len(users) != 1 {
|
||||
t.Fatalf("users: expected 1, got %d (output=%s)", len(users), stdout.String())
|
||||
@@ -1365,7 +1358,6 @@ func TestSearchUser_Integration_NoAutoPaginationFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFanout_FilterAppliedToEachQuery verifies shared fanout filters reach every request.
|
||||
func TestFanout_FilterAppliedToEachQuery(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, searchUserDefaultConfig())
|
||||
stub := &httpmock.Stub{
|
||||
@@ -1407,7 +1399,6 @@ func TestFanout_FilterAppliedToEachQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFanout_PartialFailure_ExitZero verifies partial fanout failures keep notices.
|
||||
func TestFanout_PartialFailure_ExitZero(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, searchUserDefaultConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -1415,7 +1406,6 @@ func TestFanout_PartialFailure_ExitZero(t *testing.T) {
|
||||
BodyFilter: func(b []byte) bool { return strings.Contains(string(b), `"alice"`) },
|
||||
Body: map[string]interface{}{"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"notice": "The query is too long and has been truncated to the first 50 characters for search.",
|
||||
"items": []interface{}{map[string]interface{}{"id": "ou_a"}},
|
||||
"has_more": false,
|
||||
}},
|
||||
@@ -1442,17 +1432,10 @@ func TestFanout_PartialFailure_ExitZero(t *testing.T) {
|
||||
if len(users) != 1 {
|
||||
t.Errorf("users: expected 1 (alice), got %d; stdout=%s", len(users), stdout.String())
|
||||
}
|
||||
if data["notice"] != "The query is too long and has been truncated to the first 50 characters for search." {
|
||||
t.Fatalf("data.notice = %v", data["notice"])
|
||||
}
|
||||
queries := data["queries"].([]interface{})
|
||||
if len(queries) != 2 {
|
||||
t.Fatalf("queries: expected 2, got %d", len(queries))
|
||||
}
|
||||
q0 := queries[0].(map[string]interface{})
|
||||
if q0["notice"] != "The query is too long and has been truncated to the first 50 characters for search." {
|
||||
t.Fatalf("queries[0].notice = %v", q0["notice"])
|
||||
}
|
||||
q1 := queries[1].(map[string]interface{})
|
||||
if !strings.HasPrefix(q1["error"].(string), "HTTP 500") {
|
||||
t.Errorf("queries[1].error: got %q", q1["error"])
|
||||
|
||||
@@ -74,9 +74,6 @@ var DocsSearch = common.Shortcut{
|
||||
"page_token": data["page_token"],
|
||||
"results": normalizedItems,
|
||||
}
|
||||
if notice, _ := data["notice"].(string); notice != "" {
|
||||
resultData["notice"] = notice
|
||||
}
|
||||
|
||||
runtime.OutFormat(resultData, &output.Meta{Count: len(normalizedItems)}, func(w io.Writer) {
|
||||
if len(normalizedItems) == 0 {
|
||||
|
||||
@@ -7,48 +7,8 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
)
|
||||
|
||||
// TestDocsSearchExecutePassesThroughNotice verifies docs +search preserves notices.
|
||||
func TestDocsSearchExecutePassesThroughNotice(t *testing.T) {
|
||||
const notice = "The query is too long and has been truncated to the first 50 characters for search."
|
||||
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-search-notice"))
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/search/v2/doc_wiki/search",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"notice": notice,
|
||||
"res_units": []interface{}{},
|
||||
"total": 0,
|
||||
"has_more": false,
|
||||
"page_token": "",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := mountAndRunDocs(t, DocsSearch, []string{"+search", "--query", "incident", "--format", "json", "--as", "user"}, f, stdout); err != nil {
|
||||
t.Fatalf("DocsSearch.Execute() error = %v", err)
|
||||
}
|
||||
reg.Verify(t)
|
||||
|
||||
var env map[string]interface{}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
|
||||
t.Fatalf("json.Unmarshal(stdout) error = %v\nstdout=%s", err, stdout.String())
|
||||
}
|
||||
data, _ := env["data"].(map[string]interface{})
|
||||
if got, _ := data["notice"].(string); got != notice {
|
||||
t.Fatalf("data.notice = %q, want %q; data=%#v", got, notice, data)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddIsoTimeFieldsSupportsJSONNumber verifies JSON numbers get ISO fields.
|
||||
func TestAddIsoTimeFieldsSupportsJSONNumber(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ const (
|
||||
var DriveAddComment = common.Shortcut{
|
||||
Service: "drive",
|
||||
Command: "+add-comment",
|
||||
Description: "Add a comment to doc/docx/file/sheet/slides/base(bitable); file targets support selected extensions and full comments only",
|
||||
Description: "Add a comment to doc/docx/file/sheet/slides; file targets support selected extensions and full comments only",
|
||||
Risk: "write",
|
||||
Scopes: []string{
|
||||
"drive:drive.metadata:readonly",
|
||||
@@ -131,12 +131,12 @@ var DriveAddComment = common.Shortcut{
|
||||
},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
Flags: []common.Flag{
|
||||
{Name: "doc", Desc: "document URL/token, file URL/token, sheet/slides/base/bitable URL, or wiki URL that resolves to doc/docx/file/sheet/slides/base(bitable)", Required: true},
|
||||
{Name: "type", Desc: "document type: doc, docx, file, sheet, slides, bitable, base (required when --doc is a bare token; auto-detected for URLs; use bitable as the wire value, base is accepted as a compatibility alias)", Enum: []string{"doc", "docx", "file", "sheet", "slides", "bitable", "base"}},
|
||||
{Name: "doc", Desc: "document URL/token, file URL/token, sheet/slides URL, or wiki URL that resolves to doc/docx/file/sheet/slides", Required: true},
|
||||
{Name: "type", Desc: "document type: doc, docx, file, sheet, slides (required when --doc is a bare token; auto-detected for URLs)", Enum: []string{"doc", "docx", "file", "sheet", "slides"}},
|
||||
{Name: "content", Desc: "reply_elements JSON string", Required: true, Input: []string{common.File, common.Stdin}},
|
||||
{Name: "full-comment", Type: "bool", Desc: "create a full-document comment; also the default when no location is provided"},
|
||||
{Name: "selection-with-ellipsis", Desc: "target content locator (plain text or 'start...end')"},
|
||||
{Name: "block-id", Desc: "for docx: anchor block ID; for sheet: <sheetId>!<cell>; for slides: <slide-block-type>!<xml-id>; for base(bitable): <table-id>!<record-id>!<view-id>"},
|
||||
{Name: "block-id", Desc: "for docx: anchor block ID; for sheet: <sheetId>!<cell> (e.g. a281f9!D6); for slides: <slide-block-type>!<xml-id> (e.g. shape!bPq)"},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
docRef, err := parseCommentDocRef(runtime.Str("doc"), runtime.Str("type"))
|
||||
@@ -148,17 +148,6 @@ var DriveAddComment = common.Shortcut{
|
||||
return err
|
||||
}
|
||||
|
||||
if docRef.Kind == "base" {
|
||||
if runtime.Bool("full-comment") {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--full-comment is not applicable for base(bitable) comments; use --block-id <table-id>!<record-id>!<view-id>").WithParam("--full-comment")
|
||||
}
|
||||
if strings.TrimSpace(runtime.Str("selection-with-ellipsis")) != "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--selection-with-ellipsis is not applicable for base(bitable) comments; use --block-id <table-id>!<record-id>!<view-id>").WithParam("--selection-with-ellipsis")
|
||||
}
|
||||
_, err := parseBaseCommentAnchor(runtime)
|
||||
return err
|
||||
}
|
||||
|
||||
// Sheet comment validation.
|
||||
if docRef.Kind == "sheet" {
|
||||
blockID := strings.TrimSpace(runtime.Str("block-id"))
|
||||
@@ -199,7 +188,7 @@ var DriveAddComment = common.Shortcut{
|
||||
return validateFileCommentMode(mode, "")
|
||||
}
|
||||
if mode == commentModeLocal && docRef.Kind == "doc" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "local comments only support docx, sheet, slides, and base(bitable); old doc format only supports full comments")
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "local comments only support docx, sheet, and slides; old doc format only supports full comments")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -226,23 +215,6 @@ var DriveAddComment = common.Shortcut{
|
||||
resolvedToken = target.FileToken
|
||||
}
|
||||
|
||||
if resolvedKind == "base" {
|
||||
anchor, err := parseBaseCommentAnchor(runtime)
|
||||
if err != nil {
|
||||
return common.NewDryRunAPI().Set("error", err.Error())
|
||||
}
|
||||
commentBody := buildBaseCommentCreateV2Request(replyElements, anchor)
|
||||
desc := "1-step request: create base(bitable) record-local comment"
|
||||
if isWiki {
|
||||
desc = "2-step orchestration: resolve wiki -> create base(bitable) record-local comment"
|
||||
}
|
||||
return common.NewDryRunAPI().
|
||||
Desc(desc).
|
||||
POST("/open-apis/drive/v1/files/:file_token/new_comments").
|
||||
Body(commentBody).
|
||||
Set("file_token", resolvedToken)
|
||||
}
|
||||
|
||||
// Sheet comment dry-run.
|
||||
if resolvedKind == "sheet" {
|
||||
anchor, _ := parseSheetCellRef(blockID)
|
||||
@@ -380,14 +352,6 @@ var DriveAddComment = common.Shortcut{
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
// Sheet comment: direct URL or token fast path.
|
||||
docRef, _ := parseCommentDocRef(runtime.Str("doc"), runtime.Str("type"))
|
||||
if docRef.Kind == "base" {
|
||||
return executeBaseComment(runtime, resolvedCommentTarget{
|
||||
DocID: docRef.Token,
|
||||
FileToken: docRef.Token,
|
||||
FileType: "base",
|
||||
ResolvedBy: "base",
|
||||
})
|
||||
}
|
||||
if docRef.Kind == "sheet" {
|
||||
return executeSheetComment(runtime, docRef)
|
||||
}
|
||||
@@ -411,9 +375,6 @@ var DriveAddComment = common.Shortcut{
|
||||
if target.FileType == "slides" {
|
||||
return executeSlidesComment(runtime, commentDocRef{Kind: "slides", Token: target.FileToken})
|
||||
}
|
||||
if target.FileType == "base" {
|
||||
return executeBaseComment(runtime, target)
|
||||
}
|
||||
if target.FileType == "file" {
|
||||
return executeFileComment(runtime, target)
|
||||
}
|
||||
@@ -521,12 +482,6 @@ func parseCommentDocRef(input, docType string) (commentDocRef, error) {
|
||||
if token, ok := extractURLToken(raw, "/sheets/"); ok {
|
||||
return commentDocRef{Kind: "sheet", Token: token}, nil
|
||||
}
|
||||
if token, ok := extractURLToken(raw, "/base/"); ok {
|
||||
return commentDocRef{Kind: "base", Token: token}, nil
|
||||
}
|
||||
if token, ok := extractURLToken(raw, "/bitable/"); ok {
|
||||
return commentDocRef{Kind: "base", Token: token}, nil
|
||||
}
|
||||
if token, ok := extractURLToken(raw, "/file/"); ok {
|
||||
return commentDocRef{Kind: "file", Token: token}, nil
|
||||
}
|
||||
@@ -540,7 +495,7 @@ func parseCommentDocRef(input, docType string) (commentDocRef, error) {
|
||||
return commentDocRef{Kind: "doc", Token: token}, nil
|
||||
}
|
||||
if strings.Contains(raw, "://") {
|
||||
return commentDocRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "unsupported --doc input %q: use a doc/docx/file/sheet/slides/base/bitable URL, a token with --type, or a wiki URL that resolves to doc/docx/file/sheet/slides/base(bitable)", raw).WithParam("--doc")
|
||||
return commentDocRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "unsupported --doc input %q: use a doc/docx/file/sheet/slides URL, a token with --type, or a wiki URL that resolves to doc/docx/file/sheet/slides", raw).WithParam("--doc")
|
||||
}
|
||||
if strings.ContainsAny(raw, "/?#") {
|
||||
return commentDocRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "unsupported --doc input %q: use a token with --type, or a wiki URL", raw).WithParam("--doc")
|
||||
@@ -549,10 +504,7 @@ func parseCommentDocRef(input, docType string) (commentDocRef, error) {
|
||||
// Bare token: --type is required.
|
||||
docType = strings.TrimSpace(docType)
|
||||
if docType == "" {
|
||||
return commentDocRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--type is required when --doc is a bare token (allowed values: doc, docx, file, sheet, slides, bitable, base; use bitable as the wire value, base is accepted as a compatibility alias)").WithParam("--type")
|
||||
}
|
||||
if docType == "bitable" || docType == "base" {
|
||||
return commentDocRef{Kind: "base", Token: raw}, nil
|
||||
return commentDocRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--type is required when --doc is a bare token (allowed values: doc, docx, file, sheet, slides)").WithParam("--type")
|
||||
}
|
||||
return commentDocRef{Kind: docType, Token: raw}, nil
|
||||
}
|
||||
@@ -563,11 +515,11 @@ func resolveCommentTarget(ctx context.Context, runtime *common.RuntimeContext, i
|
||||
return resolvedCommentTarget{}, err
|
||||
}
|
||||
|
||||
if docRef.Kind == "docx" || docRef.Kind == "doc" || docRef.Kind == "file" || docRef.Kind == "sheet" || docRef.Kind == "slides" || docRef.Kind == "base" {
|
||||
if docRef.Kind == "docx" || docRef.Kind == "doc" || docRef.Kind == "file" || docRef.Kind == "sheet" || docRef.Kind == "slides" {
|
||||
if mode == commentModeLocal {
|
||||
switch docRef.Kind {
|
||||
case "doc":
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "local comments only support docx, sheet, slides, and base(bitable); old doc format only supports full comments")
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "local comments only support docx, sheet, and slides; old doc format only supports full comments")
|
||||
case "file":
|
||||
if err := validateFileCommentMode(mode, ""); err != nil {
|
||||
return resolvedCommentTarget{}, err
|
||||
@@ -605,22 +557,6 @@ func resolveCommentTarget(ctx context.Context, runtime *common.RuntimeContext, i
|
||||
if objType == "slides" && strings.TrimSpace(runtime.Str("selection-with-ellipsis")) != "" {
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "wiki resolved to %q, but --selection-with-ellipsis is not applicable for slide comments; use --block-id <slide-block-type>!<xml-id>", objType)
|
||||
}
|
||||
if objType == "bitable" || objType == "base" {
|
||||
if runtime.Bool("full-comment") {
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "wiki resolved to %q, but --full-comment is not applicable for base(bitable) comments; use --block-id <table-id>!<record-id>!<view-id>", objType).WithParam("--full-comment")
|
||||
}
|
||||
if strings.TrimSpace(runtime.Str("selection-with-ellipsis")) != "" {
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "wiki resolved to %q, but --selection-with-ellipsis is not applicable for base(bitable) comments; use --block-id <table-id>!<record-id>!<view-id>", objType).WithParam("--selection-with-ellipsis")
|
||||
}
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Resolved wiki to base: %s\n", common.MaskToken(objToken))
|
||||
return resolvedCommentTarget{
|
||||
DocID: objToken,
|
||||
FileToken: objToken,
|
||||
FileType: "base",
|
||||
ResolvedBy: "wiki",
|
||||
WikiToken: docRef.Token,
|
||||
}, nil
|
||||
}
|
||||
if objType == "sheet" {
|
||||
// Sheet comments are handled via the sheet fast path in Execute.
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Resolved wiki to %s: %s\n", objType, common.MaskToken(objToken))
|
||||
@@ -656,10 +592,10 @@ func resolveCommentTarget(ctx context.Context, runtime *common.RuntimeContext, i
|
||||
}, nil
|
||||
}
|
||||
if mode == commentModeLocal && objType != "docx" {
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "wiki resolved to %q, but local comments only support docx, sheet, slides, and base(bitable); for sheet use --block-id <sheetId>!<cell>, for slides use --block-id <slide-block-type>!<xml-id>, for base use --block-id <table-id>!<record-id>!<view-id>", objType)
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "wiki resolved to %q, but local comments only support docx, sheet, and slides; for sheet use --block-id <sheetId>!<cell>, for slides use --block-id <slide-block-type>!<xml-id>", objType)
|
||||
}
|
||||
if mode == commentModeFull && objType != "docx" && objType != "doc" {
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "wiki resolved to %q, but comments only support doc/docx/file/sheet/slides/base(bitable)", objType)
|
||||
return resolvedCommentTarget{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "wiki resolved to %q, but comments only support doc/docx/file/sheet/slides", objType)
|
||||
}
|
||||
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Resolved wiki to %s: %s\n", objType, common.MaskToken(objToken))
|
||||
@@ -851,12 +787,6 @@ type sheetAnchor struct {
|
||||
Row int
|
||||
}
|
||||
|
||||
type baseAnchor struct {
|
||||
BlockID string
|
||||
BaseRecordID string
|
||||
BaseViewID string
|
||||
}
|
||||
|
||||
func buildCommentCreateV2Request(fileType, blockID, slideBlockType string, replyElements []map[string]interface{}, sheet *sheetAnchor) map[string]interface{} {
|
||||
body := map[string]interface{}{
|
||||
"file_type": fileType,
|
||||
@@ -883,18 +813,6 @@ func buildCommentCreateV2Request(fileType, blockID, slideBlockType string, reply
|
||||
return body
|
||||
}
|
||||
|
||||
func buildBaseCommentCreateV2Request(replyElements []map[string]interface{}, anchor baseAnchor) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"file_type": "bitable",
|
||||
"reply_elements": replyElements,
|
||||
"anchor": map[string]interface{}{
|
||||
"block_id": anchor.BlockID,
|
||||
"base_record_id": anchor.BaseRecordID,
|
||||
"base_view_id": anchor.BaseViewID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func anchorBlockIDForDryRun(blockID string) string {
|
||||
if strings.TrimSpace(blockID) != "" {
|
||||
return strings.TrimSpace(blockID)
|
||||
@@ -902,26 +820,6 @@ func anchorBlockIDForDryRun(blockID string) string {
|
||||
return "<anchor_block_id>"
|
||||
}
|
||||
|
||||
func parseBaseCommentAnchor(runtime *common.RuntimeContext) (baseAnchor, error) {
|
||||
blockID := strings.TrimSpace(runtime.Str("block-id"))
|
||||
if blockID == "" {
|
||||
return baseAnchor{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--block-id is required for base(bitable) record-local comments (format: <table-id>!<record-id>!<view-id>, e.g. tbl9mp6fj9kDKHQV!recBIBgGmb!vewc46MG1R)").WithParam("--block-id")
|
||||
}
|
||||
return parseBaseBlockRef(blockID)
|
||||
}
|
||||
|
||||
func parseBaseBlockRef(blockID string) (baseAnchor, error) {
|
||||
parts := strings.Split(strings.TrimSpace(blockID), "!")
|
||||
if len(parts) != 3 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" || strings.TrimSpace(parts[2]) == "" {
|
||||
return baseAnchor{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "base(bitable) record-local comments require --block-id in <table-id>!<record-id>!<view-id> format, e.g. tbl9mp6fj9kDKHQV!recBIBgGmb!vewc46MG1R").WithParam("--block-id")
|
||||
}
|
||||
return baseAnchor{
|
||||
BlockID: strings.TrimSpace(parts[0]),
|
||||
BaseRecordID: strings.TrimSpace(parts[1]),
|
||||
BaseViewID: strings.TrimSpace(parts[2]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseSlidesBlockRef(blockID string) (string, string, error) {
|
||||
blockID = strings.TrimSpace(blockID)
|
||||
if blockID == "" {
|
||||
@@ -1132,53 +1030,6 @@ func executeSheetComment(runtime *common.RuntimeContext, docRef commentDocRef) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeBaseComment(runtime *common.RuntimeContext, target resolvedCommentTarget) error {
|
||||
replyElements, err := parseCommentReplyElements(runtime.Str("content"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
anchor, err := parseBaseCommentAnchor(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
requestPath := fmt.Sprintf("/open-apis/drive/v1/files/%s/new_comments", validate.EncodePathSegment(target.FileToken))
|
||||
requestBody := buildBaseCommentCreateV2Request(replyElements, anchor)
|
||||
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Creating base(bitable) record-local comment in %s (table=%s, record=%s, view=%s)\n",
|
||||
common.MaskToken(target.FileToken), anchor.BlockID, anchor.BaseRecordID, anchor.BaseViewID)
|
||||
|
||||
data, err := runtime.CallAPITyped("POST", requestPath, nil, requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := map[string]interface{}{
|
||||
"file_token": target.FileToken,
|
||||
"file_type": "bitable",
|
||||
"resolved_by": target.ResolvedBy,
|
||||
"comment_mode": "base_record",
|
||||
"base_block_id": anchor.BlockID,
|
||||
"base_record_id": anchor.BaseRecordID,
|
||||
"base_view_id": anchor.BaseViewID,
|
||||
}
|
||||
if commentID := data["comment_id"]; commentID != nil {
|
||||
out["comment_id"] = commentID
|
||||
}
|
||||
if replyID := data["reply_id"]; replyID != nil {
|
||||
out["reply_id"] = replyID
|
||||
}
|
||||
if createdAt := firstPresentValue(data, "created_at", "create_time"); createdAt != nil {
|
||||
out["created_at"] = createdAt
|
||||
}
|
||||
if target.WikiToken != "" {
|
||||
out["wiki_token"] = target.WikiToken
|
||||
}
|
||||
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeFileComment(runtime *common.RuntimeContext, target resolvedCommentTarget) error {
|
||||
replyElements, err := parseCommentReplyElements(runtime.Str("content"))
|
||||
if err != nil {
|
||||
|
||||
@@ -133,20 +133,6 @@ func TestParseCommentDocRef(t *testing.T) {
|
||||
wantKind: "file",
|
||||
wantToken: "fileToken",
|
||||
},
|
||||
{
|
||||
name: "raw token with type bitable",
|
||||
input: "baseToken",
|
||||
docType: "bitable",
|
||||
wantKind: "base",
|
||||
wantToken: "baseToken",
|
||||
},
|
||||
{
|
||||
name: "raw token with type base alias",
|
||||
input: "baseToken",
|
||||
docType: "base",
|
||||
wantKind: "base",
|
||||
wantToken: "baseToken",
|
||||
},
|
||||
{
|
||||
name: "raw token without type",
|
||||
input: "xxxxxx",
|
||||
@@ -170,18 +156,6 @@ func TestParseCommentDocRef(t *testing.T) {
|
||||
wantKind: "file",
|
||||
wantToken: "boxcn123",
|
||||
},
|
||||
{
|
||||
name: "base url",
|
||||
input: "https://example.larksuite.com/base/baseToken123?table=tbl1",
|
||||
wantKind: "base",
|
||||
wantToken: "baseToken123",
|
||||
},
|
||||
{
|
||||
name: "bitable url",
|
||||
input: "https://example.larksuite.com/bitable/baseToken456?table=tbl1",
|
||||
wantKind: "base",
|
||||
wantToken: "baseToken456",
|
||||
},
|
||||
{
|
||||
name: "unsupported url",
|
||||
input: "https://example.com/not-a-doc",
|
||||
@@ -752,35 +726,6 @@ func TestBuildCommentCreateV2RequestSheetOverridesBlockID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBaseCommentCreateV2Request(t *testing.T) {
|
||||
t.Parallel()
|
||||
replyElements := []map[string]interface{}{
|
||||
{"type": "text", "text": "base comment"},
|
||||
}
|
||||
got := buildBaseCommentCreateV2Request(replyElements, baseAnchor{
|
||||
BlockID: "tbl9mp6fj9kDKHQV",
|
||||
BaseRecordID: "recBIBgGmb",
|
||||
BaseViewID: "vewc46MG1R",
|
||||
})
|
||||
|
||||
if got["file_type"] != "bitable" {
|
||||
t.Fatalf("expected file_type bitable, got %#v", got["file_type"])
|
||||
}
|
||||
anchor, ok := got["anchor"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected anchor map, got %#v", got["anchor"])
|
||||
}
|
||||
if anchor["block_id"] != "tbl9mp6fj9kDKHQV" {
|
||||
t.Fatalf("expected block_id tbl9mp6fj9kDKHQV, got %#v", anchor["block_id"])
|
||||
}
|
||||
if anchor["base_record_id"] != "recBIBgGmb" {
|
||||
t.Fatalf("expected base_record_id recBIBgGmb, got %#v", anchor["base_record_id"])
|
||||
}
|
||||
if anchor["base_view_id"] != "vewc46MG1R" {
|
||||
t.Fatalf("expected base_view_id vewc46MG1R, got %#v", anchor["base_view_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sheet cell ref parsing tests ────────────────────────────────────────────
|
||||
|
||||
func TestParseSheetCellRef(t *testing.T) {
|
||||
@@ -1040,78 +985,6 @@ func TestFileCommentValidateRejectsSelectionWithEllipsis(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseCommentValidateMissingBlockID(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DriveAddComment, []string{
|
||||
"+add-comment",
|
||||
"--doc", "https://example.larksuite.com/base/baseToken",
|
||||
"--content", `[{"type":"text","text":"test"}]`,
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), "--block-id is required") {
|
||||
t.Fatalf("expected block-id required error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseCommentValidateMalformedBlockID(t *testing.T) {
|
||||
cases := []string{
|
||||
"tbl9mp6fj9kDKHQV",
|
||||
"tbl9mp6fj9kDKHQV!recBIBgGmb",
|
||||
"tbl9mp6fj9kDKHQV!!vewc46MG1R",
|
||||
}
|
||||
for _, blockID := range cases {
|
||||
t.Run(blockID, func(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DriveAddComment, []string{
|
||||
"+add-comment",
|
||||
"--doc", "https://example.larksuite.com/base/baseToken",
|
||||
"--content", `[{"type":"text","text":"test"}]`,
|
||||
"--block-id", blockID,
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), "<table-id>!<record-id>!<view-id>") {
|
||||
t.Fatalf("expected block-id format error, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseCommentValidateRejectsIncompatibleFlags(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "full comment",
|
||||
args: []string{"--full-comment"},
|
||||
wantErr: "--full-comment is not applicable for base(bitable) comments",
|
||||
},
|
||||
{
|
||||
name: "selection",
|
||||
args: []string{"--selection-with-ellipsis", "some text"},
|
||||
wantErr: "--selection-with-ellipsis is not applicable for base(bitable) comments",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
args := []string{
|
||||
"+add-comment",
|
||||
"--doc", "https://example.larksuite.com/base/baseToken",
|
||||
"--content", `[{"type":"text","text":"test"}]`,
|
||||
"--block-id", "tbl9mp6fj9kDKHQV!recBIBgGmb!vewc46MG1R",
|
||||
"--as", "user",
|
||||
}
|
||||
args = append(args, tc.args...)
|
||||
err := mountAndRunDrive(t, DriveAddComment, args, f, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("expected %q error, got: %v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Slides comment execute tests ────────────────────────────────────────────
|
||||
|
||||
func TestSlidesCommentExecuteSuccess(t *testing.T) {
|
||||
@@ -1322,87 +1195,6 @@ func TestSheetCommentViaWikiMissingBlockID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseCommentExecuteSuccess(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
createStub := &httpmock.Stub{
|
||||
Method: "POST", URL: "/open-apis/drive/v1/files/baseToken/new_comments",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"comment_id": "baseComment123",
|
||||
"reply_id": "baseReply123",
|
||||
"created_at": 1700000000,
|
||||
},
|
||||
},
|
||||
}
|
||||
reg.Register(createStub)
|
||||
err := mountAndRunDrive(t, DriveAddComment, []string{
|
||||
"+add-comment",
|
||||
"--doc", "https://example.larksuite.com/base/baseToken",
|
||||
"--content", `[{"type":"text","text":"请看这条记录"}]`,
|
||||
"--block-id", "tbl9mp6fj9kDKHQV!recBIBgGmb!vewc46MG1R",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var requestBody map[string]interface{}
|
||||
if err := json.Unmarshal(createStub.CapturedBody, &requestBody); err != nil {
|
||||
t.Fatalf("failed to decode captured body: %v\nbody:\n%s", err, string(createStub.CapturedBody))
|
||||
}
|
||||
if got := mustStringField(t, requestBody, "file_type", "request.file_type"); got != "bitable" {
|
||||
t.Fatalf("request file_type = %q, want bitable", got)
|
||||
}
|
||||
anchor := mustMapValue(t, requestBody["anchor"], "request.anchor")
|
||||
if got := mustStringField(t, anchor, "block_id", "request.anchor.block_id"); got != "tbl9mp6fj9kDKHQV" {
|
||||
t.Fatalf("request block_id = %q, want tbl9mp6fj9kDKHQV", got)
|
||||
}
|
||||
if got := mustStringField(t, anchor, "base_record_id", "request.anchor.base_record_id"); got != "recBIBgGmb" {
|
||||
t.Fatalf("request base_record_id = %q, want recBIBgGmb", got)
|
||||
}
|
||||
if got := mustStringField(t, anchor, "base_view_id", "request.anchor.base_view_id"); got != "vewc46MG1R" {
|
||||
t.Fatalf("request base_view_id = %q, want vewc46MG1R", got)
|
||||
}
|
||||
|
||||
out := decodeJSONMap(t, stdout.String())
|
||||
data := mustMapValue(t, out["data"], "data")
|
||||
if got := mustStringField(t, data, "file_type", "data.file_type"); got != "bitable" {
|
||||
t.Fatalf("stdout file_type = %q, want bitable\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
if got := mustStringField(t, data, "comment_mode", "data.comment_mode"); got != "base_record" {
|
||||
t.Fatalf("stdout comment_mode = %q, want base_record\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
if got := mustStringField(t, data, "reply_id", "data.reply_id"); got != "baseReply123" {
|
||||
t.Fatalf("stdout reply_id = %q, want baseReply123\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseCommentExecuteBareToken(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST", URL: "/open-apis/drive/v1/files/baseBareToken/new_comments",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "success",
|
||||
"data": map[string]interface{}{"comment_id": "baseBareComment"},
|
||||
},
|
||||
})
|
||||
err := mountAndRunDrive(t, DriveAddComment, []string{
|
||||
"+add-comment",
|
||||
"--doc", "baseBareToken",
|
||||
"--type", "bitable",
|
||||
"--content", `[{"type":"text","text":"ok"}]`,
|
||||
"--block-id", "tbl9mp6fj9kDKHQV!recBIBgGmb!vewc46MG1R",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "baseBareComment") {
|
||||
t.Fatalf("stdout missing comment_id: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileCommentExecuteSuccess(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -1641,40 +1433,6 @@ func TestDryRunSlidesDirectURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDryRunBaseDirectURL(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DriveAddComment, []string{
|
||||
"+add-comment",
|
||||
"--doc", "https://example.larksuite.com/base/baseToken",
|
||||
"--content", `[{"type":"text","text":"test"}]`,
|
||||
"--block-id", "tbl9mp6fj9kDKHQV!recBIBgGmb!vewc46MG1R",
|
||||
"--dry-run", "--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "record-local comment") {
|
||||
t.Fatalf("dry-run output missing record-local comment: %s", stdout.String())
|
||||
}
|
||||
out := decodeJSONMap(t, stdout.String())
|
||||
api := mustSliceValue(t, out["api"], "api")
|
||||
call := mustMapValue(t, api[0], "api[0]")
|
||||
body := mustMapValue(t, call["body"], "api[0].body")
|
||||
anchor := mustMapValue(t, body["anchor"], "api[0].body.anchor")
|
||||
if got := mustStringField(t, body, "file_type", "api[0].body.file_type"); got != "bitable" {
|
||||
t.Fatalf("dry-run body.file_type = %q, want bitable\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
if got := mustStringField(t, anchor, "block_id", "api[0].body.anchor.block_id"); got != "tbl9mp6fj9kDKHQV" {
|
||||
t.Fatalf("dry-run body.anchor.block_id = %q, want tbl9mp6fj9kDKHQV\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
if got := mustStringField(t, anchor, "base_record_id", "api[0].body.anchor.base_record_id"); got != "recBIBgGmb" {
|
||||
t.Fatalf("dry-run body.anchor.base_record_id = %q, want recBIBgGmb\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
if got := mustStringField(t, anchor, "base_view_id", "api[0].body.anchor.base_view_id"); got != "vewc46MG1R" {
|
||||
t.Fatalf("dry-run body.anchor.base_view_id = %q, want vewc46MG1R\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDryRunWikiResolvesToSlides(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -1878,92 +1636,25 @@ func TestResolveWikiToDocxFullComment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveWikiToBaseComment(t *testing.T) {
|
||||
for _, objType := range []string{"bitable", "base"} {
|
||||
t.Run(objType, func(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET", URL: "/open-apis/wiki/v2/spaces/get_node",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"node": map[string]interface{}{"obj_type": objType, "obj_token": "bitToken"},
|
||||
},
|
||||
},
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST", URL: "/open-apis/drive/v1/files/bitToken/new_comments",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "success",
|
||||
"data": map[string]interface{}{"comment_id": "wikiBaseComment", "reply_id": "wikiBaseReply"},
|
||||
},
|
||||
})
|
||||
err := mountAndRunDrive(t, DriveAddComment, []string{
|
||||
"+add-comment",
|
||||
"--doc", "https://example.larksuite.com/wiki/wikiToken",
|
||||
"--content", `[{"type":"text","text":"test"}]`,
|
||||
"--block-id", "tbl9mp6fj9kDKHQV!recBIBgGmb!vewc46MG1R",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "wikiBaseComment") {
|
||||
t.Fatalf("stdout missing comment_id: %s", stdout.String())
|
||||
}
|
||||
out := decodeJSONMap(t, stdout.String())
|
||||
data := mustMapValue(t, out["data"], "data")
|
||||
if got := mustStringField(t, data, "file_type", "data.file_type"); got != "bitable" {
|
||||
t.Fatalf("stdout file_type = %q, want bitable\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
if got := mustStringField(t, data, "wiki_token", "data.wiki_token"); got != "wikiToken" {
|
||||
t.Fatalf("stdout wiki_token = %q, want wikiToken\nstdout:\n%s", got, stdout.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveWikiToBaseRejectsIncompatibleFlags(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "full comment",
|
||||
args: []string{"--full-comment"},
|
||||
wantErr: "--full-comment is not applicable for base(bitable) comments",
|
||||
func TestResolveWikiToUnsupportedType(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET", URL: "/open-apis/wiki/v2/spaces/get_node",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"node": map[string]interface{}{"obj_type": "bitable", "obj_token": "bitToken"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "selection",
|
||||
args: []string{"--selection-with-ellipsis", "some text"},
|
||||
wantErr: "--selection-with-ellipsis is not applicable for base(bitable) comments",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET", URL: "/open-apis/wiki/v2/spaces/get_node",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"node": map[string]interface{}{"obj_type": "bitable", "obj_token": "bitToken"},
|
||||
},
|
||||
},
|
||||
})
|
||||
args := []string{
|
||||
"+add-comment",
|
||||
"--doc", "https://example.larksuite.com/wiki/wikiToken",
|
||||
"--content", `[{"type":"text","text":"test"}]`,
|
||||
"--as", "user",
|
||||
}
|
||||
args = append(args, tc.args...)
|
||||
err := mountAndRunDrive(t, DriveAddComment, args, f, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("expected %q error, got: %v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
})
|
||||
err := mountAndRunDrive(t, DriveAddComment, []string{
|
||||
"+add-comment",
|
||||
"--doc", "https://example.larksuite.com/wiki/wikiToken",
|
||||
"--content", `[{"type":"text","text":"test"}]`,
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), "only support doc/docx/file/sheet/slides") {
|
||||
t.Fatalf("expected unsupported type error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2044,7 +1735,7 @@ func TestDocOldFormatLocalCommentRejected(t *testing.T) {
|
||||
"--block-id", "blk_123",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), "only support docx, sheet, slides, and base(bitable)") {
|
||||
if err == nil || !strings.Contains(err.Error(), "only support docx, sheet, and slides") {
|
||||
t.Fatalf("expected local comment rejection for old doc, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,236 +40,302 @@ var DriveExport = common.Shortcut{
|
||||
{Name: "overwrite", Type: "bool", Desc: "overwrite existing output file"},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateDriveExportSpec(driveExportSpec{
|
||||
Token: runtime.Str("token"),
|
||||
DocType: runtime.Str("doc-type"),
|
||||
FileExtension: runtime.Str("file-extension"),
|
||||
SubID: runtime.Str("sub-id"),
|
||||
OnlySchema: runtime.Bool("only-schema"),
|
||||
})
|
||||
return ValidateExport(exportParamsFromFlags(runtime))
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
spec := driveExportSpec{
|
||||
Token: runtime.Str("token"),
|
||||
DocType: runtime.Str("doc-type"),
|
||||
FileExtension: runtime.Str("file-extension"),
|
||||
SubID: runtime.Str("sub-id"),
|
||||
OnlySchema: runtime.Bool("only-schema"),
|
||||
}
|
||||
// Markdown export is a special case: docx markdown comes from the V2
|
||||
// docs_ai fetch API directly instead of the Drive export task API.
|
||||
if spec.FileExtension == "markdown" {
|
||||
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", validate.EncodePathSegment(spec.Token))
|
||||
dr := common.NewDryRunAPI().
|
||||
Desc("2-step orchestration: fetch docx markdown -> write local file").
|
||||
POST(apiPath).
|
||||
Body(map[string]interface{}{
|
||||
"format": "markdown",
|
||||
}).
|
||||
Set("output_dir", runtime.Str("output-dir"))
|
||||
if name := strings.TrimSpace(runtime.Str("file-name")); name != "" {
|
||||
dr.Set("file_name", ensureExportFileExtension(sanitizeExportFileName(name, spec.Token), spec.FileExtension))
|
||||
}
|
||||
return dr
|
||||
}
|
||||
return PlanExportDryRun(runtime, exportParamsFromFlags(runtime))
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return RunExport(ctx, runtime, exportParamsFromFlags(runtime))
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"token": spec.Token,
|
||||
"type": spec.DocType,
|
||||
"file_extension": spec.FileExtension,
|
||||
}
|
||||
if strings.TrimSpace(spec.SubID) != "" {
|
||||
body["sub_id"] = spec.SubID
|
||||
}
|
||||
if spec.OnlySchema {
|
||||
body["only_schema"] = true
|
||||
}
|
||||
// ExportParams holds the user-facing inputs for an export flow, decoupled from
|
||||
// cobra flags so other command groups (e.g. sheets +workbook-export) can reuse
|
||||
// the drive export implementation. An empty OutputDir means "create the export
|
||||
// task and poll, but do not download" — callers that only need the ready file
|
||||
// token / status get it back without writing a local file.
|
||||
type ExportParams struct {
|
||||
Token string
|
||||
DocType string
|
||||
FileExtension string
|
||||
SubID string
|
||||
OnlySchema bool
|
||||
OutputDir string
|
||||
FileName string
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
func (p ExportParams) spec() driveExportSpec {
|
||||
return driveExportSpec{
|
||||
Token: p.Token,
|
||||
DocType: p.DocType,
|
||||
FileExtension: p.FileExtension,
|
||||
SubID: p.SubID,
|
||||
OnlySchema: p.OnlySchema,
|
||||
}
|
||||
}
|
||||
|
||||
// exportParamsFromFlags reads the standard drive +export flag set.
|
||||
func exportParamsFromFlags(runtime *common.RuntimeContext) ExportParams {
|
||||
// drive +export always downloads; an empty --output-dir historically means
|
||||
// the current directory (saveContentToOutputDir maps "" -> "."), so normalize
|
||||
// it here to keep behavior identical and stay off the export-only ("" => skip
|
||||
// download) path that only sheets +workbook-export uses.
|
||||
outputDir := runtime.Str("output-dir")
|
||||
if outputDir == "" {
|
||||
outputDir = "."
|
||||
}
|
||||
return ExportParams{
|
||||
Token: runtime.Str("token"),
|
||||
DocType: runtime.Str("doc-type"),
|
||||
FileExtension: runtime.Str("file-extension"),
|
||||
SubID: runtime.Str("sub-id"),
|
||||
OnlySchema: runtime.Bool("only-schema"),
|
||||
OutputDir: outputDir,
|
||||
FileName: strings.TrimSpace(runtime.Str("file-name")),
|
||||
Overwrite: runtime.Bool("overwrite"),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateExport runs the CLI-level export constraint checks.
|
||||
func ValidateExport(p ExportParams) error {
|
||||
return validateDriveExportSpec(p.spec())
|
||||
}
|
||||
|
||||
// PlanExportDryRun builds the dry-run plan for an export without performing I/O.
|
||||
func PlanExportDryRun(runtime *common.RuntimeContext, p ExportParams) *common.DryRunAPI {
|
||||
spec := p.spec()
|
||||
// Markdown export is a special case: docx markdown comes from the V2
|
||||
// docs_ai fetch API directly instead of the Drive export task API.
|
||||
if spec.FileExtension == "markdown" {
|
||||
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", validate.EncodePathSegment(spec.Token))
|
||||
dr := common.NewDryRunAPI().
|
||||
Desc("3-step orchestration: create export task -> limited polling -> download file").
|
||||
POST("/open-apis/drive/v1/export_tasks").
|
||||
Body(body).
|
||||
Set("output_dir", runtime.Str("output-dir"))
|
||||
if name := strings.TrimSpace(runtime.Str("file-name")); name != "" {
|
||||
Desc("2-step orchestration: fetch docx markdown -> write local file").
|
||||
POST(apiPath).
|
||||
Body(map[string]interface{}{
|
||||
"format": "markdown",
|
||||
}).
|
||||
Set("output_dir", p.OutputDir)
|
||||
if name := strings.TrimSpace(p.FileName); name != "" {
|
||||
dr.Set("file_name", ensureExportFileExtension(sanitizeExportFileName(name, spec.Token), spec.FileExtension))
|
||||
}
|
||||
return dr
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
spec := driveExportSpec{
|
||||
Token: runtime.Str("token"),
|
||||
DocType: runtime.Str("doc-type"),
|
||||
FileExtension: runtime.Str("file-extension"),
|
||||
SubID: runtime.Str("sub-id"),
|
||||
OnlySchema: runtime.Bool("only-schema"),
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"token": spec.Token,
|
||||
"type": spec.DocType,
|
||||
"file_extension": spec.FileExtension,
|
||||
}
|
||||
if strings.TrimSpace(spec.SubID) != "" {
|
||||
body["sub_id"] = spec.SubID
|
||||
}
|
||||
if spec.OnlySchema {
|
||||
body["only_schema"] = true
|
||||
}
|
||||
|
||||
dr := common.NewDryRunAPI().
|
||||
Desc("3-step orchestration: create export task -> limited polling -> download file").
|
||||
POST("/open-apis/drive/v1/export_tasks").
|
||||
Body(body).
|
||||
Set("output_dir", p.OutputDir)
|
||||
if name := strings.TrimSpace(p.FileName); name != "" {
|
||||
dr.Set("file_name", ensureExportFileExtension(sanitizeExportFileName(name, spec.Token), spec.FileExtension))
|
||||
}
|
||||
return dr
|
||||
}
|
||||
|
||||
// RunExport drives create export task -> bounded poll -> optional download. It
|
||||
// is the shared core behind both drive +export and sheets +workbook-export. An
|
||||
// empty p.OutputDir skips the download step and returns the ready file token.
|
||||
func RunExport(ctx context.Context, runtime *common.RuntimeContext, p ExportParams) error {
|
||||
spec := p.spec()
|
||||
outputDir := p.OutputDir
|
||||
preferredFileName := strings.TrimSpace(p.FileName)
|
||||
overwrite := p.Overwrite
|
||||
|
||||
// Markdown export bypasses the async export task and writes the fetched
|
||||
// markdown content directly to disk. Uses the V2 docs_ai fetch API for
|
||||
// higher-quality Lark-flavored Markdown output.
|
||||
if spec.FileExtension == "markdown" {
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Exporting docx as markdown: %s\n", common.MaskToken(spec.Token))
|
||||
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", validate.EncodePathSegment(spec.Token))
|
||||
data, err := runtime.CallAPITyped(
|
||||
"POST",
|
||||
apiPath,
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"format": "markdown",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputDir := runtime.Str("output-dir")
|
||||
preferredFileName := strings.TrimSpace(runtime.Str("file-name"))
|
||||
overwrite := runtime.Bool("overwrite")
|
||||
|
||||
// Markdown export bypasses the async export task and writes the fetched
|
||||
// markdown content directly to disk. Uses the V2 docs_ai fetch API for
|
||||
// higher-quality Lark-flavored Markdown output.
|
||||
if spec.FileExtension == "markdown" {
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Exporting docx as markdown: %s\n", common.MaskToken(spec.Token))
|
||||
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", validate.EncodePathSegment(spec.Token))
|
||||
data, err := runtime.CallAPITyped(
|
||||
"POST",
|
||||
apiPath,
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"format": "markdown",
|
||||
},
|
||||
)
|
||||
// Extract content from the V2 response: data.document.content
|
||||
doc, ok := data["document"].(map[string]interface{})
|
||||
if !ok {
|
||||
return errs.NewInternalError(errs.SubtypeInvalidResponse, "invalid markdown fetch response: missing document object")
|
||||
}
|
||||
content, ok := doc["content"].(string)
|
||||
if !ok {
|
||||
return errs.NewInternalError(errs.SubtypeInvalidResponse, "invalid markdown fetch response: missing document.content")
|
||||
}
|
||||
|
||||
fileName := preferredFileName
|
||||
if fileName == "" {
|
||||
// Prefer the remote title for the exported file name, but still fall
|
||||
// back to the token if metadata is empty.
|
||||
title, err := common.FetchDriveMetaTitle(runtime, spec.Token, spec.DocType)
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Title lookup failed, using token as filename: %v\n", err)
|
||||
title = spec.Token
|
||||
}
|
||||
fileName = title
|
||||
}
|
||||
fileName = ensureExportFileExtension(sanitizeExportFileName(fileName, spec.Token), spec.FileExtension)
|
||||
savedPath, err := saveContentToOutputDir(runtime.FileIO(), outputDir, fileName, []byte(content), overwrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract content from the V2 response: data.document.content
|
||||
doc, ok := data["document"].(map[string]interface{})
|
||||
if !ok {
|
||||
return errs.NewInternalError(errs.SubtypeInvalidResponse, "invalid markdown fetch response: missing document object")
|
||||
runtime.Out(map[string]interface{}{
|
||||
"token": spec.Token,
|
||||
"doc_type": spec.DocType,
|
||||
"file_extension": spec.FileExtension,
|
||||
"file_name": filepath.Base(savedPath),
|
||||
"saved_path": savedPath,
|
||||
"size_bytes": len(content),
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
ticket, err := createDriveExportTask(runtime, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Created export task: %s\n", ticket)
|
||||
|
||||
var lastStatus driveExportStatus
|
||||
var lastPollErr error
|
||||
hasObservedStatus := false
|
||||
// Keep the command responsive by polling for a bounded window. If the task
|
||||
// is still running after that, return a resume command instead of blocking.
|
||||
for attempt := 1; attempt <= driveExportPollAttempts; attempt++ {
|
||||
if attempt > 1 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(driveExportPollInterval):
|
||||
}
|
||||
content, ok := doc["content"].(string)
|
||||
if !ok {
|
||||
return errs.NewInternalError(errs.SubtypeInvalidResponse, "invalid markdown fetch response: missing document.content")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := getDriveExportStatus(runtime, spec.Token, ticket)
|
||||
if err != nil {
|
||||
// Treat polling failures as transient so short-lived backend hiccups
|
||||
// do not immediately fail an otherwise healthy export task.
|
||||
lastPollErr = err
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Export status attempt %d/%d failed: %v\n", attempt, driveExportPollAttempts, err)
|
||||
continue
|
||||
}
|
||||
lastStatus = status
|
||||
hasObservedStatus = true
|
||||
|
||||
if status.Ready() {
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Export task completed: %s\n", common.MaskToken(status.FileToken))
|
||||
|
||||
// Export-only mode: caller wants the ready file token / metadata but
|
||||
// no local download (e.g. sheets +workbook-export without an output
|
||||
// path). Skip the download and return the status envelope.
|
||||
if strings.TrimSpace(outputDir) == "" {
|
||||
runtime.Out(map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
"token": spec.Token,
|
||||
"doc_type": spec.DocType,
|
||||
"file_extension": spec.FileExtension,
|
||||
"file_token": status.FileToken,
|
||||
"file_name": status.FileName,
|
||||
"file_size": status.FileSize,
|
||||
"ready": true,
|
||||
"downloaded": false,
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
fileName := preferredFileName
|
||||
if fileName == "" {
|
||||
// Prefer the remote title for the exported file name, but still fall
|
||||
// back to the token if metadata is empty.
|
||||
title, err := common.FetchDriveMetaTitle(runtime, spec.Token, spec.DocType)
|
||||
if err != nil {
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Title lookup failed, using token as filename: %v\n", err)
|
||||
title = spec.Token
|
||||
}
|
||||
fileName = title
|
||||
fileName = status.FileName
|
||||
}
|
||||
fileName = ensureExportFileExtension(sanitizeExportFileName(fileName, spec.Token), spec.FileExtension)
|
||||
savedPath, err := saveContentToOutputDir(runtime.FileIO(), outputDir, fileName, []byte(content), overwrite)
|
||||
out, err := downloadDriveExportFile(ctx, runtime, status.FileToken, outputDir, fileName, overwrite)
|
||||
if err != nil {
|
||||
return err
|
||||
recoveryCommand := driveExportDownloadCommand(status.FileToken, fileName, outputDir, overwrite)
|
||||
hint := fmt.Sprintf(
|
||||
"the export artifact is already ready (ticket=%s, file_token=%s)\nretry download with: %s",
|
||||
ticket,
|
||||
status.FileToken,
|
||||
recoveryCommand,
|
||||
)
|
||||
return appendDriveExportRecoveryHint(err, hint)
|
||||
}
|
||||
|
||||
runtime.Out(map[string]interface{}{
|
||||
"token": spec.Token,
|
||||
"doc_type": spec.DocType,
|
||||
"file_extension": spec.FileExtension,
|
||||
"file_name": filepath.Base(savedPath),
|
||||
"saved_path": savedPath,
|
||||
"size_bytes": len(content),
|
||||
}, nil)
|
||||
out["ticket"] = ticket
|
||||
out["doc_type"] = spec.DocType
|
||||
out["file_extension"] = spec.FileExtension
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
ticket, err := createDriveExportTask(runtime, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Created export task: %s\n", ticket)
|
||||
|
||||
var lastStatus driveExportStatus
|
||||
var lastPollErr error
|
||||
hasObservedStatus := false
|
||||
// Keep the command responsive by polling for a bounded window. If the task
|
||||
// is still running after that, return a resume command instead of blocking.
|
||||
for attempt := 1; attempt <= driveExportPollAttempts; attempt++ {
|
||||
if attempt > 1 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(driveExportPollInterval):
|
||||
}
|
||||
if status.Failed() {
|
||||
msg := strings.TrimSpace(status.JobErrorMsg)
|
||||
if msg == "" {
|
||||
msg = status.StatusLabel()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := getDriveExportStatus(runtime, spec.Token, ticket)
|
||||
if err != nil {
|
||||
// Treat polling failures as transient so short-lived backend hiccups
|
||||
// do not immediately fail an otherwise healthy export task.
|
||||
lastPollErr = err
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Export status attempt %d/%d failed: %v\n", attempt, driveExportPollAttempts, err)
|
||||
continue
|
||||
}
|
||||
lastStatus = status
|
||||
hasObservedStatus = true
|
||||
|
||||
if status.Ready() {
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Export task completed: %s\n", common.MaskToken(status.FileToken))
|
||||
fileName := preferredFileName
|
||||
if fileName == "" {
|
||||
fileName = status.FileName
|
||||
}
|
||||
fileName = ensureExportFileExtension(sanitizeExportFileName(fileName, spec.Token), spec.FileExtension)
|
||||
out, err := downloadDriveExportFile(ctx, runtime, status.FileToken, outputDir, fileName, overwrite)
|
||||
if err != nil {
|
||||
recoveryCommand := driveExportDownloadCommand(status.FileToken, fileName, outputDir, overwrite)
|
||||
hint := fmt.Sprintf(
|
||||
"the export artifact is already ready (ticket=%s, file_token=%s)\nretry download with: %s",
|
||||
ticket,
|
||||
status.FileToken,
|
||||
recoveryCommand,
|
||||
)
|
||||
return appendDriveExportRecoveryHint(err, hint)
|
||||
}
|
||||
out["ticket"] = ticket
|
||||
out["doc_type"] = spec.DocType
|
||||
out["file_extension"] = spec.FileExtension
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
if status.Failed() {
|
||||
msg := strings.TrimSpace(status.JobErrorMsg)
|
||||
if msg == "" {
|
||||
msg = status.StatusLabel()
|
||||
}
|
||||
return errs.NewAPIError(errs.SubtypeServerError, "export task failed: %s (ticket=%s)", msg, ticket)
|
||||
}
|
||||
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Export status %d/%d: %s\n", attempt, driveExportPollAttempts, status.StatusLabel())
|
||||
return errs.NewAPIError(errs.SubtypeServerError, "export task failed: %s (ticket=%s)", msg, ticket)
|
||||
}
|
||||
|
||||
nextCommand := driveExportTaskResultCommand(ticket, spec.Token)
|
||||
if !hasObservedStatus && lastPollErr != nil {
|
||||
hint := fmt.Sprintf(
|
||||
"the export task was created but every status poll failed (ticket=%s)\nretry status lookup with: %s",
|
||||
ticket,
|
||||
nextCommand,
|
||||
)
|
||||
return appendDriveExportRecoveryHint(lastPollErr, hint)
|
||||
}
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Export status %d/%d: %s\n", attempt, driveExportPollAttempts, status.StatusLabel())
|
||||
}
|
||||
|
||||
failed := false
|
||||
var jobStatus interface{}
|
||||
jobStatusLabel := "unknown"
|
||||
if hasObservedStatus {
|
||||
failed = lastStatus.Failed()
|
||||
jobStatus = lastStatus.JobStatus
|
||||
jobStatusLabel = lastStatus.StatusLabel()
|
||||
}
|
||||
// Return the last observed status so callers can resume from a known task
|
||||
// state instead of losing all progress information on timeout.
|
||||
result := map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
"token": spec.Token,
|
||||
"doc_type": spec.DocType,
|
||||
"file_extension": spec.FileExtension,
|
||||
"ready": false,
|
||||
"failed": failed,
|
||||
"job_status": jobStatus,
|
||||
"job_status_label": jobStatusLabel,
|
||||
"timed_out": true,
|
||||
"next_command": nextCommand,
|
||||
}
|
||||
if preferredFileName != "" {
|
||||
result["file_name"] = ensureExportFileExtension(sanitizeExportFileName(preferredFileName, spec.Token), spec.FileExtension)
|
||||
}
|
||||
runtime.Out(result, nil)
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Export task is still in progress. Continue with: %s\n", nextCommand)
|
||||
return nil
|
||||
},
|
||||
nextCommand := driveExportTaskResultCommand(ticket, spec.Token)
|
||||
if !hasObservedStatus && lastPollErr != nil {
|
||||
hint := fmt.Sprintf(
|
||||
"the export task was created but every status poll failed (ticket=%s)\nretry status lookup with: %s",
|
||||
ticket,
|
||||
nextCommand,
|
||||
)
|
||||
return appendDriveExportRecoveryHint(lastPollErr, hint)
|
||||
}
|
||||
|
||||
failed := false
|
||||
var jobStatus interface{}
|
||||
jobStatusLabel := "unknown"
|
||||
if hasObservedStatus {
|
||||
failed = lastStatus.Failed()
|
||||
jobStatus = lastStatus.JobStatus
|
||||
jobStatusLabel = lastStatus.StatusLabel()
|
||||
}
|
||||
// Return the last observed status so callers can resume from a known task
|
||||
// state instead of losing all progress information on timeout.
|
||||
result := map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
"token": spec.Token,
|
||||
"doc_type": spec.DocType,
|
||||
"file_extension": spec.FileExtension,
|
||||
"ready": false,
|
||||
"failed": failed,
|
||||
"job_status": jobStatus,
|
||||
"job_status_label": jobStatusLabel,
|
||||
"timed_out": true,
|
||||
"next_command": nextCommand,
|
||||
}
|
||||
if preferredFileName != "" {
|
||||
result["file_name"] = ensureExportFileExtension(sanitizeExportFileName(preferredFileName, spec.Token), spec.FileExtension)
|
||||
}
|
||||
runtime.Out(result, nil)
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Export task is still in progress. Continue with: %s\n", nextCommand)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -497,6 +497,72 @@ func TestDriveExportAsyncSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDriveExportEmptyOutputDirDownloadsToCwd guards the export refactor: an
|
||||
// explicit empty --output-dir must still download to the current directory
|
||||
// (normalized to "."), not trigger the export-only no-download path that the
|
||||
// shared RunExport core uses for sheets +workbook-export.
|
||||
func TestDriveExportEmptyOutputDirDownloadsToCwd(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/drive/v1/export_tasks",
|
||||
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"ticket": "tk_e"}},
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/drive/v1/export_tasks/tk_e",
|
||||
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{
|
||||
"result": map[string]interface{}{
|
||||
"job_status": 0, "file_token": "box_e", "file_name": "report",
|
||||
"file_extension": "pdf", "type": "docx", "file_size": 3,
|
||||
},
|
||||
}},
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/drive/v1/export_tasks/file/box_e/download",
|
||||
Status: 200,
|
||||
RawBody: []byte("pdf"),
|
||||
Headers: http.Header{
|
||||
"Content-Type": []string{"application/pdf"},
|
||||
"Content-Disposition": []string{`attachment; filename="report.pdf"`},
|
||||
},
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
withDriveWorkingDir(t, tmpDir)
|
||||
|
||||
prevAttempts, prevInterval := driveExportPollAttempts, driveExportPollInterval
|
||||
driveExportPollAttempts, driveExportPollInterval = 1, 0
|
||||
t.Cleanup(func() {
|
||||
driveExportPollAttempts, driveExportPollInterval = prevAttempts, prevInterval
|
||||
})
|
||||
|
||||
err := mountAndRunDrive(t, DriveExport, []string{
|
||||
"+export",
|
||||
"--token", "docx123",
|
||||
"--doc-type", "docx",
|
||||
"--file-extension", "pdf",
|
||||
"--output-dir", "",
|
||||
"--as", "bot",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Empty --output-dir must still write to cwd, not skip the download.
|
||||
data, err := os.ReadFile(filepath.Join(tmpDir, "report.pdf"))
|
||||
if err != nil {
|
||||
t.Fatalf("empty --output-dir should still download to cwd: %v", err)
|
||||
}
|
||||
if string(data) != "pdf" {
|
||||
t.Fatalf("downloaded content = %q", string(data))
|
||||
}
|
||||
if strings.Contains(stdout.String(), `"downloaded": false`) {
|
||||
t.Fatalf("export-only path must not trigger for drive +export: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveExportAsyncUsesProvidedFileName(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
|
||||
@@ -35,132 +35,164 @@ var DriveImport = common.Shortcut{
|
||||
{Name: "target-token", Desc: "existing token to import data into (only for type=bitable); when set, data is mounted into this bitable instead of creating a new one"},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateDriveImportSpec(driveImportSpec{
|
||||
FilePath: runtime.Str("file"),
|
||||
DocType: strings.ToLower(runtime.Str("type")),
|
||||
FolderToken: runtime.Str("folder-token"),
|
||||
Name: runtime.Str("name"),
|
||||
TargetToken: runtime.Str("target-token"),
|
||||
})
|
||||
return ValidateImport(importParamsFromFlags(runtime))
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
spec := driveImportSpec{
|
||||
FilePath: runtime.Str("file"),
|
||||
DocType: strings.ToLower(runtime.Str("type")),
|
||||
FolderToken: runtime.Str("folder-token"),
|
||||
Name: runtime.Str("name"),
|
||||
TargetToken: runtime.Str("target-token"),
|
||||
}
|
||||
fileSize, err := preflightDriveImportFile(runtime.FileIO(), &spec)
|
||||
if err != nil {
|
||||
return common.NewDryRunAPI().Set("error", err.Error())
|
||||
}
|
||||
if valErr := validateDriveImportSpec(spec); valErr != nil {
|
||||
return common.NewDryRunAPI().Set("error", valErr.Error())
|
||||
}
|
||||
|
||||
dry := common.NewDryRunAPI()
|
||||
dry.Desc("Upload file (single-part or multipart) -> create import task -> poll status")
|
||||
|
||||
appendDriveImportFolderTokenWikiCheckDryRun(dry, spec)
|
||||
appendDriveImportUploadDryRun(dry, spec, fileSize)
|
||||
|
||||
dry.POST("/open-apis/drive/v1/import_tasks").
|
||||
Desc("[2] Create import task").
|
||||
Body(spec.CreateTaskBody("<file_token>"))
|
||||
|
||||
dry.GET("/open-apis/drive/v1/import_tasks/:ticket").
|
||||
Desc("[3] Poll import task result").
|
||||
Set("ticket", "<ticket>")
|
||||
if runtime.IsBot() {
|
||||
dry.Desc("After the import result returns the final cloud document target in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on it.")
|
||||
}
|
||||
|
||||
return dry
|
||||
return PlanImportDryRun(runtime, importParamsFromFlags(runtime))
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
spec := driveImportSpec{
|
||||
FilePath: runtime.Str("file"),
|
||||
DocType: strings.ToLower(runtime.Str("type")),
|
||||
FolderToken: runtime.Str("folder-token"),
|
||||
Name: runtime.Str("name"),
|
||||
TargetToken: runtime.Str("target-token"),
|
||||
}
|
||||
if _, err := preflightDriveImportFile(runtime.FileIO(), &spec); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rejectDriveImportWikiFolderToken(runtime, spec.FolderToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 1: Upload file as media
|
||||
fileToken, uploadErr := uploadMediaForImport(ctx, runtime, spec.FilePath, spec.SourceFileName(), spec.DocType)
|
||||
if uploadErr != nil {
|
||||
return uploadErr
|
||||
}
|
||||
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Creating import task for %s as %s...\n", spec.TargetFileName(), spec.DocType)
|
||||
|
||||
// Step 2: Create import task
|
||||
ticket, err := createDriveImportTask(runtime, spec, fileToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 3: Poll task
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Polling import task %s...\n", ticket)
|
||||
|
||||
status, ready, err := pollDriveImportTask(runtime, ticket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Some intermediate responses omit the final type, so fall back to the
|
||||
// requested type to keep the output shape stable.
|
||||
resultType := status.DocType
|
||||
if resultType == "" {
|
||||
resultType = spec.DocType
|
||||
}
|
||||
out := map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
"type": resultType,
|
||||
"ready": ready,
|
||||
"job_status": status.JobStatus,
|
||||
"job_status_label": status.StatusLabel(),
|
||||
}
|
||||
if status.Token != "" {
|
||||
out["token"] = status.Token
|
||||
}
|
||||
if statusURL := strings.TrimSpace(status.URL); statusURL != "" {
|
||||
out["url"] = statusURL
|
||||
} else if status.Token != "" {
|
||||
if u := common.BuildResourceURL(runtime.Config.Brand, normalizeDriveImportKindForURL(resultType, spec.DocType), status.Token); u != "" {
|
||||
out["url"] = u
|
||||
}
|
||||
}
|
||||
if status.JobErrorMsg != "" {
|
||||
out["job_error_msg"] = status.JobErrorMsg
|
||||
}
|
||||
if status.Extra != nil {
|
||||
out["extra"] = status.Extra
|
||||
}
|
||||
if !ready {
|
||||
nextCommand := driveImportTaskResultCommand(ticket)
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Import task is still in progress. Continue with: %s\n", nextCommand)
|
||||
out["timed_out"] = true
|
||||
out["next_command"] = nextCommand
|
||||
}
|
||||
if ready {
|
||||
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, common.GetString(out, "token"), resultType); grant != nil {
|
||||
out["permission_grant"] = grant
|
||||
}
|
||||
}
|
||||
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
return RunImport(ctx, runtime, importParamsFromFlags(runtime))
|
||||
},
|
||||
}
|
||||
|
||||
// ImportParams holds the user-facing inputs for an import flow, decoupled from
|
||||
// cobra flags so other command groups (e.g. sheets +workbook-import) can reuse
|
||||
// the drive import implementation without taking a dependency on a --type flag.
|
||||
type ImportParams struct {
|
||||
File string
|
||||
DocType string
|
||||
FolderToken string
|
||||
Name string
|
||||
TargetToken string
|
||||
}
|
||||
|
||||
func (p ImportParams) spec() driveImportSpec {
|
||||
return driveImportSpec{
|
||||
FilePath: p.File,
|
||||
DocType: strings.ToLower(p.DocType),
|
||||
FolderToken: p.FolderToken,
|
||||
Name: p.Name,
|
||||
TargetToken: p.TargetToken,
|
||||
}
|
||||
}
|
||||
|
||||
// importParamsFromFlags reads the standard drive +import flag set.
|
||||
func importParamsFromFlags(runtime *common.RuntimeContext) ImportParams {
|
||||
return ImportParams{
|
||||
File: runtime.Str("file"),
|
||||
DocType: runtime.Str("type"),
|
||||
FolderToken: runtime.Str("folder-token"),
|
||||
Name: runtime.Str("name"),
|
||||
TargetToken: runtime.Str("target-token"),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateImport runs the CLI-level compatibility checks for an import.
|
||||
func ValidateImport(p ImportParams) error {
|
||||
return validateDriveImportSpec(p.spec())
|
||||
}
|
||||
|
||||
// PlanImportDryRun builds the dry-run plan (upload -> create task -> poll) for
|
||||
// an import without performing any network or file I/O beyond a local stat.
|
||||
func PlanImportDryRun(runtime *common.RuntimeContext, p ImportParams) *common.DryRunAPI {
|
||||
spec := p.spec()
|
||||
fileSize, err := preflightDriveImportFile(runtime.FileIO(), &spec)
|
||||
if err != nil {
|
||||
return common.NewDryRunAPI().Set("error", err.Error())
|
||||
}
|
||||
if valErr := validateDriveImportSpec(spec); valErr != nil {
|
||||
return common.NewDryRunAPI().Set("error", valErr.Error())
|
||||
}
|
||||
|
||||
dry := common.NewDryRunAPI()
|
||||
dry.Desc("Upload file (single-part or multipart) -> create import task -> poll status")
|
||||
|
||||
appendDriveImportFolderTokenWikiCheckDryRun(dry, spec)
|
||||
appendDriveImportUploadDryRun(dry, spec, fileSize)
|
||||
|
||||
dry.POST("/open-apis/drive/v1/import_tasks").
|
||||
Desc("[2] Create import task").
|
||||
Body(spec.CreateTaskBody("<file_token>"))
|
||||
|
||||
dry.GET("/open-apis/drive/v1/import_tasks/:ticket").
|
||||
Desc("[3] Poll import task result").
|
||||
Set("ticket", "<ticket>")
|
||||
if runtime.IsBot() {
|
||||
dry.Desc("After the import result returns the final cloud document target in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on it.")
|
||||
}
|
||||
|
||||
return dry
|
||||
}
|
||||
|
||||
// RunImport executes the full import flow: upload media -> create import task ->
|
||||
// bounded poll, then writes the result envelope to the runtime output. It is
|
||||
// the shared core behind both drive +import and sheets +workbook-import.
|
||||
func RunImport(ctx context.Context, runtime *common.RuntimeContext, p ImportParams) error {
|
||||
spec := p.spec()
|
||||
if _, err := preflightDriveImportFile(runtime.FileIO(), &spec); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rejectDriveImportWikiFolderToken(runtime, spec.FolderToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 1: Upload file as media
|
||||
fileToken, uploadErr := uploadMediaForImport(ctx, runtime, spec.FilePath, spec.SourceFileName(), spec.DocType)
|
||||
if uploadErr != nil {
|
||||
return uploadErr
|
||||
}
|
||||
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Creating import task for %s as %s...\n", spec.TargetFileName(), spec.DocType)
|
||||
|
||||
// Step 2: Create import task
|
||||
ticket, err := createDriveImportTask(runtime, spec, fileToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 3: Poll task
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Polling import task %s...\n", ticket)
|
||||
|
||||
status, ready, err := pollDriveImportTask(runtime, ticket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Some intermediate responses omit the final type, so fall back to the
|
||||
// requested type to keep the output shape stable.
|
||||
resultType := status.DocType
|
||||
if resultType == "" {
|
||||
resultType = spec.DocType
|
||||
}
|
||||
out := map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
"type": resultType,
|
||||
"ready": ready,
|
||||
"job_status": status.JobStatus,
|
||||
"job_status_label": status.StatusLabel(),
|
||||
}
|
||||
if status.Token != "" {
|
||||
out["token"] = status.Token
|
||||
}
|
||||
if statusURL := strings.TrimSpace(status.URL); statusURL != "" {
|
||||
out["url"] = statusURL
|
||||
} else if status.Token != "" {
|
||||
if u := common.BuildResourceURL(runtime.Config.Brand, normalizeDriveImportKindForURL(resultType, spec.DocType), status.Token); u != "" {
|
||||
out["url"] = u
|
||||
}
|
||||
}
|
||||
if status.JobErrorMsg != "" {
|
||||
out["job_error_msg"] = status.JobErrorMsg
|
||||
}
|
||||
if status.Extra != nil {
|
||||
out["extra"] = status.Extra
|
||||
}
|
||||
if !ready {
|
||||
nextCommand := driveImportTaskResultCommand(ticket)
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Import task is still in progress. Continue with: %s\n", nextCommand)
|
||||
out["timed_out"] = true
|
||||
out["next_command"] = nextCommand
|
||||
}
|
||||
if ready {
|
||||
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, common.GetString(out, "token"), resultType); grant != nil {
|
||||
out["permission_grant"] = grant
|
||||
}
|
||||
}
|
||||
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func preflightDriveImportFile(fio fileio.FileIO, spec *driveImportSpec) (int64, error) {
|
||||
// Keep dry-run and execution aligned on path normalization, file existence,
|
||||
// and format-specific size limits before planning the upload path.
|
||||
|
||||
@@ -149,9 +149,6 @@ var DriveSearch = common.Shortcut{
|
||||
"page_token": data["page_token"],
|
||||
"results": normalizedItems,
|
||||
}
|
||||
if notice, _ := data["notice"].(string); notice != "" {
|
||||
resultData["notice"] = notice
|
||||
}
|
||||
|
||||
runtime.OutFormat(resultData, &output.Meta{Count: len(normalizedItems)}, func(w io.Writer) {
|
||||
renderDriveSearchTable(w, data, normalizedItems)
|
||||
|
||||
@@ -14,49 +14,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/errclass"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
// TestDriveSearchExecutePassesThroughNotice verifies drive +search preserves notices.
|
||||
func TestDriveSearchExecutePassesThroughNotice(t *testing.T) {
|
||||
const notice = "The query is too long and has been truncated to the first 50 characters for search."
|
||||
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/search/v2/doc_wiki/search",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"notice": notice,
|
||||
"res_units": []interface{}{},
|
||||
"total": 0,
|
||||
"has_more": false,
|
||||
"page_token": "",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := mountAndRunDrive(t, DriveSearch, []string{"+search", "--query", "incident", "--format", "json", "--as", "user"}, f, stdout); err != nil {
|
||||
t.Fatalf("DriveSearch.Execute() error = %v", err)
|
||||
}
|
||||
reg.Verify(t)
|
||||
|
||||
var env map[string]interface{}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
|
||||
t.Fatalf("json.Unmarshal(stdout) error = %v\nstdout=%s", err, stdout.String())
|
||||
}
|
||||
data, _ := env["data"].(map[string]interface{})
|
||||
if got, _ := data["notice"].(string); got != notice {
|
||||
t.Fatalf("data.notice = %q, want %q; data=%#v", got, notice, data)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampOpenedTimeWindow covers opened-time clamping and slice notices.
|
||||
// TestClampOpenedTimeWindow covers the 3-month / 1-year boundary logic that
|
||||
// narrows --opened-since / --opened-until and generates the multi-slice notice.
|
||||
func TestClampOpenedTimeWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ func mustMarshalDryRun(t *testing.T, v interface{}) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// newTestRuntimeContext builds a RuntimeContext with string and bool test flags.
|
||||
// newTestRuntimeContext builds a *common.RuntimeContext backed by a cobra
|
||||
// command whose flags are populated from the provided string and bool maps,
|
||||
// for unit-testing shortcut bodies, validators, and dry-run shapes.
|
||||
func newTestRuntimeContext(t *testing.T, stringFlags map[string]string, boolFlags map[string]bool) *common.RuntimeContext {
|
||||
t.Helper()
|
||||
|
||||
@@ -57,38 +59,9 @@ func newTestRuntimeContext(t *testing.T, stringFlags map[string]string, boolFlag
|
||||
return &common.RuntimeContext{Cmd: cmd}
|
||||
}
|
||||
|
||||
// newChatSearchTestRuntimeContext builds a chat-search RuntimeContext with typed flags.
|
||||
func newChatSearchTestRuntimeContext(t *testing.T, stringFlags map[string]string, boolFlags map[string]bool) *common.RuntimeContext {
|
||||
t.Helper()
|
||||
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
cmd.Flags().Int("page-size", 20, "")
|
||||
for name := range stringFlags {
|
||||
if name == "page-size" {
|
||||
continue
|
||||
}
|
||||
cmd.Flags().String(name, "", "")
|
||||
}
|
||||
for name := range boolFlags {
|
||||
cmd.Flags().Bool(name, false, "")
|
||||
}
|
||||
if err := cmd.ParseFlags(nil); err != nil {
|
||||
t.Fatalf("ParseFlags() error = %v", err)
|
||||
}
|
||||
for name, val := range stringFlags {
|
||||
if err := cmd.Flags().Set(name, val); err != nil {
|
||||
t.Fatalf("Flags().Set(%q) error = %v", name, err)
|
||||
}
|
||||
}
|
||||
for name, val := range boolFlags {
|
||||
if err := cmd.Flags().Set(name, map[bool]string{true: "true", false: "false"}[val]); err != nil {
|
||||
t.Fatalf("Flags().Set(%q) error = %v", name, err)
|
||||
}
|
||||
}
|
||||
return &common.RuntimeContext{Cmd: cmd}
|
||||
}
|
||||
|
||||
// newMessagesSearchTestRuntimeContext builds a messages-search RuntimeContext.
|
||||
// newMessagesSearchTestRuntimeContext is the messages-search variant of
|
||||
// newTestRuntimeContext: registers the search-specific --page-size flag
|
||||
// before applying caller-provided values.
|
||||
func newMessagesSearchTestRuntimeContext(t *testing.T, stringFlags map[string]string, boolFlags map[string]bool) *common.RuntimeContext {
|
||||
t.Helper()
|
||||
|
||||
@@ -258,7 +231,6 @@ func TestIsMediaKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestShortcutValidateBranches covers direct shortcut validation branches.
|
||||
func TestShortcutValidateBranches(t *testing.T) {
|
||||
|
||||
t.Run("ImChatCreate valid", func(t *testing.T) {
|
||||
@@ -325,7 +297,7 @@ func TestShortcutValidateBranches(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ImChatSearch invalid page size", func(t *testing.T) {
|
||||
runtime := newChatSearchTestRuntimeContext(t, map[string]string{
|
||||
runtime := newTestRuntimeContext(t, map[string]string{
|
||||
"query": "ok",
|
||||
"page-size": "0",
|
||||
}, nil)
|
||||
@@ -335,13 +307,12 @@ func TestShortcutValidateBranches(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ImChatSearch allows long query for server-side notice", func(t *testing.T) {
|
||||
runtime := newChatSearchTestRuntimeContext(t, map[string]string{
|
||||
"query": strings.Repeat("q", 81),
|
||||
"page-size": "20",
|
||||
t.Run("ImChatSearch query too long", func(t *testing.T) {
|
||||
runtime := newTestRuntimeContext(t, map[string]string{
|
||||
"query": strings.Repeat("q", 65),
|
||||
}, nil)
|
||||
err := ImChatSearch.Validate(context.Background(), runtime)
|
||||
if err != nil {
|
||||
if err == nil || !strings.Contains(err.Error(), "--query exceeds the maximum of 64 characters") {
|
||||
t.Fatalf("ImChatSearch.Validate() error = %v", err)
|
||||
}
|
||||
})
|
||||
@@ -636,7 +607,6 @@ func TestShortcutValidateBranches(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestMessagesSearchPaginationConfig verifies page-all and page-limit behavior.
|
||||
func TestMessagesSearchPaginationConfig(t *testing.T) {
|
||||
t.Run("default single page", func(t *testing.T) {
|
||||
runtime := newMessagesSearchTestRuntimeContext(t, nil, nil)
|
||||
@@ -680,7 +650,8 @@ func TestMessagesSearchPaginationConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestShortcutDryRunShapes verifies shortcut dry-run API paths and payloads.
|
||||
// TestShortcutDryRunShapes verifies that each shortcut's DryRun function
|
||||
// produces the expected API path, query parameters, and request body.
|
||||
func TestShortcutDryRunShapes(t *testing.T) {
|
||||
t.Run("ImChatCreate dry run includes params and body", func(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
@@ -703,19 +674,19 @@ func TestShortcutDryRunShapes(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ImChatSearch dry run includes built params", func(t *testing.T) {
|
||||
runtime := newChatSearchTestRuntimeContext(t, map[string]string{
|
||||
runtime := newTestRuntimeContext(t, map[string]string{
|
||||
"query": "team-alpha",
|
||||
"page-size": "50",
|
||||
"page-token": "next_page",
|
||||
}, nil)
|
||||
got := mustMarshalDryRun(t, ImChatSearch.DryRun(context.Background(), runtime))
|
||||
if !strings.Contains(got, `"/open-apis/im/v2/chats/search"`) || !strings.Contains(got, `"page_size":50`) || !strings.Contains(got, `"query":"\"team-alpha\""`) {
|
||||
if !strings.Contains(got, `"/open-apis/im/v2/chats/search"`) || !strings.Contains(got, `"page_size":20`) || !strings.Contains(got, `"query":"\"team-alpha\""`) {
|
||||
t.Fatalf("ImChatSearch.DryRun() = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ImChatSearch dry run still works with --exclude-muted set", func(t *testing.T) {
|
||||
runtime := newChatSearchTestRuntimeContext(t, map[string]string{
|
||||
runtime := newTestRuntimeContext(t, map[string]string{
|
||||
"query": "team-alpha",
|
||||
}, map[string]bool{
|
||||
"exclude-muted": true,
|
||||
|
||||
@@ -29,7 +29,7 @@ var ImChatSearch = common.Shortcut{
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "query", Desc: "search keyword (server may return data.notice for overly long input)"},
|
||||
{Name: "query", Desc: "search keyword (max 64 chars)"},
|
||||
{Name: "search-types", Desc: "chat types, comma-separated (private, external, public_joined, public_not_joined)"},
|
||||
{Name: "chat-modes", Desc: "filter by chat mode, comma-separated (group, topic)"},
|
||||
{Name: "member-ids", Desc: "filter by member open_ids, comma-separated"},
|
||||
@@ -50,7 +50,7 @@ var ImChatSearch = common.Shortcut{
|
||||
Params(params).
|
||||
Body(body)
|
||||
},
|
||||
// Validate enforces query/member-ids presence, search-types
|
||||
// Validate enforces query/member-ids presence, --query rune cap, search-types
|
||||
// enum, --member-ids count and format, and --page-size bounds.
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
query := runtime.Str("query")
|
||||
@@ -58,6 +58,9 @@ var ImChatSearch = common.Shortcut{
|
||||
if query == "" && memberIDs == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--query and --member-ids cannot both be empty; provide at least one (e.g. --query \"team-name\" or --member-ids \"ou_xxx\")")
|
||||
}
|
||||
if query != "" && len([]rune(query)) > 64 {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--query exceeds the maximum of 64 characters (got %d)", len([]rune(query))).WithParam("--query")
|
||||
}
|
||||
if st := runtime.Str("search-types"); st != "" {
|
||||
allowed := map[string]struct{}{
|
||||
"private": {},
|
||||
@@ -148,9 +151,6 @@ var ImChatSearch = common.Shortcut{
|
||||
"has_more": hasMore,
|
||||
"page_token": pageToken,
|
||||
}
|
||||
if notice, _ := resData["notice"].(string); notice != "" {
|
||||
outData["notice"] = notice
|
||||
}
|
||||
if mfOut.Meta.Applied != "" {
|
||||
outData["filter"] = MuteFilterMetaToMap(mfOut.Meta)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ var ImMessagesSearch = common.Shortcut{
|
||||
return err
|
||||
}
|
||||
|
||||
rawItems, hasMore, nextPageToken, truncatedByLimit, pageLimit, notice, err := searchMessages(runtime, req)
|
||||
rawItems, hasMore, nextPageToken, truncatedByLimit, pageLimit, err := searchMessages(runtime, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -103,9 +103,6 @@ var ImMessagesSearch = common.Shortcut{
|
||||
"has_more": hasMore,
|
||||
"page_token": nextPageToken,
|
||||
}
|
||||
if notice != "" {
|
||||
outData["notice"] = notice
|
||||
}
|
||||
runtime.OutFormat(outData, nil, func(w io.Writer) {
|
||||
fmt.Fprintln(w, "No matching messages found.")
|
||||
})
|
||||
@@ -134,9 +131,6 @@ var ImMessagesSearch = common.Shortcut{
|
||||
"page_token": nextPageToken,
|
||||
"note": "failed to fetch message details, returning ID list only",
|
||||
}
|
||||
if notice != "" {
|
||||
outData["notice"] = notice
|
||||
}
|
||||
runtime.OutFormat(outData, nil, func(w io.Writer) {
|
||||
fmt.Fprintf(w, "Found %d messages (failed to fetch details):\n", len(messageIds))
|
||||
for _, id := range messageIds {
|
||||
@@ -212,9 +206,6 @@ var ImMessagesSearch = common.Shortcut{
|
||||
"has_more": hasMore,
|
||||
"page_token": nextPageToken,
|
||||
}
|
||||
if notice != "" {
|
||||
outData["notice"] = notice
|
||||
}
|
||||
runtime.OutFormat(outData, nil, func(w io.Writer) {
|
||||
if len(enriched) == 0 {
|
||||
fmt.Fprintln(w, "No matching messages found.")
|
||||
@@ -386,7 +377,6 @@ func buildMessagesSearchRequest(runtime *common.RuntimeContext) (*messagesSearch
|
||||
}, nil
|
||||
}
|
||||
|
||||
// messagesSearchPaginationConfig derives auto-pagination mode and page limit.
|
||||
func messagesSearchPaginationConfig(runtime *common.RuntimeContext) (autoPaginate bool, pageLimit int) {
|
||||
autoPaginate = runtime.Bool("page-all")
|
||||
if runtime.Cmd != nil && runtime.Cmd.Flags().Changed("page-limit") {
|
||||
@@ -402,8 +392,7 @@ func messagesSearchPaginationConfig(runtime *common.RuntimeContext) (autoPaginat
|
||||
return autoPaginate, pageLimit
|
||||
}
|
||||
|
||||
// searchMessages fetches message search pages and returns the first server notice.
|
||||
func searchMessages(runtime *common.RuntimeContext, req *messagesSearchRequest) ([]interface{}, bool, string, bool, int, string, error) {
|
||||
func searchMessages(runtime *common.RuntimeContext, req *messagesSearchRequest) ([]interface{}, bool, string, bool, int, error) {
|
||||
autoPaginate, pageLimit := messagesSearchPaginationConfig(runtime)
|
||||
pageToken := ""
|
||||
if tokens := req.params["page_token"]; len(tokens) > 0 {
|
||||
@@ -421,7 +410,6 @@ func searchMessages(runtime *common.RuntimeContext, req *messagesSearchRequest)
|
||||
lastPageToken string
|
||||
truncatedByLimit bool
|
||||
pageCount int
|
||||
notice string
|
||||
)
|
||||
|
||||
for {
|
||||
@@ -435,12 +423,9 @@ func searchMessages(runtime *common.RuntimeContext, req *messagesSearchRequest)
|
||||
|
||||
searchData, err := runtime.DoAPIJSONTyped(http.MethodPost, "/open-apis/im/v1/messages/search", params, req.body)
|
||||
if err != nil {
|
||||
return nil, false, "", false, pageLimit, "", err
|
||||
return nil, false, "", false, pageLimit, err
|
||||
}
|
||||
|
||||
if notice == "" {
|
||||
notice, _ = searchData["notice"].(string)
|
||||
}
|
||||
items, _ := searchData["items"].([]interface{})
|
||||
allItems = append(allItems, items...)
|
||||
lastHasMore, lastPageToken = common.PaginationMeta(searchData)
|
||||
@@ -456,10 +441,9 @@ func searchMessages(runtime *common.RuntimeContext, req *messagesSearchRequest)
|
||||
pageToken = lastPageToken
|
||||
}
|
||||
|
||||
return allItems, lastHasMore, lastPageToken, truncatedByLimit, pageLimit, notice, nil
|
||||
return allItems, lastHasMore, lastPageToken, truncatedByLimit, pageLimit, nil
|
||||
}
|
||||
|
||||
// batchMGetMessages fetches message details in API-sized batches.
|
||||
func batchMGetMessages(runtime *common.RuntimeContext, messageIds []string) ([]interface{}, error) {
|
||||
var items []interface{}
|
||||
for _, batch := range chunkStrings(messageIds, messagesSearchMGetBatchSize) {
|
||||
@@ -473,7 +457,6 @@ func batchMGetMessages(runtime *common.RuntimeContext, messageIds []string) ([]i
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// batchQueryChatContexts fetches chat metadata best-effort for message rows.
|
||||
func batchQueryChatContexts(runtime *common.RuntimeContext, chatIds []string) map[string]map[string]interface{} {
|
||||
chatContexts := map[string]map[string]interface{}{}
|
||||
// Best-effort: a failed chunk only loses its own entries.
|
||||
@@ -483,7 +466,6 @@ func batchQueryChatContexts(runtime *common.RuntimeContext, chatIds []string) ma
|
||||
return chatContexts
|
||||
}
|
||||
|
||||
// chunkStrings splits a string slice into fixed-size batches.
|
||||
func chunkStrings(items []string, chunkSize int) [][]string {
|
||||
if len(items) == 0 || chunkSize <= 0 {
|
||||
return nil
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// TestImChatSearchExecutePassesThroughNotice verifies chat search notice output.
|
||||
func TestImChatSearchExecutePassesThroughNotice(t *testing.T) {
|
||||
const notice = "The query is too long and has been truncated to the first 50 characters for search."
|
||||
longQuery := strings.Repeat("q", 81)
|
||||
|
||||
runtime := newBotShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.Contains(req.URL.Path, "/open-apis/im/v2/chats/search") {
|
||||
return nil, fmt.Errorf("unexpected request: %s", req.URL.String())
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
return nil, fmt.Errorf("decode request body: %w", err)
|
||||
}
|
||||
if got, _ := body["query"].(string); got != longQuery {
|
||||
return nil, fmt.Errorf("body.query = %q, want %q", got, longQuery)
|
||||
}
|
||||
return shortcutJSONResponse(200, map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"notice": notice,
|
||||
"items": []interface{}{},
|
||||
"total": 0,
|
||||
"has_more": false,
|
||||
"page_token": "",
|
||||
},
|
||||
}), nil
|
||||
}))
|
||||
runtime.Cmd = newChatSearchNoticeTestCommand(t, longQuery)
|
||||
runtime.Format = "json"
|
||||
|
||||
if err := ImChatSearch.Execute(context.Background(), runtime); err != nil {
|
||||
t.Fatalf("ImChatSearch.Execute() error = %v", err)
|
||||
}
|
||||
|
||||
data := decodeShortcutData(t, runtime)
|
||||
if got, _ := data["notice"].(string); got != notice {
|
||||
t.Fatalf("data.notice = %q, want %q; data=%#v", got, notice, data)
|
||||
}
|
||||
}
|
||||
|
||||
// TestImMessagesSearchExecutePassesThroughNotice verifies message search notice output.
|
||||
func TestImMessagesSearchExecutePassesThroughNotice(t *testing.T) {
|
||||
const notice = "The query is too long and has been truncated to the first 50 characters for search."
|
||||
|
||||
runtime := newMessagesSearchRuntime(t, map[string]string{
|
||||
"query": "incident",
|
||||
}, nil, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.Contains(req.URL.Path, "/open-apis/im/v1/messages/search") {
|
||||
return nil, fmt.Errorf("unexpected request: %s", req.URL.String())
|
||||
}
|
||||
return shortcutJSONResponse(200, map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"notice": notice,
|
||||
"items": []interface{}{},
|
||||
"has_more": false,
|
||||
"page_token": "",
|
||||
},
|
||||
}), nil
|
||||
}))
|
||||
runtime.Format = "json"
|
||||
|
||||
if err := ImMessagesSearch.Execute(context.Background(), runtime); err != nil {
|
||||
t.Fatalf("ImMessagesSearch.Execute() error = %v", err)
|
||||
}
|
||||
|
||||
data := decodeShortcutData(t, runtime)
|
||||
if got, _ := data["notice"].(string); got != notice {
|
||||
t.Fatalf("data.notice = %q, want %q; data=%#v", got, notice, data)
|
||||
}
|
||||
}
|
||||
|
||||
// newChatSearchNoticeTestCommand builds a typed chat-search command for notice tests.
|
||||
func newChatSearchNoticeTestCommand(t *testing.T, query string) *cobra.Command {
|
||||
t.Helper()
|
||||
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
for _, name := range []string{"query", "search-types", "member-ids", "sort-by", "page-token"} {
|
||||
cmd.Flags().String(name, "", "")
|
||||
}
|
||||
for _, name := range []string{"is-manager", "disable-search-by-user", "exclude-muted"} {
|
||||
cmd.Flags().Bool(name, false, "")
|
||||
}
|
||||
cmd.Flags().Int("page-size", 20, "")
|
||||
if err := cmd.ParseFlags(nil); err != nil {
|
||||
t.Fatalf("ParseFlags() error = %v", err)
|
||||
}
|
||||
if err := cmd.Flags().Set("query", query); err != nil {
|
||||
t.Fatalf("Flags().Set(query) error = %v", err)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// decodeShortcutData extracts the JSON envelope data object from shortcut output.
|
||||
func decodeShortcutData(t *testing.T, runtime *common.RuntimeContext) map[string]interface{} {
|
||||
t.Helper()
|
||||
|
||||
out, ok := runtime.Factory.IOStreams.Out.(*bytes.Buffer)
|
||||
if !ok {
|
||||
t.Fatalf("stdout buffer has type %T", runtime.Factory.IOStreams.Out)
|
||||
}
|
||||
var env map[string]interface{}
|
||||
if err := json.Unmarshal(out.Bytes(), &env); err != nil {
|
||||
t.Fatalf("json.Unmarshal(stdout) error = %v\nstdout=%s", err, out.String())
|
||||
}
|
||||
data, ok := env["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("envelope data missing or wrong type: %#v", env)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -159,7 +159,6 @@ var MailTriage = common.Shortcut{
|
||||
var messages []map[string]interface{}
|
||||
var hasMore bool
|
||||
var nextPageToken string
|
||||
var notice string
|
||||
|
||||
useSearch, err := resolveTriagePath(parsed, query, filter)
|
||||
if err != nil {
|
||||
@@ -190,9 +189,6 @@ var MailTriage = common.Shortcut{
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if notice == "" {
|
||||
notice, _ = searchData["notice"].(string)
|
||||
}
|
||||
pageMessages := buildTriageMessagesFromSearchItems(searchData["items"])
|
||||
messages = append(messages, pageMessages...)
|
||||
pageHasMore, _ := searchData["has_more"].(bool)
|
||||
@@ -286,14 +282,8 @@ var MailTriage = common.Shortcut{
|
||||
"has_more": hasMore,
|
||||
"page_token": nextPageToken,
|
||||
}
|
||||
if notice != "" {
|
||||
outData["notice"] = notice
|
||||
}
|
||||
output.PrintJson(runtime.IO().Out, outData)
|
||||
default: // "table"
|
||||
if notice != "" {
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "notice: %s\n", notice)
|
||||
}
|
||||
if len(messages) == 0 {
|
||||
fmt.Fprintln(runtime.IO().ErrOut, "No messages found.")
|
||||
return nil
|
||||
|
||||
@@ -1478,16 +1478,14 @@ func boolPtr(v bool) *bool { return &v }
|
||||
|
||||
// --- mailbox_id preservation tests ---
|
||||
|
||||
// TestMailTriageStructuredOutputPreservesMailboxID verifies mailbox and notice metadata.
|
||||
func TestMailTriageStructuredOutputPreservesMailboxID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mailbox string
|
||||
format string
|
||||
args []string
|
||||
register func(*httpmock.Registry, string)
|
||||
wantCount int
|
||||
wantNotice string
|
||||
name string
|
||||
mailbox string
|
||||
format string
|
||||
args []string
|
||||
register func(*httpmock.Registry, string)
|
||||
wantCount int
|
||||
}{
|
||||
{
|
||||
name: "list json default mailbox",
|
||||
@@ -1524,10 +1522,9 @@ func TestMailTriageStructuredOutputPreservesMailboxID(t *testing.T) {
|
||||
register: func(reg *httpmock.Registry, mailbox string) {
|
||||
registerMailTriageSearchStub(reg, mailbox, []interface{}{
|
||||
mailTriageSearchItem("search_pub_001", "Shared search"),
|
||||
}, false, "", "The query is too long and has been truncated to the first 50 characters for search.")
|
||||
}, false, "")
|
||||
},
|
||||
wantCount: 1,
|
||||
wantNotice: "The query is too long and has been truncated to the first 50 characters for search.",
|
||||
wantCount: 1,
|
||||
},
|
||||
{
|
||||
name: "empty list json keeps top-level mailbox",
|
||||
@@ -1562,9 +1559,6 @@ func TestMailTriageStructuredOutputPreservesMailboxID(t *testing.T) {
|
||||
if data["mailbox_id"] != tt.mailbox {
|
||||
t.Fatalf("top-level mailbox_id mismatch: got %v, want %q", data["mailbox_id"], tt.mailbox)
|
||||
}
|
||||
if tt.wantNotice != "" && data["notice"] != tt.wantNotice {
|
||||
t.Fatalf("notice mismatch: got %v, want %q", data["notice"], tt.wantNotice)
|
||||
}
|
||||
messages := mailTriageMessagesFromOutput(t, data)
|
||||
if len(messages) != tt.wantCount {
|
||||
t.Fatalf("message count mismatch: got %d, want %d", len(messages), tt.wantCount)
|
||||
@@ -1578,7 +1572,6 @@ func TestMailTriageStructuredOutputPreservesMailboxID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMailTriageMissingMessageMetadataStillGetsMailboxID verifies fallback rows keep mailbox IDs.
|
||||
func TestMailTriageMissingMessageMetadataStillGetsMailboxID(t *testing.T) {
|
||||
f, stdout, _, reg := mailShortcutTestFactory(t)
|
||||
defer reg.Verify(t)
|
||||
@@ -1611,7 +1604,6 @@ func TestMailTriageMissingMessageMetadataStillGetsMailboxID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMailTriageTableOutputPreservesMailboxContext verifies public mailbox table hints.
|
||||
func TestMailTriageTableOutputPreservesMailboxContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1662,33 +1654,6 @@ func TestMailTriageTableOutputPreservesMailboxContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMailTriageDefaultTableOutputPrintsSearchNoticeToStderr verifies stderr notices.
|
||||
func TestMailTriageDefaultTableOutputPrintsSearchNoticeToStderr(t *testing.T) {
|
||||
const notice = "The query is too long and has been truncated to the first 50 characters for search."
|
||||
|
||||
f, stdout, stderr, reg := mailShortcutTestFactory(t)
|
||||
defer reg.Verify(t)
|
||||
|
||||
registerMailTriageSearchStub(reg, "me", []interface{}{
|
||||
mailTriageSearchItem("msg_search_notice", "Search notice result"),
|
||||
}, false, "", notice)
|
||||
|
||||
if err := runMountedMailShortcut(t, MailTriage, []string{
|
||||
"+triage",
|
||||
"--query", strings.Repeat("q", 81),
|
||||
}, f, stdout); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if out := stdout.String(); !strings.Contains(out, "msg_search_notice") {
|
||||
t.Fatalf("stdout should contain table row, got:\n%s", out)
|
||||
}
|
||||
if errOut := stderr.String(); !strings.Contains(errOut, "notice: "+notice) {
|
||||
t.Fatalf("stderr should contain search notice, got:\n%s", errOut)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeMailTriageJSONOutput decodes structured triage output for assertions.
|
||||
func decodeMailTriageJSONOutput(t *testing.T, stdout interface{ Bytes() []byte }) map[string]interface{} {
|
||||
t.Helper()
|
||||
var data map[string]interface{}
|
||||
@@ -1698,7 +1663,6 @@ func decodeMailTriageJSONOutput(t *testing.T, stdout interface{ Bytes() []byte }
|
||||
return data
|
||||
}
|
||||
|
||||
// mailTriageMessagesFromOutput extracts triage messages as object maps.
|
||||
func mailTriageMessagesFromOutput(t *testing.T, data map[string]interface{}) []map[string]interface{} {
|
||||
t.Helper()
|
||||
rawMessages, ok := data["messages"].([]interface{})
|
||||
@@ -1751,8 +1715,7 @@ func registerMailTriageBatchStub(reg *httpmock.Registry, mailbox string, message
|
||||
})
|
||||
}
|
||||
|
||||
// registerMailTriageSearchStub registers a mailbox search response for triage tests.
|
||||
func registerMailTriageSearchStub(reg *httpmock.Registry, mailbox string, items []interface{}, hasMore bool, pageToken string, notices ...string) {
|
||||
func registerMailTriageSearchStub(reg *httpmock.Registry, mailbox string, items []interface{}, hasMore bool, pageToken string) {
|
||||
data := map[string]interface{}{
|
||||
"items": items,
|
||||
"has_more": hasMore,
|
||||
@@ -1760,9 +1723,6 @@ func registerMailTriageSearchStub(reg *httpmock.Registry, mailbox string, items
|
||||
if pageToken != "" {
|
||||
data["page_token"] = pageToken
|
||||
}
|
||||
if len(notices) > 0 && notices[0] != "" {
|
||||
data["notice"] = notices[0]
|
||||
}
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: mailboxPath(mailbox, "search"),
|
||||
|
||||
@@ -308,9 +308,6 @@ var MinutesSearch = common.Shortcut{
|
||||
"has_more": data["has_more"],
|
||||
"page_token": data["page_token"],
|
||||
}
|
||||
if notice, _ := data["notice"].(string); notice != "" {
|
||||
outData["notice"] = notice
|
||||
}
|
||||
|
||||
runtime.OutFormat(outData, &output.Meta{Count: len(rows)}, func(w io.Writer) {
|
||||
if len(rows) == 0 {
|
||||
|
||||
@@ -609,8 +609,6 @@ func TestMinutesSearchExecuteShowsPaginationHintForTableFormat(t *testing.T) {
|
||||
func TestMinutesSearchExecuteJSONCountUsesRenderedRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const notice = "The query is too long and has been truncated to the first 50 characters for search."
|
||||
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
@@ -619,7 +617,6 @@ func TestMinutesSearchExecuteJSONCountUsesRenderedRows(t *testing.T) {
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"notice": notice,
|
||||
"items": []interface{}{
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
@@ -644,9 +641,6 @@ func TestMinutesSearchExecuteJSONCountUsesRenderedRows(t *testing.T) {
|
||||
reg.Verify(t)
|
||||
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
Notice string `json:"notice"`
|
||||
} `json:"data"`
|
||||
Meta struct {
|
||||
Count int `json:"count"`
|
||||
} `json:"meta"`
|
||||
@@ -657,9 +651,6 @@ func TestMinutesSearchExecuteJSONCountUsesRenderedRows(t *testing.T) {
|
||||
if envelope.Meta.Count != 1 {
|
||||
t.Fatalf("meta.count = %d, want 1", envelope.Meta.Count)
|
||||
}
|
||||
if envelope.Data.Notice != notice {
|
||||
t.Fatalf("data.notice = %q, want %q", envelope.Data.Notice, notice)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMinuteSearchFieldExtractors verifies field extractors read populated metadata correctly.
|
||||
|
||||
@@ -177,6 +177,18 @@ func TestBatchOp_BodyMatchesStandalone(t *testing.T) {
|
||||
args: []string{"--sheet-id", "sh1", "--color", "#FF0000"},
|
||||
subInput: `{"sheet-id":"sh1","color":"#FF0000"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+sheet-show-gridline",
|
||||
sc: SheetShowGridline,
|
||||
args: []string{"--sheet-id", "sh1"},
|
||||
subInput: `{"sheet-id":"sh1"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+sheet-hide-gridline",
|
||||
sc: SheetHideGridline,
|
||||
args: []string{"--sheet-id", "sh1"},
|
||||
subInput: `{"sheet-id":"sh1"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+dropdown-set",
|
||||
sc: DropdownSet,
|
||||
|
||||
@@ -150,6 +150,12 @@ var batchOpDispatch = map[string]batchOpMapping{
|
||||
return sheetVisibilityInput(fv, t, sid, sn, "unhide")
|
||||
}},
|
||||
"+sheet-set-tab-color": {"modify_workbook_structure", sheetSetTabColorInput},
|
||||
"+sheet-show-gridline": {"modify_workbook_structure", func(fv flagView, t, sid, sn string) (map[string]interface{}, error) {
|
||||
return sheetVisibilityInput(fv, t, sid, sn, "show_gridline")
|
||||
}},
|
||||
"+sheet-hide-gridline": {"modify_workbook_structure", func(fv flagView, t, sid, sn string) (map[string]interface{}, error) {
|
||||
return sheetVisibilityInput(fv, t, sid, sn, "hide_gridline")
|
||||
}},
|
||||
|
||||
// ─── 对象族 CRUD (manage_*_object, operation 区分) ─────────────
|
||||
"+chart-create": {"manage_chart_object", objCreateTranslate(chartSpec)},
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Insert position; appended to the end when omitted",
|
||||
"desc": "Insert position (0-based); appended to the end when omitted",
|
||||
"default": "-1"
|
||||
},
|
||||
{
|
||||
@@ -413,6 +413,86 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"+sheet-hide-gridline": {
|
||||
"risk": "write",
|
||||
"flags": [
|
||||
{
|
||||
"name": "url",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet URL (XOR with `--spreadsheet-token`)"
|
||||
},
|
||||
{
|
||||
"name": "spreadsheet-token",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet token (XOR with `--url`)"
|
||||
},
|
||||
{
|
||||
"name": "sheet-id",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Sheet reference_id (XOR with `--sheet-name`)"
|
||||
},
|
||||
{
|
||||
"name": "sheet-name",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"+sheet-show-gridline": {
|
||||
"risk": "write",
|
||||
"flags": [
|
||||
{
|
||||
"name": "url",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet URL (XOR with `--spreadsheet-token`)"
|
||||
},
|
||||
{
|
||||
"name": "spreadsheet-token",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet token (XOR with `--url`)"
|
||||
},
|
||||
{
|
||||
"name": "sheet-id",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Sheet reference_id (XOR with `--sheet-name`)"
|
||||
},
|
||||
{
|
||||
"name": "sheet-name",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"+workbook-create": {
|
||||
"risk": "write",
|
||||
"flags": [
|
||||
@@ -431,27 +511,45 @@
|
||||
"desc": "Target folder token; placed at the drive root when omitted"
|
||||
},
|
||||
{
|
||||
"name": "headers",
|
||||
"name": "values",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Header row as a JSON array: `[\"Col A\",\"Col B\"]`",
|
||||
"desc": "Untyped initial data as one 2D JSON array (`[[\"alice\",95]]`); values are written as-is with their type auto-detected, through the same batched set_cell_range path as --sheets — pair with --styles for number formats, colors, merges, and row/col sizes",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "values",
|
||||
"name": "sheets",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Initial data as a 2D JSON array: `[[\"alice\",95]]`",
|
||||
"desc": "Typed table payload as JSON (same shape as `+table-put`): a top-level `sheets` array, each item `{name, start_cell?, mode?, header?, allow_overwrite?, columns:[\"colA\",\"colB\",...], data:[[...]], dtypes?:{colA:pandasDtype, ...}, formats?:{colA:numberFormat, ...}}`. Agents typically build it from a DataFrame via `{**json.loads(df.to_json(orient=\"split\")), \"dtypes\": df.dtypes.astype(str).to_dict()}`. Mutually exclusive with --values and --dataframe. Creates the workbook, then writes typed type-faithful data (dates land as real dates, numbers keep precision).",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "styles",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Initial visual operations as JSON: top-level `{styles:[...]}`. Each item corresponds to one target sheet and must include `name`, plus at least one of `cell_styles` / `row_sizes` / `col_sizes` / `cell_merges`. `cell_styles` entries use +cells-set-style fields with a cell range; row/col sizes use dimension ranges plus type/size; merges use cell ranges plus optional merge_type. With --sheets, styles array length/order/name must match --sheets.sheets. With --values, pass exactly one styles item for the initial sheet (its name is ignored).",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dataframe",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Single-sheet typed table from one Arrow IPC file (Feather v2 — what `pandas.DataFrame.to_feather()` writes), mutually exclusive with --values and --sheets. Pass `@<path>` for a file or `-` for binary stdin (same convention as other input flags). Arrow bytes are read raw — no TrimSpace / BOM strip — so the IPC magic survives intact (unlike text input flags). Column types come from the Arrow schema; per-column `number_format` may be set via Arrow field metadata. Creates the workbook and fills its default sheet (`Sheet1` — adopted in place, no empty Sheet1 left behind). For multi-sheet or non-default placement, use `--sheets` instead."
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
@@ -513,6 +611,32 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"+workbook-import": {
|
||||
"risk": "write",
|
||||
"flags": [
|
||||
{
|
||||
"name": "file",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Local file path (.xlsx / .xls / .csv)"
|
||||
},
|
||||
{
|
||||
"name": "folder-token",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Target folder token; imported to the cloud drive root when omitted"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Imported spreadsheet name; defaults to the local file name without its extension"
|
||||
}
|
||||
]
|
||||
},
|
||||
"+sheet-info": {
|
||||
"risk": "read",
|
||||
"flags": [
|
||||
@@ -1082,9 +1206,8 @@
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Safety cap; default 200000",
|
||||
"default": "200000",
|
||||
"hidden": true
|
||||
"desc": "Max output chars per call; default 500000 (safety cap). Large reads are usually better redirected to a file; only lower it (e.g. 25000) when you want results inline without triggering file offload, paging via has_more",
|
||||
"default": "500000"
|
||||
},
|
||||
{
|
||||
"name": "skip-hidden",
|
||||
@@ -1192,9 +1315,8 @@
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Safety cap; default 200000",
|
||||
"default": "200000",
|
||||
"hidden": true
|
||||
"desc": "Max output chars per call; default 500000 (safety cap). Large reads are usually better redirected to a file; only lower it (e.g. 25000) when you want results inline without triggering file offload, paging via has_more",
|
||||
"default": "500000"
|
||||
},
|
||||
{
|
||||
"name": "include-row-prefix",
|
||||
@@ -1212,19 +1334,72 @@
|
||||
"desc": "Skip hidden rows and columns; default `false`"
|
||||
},
|
||||
{
|
||||
"name": "rows-json",
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Print the request path and parameters without executing"
|
||||
}
|
||||
]
|
||||
},
|
||||
"+table-get": {
|
||||
"risk": "read",
|
||||
"flags": [
|
||||
{
|
||||
"name": "url",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet URL (XOR with `--spreadsheet-token`)"
|
||||
},
|
||||
{
|
||||
"name": "spreadsheet-token",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet token (XOR with `--url`)"
|
||||
},
|
||||
{
|
||||
"name": "sheet-id",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Read only this sheet (by id); omit to read all sheets"
|
||||
},
|
||||
{
|
||||
"name": "sheet-name",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Read only this sheet (by name); omit to read all sheets"
|
||||
},
|
||||
{
|
||||
"name": "range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "A1 range to read; omit to read each sheet's full used range (spans internal blank rows/columns, not just the A1 current region)"
|
||||
},
|
||||
{
|
||||
"name": "no-header",
|
||||
"kind": "own",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Return structured rows ({row_number, values:{col→cell}}) instead of CSV text; default false",
|
||||
"default": "false"
|
||||
"desc": "Treat the first row as data instead of a header (columns get positional names col1, col2, ...)"
|
||||
},
|
||||
{
|
||||
"name": "dataframe-out",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Write the typed table as one Arrow IPC file (Feather v2) instead of the default JSON. Pass `@<path>` for a file or `-` for binary stdout (same convention as other binary I/O flags). Mirror of the input-side `--dataframe` on `+table-put` / `+workbook-create` — pandas users round-trip via `df = pd.read_feather(\"x.arrow\")` or `pd.read_feather(io.BytesIO(stdout))`. Single-sheet only: requires `--sheet-id` or `--sheet-name`; whole-workbook reads keep the default JSON path. Column types come from the typed read-back (string/number/date/bool); per-column `number_format` is preserved as Arrow field metadata so the Arrow file can round-trip straight back through `+table-put --dataframe`."
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Print the request path and parameters without executing"
|
||||
"desc": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1849,7 +2024,7 @@
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "RFC 4180 CSV text; plain values only (no formulas / styles / comments)",
|
||||
"desc": "RFC 4180 CSV text; values or formulas (a leading = is evaluated as a formula); no styles / comments / images (use +cells-set for those).",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
@@ -1880,6 +2055,61 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"+table-put": {
|
||||
"risk": "write",
|
||||
"flags": [
|
||||
{
|
||||
"name": "url",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet URL to write into (XOR with `--spreadsheet-token`)"
|
||||
},
|
||||
{
|
||||
"name": "spreadsheet-token",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet token to write into (XOR with `--url`)"
|
||||
},
|
||||
{
|
||||
"name": "sheets",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Typed table payload (pandas-DataFrame-shaped) as JSON, XOR with `--dataframe`: a top-level `sheets` array, each item `{name, start_cell?, mode?, header?, allow_overwrite?, columns:[\"colA\",\"colB\",...], data:[[...]], dtypes?:{colA:pandasDtype, ...}, formats?:{colA:numberFormat, ...}}`. Agents typically build it with `{**json.loads(df.to_json(orient=\"split\")), \"dtypes\": df.dtypes.astype(str).to_dict()}`. `dtypes` values are pandas dtype strings (`int64`, `float64`, `Int64`, `bool`, `boolean`, `datetime64[ns]`, `object`, ...); the writer maps them to internal string/number/date/bool — omit `dtypes` and a column writes as text (good for raw CSV-shaped data). `formats[col]` is an Excel number_format string (e.g. `#,##0.00`, `0.0%`, `yyyy-mm`); when absent, date columns default to `yyyy-mm-dd` and string columns to text format (`@`).",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dataframe",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Single-sheet typed table from one Arrow IPC file (a.k.a. Feather v2 — what `pandas.DataFrame.to_feather()` writes), XOR with `--sheets`. Pass `@<path>` for a file or `-` for binary stdin (same convention as other input flags). Arrow bytes are read raw — no TrimSpace / BOM strip — so the IPC magic survives intact (unlike text input flags). Column types come from the Arrow schema (int*/uint*/float* → number, date32/date64/timestamp → date, utf8/large_utf8 → string, bool → bool); per-column `number_format` may be set via Arrow field metadata (`pa.field(\"price\", pa.float64(), metadata={b\"number_format\": b\"$#,##0.00\"})`). Writes the sheet at default placement: name `Sheet1` (created when absent), overwrite from A1 with header. For a different sheet name, anchor, mode, or to write multiple sheets, use `--sheets` instead."
|
||||
},
|
||||
{
|
||||
"name": "styles",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Visual operations applied after the typed write, as JSON: top-level `{styles:[...]}`. Each item corresponds to one written sheet and must include `name`, plus at least one of `cell_styles` / `row_sizes` / `col_sizes` / `cell_merges`. `cell_styles` entries use +cells-set-style fields with a cell range; row/col sizes use dimension ranges plus type/size; merges use cell ranges plus optional merge_type. The styles array length/order/name must match the written sheets: with --sheets, match --sheets.sheets; with --dataframe (single sheet named Sheet1), pass exactly one styles item with name `Sheet1`. Run `+table-put --print-schema --flag-name styles` for the full cell_styles field schema.",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"+cells-clear": {
|
||||
"risk": "high-risk-write",
|
||||
"flags": [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"schema_version": "2",
|
||||
"schema_version": "3",
|
||||
"flags": {
|
||||
"+batch-update": {
|
||||
"operations": {
|
||||
@@ -454,7 +454,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"link": {
|
||||
"description": "超链接地址(仅 type='link' 时必填)",
|
||||
"description": "超链接地址(type='link' 时必填);@文档 mention(mention_type 非 0)时也必填,传文档 URL(如搜索结果里的文档链接),否则卡片不可点。@人(mention_type=0)不需要传",
|
||||
"type": "string"
|
||||
},
|
||||
"mention_token": {
|
||||
@@ -462,8 +462,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"mention_type": {
|
||||
"description": "@提及类型编号(仅 type='mention' 时可选)",
|
||||
"type": "number"
|
||||
"description": "@提及类型编号(仅 type='mention' 时可选)。0 或不填=@用户;@文件时按类型取:1=文档 3=电子表格 8=多维表格 11=思维笔记 12=文件 15=旧版幻灯片 16=知识库 22=新版文档 30=幻灯片 38=画板",
|
||||
"type": "number",
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
8,
|
||||
11,
|
||||
12,
|
||||
15,
|
||||
16,
|
||||
22,
|
||||
30,
|
||||
38
|
||||
]
|
||||
},
|
||||
"notify": {
|
||||
"description": "是否发送通知(仅 type='mention' 时可选,默认 true)",
|
||||
@@ -1730,11 +1743,12 @@
|
||||
},
|
||||
"aggregateType": {
|
||||
"type": "string",
|
||||
"description": "汇总方式,默认为'sum',仅在 aggregate 为 true 时生效",
|
||||
"description": "汇总方式,默认为'sum',仅在 aggregate 为 true 时生效。count 只统计数值单元格;counta 统计所有非空单元格(含文本),按文本/分类列统计出现次数(如各类别的数量、频次分布)时用 counta。",
|
||||
"enum": [
|
||||
"sum",
|
||||
"average",
|
||||
"count",
|
||||
"counta",
|
||||
"min",
|
||||
"max",
|
||||
"median"
|
||||
@@ -1787,11 +1801,7 @@
|
||||
"data"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"size"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"+chart-update": {
|
||||
@@ -2769,11 +2779,12 @@
|
||||
},
|
||||
"aggregateType": {
|
||||
"type": "string",
|
||||
"description": "汇总方式,默认为'sum',仅在 aggregate 为 true 时生效",
|
||||
"description": "汇总方式,默认为'sum',仅在 aggregate 为 true 时生效。count 只统计数值单元格;counta 统计所有非空单元格(含文本),按文本/分类列统计出现次数(如各类别的数量、频次分布)时用 counta。",
|
||||
"enum": [
|
||||
"sum",
|
||||
"average",
|
||||
"count",
|
||||
"counta",
|
||||
"min",
|
||||
"max",
|
||||
"median"
|
||||
@@ -2826,11 +2837,7 @@
|
||||
"data"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"size"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"+cond-format-create": {
|
||||
@@ -6249,6 +6256,744 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"+table-put": {
|
||||
"sheets": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "一个或多个子表的 typed 数据,每个数组元素写入一张子表;支持多 DataFrame → 多子表一次写入。整体形状对齐 pandas `df.to_json(orient=\"split\")`:列名走 `columns`、二维取值走 `data`、每列的 pandas dtype 走 `dtypes`、可选的展示格式走 `formats`。一行式用法:`{**json.loads(df.to_json(orient=\"split\")), \"dtypes\": df.dtypes.astype(str).to_dict()}`。",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"columns",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "目标子表名。按名匹配已有子表;不存在则新建该子表。同一次调用内子表名不可重复。"
|
||||
},
|
||||
"start_cell": {
|
||||
"type": "string",
|
||||
"default": "A1",
|
||||
"description": "写入起点单元格(A1 记法,如 \"B2\"),默认 \"A1\"。mode=append 时忽略其行号、仅沿用其列。"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"overwrite",
|
||||
"append"
|
||||
],
|
||||
"default": "overwrite",
|
||||
"description": "overwrite(默认):从 start_cell 起写「表头 + 数据」块;append:把数据追加到子表已有数据下方(默认不重复表头)。"
|
||||
},
|
||||
"header": {
|
||||
"type": "boolean",
|
||||
"description": "是否写一行列名表头。省略时按 mode 取默认:overwrite→true、append→false(避免在已有表头下重复);显式给值可覆盖。"
|
||||
},
|
||||
"allow_overwrite": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "为 false 时,若写入会落在非空单元格则拒写以保护原数据(返回 partial_success)。默认 true。"
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "列名字符串数组,顺序与 `data` 中每行取值一一对应。同一子表内列名不可重复。",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"description": "数据行;每行是一个数组,长度必须等于 `columns` 数。元素按 `dtypes` 推得的列类型取值(date 列写 ISO yyyy-mm-dd 字符串、number 列写数值、bool 列写布尔、其余写文本),null 表示空单元格。",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"description": "单元格值:date→ISO yyyy-mm-dd 字符串;number→数值(json.Number 精度保留);bool→布尔;string→文本;null→空单元格。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dtypes": {
|
||||
"type": "object",
|
||||
"description": "可选。列名 → pandas dtype 字符串的映射;缺失项默认按 object(string + 文本格式 `@`)处理,所以省略整段时整张表按文本写入(导入 CSV-shaped 数据的最简形态)。dtype 解析规则:`int*` / `uint*` / `Int*` / `UInt*` / `float*` / `Float*` / `complex*` → number(精度保留),`bool` / `boolean` → bool,`datetime64[ns]` / 含时区的 `datetime64[ns, UTC]` 等 → date(默认 `yyyy-mm-dd` 格式),`object` / `string` / `category` / 未识别 → string + 文本格式 `@`(数字样字符串如「00123」不会塌缩成数字)。",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"formats": {
|
||||
"type": "object",
|
||||
"description": "可选。列名 → Excel number_format 字符串的映射,覆盖 dtype 自带的默认格式(金额 `#,##0.00`、百分比 `0.0%`、自定义日期 `yyyy-mm` 等)。percent 列的数值尺度由调用方负责(0.0469 配 `0.00%` 显示 4.69%)。",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"cell_merges": {
|
||||
"description": "单元格合并操作数组;range 使用 A1 单元格范围,merge_type 默认 all。",
|
||||
"items": {
|
||||
"properties": {
|
||||
"merge_type": {
|
||||
"enum": [
|
||||
"all",
|
||||
"rows",
|
||||
"columns"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"range": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"range"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"cell_styles": {
|
||||
"description": "单元格样式操作数组;每项用 A1 单元格 range 指定范围,字段名与 +cells-set-style 对齐。",
|
||||
"items": {
|
||||
"properties": {
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"border_styles": {
|
||||
"type": "object",
|
||||
"description": "边框配置,结构同 +cells-set-style --border-styles。",
|
||||
"properties": {
|
||||
"bottom": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "边框颜色(十六进制,例如 \"#000000\")",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"description": "边框线型;传 \"none\" 表示清除该方向边框(无边框线)",
|
||||
"enum": [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"description": "边框粗细/线宽",
|
||||
"enum": [
|
||||
"thin",
|
||||
"medium",
|
||||
"thick"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"left": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "边框颜色(十六进制,例如 \"#000000\")",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"description": "边框线型;传 \"none\" 表示清除该方向边框(无边框线)",
|
||||
"enum": [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"description": "边框粗细/线宽",
|
||||
"enum": [
|
||||
"thin",
|
||||
"medium",
|
||||
"thick"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"right": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "边框颜色(十六进制,例如 \"#000000\")",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"description": "边框线型;传 \"none\" 表示清除该方向边框(无边框线)",
|
||||
"enum": [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"description": "边框粗细/线宽",
|
||||
"enum": [
|
||||
"thin",
|
||||
"medium",
|
||||
"thick"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"top": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "边框颜色(十六进制,例如 \"#000000\")",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"description": "边框线型;传 \"none\" 表示清除该方向边框(无边框线)",
|
||||
"enum": [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"description": "边框粗细/线宽",
|
||||
"enum": [
|
||||
"thin",
|
||||
"medium",
|
||||
"thick"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"font_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"font_line": {
|
||||
"enum": [
|
||||
"none",
|
||||
"underline",
|
||||
"line-through"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"font_size": {
|
||||
"type": "number"
|
||||
},
|
||||
"font_style": {
|
||||
"enum": [
|
||||
"normal",
|
||||
"italic"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"font_weight": {
|
||||
"enum": [
|
||||
"normal",
|
||||
"bold"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"horizontal_alignment": {
|
||||
"enum": [
|
||||
"left",
|
||||
"center",
|
||||
"right"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"number_format": {
|
||||
"type": "string"
|
||||
},
|
||||
"range": {
|
||||
"description": "A1 单元格范围,必须落在该子表本次写入区域内;例如 A1:B1、B2。",
|
||||
"type": "string"
|
||||
},
|
||||
"vertical_alignment": {
|
||||
"enum": [
|
||||
"top",
|
||||
"middle",
|
||||
"bottom"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"word_wrap": {
|
||||
"enum": [
|
||||
"overflow",
|
||||
"auto-wrap",
|
||||
"word-clip"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"range"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"col_sizes": {
|
||||
"description": "列宽操作数组;range 使用列范围如 A:C,type 为 pixel/standard,pixel 需要 size。",
|
||||
"items": {
|
||||
"properties": {
|
||||
"range": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"pixel",
|
||||
"standard"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"range",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "子表名。--sheets 模式下必须与同位置 --sheets.sheets[].name 一致;--values 模式下建议写 Sheet1(其 name 会被忽略)。",
|
||||
"type": "string"
|
||||
},
|
||||
"row_sizes": {
|
||||
"description": "行高操作数组;range 使用行范围如 1:3,type 为 pixel/standard/auto,pixel 需要 size。",
|
||||
"items": {
|
||||
"properties": {
|
||||
"range": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"pixel",
|
||||
"standard",
|
||||
"auto"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"range",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"+workbook-create": {
|
||||
"sheets": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "一个或多个子表的 typed 数据,每个数组元素写入一张子表;支持多 DataFrame → 多子表一次写入。整体形状对齐 pandas `df.to_json(orient=\"split\")`:列名走 `columns`、二维取值走 `data`、每列的 pandas dtype 走 `dtypes`、可选的展示格式走 `formats`。一行式用法:`{**json.loads(df.to_json(orient=\"split\")), \"dtypes\": df.dtypes.astype(str).to_dict()}`。",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"columns",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "目标子表名。按名匹配已有子表;不存在则新建该子表。同一次调用内子表名不可重复。"
|
||||
},
|
||||
"start_cell": {
|
||||
"type": "string",
|
||||
"default": "A1",
|
||||
"description": "写入起点单元格(A1 记法,如 \"B2\"),默认 \"A1\"。mode=append 时忽略其行号、仅沿用其列。"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"overwrite",
|
||||
"append"
|
||||
],
|
||||
"default": "overwrite",
|
||||
"description": "overwrite(默认):从 start_cell 起写「表头 + 数据」块;append:把数据追加到子表已有数据下方(默认不重复表头)。"
|
||||
},
|
||||
"header": {
|
||||
"type": "boolean",
|
||||
"description": "是否写一行列名表头。省略时按 mode 取默认:overwrite→true、append→false(避免在已有表头下重复);显式给值可覆盖。"
|
||||
},
|
||||
"allow_overwrite": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "为 false 时,若写入会落在非空单元格则拒写以保护原数据(返回 partial_success)。默认 true。"
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "列名字符串数组,顺序与 `data` 中每行取值一一对应。同一子表内列名不可重复。",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"description": "数据行;每行是一个数组,长度必须等于 `columns` 数。元素按 `dtypes` 推得的列类型取值(date 列写 ISO yyyy-mm-dd 字符串、number 列写数值、bool 列写布尔、其余写文本),null 表示空单元格。",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"description": "单元格值:date→ISO yyyy-mm-dd 字符串;number→数值(json.Number 精度保留);bool→布尔;string→文本;null→空单元格。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dtypes": {
|
||||
"type": "object",
|
||||
"description": "可选。列名 → pandas dtype 字符串的映射;缺失项默认按 object(string + 文本格式 `@`)处理,所以省略整段时整张表按文本写入(导入 CSV-shaped 数据的最简形态)。dtype 解析规则:`int*` / `uint*` / `Int*` / `UInt*` / `float*` / `Float*` / `complex*` → number(精度保留),`bool` / `boolean` → bool,`datetime64[ns]` / 含时区的 `datetime64[ns, UTC]` 等 → date(默认 `yyyy-mm-dd` 格式),`object` / `string` / `category` / 未识别 → string + 文本格式 `@`(数字样字符串如「00123」不会塌缩成数字)。",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"formats": {
|
||||
"type": "object",
|
||||
"description": "可选。列名 → Excel number_format 字符串的映射,覆盖 dtype 自带的默认格式(金额 `#,##0.00`、百分比 `0.0%`、自定义日期 `yyyy-mm` 等)。percent 列的数值尺度由调用方负责(0.0469 配 `0.00%` 显示 4.69%)。",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"cell_merges": {
|
||||
"description": "单元格合并操作数组;range 使用 A1 单元格范围,merge_type 默认 all。",
|
||||
"items": {
|
||||
"properties": {
|
||||
"merge_type": {
|
||||
"enum": [
|
||||
"all",
|
||||
"rows",
|
||||
"columns"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"range": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"range"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"cell_styles": {
|
||||
"description": "单元格样式操作数组;每项用 A1 单元格 range 指定范围,字段名与 +cells-set-style 对齐。",
|
||||
"items": {
|
||||
"properties": {
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"border_styles": {
|
||||
"type": "object",
|
||||
"description": "边框配置,结构同 +cells-set-style --border-styles。",
|
||||
"properties": {
|
||||
"bottom": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "边框颜色(十六进制,例如 \"#000000\")",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"description": "边框线型;传 \"none\" 表示清除该方向边框(无边框线)",
|
||||
"enum": [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"description": "边框粗细/线宽",
|
||||
"enum": [
|
||||
"thin",
|
||||
"medium",
|
||||
"thick"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"left": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "边框颜色(十六进制,例如 \"#000000\")",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"description": "边框线型;传 \"none\" 表示清除该方向边框(无边框线)",
|
||||
"enum": [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"description": "边框粗细/线宽",
|
||||
"enum": [
|
||||
"thin",
|
||||
"medium",
|
||||
"thick"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"right": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "边框颜色(十六进制,例如 \"#000000\")",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"description": "边框线型;传 \"none\" 表示清除该方向边框(无边框线)",
|
||||
"enum": [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"description": "边框粗细/线宽",
|
||||
"enum": [
|
||||
"thin",
|
||||
"medium",
|
||||
"thick"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"top": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "边框颜色(十六进制,例如 \"#000000\")",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"description": "边框线型;传 \"none\" 表示清除该方向边框(无边框线)",
|
||||
"enum": [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"description": "边框粗细/线宽",
|
||||
"enum": [
|
||||
"thin",
|
||||
"medium",
|
||||
"thick"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"font_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"font_line": {
|
||||
"enum": [
|
||||
"none",
|
||||
"underline",
|
||||
"line-through"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"font_size": {
|
||||
"type": "number"
|
||||
},
|
||||
"font_style": {
|
||||
"enum": [
|
||||
"normal",
|
||||
"italic"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"font_weight": {
|
||||
"enum": [
|
||||
"normal",
|
||||
"bold"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"horizontal_alignment": {
|
||||
"enum": [
|
||||
"left",
|
||||
"center",
|
||||
"right"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"number_format": {
|
||||
"type": "string"
|
||||
},
|
||||
"range": {
|
||||
"description": "A1 单元格范围,必须落在该子表本次写入区域内;例如 A1:B1、B2。",
|
||||
"type": "string"
|
||||
},
|
||||
"vertical_alignment": {
|
||||
"enum": [
|
||||
"top",
|
||||
"middle",
|
||||
"bottom"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"word_wrap": {
|
||||
"enum": [
|
||||
"overflow",
|
||||
"auto-wrap",
|
||||
"word-clip"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"range"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"col_sizes": {
|
||||
"description": "列宽操作数组;range 使用列范围如 A:C,type 为 pixel/standard,pixel 需要 size。",
|
||||
"items": {
|
||||
"properties": {
|
||||
"range": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"pixel",
|
||||
"standard"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"range",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "子表名。--sheets 模式下必须与同位置 --sheets.sheets[].name 一致;--values 模式下建议写 Sheet1(其 name 会被忽略)。",
|
||||
"type": "string"
|
||||
},
|
||||
"row_sizes": {
|
||||
"description": "行高操作数组;range 使用行范围如 1:3,type 为 pixel/standard/auto,pixel 需要 size。",
|
||||
"items": {
|
||||
"properties": {
|
||||
"range": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"pixel",
|
||||
"standard",
|
||||
"auto"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"range",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,97 @@ func TestExecute_WorkbookInfo_ToolError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecute_WikiURLResolvesToSheet covers the two-step wiki path: a /wiki/
|
||||
// URL is resolved via get_node to its spreadsheet obj_token, which then feeds
|
||||
// the tool invoke. The tool stub is keyed on the resolved obj_token, so the
|
||||
// test would fail if the node_token were used unresolved.
|
||||
func TestExecute_WikiURLResolvesToSheet(t *testing.T) {
|
||||
t.Parallel()
|
||||
getNode := &httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/wiki/v2/spaces/get_node",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"node": map[string]interface{}{
|
||||
"obj_type": "sheet",
|
||||
"obj_token": testToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tool := toolOutputStub(testToken, "read", `{"sheets":[{"sheet_id":"sh1","title":"Sheet1","index":0}]}`)
|
||||
out, err := runShortcutWithStubs(t, WorkbookInfo,
|
||||
[]string{"--url", "https://example.feishu.cn/wiki/wikTestNODE"}, getNode, tool)
|
||||
if err != nil {
|
||||
t.Fatalf("execute failed: %v\nout=%s", err, out)
|
||||
}
|
||||
data := decodeEnvelopeData(t, out)
|
||||
if sheets, _ := data["sheets"].([]interface{}); len(sheets) != 1 {
|
||||
t.Fatalf("sheets len = %d, want 1; out=%s", len(sheets), out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecute_WikiURLWrongObjType rejects a wiki node that resolves to a
|
||||
// non-spreadsheet obj_type before any tool invoke.
|
||||
func TestExecute_WikiURLWrongObjType(t *testing.T) {
|
||||
t.Parallel()
|
||||
getNode := &httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/wiki/v2/spaces/get_node",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"node": map[string]interface{}{
|
||||
"obj_type": "docx",
|
||||
"obj_token": "docABC",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
out, err := runShortcutWithStubs(t, WorkbookInfo,
|
||||
[]string{"--url", "https://example.feishu.cn/wiki/wikTestNODE"}, getNode)
|
||||
if err == nil {
|
||||
t.Fatalf("want error for non-sheet wiki node; out=%s", out)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "obj_type") {
|
||||
t.Fatalf("error = %v, want mention of obj_type", err)
|
||||
}
|
||||
var ve *errs.ValidationError
|
||||
if !errors.As(err, &ve) {
|
||||
t.Fatalf("wrong-obj_type error = %T, want *errs.ValidationError", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecute_WikiURLIncompleteNode treats an incomplete get_node response
|
||||
// (missing obj_type/obj_token) as an internal/server error, not a user --url
|
||||
// validation error.
|
||||
func TestExecute_WikiURLIncompleteNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
getNode := &httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/wiki/v2/spaces/get_node",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"node": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := runShortcutWithStubs(t, WorkbookInfo,
|
||||
[]string{"--url", "https://example.feishu.cn/wiki/wikTestNODE"}, getNode)
|
||||
if err == nil {
|
||||
t.Fatal("want error for incomplete get_node node data")
|
||||
}
|
||||
var ve *errs.ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
t.Fatalf("incomplete-data error classified as validation (%v); want internal", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecute_SheetMove_LookupsIndex covers the two-step path: SheetMove
|
||||
// when only --sheet-name is given (and --source-index omitted) first
|
||||
// reads the workbook structure to derive sheet_id + source_index, then
|
||||
@@ -365,14 +456,17 @@ func TestExecute_WorkbookCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
// Initial fill first reads the workbook structure to resolve the default
|
||||
// sheet's id (the create response doesn't echo it), then writes.
|
||||
// The write reads the workbook structure to resolve the default sheet's id
|
||||
// (the create response doesn't echo it). lookupFirstSheetID and
|
||||
// writeTypedSheets' listSheetIDsByName both read it — one reusable stub serves
|
||||
// both. The synthesized sheet is named "Sheet1", matching the default sheet,
|
||||
// so it's adopted in place (no rename).
|
||||
structure := toolOutputStub("shtcnBRAND", "read", `{"sheets":[{"sheet_id":"shtFirst","sheet_name":"Sheet1","index":0}]}`)
|
||||
structure.Reusable = true
|
||||
fill := toolOutputStub("shtcnBRAND", "write", `{"updated_cells":4}`)
|
||||
out, err := runShortcutWithStubs(t, WorkbookCreate, []string{
|
||||
"--title", "Sales",
|
||||
"--headers", `["Name","Score"]`,
|
||||
"--values", `[["alice",95]]`,
|
||||
"--values", `[["Name","Score"],["alice",95]]`,
|
||||
}, create, structure, fill)
|
||||
if err != nil {
|
||||
t.Fatalf("execute failed: %v\nout=%s", err, out)
|
||||
@@ -382,8 +476,8 @@ func TestExecute_WorkbookCreate(t *testing.T) {
|
||||
if ss["spreadsheet_token"] != "shtcnBRAND" {
|
||||
t.Errorf("spreadsheet_token = %v", ss["spreadsheet_token"])
|
||||
}
|
||||
if data["initial_fill"] == nil {
|
||||
t.Errorf("initial_fill missing in envelope")
|
||||
if sheets, _ := data["sheets"].([]interface{}); len(sheets) != 1 {
|
||||
t.Errorf("sheets summary missing in envelope; got %#v", data["sheets"])
|
||||
}
|
||||
// The fill must target the resolved first sheet, not an empty selector.
|
||||
fillInput := decodeToolInput(t, decodeRawEnvelopeBody(t, fill.CapturedBody), "set_cell_range")
|
||||
@@ -393,14 +487,13 @@ func TestExecute_WorkbookCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestExecute_WorkbookCreate_EmptyArraysSkipFill locks the fix for the nil-map
|
||||
// panic / illegal-range bug: --values '[]' or --headers '[]' must short-circuit
|
||||
// the initial fill (no structure/fill calls fire) and finish with the
|
||||
// spreadsheet created but no initial_fill — never panic on a nil fill map.
|
||||
// panic / illegal-range bug: --values '[]' must short-circuit the initial fill
|
||||
// (no structure/fill calls fire) and finish with the spreadsheet created but no
|
||||
// sheets summary — never panic on a nil payload.
|
||||
func TestExecute_WorkbookCreate_EmptyArraysSkipFill(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tc := range []struct{ name, flag, val string }{
|
||||
{"empty values", "--values", "[]"},
|
||||
{"empty headers", "--headers", "[]"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -421,8 +514,8 @@ func TestExecute_WorkbookCreate_EmptyArraysSkipFill(t *testing.T) {
|
||||
t.Fatalf("execute failed: %v\nout=%s", err, out)
|
||||
}
|
||||
data := decodeEnvelopeData(t, out)
|
||||
if data["initial_fill"] != nil {
|
||||
t.Errorf("initial_fill should be absent for %s %s; got %#v", tc.flag, tc.val, data["initial_fill"])
|
||||
if data["sheets"] != nil {
|
||||
t.Errorf("sheets should be absent for %s %s; got %#v", tc.flag, tc.val, data["sheets"])
|
||||
}
|
||||
if ss, _ := data["spreadsheet"].(map[string]interface{}); ss["spreadsheet_token"] != "shtNEW" {
|
||||
t.Errorf("spreadsheet_token = %v, want shtNEW", ss["spreadsheet_token"])
|
||||
|
||||
@@ -80,3 +80,28 @@ func flagsFor(command string) []common.Flag {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// flagAcceptsStdin reports whether the (command, flag) pair declares stdin as
|
||||
// an input source in flag-defs.json. Used to decide whether an "invalid JSON"
|
||||
// error should also steer the caller toward stdin. It runs on an error path,
|
||||
// so it returns false for an unknown command/flag rather than panicking the
|
||||
// way flagsFor does.
|
||||
func flagAcceptsStdin(command, name string) bool {
|
||||
defs, _ := loadFlagDefs()
|
||||
spec, ok := defs[command]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, df := range spec.Flags {
|
||||
if df.Name != name {
|
||||
continue
|
||||
}
|
||||
for _, in := range df.Input {
|
||||
if in == common.Stdin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
|
||||
{Name: "range", Kind: "own", Type: "string", Required: "required", Desc: "A1 range, e.g. `A1:F10` (no sheet prefix — use `--sheet-id` / `--sheet-name` to select the sheet)"},
|
||||
{Name: "include", Kind: "own", Type: "string_slice", Required: "optional", Desc: "Comma-separated info categories to include", Enum: []string{"value", "formula", "style", "comment", "data_validation"}},
|
||||
{Name: "max-chars", Kind: "own", Type: "int", Required: "optional", Desc: "Safety cap; default 200000", Default: "200000", Hidden: true},
|
||||
{Name: "max-chars", Kind: "own", Type: "int", Required: "optional", Desc: "Max output chars per call; default 500000 (safety cap). Large reads are usually better redirected to a file; only lower it (e.g. 25000) when you want results inline without triggering file offload, paging via has_more", Default: "500000"},
|
||||
{Name: "skip-hidden", Kind: "own", Type: "bool", Required: "optional", Desc: "Skip hidden rows and columns; default `false`"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
@@ -305,10 +305,9 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "sheet-id", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet reference_id (XOR with `--sheet-name`)"},
|
||||
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
|
||||
{Name: "range", Kind: "own", Type: "string", Required: "required", Desc: "A1 range, e.g. `A1:F30` (no sheet prefix — use `--sheet-id` / `--sheet-name` to select the sheet)"},
|
||||
{Name: "max-chars", Kind: "own", Type: "int", Required: "optional", Desc: "Safety cap; default 200000", Default: "200000", Hidden: true},
|
||||
{Name: "max-chars", Kind: "own", Type: "int", Required: "optional", Desc: "Max output chars per call; default 500000 (safety cap). Large reads are usually better redirected to a file; only lower it (e.g. 25000) when you want results inline without triggering file offload, paging via has_more", Default: "500000"},
|
||||
{Name: "include-row-prefix", Kind: "own", Type: "bool", Required: "optional", Desc: "Whether to prefix each row with `[row=N]`; default `true`", Default: "true"},
|
||||
{Name: "skip-hidden", Kind: "own", Type: "bool", Required: "optional", Desc: "Skip hidden rows and columns; default `false`"},
|
||||
{Name: "rows-json", Kind: "own", Type: "bool", Required: "optional", Desc: "Return structured rows ({row_number, values:{col→cell}}) instead of CSV text; default false", Default: "false"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional", Desc: "Print the request path and parameters without executing"},
|
||||
},
|
||||
},
|
||||
@@ -320,7 +319,7 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "sheet-id", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet reference_id (XOR with `--sheet-name`)"},
|
||||
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
|
||||
{Name: "start-cell", Kind: "own", Type: "string", Required: "required", Desc: "Top-left A1 anchor (e.g. `A1`, `B5`; no sheet prefix — use `--sheet-id` / `--sheet-name` to select the sheet); must be a single cell, range notation not accepted; the bottom-right is inferred from CSV row/column counts", Default: "A1"},
|
||||
{Name: "csv", Kind: "own", Type: "string", Required: "required", Desc: "RFC 4180 CSV text; plain values only (no formulas / styles / comments)", Input: []string{"file", "stdin"}},
|
||||
{Name: "csv", Kind: "own", Type: "string", Required: "required", Desc: "RFC 4180 CSV text; values or formulas (a leading = is evaluated as a formula); no styles / comments / images (use +cells-set for those).", Input: []string{"file", "stdin"}},
|
||||
{Name: "allow-overwrite", Kind: "own", Type: "bool", Required: "optional", Desc: "Allow overwriting (default true); set false to error if any target cell is non-empty", Default: "true"},
|
||||
{Name: "range", Kind: "own", Type: "string", Required: "optional", Desc: "alias for --start-cell (parity with +csv-get / +cells-set, which locate with --range); a range like A1:H17 collapses to its top-left cell", Hidden: true},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
@@ -766,7 +765,7 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
|
||||
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
|
||||
{Name: "title", Kind: "own", Type: "string", Required: "required", Desc: "New sheet title"},
|
||||
{Name: "index", Kind: "own", Type: "int", Required: "optional", Desc: "Insert position; appended to the end when omitted", Default: "-1"},
|
||||
{Name: "index", Kind: "own", Type: "int", Required: "optional", Desc: "Insert position (0-based); appended to the end when omitted", Default: "-1"},
|
||||
{Name: "row-count", Kind: "own", Type: "int", Required: "optional", Desc: "Initial row count (default 200, max 50000)", Default: "200"},
|
||||
{Name: "col-count", Kind: "own", Type: "int", Required: "optional", Desc: "Initial column count (default 20, max 200)", Default: "20"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
@@ -793,6 +792,16 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+sheet-hide-gridline": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
|
||||
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
|
||||
{Name: "sheet-id", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet reference_id (XOR with `--sheet-name`)"},
|
||||
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+sheet-info": {
|
||||
Risk: "read",
|
||||
Flags: []flagDef{
|
||||
@@ -839,6 +848,16 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+sheet-show-gridline": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
|
||||
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
|
||||
{Name: "sheet-id", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet reference_id (XOR with `--sheet-name`)"},
|
||||
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+sheet-unhide": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
@@ -895,13 +914,39 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+table-get": {
|
||||
Risk: "read",
|
||||
Flags: []flagDef{
|
||||
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
|
||||
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
|
||||
{Name: "sheet-id", Kind: "own", Type: "string", Required: "optional", Desc: "Read only this sheet (by id); omit to read all sheets"},
|
||||
{Name: "sheet-name", Kind: "own", Type: "string", Required: "optional", Desc: "Read only this sheet (by name); omit to read all sheets"},
|
||||
{Name: "range", Kind: "own", Type: "string", Required: "optional", Desc: "A1 range to read; omit to read each sheet's full used range (spans internal blank rows/columns, not just the A1 current region)"},
|
||||
{Name: "no-header", Kind: "own", Type: "bool", Required: "optional", Desc: "Treat the first row as data instead of a header (columns get positional names col1, col2, ...)"},
|
||||
{Name: "dataframe-out", Kind: "own", Type: "string", Required: "optional", Desc: "Write the typed table as one Arrow IPC file (Feather v2) instead of the default JSON. Pass `@<path>` for a file or `-` for binary stdout (same convention as other binary I/O flags). Mirror of the input-side `--dataframe` on `+table-put` / `+workbook-create` — pandas users round-trip via `df = pd.read_feather(\"x.arrow\")` or `pd.read_feather(io.BytesIO(stdout))`. Single-sheet only: requires `--sheet-id` or `--sheet-name`; whole-workbook reads keep the default JSON path. Column types come from the typed read-back (string/number/date/bool); per-column `number_format` is preserved as Arrow field metadata so the Arrow file can round-trip straight back through `+table-put --dataframe`."},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+table-put": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL to write into (XOR with `--spreadsheet-token`)"},
|
||||
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token to write into (XOR with `--url`)"},
|
||||
{Name: "sheets", Kind: "own", Type: "string", Required: "xor", Desc: "Typed table payload (pandas-DataFrame-shaped) as JSON, XOR with `--dataframe`: a top-level `sheets` array, each item `{name, start_cell?, mode?, header?, allow_overwrite?, columns:[\"colA\",\"colB\",...], data:[[...]], dtypes?:{colA:pandasDtype, ...}, formats?:{colA:numberFormat, ...}}`. Agents typically build it with `{**json.loads(df.to_json(orient=\"split\")), \"dtypes\": df.dtypes.astype(str).to_dict()}`. `dtypes` values are pandas dtype strings (`int64`, `float64`, `Int64`, `bool`, `boolean`, `datetime64[ns]`, `object`, ...); the writer maps them to internal string/number/date/bool — omit `dtypes` and a column writes as text (good for raw CSV-shaped data). `formats[col]` is an Excel number_format string (e.g. `#,##0.00`, `0.0%`, `yyyy-mm`); when absent, date columns default to `yyyy-mm-dd` and string columns to text format (`@`).", Input: []string{"file", "stdin"}},
|
||||
{Name: "dataframe", Kind: "own", Type: "string", Required: "xor", Desc: "Single-sheet typed table from one Arrow IPC file (a.k.a. Feather v2 — what `pandas.DataFrame.to_feather()` writes), XOR with `--sheets`. Pass `@<path>` for a file or `-` for binary stdin (same convention as other input flags). Arrow bytes are read raw — no TrimSpace / BOM strip — so the IPC magic survives intact (unlike text input flags). Column types come from the Arrow schema (int*/uint*/float* → number, date32/date64/timestamp → date, utf8/large_utf8 → string, bool → bool); per-column `number_format` may be set via Arrow field metadata (`pa.field(\"price\", pa.float64(), metadata={b\"number_format\": b\"$#,##0.00\"})`). Writes the sheet at default placement: name `Sheet1` (created when absent), overwrite from A1 with header. For a different sheet name, anchor, mode, or to write multiple sheets, use `--sheets` instead."},
|
||||
{Name: "styles", Kind: "own", Type: "string", Required: "optional", Desc: "Visual operations applied after the typed write, as JSON: top-level `{styles:[...]}`. Each item corresponds to one written sheet and must include `name`, plus at least one of `cell_styles` / `row_sizes` / `col_sizes` / `cell_merges`. `cell_styles` entries use +cells-set-style fields with a cell range; row/col sizes use dimension ranges plus type/size; merges use cell ranges plus optional merge_type. The styles array length/order/name must match the written sheets: with --sheets, match --sheets.sheets; with --dataframe (single sheet named Sheet1), pass exactly one styles item with name `Sheet1`.", Input: []string{"file", "stdin"}},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+workbook-create": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
{Name: "title", Kind: "own", Type: "string", Required: "required", Desc: "Spreadsheet title"},
|
||||
{Name: "folder-token", Kind: "own", Type: "string", Required: "optional", Desc: "Target folder token; placed at the drive root when omitted"},
|
||||
{Name: "headers", Kind: "own", Type: "string", Required: "optional", Desc: "Header row as a JSON array: `[\"Col A\",\"Col B\"]`", Input: []string{"file", "stdin"}},
|
||||
{Name: "values", Kind: "own", Type: "string", Required: "optional", Desc: "Initial data as a 2D JSON array: `[[\"alice\",95]]`", Input: []string{"file", "stdin"}},
|
||||
{Name: "values", Kind: "own", Type: "string", Required: "optional", Desc: "Untyped initial data as one 2D JSON array (`[[\"alice\",95]]`); values are written as-is with their type auto-detected, through the same batched set_cell_range path as --sheets — pair with --styles for number formats, colors, merges, and row/col sizes", Input: []string{"file", "stdin"}},
|
||||
{Name: "sheets", Kind: "own", Type: "string", Required: "optional", Desc: "Typed table payload as JSON (same shape as `+table-put`): a top-level `sheets` array, each item `{name, start_cell?, mode?, header?, allow_overwrite?, columns:[\"colA\",\"colB\",...], data:[[...]], dtypes?:{colA:pandasDtype, ...}, formats?:{colA:numberFormat, ...}}`. Agents typically build it from a DataFrame via `{**json.loads(df.to_json(orient=\"split\")), \"dtypes\": df.dtypes.astype(str).to_dict()}`. Mutually exclusive with --values and --dataframe. Creates the workbook, then writes typed type-faithful data (dates land as real dates, numbers keep precision).", Input: []string{"file", "stdin"}},
|
||||
{Name: "styles", Kind: "own", Type: "string", Required: "optional", Desc: "Initial visual operations as JSON: top-level `{styles:[...]}`. Each item corresponds to one target sheet and must include `name`, plus at least one of `cell_styles` / `row_sizes` / `col_sizes` / `cell_merges`. `cell_styles` entries use +cells-set-style fields with a cell range; row/col sizes use dimension ranges plus type/size; merges use cell ranges plus optional merge_type. With --sheets, styles array length/order/name must match --sheets.sheets. With --values, pass exactly one styles item for the initial sheet (its name is ignored).", Input: []string{"file", "stdin"}},
|
||||
{Name: "dataframe", Kind: "own", Type: "string", Required: "optional", Desc: "Single-sheet typed table from one Arrow IPC file (Feather v2 — what `pandas.DataFrame.to_feather()` writes), mutually exclusive with --values and --sheets. Pass `@<path>` for a file or `-` for binary stdin (same convention as other input flags). Arrow bytes are read raw — no TrimSpace / BOM strip — so the IPC magic survives intact (unlike text input flags). Column types come from the Arrow schema; per-column `number_format` may be set via Arrow field metadata. Creates the workbook and fills its default sheet (`Sheet1` — adopted in place, no empty Sheet1 left behind). For multi-sheet or non-default placement, use `--sheets` instead."},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
@@ -916,6 +961,14 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+workbook-import": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
{Name: "file", Kind: "own", Type: "string", Required: "required", Desc: "Local file path (.xlsx / .xls / .csv)"},
|
||||
{Name: "folder-token", Kind: "own", Type: "string", Required: "optional", Desc: "Target folder token; imported to the cloud drive root when omitted"},
|
||||
{Name: "name", Kind: "own", Type: "string", Required: "optional", Desc: "Imported spreadsheet name; defaults to the local file name without its extension"},
|
||||
},
|
||||
},
|
||||
"+workbook-info": {
|
||||
Risk: "read",
|
||||
Flags: []flagDef{
|
||||
|
||||
@@ -65,9 +65,9 @@ func TestFlagsFor_MapsAllFields(t *testing.T) {
|
||||
if url == nil || url.Required {
|
||||
t.Errorf("+sheet-create --url should not be cobra-required: %+v", url)
|
||||
}
|
||||
// hidden + int default
|
||||
// visible + int default
|
||||
cap := byName("+cells-get", "max-chars")
|
||||
if cap == nil || !cap.Hidden || cap.Default != "200000" {
|
||||
if cap == nil || cap.Hidden || cap.Default != "500000" {
|
||||
t.Errorf("+cells-get --max-chars not mapped: %+v", cap)
|
||||
}
|
||||
// input sources
|
||||
@@ -140,3 +140,24 @@ func TestFlagsFor_EveryRegisteredCommandHasDefs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlagAcceptsStdin verifies the stdin-capability probe that decides whether
|
||||
// an "invalid JSON" error should also steer the caller toward stdin: a composite
|
||||
// flag (cells) accepts stdin, a plain locator (spreadsheet-token) does not, and
|
||||
// an unknown command/flag returns false without panicking (it runs on an error
|
||||
// path, unlike flagsFor).
|
||||
func TestFlagAcceptsStdin(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !flagAcceptsStdin("+cells-set", "cells") {
|
||||
t.Error("+cells-set --cells should accept stdin")
|
||||
}
|
||||
if flagAcceptsStdin("+cells-set", "spreadsheet-token") {
|
||||
t.Error("--spreadsheet-token should not accept stdin")
|
||||
}
|
||||
if flagAcceptsStdin("+nope", "cells") {
|
||||
t.Error("unknown command should be false (and must not panic)")
|
||||
}
|
||||
if flagAcceptsStdin("+cells-set", "nope") {
|
||||
t.Error("unknown flag should be false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ func validateParsedJSONFlag(fv flagView, name string, value interface{}) error {
|
||||
var parseJSONFlagSkip = map[string]struct{}{
|
||||
"properties": {},
|
||||
"operations": {},
|
||||
"styles": {},
|
||||
}
|
||||
|
||||
// validateValueAgainstSchema is the (command, flag) → schema → check
|
||||
@@ -93,7 +94,17 @@ func validateValueAgainstSchema(fv flagView, name string, value interface{}) err
|
||||
var schema schemaProperty
|
||||
json.Unmarshal(raw, &schema)
|
||||
if vErr := validateAgainstSchema(value, &schema, ""); vErr != nil {
|
||||
return sheetsValidationForFlag(name, "--%s: %s", name, vErr.Error())
|
||||
// Composite-JSON shape errors (e.g. +cells-set --cells, chart
|
||||
// --properties) are the highest-frequency usage-layer failure for
|
||||
// sheets, and agents often burn several retries guessing the shape.
|
||||
// Point them straight at --print-schema, which dumps the exact JSON
|
||||
// Schema for this (command, flag) pair. The hint is always actionable:
|
||||
// reaching this branch means entry[name] resolved a schema from the
|
||||
// embedded index, and --print-schema reads that same index, so the
|
||||
// suggested command is guaranteed to print it.
|
||||
return sheetsValidationForFlag(name,
|
||||
"--%s: %s; run `lark-cli sheets %s --print-schema --flag-name %s` to see the expected JSON Schema",
|
||||
name, vErr.Error(), command, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -587,3 +587,27 @@ func TestValidateInputAgainstSchema_SkipOperations(t *testing.T) {
|
||||
t.Errorf("operations should be skipped; got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateValueAgainstSchema_PrintSchemaHint pins the highest-value
|
||||
// recovery affordance for composite-JSON flags: when the shape is wrong, the
|
||||
// error must point the agent straight at --print-schema (with the right
|
||||
// command + flag) instead of leaving it to guess across retries. +cells-set
|
||||
// --cells expects a 2-D array; a bare string trips the top-level type check.
|
||||
func TestValidateValueAgainstSchema_PrintSchemaHint(t *testing.T) {
|
||||
t.Parallel()
|
||||
fv := mapFlagView{command: "+cells-set"}
|
||||
err := validateValueAgainstSchema(fv, "cells", "not-an-array")
|
||||
if err == nil {
|
||||
t.Fatal("expected schema validation error for wrong --cells shape")
|
||||
}
|
||||
msg := err.Error()
|
||||
// Underlying shape error is preserved (substring callers still match).
|
||||
if !strings.Contains(msg, `expected type "array"`) {
|
||||
t.Errorf("want underlying shape error preserved; got %q", msg)
|
||||
}
|
||||
// And the actionable --print-schema hint is appended with the exact
|
||||
// command + flag, so a copy-paste fetches the schema for this pair.
|
||||
if !strings.Contains(msg, "lark-cli sheets +cells-set --print-schema --flag-name cells") {
|
||||
t.Errorf("want --print-schema hint with command+flag; got %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,6 @@ var commandsWithSchema = map[string]struct{}{
|
||||
"+range-sort": {},
|
||||
"+sparkline-create": {},
|
||||
"+sparkline-update": {},
|
||||
"+table-put": {},
|
||||
"+workbook-create": {},
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ package sheets
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
neturl "net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
@@ -48,46 +50,151 @@ func sheetsInputStatError(flag string, err error) error {
|
||||
return wrapped
|
||||
}
|
||||
|
||||
// resolveSpreadsheetToken applies the public --url / --spreadsheet-token XOR
|
||||
// pair shared by every sheets canonical shortcut and returns the resolved
|
||||
// token. Network-free, safe to call from Validate and DryRun.
|
||||
func resolveSpreadsheetToken(runtime *common.RuntimeContext) (string, error) {
|
||||
// spreadsheetRef classification: a --url / --spreadsheet-token input names a
|
||||
// spreadsheet either directly (a /sheets/ URL or raw token) or indirectly via a
|
||||
// wiki node that must be resolved to its backing spreadsheet at Execute time.
|
||||
const (
|
||||
spreadsheetRefSheet = "sheet"
|
||||
spreadsheetRefWiki = "wiki"
|
||||
)
|
||||
|
||||
// spreadsheetRef is a parsed --url / --spreadsheet-token input. A wiki ref holds
|
||||
// the still-unresolved wiki node_token; resolveSpreadsheetTokenExec turns it
|
||||
// into the real spreadsheet token at Execute time.
|
||||
type spreadsheetRef struct {
|
||||
Kind string // spreadsheetRefSheet | spreadsheetRefWiki
|
||||
Token string
|
||||
}
|
||||
|
||||
// parseSpreadsheetRef applies the public --url / --spreadsheet-token XOR pair and
|
||||
// classifies the input. Network-free, safe to call from Validate and DryRun.
|
||||
//
|
||||
// Recognized --url shapes:
|
||||
// - https://.../sheets/<token> → {sheet, token}
|
||||
// - https://.../spreadsheets/<token> → {sheet, token}
|
||||
// - https://.../wiki/<node_token> → {wiki, node_token} (resolved at Execute)
|
||||
//
|
||||
// A raw --spreadsheet-token is always treated as a spreadsheet token; wiki nodes
|
||||
// only ever arrive as a /wiki/ URL.
|
||||
func parseSpreadsheetRef(runtime *common.RuntimeContext) (spreadsheetRef, error) {
|
||||
if err := common.ExactlyOneTyped(runtime, "url", "spreadsheet-token"); err != nil {
|
||||
return "", err
|
||||
return spreadsheetRef{}, err
|
||||
}
|
||||
if token := strings.TrimSpace(runtime.Str("spreadsheet-token")); token != "" {
|
||||
if err := validate.RejectControlChars(token, "spreadsheet-token"); err != nil {
|
||||
return "", sheetsValidationCauseForFlag("spreadsheet-token", err)
|
||||
return spreadsheetRef{}, sheetsValidationCauseForFlag("spreadsheet-token", err)
|
||||
}
|
||||
return token, nil
|
||||
return spreadsheetRef{Kind: spreadsheetRefSheet, Token: token}, nil
|
||||
}
|
||||
|
||||
url := strings.TrimSpace(runtime.Str("url"))
|
||||
token := extractSpreadsheetToken(url)
|
||||
if token == "" || token == url {
|
||||
return "", sheetsValidationForFlag("url", "--url must be a spreadsheet URL like https://.../sheets/<token>")
|
||||
rawURL := strings.TrimSpace(runtime.Str("url"))
|
||||
token, kind, ok := spreadsheetURLToken(rawURL)
|
||||
if !ok {
|
||||
return spreadsheetRef{}, sheetsValidationForFlag("url", "--url must be a spreadsheet URL like https://.../sheets/<token> or a wiki URL like https://.../wiki/<token>")
|
||||
}
|
||||
if err := validate.RejectControlChars(token, "url"); err != nil {
|
||||
return "", sheetsValidationCauseForFlag("url", err)
|
||||
return spreadsheetRef{}, sheetsValidationCauseForFlag("url", err)
|
||||
}
|
||||
return token, nil
|
||||
return spreadsheetRef{Kind: kind, Token: token}, nil
|
||||
}
|
||||
|
||||
// extractSpreadsheetToken pulls the token segment out of a /sheets/<token>
|
||||
// or /spreadsheets/<token> URL. Returns the input unchanged when no known
|
||||
// prefix is present (callers must check token != originalInput).
|
||||
func extractSpreadsheetToken(input string) string {
|
||||
input = strings.TrimSpace(input)
|
||||
for _, prefix := range []string{"/sheets/", "/spreadsheets/"} {
|
||||
if idx := strings.Index(input, prefix); idx >= 0 {
|
||||
token := input[idx+len(prefix):]
|
||||
if idx2 := strings.IndexAny(token, "/?#"); idx2 >= 0 {
|
||||
token = token[:idx2]
|
||||
}
|
||||
return token
|
||||
// spreadsheetURLToken extracts the token and its kind from a Lark URL, matching
|
||||
// only on the URL *path* segment (parsed via net/url). A /wiki/ or /sheets/ that
|
||||
// appears only in the query or fragment (e.g. a redirect or anchor param) never
|
||||
// hijacks classification. Returns ok=false when no known prefix heads the path.
|
||||
func spreadsheetURLToken(rawURL string) (token, kind string, ok bool) {
|
||||
u, err := neturl.Parse(rawURL)
|
||||
if err != nil || u.Path == "" {
|
||||
return "", "", false
|
||||
}
|
||||
for _, m := range []struct {
|
||||
prefix string
|
||||
kind string
|
||||
}{
|
||||
{"/sheets/", spreadsheetRefSheet},
|
||||
{"/spreadsheets/", spreadsheetRefSheet},
|
||||
{"/wiki/", spreadsheetRefWiki},
|
||||
} {
|
||||
if seg, found := pathSegmentAfter(u.Path, m.prefix); found {
|
||||
return seg, m.kind, true
|
||||
}
|
||||
}
|
||||
return input
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// pathSegmentAfter returns the first path segment after prefix when path begins
|
||||
// with prefix, else ("", false).
|
||||
func pathSegmentAfter(path, prefix string) (string, bool) {
|
||||
if !strings.HasPrefix(path, prefix) {
|
||||
return "", false
|
||||
}
|
||||
rest := path[len(prefix):]
|
||||
if i := strings.IndexByte(rest, '/'); i >= 0 {
|
||||
rest = rest[:i]
|
||||
}
|
||||
rest = strings.TrimSpace(rest)
|
||||
if rest == "" {
|
||||
return "", false
|
||||
}
|
||||
return rest, true
|
||||
}
|
||||
|
||||
// resolveSpreadsheetToken applies the public --url / --spreadsheet-token XOR pair
|
||||
// and returns the resolved token. Network-free, safe to call from Validate and
|
||||
// DryRun.
|
||||
//
|
||||
// A /wiki/ URL yields the still-unresolved wiki node_token: turning it into the
|
||||
// backing spreadsheet token needs a get_node call, which only Execute may make.
|
||||
// Validate/DryRun only need a non-empty, control-char-clean token, so the
|
||||
// node_token passes through unchanged here; Execute paths call
|
||||
// resolveSpreadsheetTokenExec instead.
|
||||
func resolveSpreadsheetToken(runtime *common.RuntimeContext) (string, error) {
|
||||
ref, err := parseSpreadsheetRef(runtime)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ref.Token, nil
|
||||
}
|
||||
|
||||
// resolveSpreadsheetTokenExec is the Execute-time counterpart of
|
||||
// resolveSpreadsheetToken: it additionally resolves a /wiki/ URL's node_token to
|
||||
// the backing spreadsheet token via wiki get_node, verifying obj_type=sheet.
|
||||
// Non-wiki inputs make no API call. Use this from every sheets Execute hook and
|
||||
// keep resolveSpreadsheetToken in Validate/DryRun so those stay network-free.
|
||||
func resolveSpreadsheetTokenExec(runtime *common.RuntimeContext) (string, error) {
|
||||
ref, err := parseSpreadsheetRef(runtime)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ref.Kind != spreadsheetRefWiki {
|
||||
return ref.Token, nil
|
||||
}
|
||||
return resolveWikiNodeToSpreadsheetToken(runtime, ref.Token)
|
||||
}
|
||||
|
||||
// resolveWikiNodeToSpreadsheetToken resolves a wiki node_token to the spreadsheet
|
||||
// obj_token it points at, erroring when the node is not a spreadsheet. The
|
||||
// wiki:node:read scope is only needed on this path, so it is enforced here rather
|
||||
// than declared unconditionally on every sheets shortcut.
|
||||
func resolveWikiNodeToSpreadsheetToken(runtime *common.RuntimeContext, nodeToken string) (string, error) {
|
||||
if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := runtime.CallAPITyped("GET", "/open-apis/wiki/v2/spaces/get_node",
|
||||
map[string]interface{}{"token": nodeToken}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
node := common.GetMap(data, "node")
|
||||
objType := common.GetString(node, "obj_type")
|
||||
objToken := common.GetString(node, "obj_token")
|
||||
if objType == "" || objToken == "" {
|
||||
return "", errs.NewInternalError(errs.SubtypeInvalidResponse, "wiki get_node returned incomplete node data for %q", nodeToken)
|
||||
}
|
||||
if objType != "sheet" {
|
||||
return "", sheetsValidationForFlag("url", "wiki URL resolves to obj_type=%q, but a spreadsheet (obj_type=sheet) is required", objType)
|
||||
}
|
||||
return objToken, nil
|
||||
}
|
||||
|
||||
// resolveSheetSelector validates the --sheet-id / --sheet-name XOR and
|
||||
@@ -241,6 +348,16 @@ func parseJSONFlag(runtime flagView, name string) (interface{}, error) {
|
||||
}
|
||||
var out interface{}
|
||||
if err := json.Unmarshal([]byte(raw), &out); err != nil {
|
||||
// Composite payloads that embed formulas / quotes / commas are the
|
||||
// classic source of this error: inlined into the shell, the JSON gets
|
||||
// mangled (e.g. `\$` → "invalid character in string escape"). For any
|
||||
// flag that accepts stdin, steer the caller there — passing the payload
|
||||
// via `--<flag> - < file` sidesteps shell escaping entirely.
|
||||
if flagAcceptsStdin(runtime.Command(), name) {
|
||||
return nil, sheetsValidationForFlag(name,
|
||||
"--%s: invalid JSON: %v; if the payload contains formulas / quotes / commas, pass it via stdin (`--%s - < file`) so the shell doesn't mangle the JSON",
|
||||
name, err, name).WithCause(err)
|
||||
}
|
||||
return nil, sheetsValidationForFlag(name, "--%s: invalid JSON: %v", name, err).WithCause(err)
|
||||
}
|
||||
// Schema-driven flag validation at the user-input boundary. Skips
|
||||
@@ -325,6 +442,72 @@ func buildCellStyleFromFlags(runtime flagView) map[string]interface{} {
|
||||
return style
|
||||
}
|
||||
|
||||
// cellStyleAliases maps shorthand cell_styles field names that models commonly
|
||||
// hallucinate (Excel / openpyxl / CSS conventions) onto the canonical field
|
||||
// names the backend expects. Only the unambiguous alignment shorthands are
|
||||
// aliased — they are the high-frequency miss; ambiguous guesses (e.g. "color",
|
||||
// "bg_color", "text_align") are intentionally left out so a wrong guess still
|
||||
// surfaces as an error rather than being silently reinterpreted.
|
||||
var cellStyleAliases = []struct{ alias, canonical string }{
|
||||
{"horizontal_align", "horizontal_alignment"},
|
||||
{"halign", "horizontal_alignment"},
|
||||
{"vertical_align", "vertical_alignment"},
|
||||
{"valign", "vertical_alignment"},
|
||||
}
|
||||
|
||||
// normalizeCellStyleAliases renames known shorthand keys in a single
|
||||
// cell_styles map to their canonical equivalents, in place, so a model that
|
||||
// writes e.g. "horizontal_align" instead of "horizontal_alignment" still
|
||||
// applies the style instead of hitting an "unsupported field" error (--styles)
|
||||
// or having the field silently dropped by the backend (typed --cells). If both
|
||||
// the shorthand and its canonical key are present it returns a validation error
|
||||
// rather than picking one. path labels the map for the error message.
|
||||
func normalizeCellStyleAliases(style map[string]interface{}, path string) error {
|
||||
if len(style) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, a := range cellStyleAliases {
|
||||
v, ok := style[a.alias]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, exists := style[a.canonical]; exists {
|
||||
return common.ValidationErrorf("%s.%s conflicts with %s; pass only %s", path, a.alias, a.canonical, a.canonical)
|
||||
}
|
||||
style[a.canonical] = v
|
||||
delete(style, a.alias)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeTypedCellsStyleAliases walks a typed --cells 2D array and applies
|
||||
// normalizeCellStyleAliases to every cell's inline cell_styles object, so the
|
||||
// alignment shorthands are accepted on +cells-set the same as on --styles.
|
||||
// Structure is checked leniently to match the pass-through contract: any
|
||||
// element that isn't the expected shape is skipped, not rejected.
|
||||
func normalizeTypedCellsStyleAliases(cells []interface{}, path string) error {
|
||||
for r, rowRaw := range cells {
|
||||
row, ok := rowRaw.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for c, cellRaw := range row {
|
||||
cell, ok := cellRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
st, ok := cell["cell_styles"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := normalizeCellStyleAliases(st, fmt.Sprintf("%s[%d][%d].cell_styles", path, r, c)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// borderStylesFromFlag parses --border-styles as a JSON object (top/bottom/
|
||||
// left/right with style sub-objects). Returns nil when the flag is empty.
|
||||
func borderStylesFromFlag(runtime flagView) (map[string]interface{}, error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user