Commit Graph

1150 Commits

Author SHA1 Message Date
yanghao.1600 aea16f1ece docs(drive): harden ingest target-space resolution, incremental key, import/image steps
10 场景 BOE 自测(全 PASS)后补四处文档缺口:

- 目标库解析健壮性(S8):按名称解析(space-list/drive+search)对
  新建/私有/未索引空间可能查不到,且 space-list --page-all 结果
  不稳定/未抽干分页。新增 Target Space Resolution 小节 + fail-safe:
  搜到 0 个不得直接判"库不存在"、搜到 1 个不臆断唯一,以 wiki
  spaces get 按 id 核验为准;同步情况2/7、Command Map、Rule 2。

- 增量比对键(S4):原文档"按 source_id(SHA-256)比对"会把被修改
  的文件误判成"删+增"(内容变则 SHA 变)。改为以 source_location
  (路径)为身份键、SHA 作变更判据,用上次 ledger 的 file→子页映射
  定位既有页 update;并写明台账缺失时的降级风险。

- import_docx 套治理表(S1/S3):--block-id 0 哨兵被服务端拒,须先
  取 title 真实 block_id;.txt 等无正文标题块的导入件改用整页
  overwrite 补 h1+治理表。

- 图片嵌入(S9):<img path="@..."> 语法当前 CLI 不接受(报5002/5004),
  改用独立命令 docs +media-insert --type image --file。

均为 workflow 专属文件,未动共享参考。门禁 kb_gate/publish_gate
/红线测试全绿。
2026-09-07 15:44:27 +08:00
yanghao.1600 a857547572 docs(drive): clarify ingest import governance-table step and no-library stop
C 层四场景 + PDF 补测(BOE 实测全 PASS)后补两处文档缺口:

- import_docx "迁入后套 6 行治理表" 缺具体做法:补明用
  docs +fetch --detail with-ids 取导入 docx 首块(title)的
  block_id,再 block_insert_after 把治理表插在标题之后、正文
  之前;不要 overwrite(丢导入正文)、不要插在最前(顶掉标题致
  节点标题 Untitled)。

- 情况2"目标库不存在"补分辨解析失败类型:飞书返回 not_found
  (131005)才是库不存在→请用户建库;invalid_parameters(131002,
  如传 URL、id 非法)是输入错误→按 Rule 1 请用户重给合法
  space_id/链接,不误导去建库。同步 Situation Routing 情况2
  与 Transition Rule 2。
2026-09-07 12:22:57 +08:00
yanghao.1600 47d5fd8801 docs(drive): fix overwrite title loss, empty-space root, ingest carrier page
C 层 BOE 自测发现三处待修:

A: docs +update overwrite 会清空 docx 的 <title>,导致 wiki 节点
   侧边栏标题退化为 Untitled(已在 BOE 实证并验证修复)。要求
   overwrite 写入内容开头携带标题元素(markdown # 标题 / XML
   <title>),文字沿用节点原标题;只用 <h1> 不设置节点标题。
   落到 wf1 Write Mode Selection + WRITE 状态,及 wf2 整页
   overwrite 路径。

B: Root Node Resolution 未覆盖空库(顶层 0 节点)定根。补:空库
   经用户确认在空间顶层新建 docx 根节点承载通用规范,再进入
   OUTLINE_PROPOSE;同步 PARSE_TARGET 状态、Command Map、
   Agent 约束与 Transition Rule。

C: wf2 未明确资料并入分类节点的方式。明确知识页一律落在目标
   分类节点的承载子页,绝不写入分类节点本体(其正文归维护规范);
   add+PDF/图片 由 docs_update 改为 node_create_docx。同步
   Write Via Selection、CONVERT_WRITE、publish 与 outputs。

门禁脚本未改;kb_gate/publish_gate/inventory 及红线测试全绿。
2026-09-07 11:13:59 +08:00
yanghao.1600 416682703d docs(drive): document scan-completeness signal in knowledge_ingest
自测发现的文档-代码缺口:inventory.py 已用 scan_complete=false / unreadable_dirs
标记不可读子目录(第三轮 P2-5),但 analyze.md 的 INVENTORY 段未写这条,读文档的
执行者只能靠推断。补明规则:盘点不完整须如实报告未覆盖目录、不当完整成功、不基于
残缺盘点入库。
2026-09-04 16:44:52 +08:00
yanghao.1600 8e449af98d docs(drive): fix missing-Wiki handoff, governance gaps, attachment path, identity
处理 PR #2566 第四轮评审的 4 条意见。

文档:
- knowledge_ingest 无库处理(P1-A):目标库不存在时停下请用户先自建知识库或
  提供已有库链接,不再指路 knowledge_base_bootstrap(后者同样只接受已有库,
  两者都不建知识空间,原路径无法到达可入库的 Wiki);同步 lark-drive /
  lark-wiki SKILL.md 路由与 Situation Routing / Transition Rules
- 纯附件路径(P2-C):区分「知识页伴随附件」(VERIFY 后仅对 verified 页面
  上传)与「纯附件」(用户只要原件、无知识页,在 CONVERT_WRITE 独立上传);
  修复上一轮把附件一律绑定 verified 页面导致纯附件永远执行不了的回归
- 命令模板身份占位(P2-D):drive +import / wiki +node-create / docs +update
  等模板的 --as user 改为 --as <runtime identity>,与 PARSE 选定身份一致,
  避免 bot 路径下 discovery 与写入身份不一致操作到不同 Drive 资源

门禁(P1-B):
- publish_gate.py 与 kb_gate.py:治理表某行整个缺失(key 省略)时视同「待确认」,
  标「已完成」会被收紧为「进行中」,防止残缺页冒充完成进生产;补两门禁回归单测

单测:inventory 23、publish_gate 44、kb_gate 25 全过
2026-09-04 16:44:52 +08:00
yanghao.1600 11ac0e8f5b docs(drive): drop unused VALID_PUBLISH_ROLES constant
全面自查发现的死常量(初版即存在,role 校验由 evaluate_item 的分支逻辑覆盖),
清理以保持门禁脚本无未用定义。publish_gate 42 单测全过。
2026-09-04 16:44:52 +08:00
yanghao.1600 ac6cae187f docs(drive): close gate routing and scan-completeness gaps from review
处理 PR #2566 第三轮评审中中肯的 6 条意见。

publish_gate.py(knowledge_ingest):
- proposed_action 完整路由:update/merge 只允许 docs_update(此前仅拦
  import_docx,node_create_docx 仍能建重复页);非发布动作(skip/reference/
  review)不得作为知识页写入(此前会 ready=true 进入 CONVERT_WRITE)

kb_gate.py(knowledge_base_bootstrap):
- page_status 完全缺失时硬拦(与 publish_gate 对齐;此前空值放行)

inventory.py(knowledge_ingest):
- os.walk 加 onerror 捕获不可读目录,产出 scan_complete=false、unreadable_dirs
  清单,主输出 ok=false 且退出码非零,避免把漏扫报成完整成功

文档:
- knowledge-base-bootstrap:修正 overwrite 新鲜读取的自相矛盾——已确认的
  has_draft 覆盖按“确认时基线一致”放行(不再要求读回 empty_placeholder 而
  卡死已确认草稿);节点树 partial(含 --page-all 默认页数上限)时 fail
  closed,不进入 OUTLINE_PROPOSE/TYPE_TRIAGE/WRITE
- knowledge-ingest:source_attachment 上传移至 VERIFY,仅对 verified 知识页
  上传原件,避免页面验证失败留孤儿附件

单测:inventory 23、publish_gate 42、kb_gate 24 全过
2026-09-04 16:44:52 +08:00
yanghao.1600 6dd22a0de3 docs(drive): enforce action routing and safe overwrite from review
处理 PR #2566 第二轮 CodeRabbit/GPT 评审中中肯的意见。

publish_gate.py(knowledge_ingest):
- 发布计划带 proposed_action 闭集,import_docx 只允许 add;update/merge
  用 import_docx 会硬拦(此前仅文档约束,门禁未强制,可能新增重复子页)

kb_gate.py(knowledge_base_bootstrap):
- new_docx 必须带确认的建节点位置(parent_node_token 或 space_id),
  否则硬拦,避免 user 身份下静默回退到个人库 my_library(与 publish_gate
  的目的地锚定保持一致)

文档:
- knowledge-base-bootstrap:overwrite 的新鲜读取时机改为“用户确认后、每次
  落笔前”,重读 draft_state 并携带 revision,避免确认等待期间协作者补入
  草稿被静默覆盖;WRITE 状态加 docs +fetch
- knowledge-ingest:update/merge 落笔前同样重读并带 revision;CONVERT_WRITE
  补 wiki +move 异步续跑(drive +task_result --scenario wiki_move)与 ready-
  state 验证;outputs schema 补 proposed_action 与 target_obj_token(图片类
  docs +update 需 docx obj_token 或 Wiki URL,裸 node token 不触发资源解析)

单测:inventory 21、publish_gate 41、kb_gate 23 全过

说明:脚本 <SKILL_ROOT> 可执行路径问题为整个 skill 体系既有约定(kb_gate、
enterprise-kb-ops 同模式),不在本 workflow 单独处理;完整 6 行治理表校验
维持既有设计(仅关键字段必填,其余允许待确认并收紧)。
2026-09-04 16:44:52 +08:00
yanghao.1600 e519064cb6 docs(drive): address CodeRabbit review on output-dir and readback
处理 PR #2566 CodeRabbit 最新一轮 OPEN 评论。

inventory.py:
- output-dir 等于 scan root 时直接拒绝(此前只处理嵌套 output-dir,相等时
  台账仍会被自身摄取);补 main() 级单测覆盖相等/外部/嵌套三种情况

文档执行纪律强化:
- knowledge-base-bootstrap:overwrite 的 draft_state 必须来自写前 fresh read,
  不得复用早期缓存或过期计划,避免误覆盖此后新增的草稿;OUTLINE_PROPOSE
  新建后用 --page-all 分页回读
- knowledge-ingest:NODE_PROPOSE 新建后 --page-all 分页回读;TARGET_ALIGN
  节点树读取不全(partial)时 fail closed,不基于残缺清单做映射或新建节点

单测:inventory 21、publish_gate 36、kb_gate 21 全过
2026-09-04 16:44:52 +08:00
yanghao.1600 869edb07a9 docs(drive): harden knowledge_ingest and kb gates from review
处理对 knowledge_ingest 与 knowledge_base_bootstrap 的评审意见,修复门禁
放行与文档自相矛盾的问题。

publish_gate.py(knowledge_ingest):
- sensitivity / conflict_status / parse_status 改为闭集 fail-closed:缺失或
  非法值一律硬拦,不再当作安全状态放行
- node_create_docx 必须带确认的建节点位置(parent_token 或 space_id),
  避免 user 身份下静默回退到个人库 my_library
- source_attachment 必须锚定目标 Wiki 节点(target_token),避免上传到
  Drive 根目录
- page_status 完全缺失时硬拦(此前空值被跳过)

inventory.py(knowledge_ingest):
- hash 前用 is_file() 过滤非普通文件,FIFO/设备不再阻塞 hash_file
- 排除嵌套在扫描根内的 output-dir,避免二次运行摄取自身台账
- 台账新增 skipped_nonregular 统计

kb_gate.py(knowledge_base_bootstrap):
- overwrite 写法 fail-closed:draft_state 缺失或未知时拒绝覆盖,避免误清空
  已有草稿

文档:
- knowledge-ingest entry:Write Via Selection 区分 add 与 update/merge
  (update/merge 走 docs_update,不用 import_docx);CONVERT_WRITE Command
  Map 补 wiki +move 与 drive +task_result(import 迁入与异步续跑)
- knowledge-ingest publish/analyze:补更新既有页流程、import 迁入的
  ready-state 验证;TARGET_ALIGN 只对 origin docx 节点 docs +fetch
- knowledge-ingest outputs:补 parent_token/space_id 字段与 fail-closed 说明
- knowledge-base-bootstrap:PARSE_TARGET 补个人库 my_library 解析分支
  (wiki spaces get 而非 space-list);门禁硬拦补 draft_state 未知项

单测:inventory 18、publish_gate 36、kb_gate 21 全过
2026-09-04 16:44:52 +08:00
yanghao.1600 b57f856725 docs(drive): add knowledge_ingest workflow
新增 knowledge_ingest workflow:把授权的本地文件盘点、去重、敏感初筛后,
据知识库维护规范映射归位,转成飞书 docx 知识页写入已有 Wiki 并写后验证。
对应企业知识库运维的资料摄取到内容发布阶段,与 knowledge_base_bootstrap
数据松耦合、只读其规范、不互相调用。

- 定级 R2-R3 / S3,状态机 PARSE_SOURCES→INVENTORY→TARGET_ALIGN→
  (NODE_PROPOSE)→ANALYZE_TRIAGE→PUBLISH_PLAN→CONVERT_WRITE→VERIFY→DONE
- entry + analyze/publish phase + outputs 四文档,按状态渐进加载
- 九情况 Situation Routing:节点不足内置 NODE_PROPOSE(据真实资料提议、
  确认后新建);无库指路 knowledge_base_bootstrap,不自建知识空间;
  无规范降级映射不强制路由
- 核心铁律:知识页必须落 obj_type=docx 可检索正文,drive +upload 只作
  来源附件、永不算页面完成
- scripts/inventory.py 盘点资料(SHA-256 去重、敏感初筛、可解析性判断、
  符号链接安全),scripts/publish_gate.py 发布门禁(载体非 docx、上传冒充
  知识页、敏感/未裁决冲突进生产等硬拦,待确认/部分解析却标已完成则收紧)
- 单测:inventory_test.py 16 项、publish_gate_test.py 28 项全过
- 在 lark-drive-workflow.md Registry 注册,lark-drive/SKILL.md 与
  lark-wiki/SKILL.md 加短路由
2026-09-04 16:44:52 +08:00
yanghao.1600 95164bcba0 docs(drive): address knowledge_base_bootstrap PR review comments
Harden kb_gate.py:
- normalize non-dict governance to {} so the skip branch no longer crashes
- block empty node_token for real writes
- block new_docx targeting a node that is already docx
- add docstrings and regression tests (14 -> 19 cases)

Enforce full node-tree reads:
- READ_STRUCTURE and PARSE_TARGET require wiki +node-list --page-all with
  recursion into has_child nodes; capped/failed reads mark partial

Resolve one writable root:
- add Root Node Resolution: general spec goes to a single root_node;
  stop for user selection when a space has multiple top-level nodes;
  non-docx root goes through new_docx or is skipped, never docs +update

Complete new_docx confirmation:
- OUTLINE_PROPOSE and WRITE_CONFIRM now show exact --title, destination
  (--parent-node-token/--space-id), --obj-type docx, and the
  node-create -> obj_token -> docs +update sequence
2026-09-04 16:44:52 +08:00
yanghao.1600 ccd3311bbf docs(drive): add write gate and action-scoped permissions to knowledge_base_bootstrap
Strengthen the knowledge_base_bootstrap workflow with two enhancements
borrowed from the enterprise-kb-ops practice.

Action-scoped permissions:
- track read / edit_existing_docx / create_node separately in runtime state
- one blocked action no longer stalls the rest; only the denied action stops
- never auto-request permission, but always report which action and nodes
- read success never implies write permission

Deterministic write gate:
- standardize each node's maintenance spec as a 6-row governance table
- add scripts/kb_gate.py (+ tests) to gate the write plan before WRITE
- hard-block non-docx carrier, missing table, empty required fields,
  invalid status, unconfirmed overwrite of a draft
- narrow (not block) unresolved fields marked done back to in-progress,
  keeping the "frame first, fill later" flow while barring incomplete
  pages from claiming completion
- an agent claim can only narrow the outcome, never bypass the gate
2026-09-04 16:44:52 +08:00
yanghao.1600 5c497fff89 docs(drive): address knowledge_base_bootstrap review comments
- outputs: load in OUTLINE_PROPOSE as well (align progressive loading)
- entry: narrow pre-write gate to docs +update; allow node-create only
  after separate outline confirmation
- entry: resolve root nodes for bare space targets via wiki +node-list
  (space root = top-level nodes, empty parent)
- entry: triage every non-docx origin node (incl. file); exhaustive
  classification so no node slips through
- outputs/entry: WRITE_CONFIRM shows node_token, command family and exact
  content/diff before the R2 write
- outputs: tag plain-text fences as text (markdownlint MD040)
2026-09-04 16:44:52 +08:00
yanghao.1600 df9a67d072 docs(drive): add knowledge_base_bootstrap workflow
Register a new lark-drive workflow that authors maintenance standards
into an existing Wiki knowledge base: read node tree and drafts,
optionally propose and create an outline when the structure is too
sparse, then write general standards to the root node and per-node
requirements to each sub-node after confirmation.

- entry + outputs reference docs (R2/S2), no phase files
- node type triage: docx writable, non-docx/shortcut skipped or new_docx
- default append (keep drafts), overwrite only placeholders
- OUTLINE_PROPOSE state supports root-only knowledge bases
- register in workflow registry; route from lark-drive and lark-wiki
2026-09-04 16:44:52 +08:00
calendar-assistant 6956ac2eab feat(calendar): remove app_link from event outputs, emphasize share link (#2618)
Drop the app_link field from +get and +search-event outputs so calendar
commands no longer surface applink URLs. Emphasize in the lark-calendar
SKILL that sharing an event to a person, chat, or document requires the
event share link (via events share_info), not an applink.
2026-09-04 13:48:32 +08:00
bubbmon233 0cf8ae81c1 feat: add mail rule shortcuts (#2327)
* feat: add mail rule shortcuts

* fix: suggest mail rule alias corrections

* Fix rule update name flag

* fix: harden mail rule update handling

Change-Type: ci-fix

* fix: preserve mail rule update raw condition fields

Change-Type: ci-fix

* test: cover mail rule shortcut branches

Add regression coverage for mail rule JSON inputs, update preservation, toggles, reorder, and parser errors.

Change-Type: ci-fix

* fix: address mail rule review feedback

* fix: align mail rule delete confirmation

* fix: harden mail rule shortcut writes

Change-Type: ci-fix

* fix: align mail rule confirmation risk

* test: remove stale rule create yes flags

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix: harden mail rule shortcut review handling

Address review feedback for rule action parameter whitelisting, raw update body construction, bot mailbox validation, delete confirmation summaries, and parser limits.

Change-Type: ci-fix

* fix: align mail rule shortcuts with final design

* docs: fix mail rule shortcut summary

---------

Co-authored-by: bubbmon233 <272202079+bubbmon233@users.noreply.github.com>
Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-03 20:01:50 +08:00
bytedance-zhangbinkai ac0f243e5b feat(base): support AI classification and AI Analysis Action (#2590)
* docs(base): sync workflow guide to current branch

* docs(base): sync workflow schema to current branch

* feat(base): support AI classification workflow validation

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* docs(base): document AI classification workflow schema

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* feat(base): validate AI classification agent data

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): relax ai classification optional fields

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): validate workflow ai analysis json

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): validate workflow ai analysis json

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): default ai classification no match action

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): keep ai analysis validation scoped

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): normalize workflow empty steps

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): reject ai classification mode input

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix: polish skill

* fix: 还原 step 判断逻辑

* fix: 调整校验逻辑组织形式

* fix: polish skill

* fix: CR Comment

* feat: support development environment overrides

* fix: CR Comment

* Revert "feat: support development environment overrides"

This reverts commit cef8f78dc6.

* fix: polish skill

* fix: compress skill

* feat: support development environment overrides

* Revert "feat: support development environment overrides"

This reverts commit cef8f78dc6.

* fix: polish skill

* feat: support development environment overrides

* fix(base): preserve omitted AI classification strategy

* Revert "feat: support development environment overrides"

This reverts commit cef8f78dc6.

* fix: polish skill

* fix: ut

* feat: support development environment overrides

* fix: 沿用全量更新逻辑

* Revert "feat: support development environment overrides"

This reverts commit f805081e89.

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-03 19:34:14 +08:00
yangr-happy 30e95091ff feat: validate generated API parameter constraints (#2514)
Co-authored-by: yangr-happy <301323675+yangr-happy@users.noreply.github.com>
2026-09-03 17:18:46 +08:00
R0bynZhu 7690ba4446 feat(slides): lint slide writes server-side, add --no-lint to opt out (#2607)
+create, +add-slide, +update-slide and +replace-slide are the four
shortcuts that change slide content, so they are the four that can ask
the backend to check the page before accepting the write. All four now
send lint_xml=true by default and lint_xml=false when --no-lint is
passed. The subject of the check is the page the write produces, not the
payload it was handed: +replace-slide submits fragments, and a fragment
that is correct on its own can still push a neighbour off the canvas.

The switch travels in the request body rather than the query string. A
query parameter has to be declared in the gateway's own api meta before
it is bound to a field, and the published definition of these endpoints
does not list one — so an undeclared parameter is dropped, the field
arrives unset, and the server reads it as "not requested". Verified
against a live backend: pages that asked to be linted were written
unlinted, with nothing anywhere to say so. Body fields ride along with
the JSON already being sent and need no registration.

The value is sent explicitly in both directions rather than omitted when
on. The parameter is newer than the registry, so the server-side default
is not something this CLI can read anywhere, and a request that states
the value keeps meaning the same thing if that default ever moves.

A refusal is passed through verbatim. The message field carries the lint
report itself — the same document the lint tool writes when it is run by
hand — and the same refusal reaches callers through `lark-cli api` as
well, where nothing rewrites it. Rendering it to prose here would give
one refusal two formats depending on which command produced it. Each
finding carries the numbers behind its own rule, and which numbers those
are differs per rule, so nothing is decoded that is not used: the report
is what the caller reads.

The refusal is recognised by its error code, 4000153, which the engine
raises for nothing else and which reaches the CLI unchanged. Matching on
the shape of the message instead would mean claiming any JSON that
resembles a report, and a false positive there rewrites the hint of an
error this code does not understand.

What the backend cannot say goes in the hint instead: how many findings
refused the write, that the page did not land, and --no-lint, which is a
CLI flag the server has never heard of. The count is summary.error_count
rather than the number of findings, because errors are what refuse a
page — the same line the lint tool draws when it is run by hand, exiting
non-zero on error_count alone. A report can arrive with warnings beside
its errors, and counting those too would send the caller hunting for
blockers that are not there. A message that does not parse still gets
the hint: the escape hatch is the half of it they cannot get anywhere
else, and withholding it over a missing number helps nobody.

The hint names no page. Every write path submits exactly one page, so a
finding's slide_number is its position inside that submission and is
always 1 — which is not the page the caller is looking for. On +create
it is actively wrong: it would read "on slide 1" next to a progress line
saying "adding slide 2/3 failed". The page number has one source, and it
is that line.

Findings that did not refuse the write come back the other way. The
backend returns them in an issues field on a response that succeeded, and
all four shortcuts now pass that field through untouched rather than
dropping it. It only ever arrives on a page that was written: anything
serious enough to refuse the write left as the error above, carrying the
same report. Dropping it would leave the caller believing the deck says
exactly what they wrote, with no way to learn otherwise short of looking
at the rendered page. It is passed through rather than reformatted so
that one field reads the same however the page was written.

+create keeps adding its pages one at a time, so a refusal there can
arrive with the presentation and some of its pages already written. It
is reported as such: the error carries the lint report and, next to it,
which page was refused and how many landed before it, so the retry adds
the rest instead of building a second deck. +replace-pages does the same
for the items in its plan.

A batch that was told to keep going reports its failures only through the
per-item records, so those carry the report, the code and the flag hint
as well; a record built from the error's message alone would have named
neither the refusal nor the way past it. The position stays on the
returning path, where it is the only thing that says how far the batch
got — beside a per-item record it would describe a batch that did not
stop.

Tests assert on the wire — the body the stub actually received — rather
than on the builder's return value, so a command that stops calling its
own builder still fails.
2026-09-03 14:04:05 +08:00
max 688de5cda3 feat(vc): distinguish detected meeting share starts (#2541) 2026-09-03 13:37:51 +08:00
木杉 6606594068 fix(suggest): surface both halves of a welded compound flag name (#2604)
`suggest.Closest` ranks flag/command suggestions by shared prefix then edit
distance. A hallucinated name welded from two real names -- e.g. `--sql-file`
from the real `--sql` and `--file` of `apps +db-execute` -- defeats both
signals: the leading half wins on prefix, and the trailing half (`file`, 4
edits from `sql-file`, budget 2) is dropped. The hint then names the flag the
caller did not want and omits the one that does exactly what they asked for.

The cost is not the rejected call. Steered to `--sql`, callers inline SQL
through the shell, where quoting mangles `DEFAULT ''` and
`current_setting(...)` into syntax errors that read as SQL-authoring bugs.
`--file` passes file contents verbatim and avoids that class entirely.

Treat a candidate that exactly equals one hyphen-delimited segment of the typed
name as plausible, however far the whole string drifted. Ranking is unchanged --
segment hits are admitted, not promoted -- so the leading segment still ranks
first, and an unrelated candidate list still yields no suggestions.
2026-09-03 11:33:52 +08:00
sang-neo03 59f6ad4900 fix(output): preserve non-data payloads in the api success envelope (#2601)
* fix: preserve non-data payload keys in SuccessEnvelopeData

When an API response uses a non-"data" key (e.g., /bot/v3/info returns
payload under "bot"), the previous implementation discarded the payload
and returned an empty object. Fall back to the envelope minus transport
fields (code, msg, data) so the business payload is preserved.

Fixes #2428

(cherry picked from commit a82585718c)

* fix(output): pass non-object bodies through and pin api envelope at command level

Follow-up to the cherry-picked fix for #2428: return nil bodies as {} and
non-object bodies untouched instead of collapsing them, align the new test
with its neighbours, and add cmd/api regression tests through the httpmock
path so the user-visible envelope is pinned where the bug was reported.

* test(output): pin null data with sibling payload, drop SDK-pinned array test

TestApiCmd_NonObjectBody_FailsLoudly asserted the SDK's pre-decode rejection
of non-object bodies, not the output-layer branch this PR added; reverting
that branch left it green. The unit test already covers the branch, so the
command-level copy is removed. Add a unit case for {"data": null, "bot": {..}},
which the previous code collapsed to {} and now returns as {"bot": {..}}.

* fix(output): normalize legacy bot payloads

---------

Co-authored-by: Wu Shuwen <108231307+dajiaohuang@users.noreply.github.com>
Co-authored-by: sang-neo03 <266690410+sang-neo03@users.noreply.github.com>
2026-09-03 01:14:22 +08:00
kele498 515f9f5a4a feat: 支持会议搜索使用机器人身份 (#2445)
sa: safe
doc: skills/lark-meeting
cfg: none
test: unit test, dry-run e2e, live TAT smoke

Co-authored-by: search_zhuhao <zhuhao.517@bytedance.com>
2026-09-02 21:33:57 +08:00
dc-bytedance d5148a88df fix: honor requiredScopes conjunction in CollectScopesForProjects (#1878) 2026-09-02 18:11:22 +08:00
Wu Shuwen 59dcdf559b docs(skills): fix broken reference links (#2485) 2026-09-02 15:32:16 +08:00
huarenmin13 d66b1cac2e fix(base): improve search recovery and form deletion safety (#2422)
- guide record-search callers to the correct flag or command path without reporting recovery-only flags as invalid input
- reject blank form question IDs before destructive deletion and preserve keep-field request semantics
- cover typed validation and dry-run request contracts, then refresh Base E2E coverage

Co-authored-by: BASE Infra Harness <ai@base-infra-harness.noreply.local>
2026-09-02 14:15:28 +08:00
bytedance-zhangbinkai 5c1aa633b6 fix(base): repair field schema template reference (#2575) 2026-09-02 10:41:26 +08:00
lark-cli-external-pr-digest[bot] 2aebe8970f chore: release v1.0.93 (#2597) v1.0.93 2026-09-01 22:40:41 +08:00
sang-neo03 d12b39cf46 feat(vfs): allow absolute paths under a built-in path policy (#2580)
* feat(vfs): allow absolute paths under a built-in path allowlist

Path flags only accepted paths relative to the working directory, so an
agent passing a full path (typically under /tmp) failed on its first call
and had to retry with a relative one.

Absolute paths are now accepted when they resolve inside a built-in
allowlist: the working directory, /tmp, and ~/files. A built-in denylist
covers system and credential locations and wins over the allowlist,
including over the working directory. Both lists are compiled in and read
no environment variable, flag, or config file, so the effective policy is
fixed by the binary; upgrading is all it takes for the new behavior to
apply.

Containment is decided by file identity (device and inode) alongside the
resolved name, because a single directory has many spellings: APFS folds
U+017F onto "s", so ".sshh" spelled with it opens ~/.ssh, and NTFS and
APFS both compare case-insensitively.

Reads are hardened where the policy applies: O_NOFOLLOW pins the final
component, O_NONBLOCK keeps a FIFO from blocking before it can be
refused, and the opened descriptor is matched against the inspected
object, rejected when it is not a regular file, and rejected when it
carries extra hard links. The relaxed local-input tier used by apps
upload keeps its own contract (symlinks are legitimate arguments there)
and gains the denylist check instead.

Two behaviors are deliberate rather than incidental. Working inside a
denylisted directory now refuses even relative paths, since the denylist
is unconditional. Running as root leaves only the working directory and
/tmp, because the home directory is then /root, itself a deny root.

Existing tests asserted the old "every absolute path is refused"
baseline; they now assert the allowlist. Traversal fixtures escape to the
filesystem root, which stays outside every allowed root on Linux, where
the temp directory that hosts t.TempDir() is /tmp itself.

* fix(vfs): close two paths around the built-in denylist

A "~/..." argument had two readings: validation expanded it to the home
directory, while a caller that keeps the original string — SafeLocalFlagPath
returns it verbatim — opens whatever "~" names in the working directory. A
symlink there carried reads past the denylist, confirmed by reading
/etc/passwd through it. Every interpretation of an argument is now checked,
so the shorthand still reaches ~/files while the literal entry cannot
escape.

With no LARKSUITE_CLI_CONFIG_DIR and no reachable home directory,
core.GetBaseConfigDir keeps credentials in a bare ".lark-cli" resolved
against the working directory, which is an allow root. That fallback is now
mirrored as a deny root, so containers whose home lookup fails do not expose
their stored tokens.

* fix(vfs): enforce hard-link checks across readers

* fix(vfs): stop an output hard link from rewriting a file outside the allowlist

A hard link has no target for name resolution to follow, so a link inside an
allowed root looked like an allowed destination while sharing its inode with a
file outside every root. A caller that truncated the approved name in place
rewrote that outside file: `auth qrcode --output <link>` reported success and
replaced a 43-byte JSON file outside the allowlist with its PNG.

Output validation now refuses an existing target that carries more than one
name, which covers callers that write directly, and auth qrcode commits
through a temp file and a rename, which replaces the directory entry and
leaves the other names alone. Writers already going through FileIO.Save were
never affected, since that path has always committed by rename.

* fix(vfs): give the hard-link refusal a workable recovery hint

The message told the caller to copy the file into an allowed directory, which
answers a question they did not ask: the file that triggers this is normally
already inside one, with every one of its names there too. It now states what
the check actually cannot do — enumerate the other names a file is reachable
by — and offers the step that works, which is to copy the file and use the
copy.

* test(vfs): pick the denylist fixture for the platform under test

Two tests reached for "/etc/passwd" as a denylisted absolute path. That path
is not absolute on Windows, so one test met the foreign-path rejection instead
of the denylist it was asserting, and the other saw the path joined to the
working directory and no rejection at all. Both now ask for a deny root that
exists on the platform running them — the credential directories under the
account home qualify everywhere — which keeps the denylist covered on Windows
rather than skipping it there.

Verified on Windows 10.0.19045 by running the package's test binary from this
branch and from main: main passed, this branch failed these two, and both pass
after the change. The other packages this branch touches were compared the
same way and their Windows results are identical on both sides.

* fix(vfs): state the hard-link check as the condition it tests

The check read as "bail out unless the target can be inspected", which
nilerr reads as an error swallowed on the way out. It now names the case it
acts on — an existing regular file with more than one name — and the comment
carries what the early return used to imply: a target that cannot be
inspected has no link count to judge, and the write layer reports the real
failure with proper typing.

* docs(vfs): scope the policy's environment claim to what holds

The header promised that neither list accepts runtime input and that no
caller controlling the environment can widen them. Two inputs contradict
that: LARKSUITE_CLI_CONFIG_DIR contributes a deny root, and where the account
database cannot name the running uid, $HOME decides where ~/files points —
reproduced in a container running as an unregistered uid, which wrote into a
directory the environment chose.

The comments now state the preference and its boundary rather than a
guarantee, and record what the boundary costs: a directory named "files"
under the named path, with the home directory itself still outside the
allowlist and every candidate home still carrying the credential deny roots.
The trustedHome note also said the pure-Go lookup falls back to $HOME
silently; it does so only when $USER is set as well, and returns an error
otherwise, which drops the ~/files root instead of moving it.

No behavior change.

* fix(auth): keep the mode of a QR output file that already exists

Committing the QR write by rename fixed a hard link from rewriting a file
outside the allowlist, but it also changed what happens to the target's mode.
A rename installs the temp file's inode, mode included, where the previous
in-place write left the existing file's mode untouched. Overwriting a target
the caller had restricted to 0600 therefore published it as 0644.

The mode now comes from the file already at the path; only a path with
nothing at it takes the default. Verified against main, which preserved 0600
here, and covered by a test that fails when the fixed mode is restored.

* test(sheets): move the csv file-alias tests onto the new path baseline

Merging main brought #2559's tests for the --file → --csv alias, written
against the policy this branch replaces. Two of them fail on it, both because
the verdict they describe moved rather than disappeared.

The out-of-tree case used /tmp, which the allowlist now accepts, so the value
came back as a missing file instead of an out-of-tree one; it now names a path
no allow root can contain. The directory case is refused when the descriptor
is inspected, before a read is attempted, so the message reads "not a regular
file". What the caller sees of both — the flag named, the cause kept, stdin
offered — is unchanged.

That message listed the kinds it refuses and omitted directories, which is how
it reached a directory test reading as a mismatch. It now names them.

* fix(im): let the path policy judge a download target

`+messages-resources-download` refused an absolute --output before the shared
policy saw it, so the flag stayed relative-only after the policy learned to
accept full paths. It is the command behind 99% of a reported 1,189 download
path errors in one week, where 97.2% of first calls passed an absolute path
and every later success had switched to a relative one.

The shape checks are gone. Both call sites already hand the result to
ResolveSavePath, which applies the allowlist, the denylist and symlink
resolution, so refusing a shape here decided nothing the policy would not
decide better — an absolute path is now answered by where it points rather
than by how it is written.

The file-key checks stay, and they are what the batch caller relies on: it
embeds the key in the path, and a key carrying a separator is refused as a
malformed key, so a traversal cannot be built from one. Verified against a
real tenant: /tmp and ~/files now save, while ~/.ssh, /etc and a path outside
every root are still refused.

* test(im): pin the download output contract the policy now decides

The dry-run suite listed an absolute path among the values --output must
refuse. That held while the command rejected the shape itself; now that the
built-in policy decides, /tmp is an allowed root and the path is accepted, so
the case asserted a rule that no longer exists.

It is replaced by the two halves of the real contract: an absolute path
inside an allowed root reaches the request, and a path that resolves outside
every root — a parent escape from this working directory, or a denylisted
directory — is still turned down as a validation error naming --output.

* fix(vfs): hold a relative path to the working directory

Accepting /tmp as an allow root gave a relative path somewhere new to go.
A process whose working directory sits under /tmp — CI runners, containers
and agent sandboxes commonly arrange that — could climb out with "../" and
still satisfy the allowlist, because the sibling it landed in was also under
/tmp. /tmp is world-writable, so that sibling can belong to another user or
another session, and the write side commits by rename, which replaces an
existing target unconditionally. The previous policy refused this: it
required every resolved path to stay under the working directory.

Naming a full path and climbing out of the working directory are different
acts and no longer share one verdict. An absolute path is judged by the
allowlist, which is what this branch set out to allow; a relative one has to
resolve inside the working directory, whatever wider root contains it.

The home denylist grows at the same time and for the same reason: the working
directory is an allow root and running from the home directory is ordinary,
so a credential store there is reachable by a relative name unless the list
covers it. It now names the common ones — netrc, git and shell credentials,
kube, docker, azure, gh, gcloud, the language package registries — and the
shell histories, which carry pasted keys as reliably as a credential file.

---------
2026-09-01 22:18:29 +08:00
xiongyuanwen-byted c6c040c2c5 refactor(sheets)!: remove legacy sheets command surface (#2572)
* refactor(sheets)!: remove legacy sheets command surface

The `shortcuts/sheets/backward` package kept 42 pre-refactor command names
(`+create`, `+read`, `+write`, `+create-sheet`, `+media-upload`, ...) alive
alongside the refactored ones. Monitoring puts their combined share below 5%,
so they are dropped along with the machinery that carried them.

Removed with the package:
- the `sheetsAliasReplacement` map and `wrapSheetsBackwardDeprecation`, which
  tagged each alias with a `_notice` deprecation envelope on execution;
- the deprecated cobra group and the custom `sheets --help` usage template that
  existed only to hide it. `applySheetsCompatGroups` becomes
  `applySheetsCommandGroups`: it still groups the `+`-shortcuts so the OpenAPI
  metaapi subcommands keep filing under cobra's stock "Additional Commands".

The deleted package owned no shared logic. Its `parent_type` mapping for image
uploads was, by its own header, a deliberate mirror of the canonical one in
`shortcuts/sheets/helpers.go`; `common.IsLocalOfficeToken` and the drive upload
helpers are untouched and keep their other callers.

E2E tests still drove the removed commands and are ported to the refactored
surface: `+workbook-create` / `+workbook-info` / `+cells-set` / `+cells-get` /
`+cells-search` / `+sheet-*`. The sub-sheet dry-run assertions had to be
rewritten rather than renamed, because the old commands posted to
`sheets/v2/sheets_batch_update` while the new ones invoke
`modify_workbook_structure` over `sheet_ai/v2`. `+update-sheet` fanned out to
`+sheet-rename` + `+sheet-hide` + `+dim-freeze`. Every migrated command was
verified against a live workbook, which is where the assertions come from:
rename and hide answer with a bare revision counter, so their effect is read
back from `+workbook-info` (`sheet_name`, `is_hidden`).

Also updated, since these referenced the removed surface:
- three `skills/lark-drive` reference docs that instructed agents to run
  `sheets +read` / `sheets +find`; these ship embedded in the binary, so the
  instructions would have produced unknown-subcommand errors;
- `skill-template/domains/sheets.md`, deleted: every sheets command it named
  was removed and the cell payload shape it taught
  (`{"type":"formula","text":...}`) is rejected by `+cells-set`;
- stale comments naming `backward.uploadSheetMediaFile` and
  `backward/helpers.go`.

`Shortcut.OnInvoke` and `internal/deprecation` now have no producers. Both are
generic framework plumbing wired into the `_notice` envelope in `cmd/root.go`,
so they are left in place; the `OnInvoke` doc comment no longer claims a caller.

BREAKING CHANGE: removes the 42 pre-refactor sheets commands (`+create`,
`+read`, `+write`, `+append`, `+find`, `+set-style`, `+create-sheet`,
`+update-sheet`, `+add-dimension`, `+set-dropdown`, `+media-upload`,
`+create-filter-view`, ...). They now fail with `unknown subcommand` and carry
no deprecation notice, so a caller still on the old names gets no migration
pointer at runtime.

Replacements for all 42, plus the differences that are not simple renames — the
cell payload vocabulary (`{"type":"formula","text":...}` is now rejected),
response field paths, and `+update-sheet` / `+update-dimension` fanning out to
several commands — are documented in
skills/lark-sheets/references/lark-sheets-legacy-command-migration.md, reachable
at runtime via:

    lark-cli skills read lark-sheets references/lark-sheets-legacy-command-migration.md

* test(sheets): close the assertion gaps found in review

- The append subtest asserted only the ok envelope, so a +cells-set that
  reported success without persisting would pass; the later +cells-search
  covers row 2 only. Read A4:C4 back and assert the row landed. Verified
  non-vacuous against a live sheet: an unwritten row returns cells carrying
  no value, so the read-back fails if the write does not persist.
- Cover the omitted-title +sheet-copy path. The comment on the empty-title
  suite states an omitted --title means "let the server name the copy", but
  nothing exercised it; the new case pins that new_name is absent from the
  payload rather than sent empty.
- Assert tool_name in the shared dry-run loop instead of only in the create
  case, so copy / delete / rename / move cannot pass by selecting a different
  tool on the same /tools/invoke_write endpoint.

* docs(sheets): fix the migration guide examples found in review

- `+table-put --sheets` requires the `{"sheets":[…]}` envelope; the `+append`
  row showed a bare array, which the flag rejects outright ("top level must be
  the object {\"sheets\":[…]}, got a bare JSON array"). Verified both forms
  against a dry-run before and after.
- Tag the diagnostic fence as `text` (markdownlint MD040).
2026-09-01 19:47:18 +08:00
hugang-lark 1d6e7731d3 feat: add shortcut for +list-attendees (#2591)
feat: optimize +freebusy shortcut

fix: hint timezone

feat: operate recurrence event
2026-09-01 19:45:42 +08:00
zhengzhijiej-tech ea17864b52 docs(sheets): clarify dropdown values and default colors (#2582)
* docs(sheets): clarify multi-select dropdown values

* docs(sheets): clarify dropdown values and default colors

* docs(sheets): address dropdown review feedback

* docs(sheets): document dropdown color readback
2026-09-01 19:28:56 +08:00
caojie0621 8a7fa53355 fix(shortcuts): remove non-actionable stderr progress (#2532)
* fix(shortcuts): remove non-actionable stderr progress

Keep successful shortcut output machine-readable by removing lifecycle, retry, and completion progress from docs, wiki, drive, and shared multipart flows. Preserve actionable warnings, fallbacks, and interactive prompts, and update stderr regression assertions.

* test(wiki): distinguish warnings from progress
2026-09-01 17:53:59 +08:00
syh-cpdsss baf9640bec Feat/okr comment (#2558)
* feat: OKR comments

* fix: CR issue

* fix: skill text & content field validation
2026-09-01 16:31:47 +08:00
huangjy-bytedance b5064991c7 fix(base): correct reminder trigger offset direction (#2584)
* fix(base): correct reminder trigger offset direction

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* docs(base): use pre-deadline reminder example

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-01 16:21:48 +08:00
xiongyuanwen-byted 835b52cc88 feat(sheets): cut the top command-error clusters from the 08-18..24 eval batch (#2559)
* feat(sheets): cut the top command-error clusters from the 08-18..24 eval batch

A trace analysis over 14,818 `lark-cli sheets` calls attributed 2,036 command
errors to 57 (subcommand, flag) groups, with the top 6 covering 76%. Four of
them are ours to fix; each is addressed at the layer that produced it.

--styles vocabulary (291 cases, the only group whose retry also failed):

  - Prescribe the border family as a whole. foldBorderFamilyAliases already
    absorbs border / borders / border_<side> / border_<attr>; what still
    reached the error path was the Lark OpenAPI's border_type (FULL_BORDER,
    OUTER_BORDER) and CSS's border_width — real vocabularies with no
    equivalent here, and border_type was the single top field in the group.
    Neither maps unambiguously onto a per-side style/weight/color triple, so
    they get one shared answer, not a silent alias.
  - Prescribe the OpenAPI's nested {range, style:{...}} envelope, plus
    bg_color / fill_color / text_color.
  - Match prescriptions on the key's letters alone, so border_type,
    borderType and border-type are one mistake, not three.
  - Collapse repeated issues in the --styles / --writes folds. One wrong
    field name in a payload styling N cells produced N identical issues, each
    re-listing the full supported vocabulary: the fold meant to save round
    trips was burying its own answer. The defect is now stated once and the
    other locations are named.

--sheets payload (419 cases):

  - Accept dtypes / formats as a positional array. The same pandas habit that
    produces `columns` and `data` produces df.dtypes.tolist(), and the
    payloads were otherwise correct. Only a 1:1 match with `columns` is
    accepted; a length mismatch is rejected rather than guessed.

+csv-put --file (35 cases):

  - Read the value as a path. --file is aliased onto --csv because agents
    reach for it, but the names promise different things — rewriting only the
    name left the path to be written into the sheet as literal text, which the
    file-path guard then rejected, with an error naming a flag the caller
    never typed. The read goes through the same cmdutil.ReadInputFile as
    @file, so the relative-path policy is unchanged and stdin stays the
    out-of-tree route. A value naming nothing readable still falls through to
    the guard, so --file holding literal CSV keeps working.

--help (88 cases of "required flag(s) ... not set"):

  - Mark required flags in the sheets help. MarkFlagRequired only sets a
    completion annotation cobra never renders, so a required flag read exactly
    like an optional one. Sourced from flag-defs, since +chart-create and
    +csv-put deliberately clear that annotation after mounting; a flag cobra
    has put in a one-required group is left unmarked, because neither member
    of such a pair is individually required.

The remaining big group (absolute paths passed to --file / @file, 636 cases)
is deliberately untouched: the cwd-relative policy is a protocol decision, and
that thread is being followed up separately.

* fix(sheets): correct --file alias provenance and mark its value resolved

Two defects in the +csv-put --file rule from the previous commit, both found
in review:

- The alias record was written from the flag-name normalizer, which pflag also
  runs for Lookup and Set — including once with the canonical name right after
  the rewrite, and again on every later lookup. `--file a.csv --csv ./b.csv`
  therefore still counted as "supplied by --file", and an explicit --csv path
  was silently read as a file instead of meeting its guard. The spelling is now
  staged by the normalizer and committed by the flag's Value, which runs once
  per real occurrence, so the last occurrence wins in either order.

- The rewritten value was not marked as read from a source, so a file whose
  contents are themselves path-shaped ("report.csv") failed csvPutInput's shape
  check — a valid CSV rejected as a caller who forgot the @. Marking it makes
  --file behave exactly like --csv @<path> all the way down, which also lets
  the guard skip it on its own rather than through a special case in Validate.

RuntimeContext gains an exported MarkInputResolved for the second half: the bit
already existed for @file / stdin, and a domain that resolves a source itself
needs to set it. No other domain calls it, so nothing else changes.

Also assert the typed contract (Param, Cause) rather than message text alone in
the style-prescription corpus and the collapsed-issue fold, per the repo's
error-test guideline.

* fix(sheets): answer every unreadable --file path under --file, and stage alias provenance only while parsing

Both from self-review of the branch.

An unreadable path passed as --file fell through to the --csv guard, which
answered naming a flag the caller never typed — and for a file that exists but
cannot be opened, prescribed "pass the same path with an @ prefix", which routes
through this very reader and fails identically. Only one case may fall through
now: a value that names nothing AND is not path-shaped, i.e. literal CSV text,
which --file accepted before this rule existed. A path-shaped value naming
nothing, an unreadable file, and a directory each answer under --file.

The alias spelling was staged with no Parsed() guard (the first commit had one;
flagalias.Bind still does). chainFlagAliases looks its aliases up while
installing and pflag normalizes on Lookup, so composing PostMount twice — which
installAliasProvenance explicitly anticipates — replayed "file" through the
already-installed normalizer at mount time, and the next real --csv occurrence
committed it: an explicit --csv path would then be read from disk instead of
meeting its guard. Verified the new regression test fails without the guard.

* fix(sheets): reset alias staging on remount, and assert cause on every --file read error

Second review pass, both valid.

FlagSet.Parsed() stays true once parsing has started, so the guard added in the
last commit only covers a remount that happens BEFORE the first parse. A remount
afterwards — its own alias lookups running through the normalizer the first pass
installed — could still leave a spelling staged for the next occurrence to
commit, which would read an explicit --csv path from disk. Re-running the
install now resets staging, closing the window from the other side. Verified the
new parse/remount/reparse test fails without the reset.

The unreadable-file and directory branches preserve the read error as Cause;
their tests now assert it, matching the sibling that already did.
2026-09-01 15:05:10 +08:00
Zhibo Wu 20ef0af8a7 fix(event): preserve UTF-8 in truncated diagnostics (#2535)
* fix(event): preserve UTF-8 in truncated diagnostics

* test(event): preserve preflight decode cause coverage
2026-09-01 00:58:18 +08:00
ViperCai fe8ce4675b docs(lark-doc): retain draft workspaces after creation (#2574)
* docs(lark-doc): retain draft workspaces after creation

* test(lark-doc): check cleanup phrases in both references
2026-08-31 20:27:40 +08:00
wanghm-bytedance a257fcbaf9 feat(base): support ranking dashboard blocks (#2528)
* feat(base): support ranking dashboard blocks

* fix(base): align ranking validation with strict schema

* fix(base): scope ranking validation to dashboards
2026-08-31 15:33:13 +08:00
liuxin-0319 2d44a5e045 feat(docs): route local Word media uploads to office mount point (#2568) 2026-08-31 15:19:48 +08:00
lark-cli-external-pr-digest[bot] 6646386e09 chore: release v1.0.92 (#2553) v1.0.92 2026-08-28 18:47:32 +08:00
xuzhigang 1181dafc76 feat(im): support rich-text message attachment zone in send/reply/mge… (#2515)
* feat(im): support rich-text message attachment zone in send/reply/mget/edit

Support the post message attachment zone (top-level files array) end to end:
- +messages-send / +messages-reply: repeatable --attachment file_key flags
  merged into the post content's files array (deduplicated).
- +messages-mget: render attachment-zone files/folders as <file>/<folder>
  tags in content, extract file keys for --download-resources.
- +messages-edit: new shortcut (PUT /open-apis/im/v1/messages/:id) with
  --set-attachments / --clear-attachments; body-only edits preserve the
  attachment zone by default.
- Attachment flags are mutually exclusive with --content carrying a files
  array (declare the zone via one or the other, not both).
- bot-only identity, matching server behavior (user token rejected).
- Fixes from review: attachments no longer bypass content mutual-exclusion
  validation (P1); merge dedups by key.
- Docs (SKILL.md, references, affordance) and unit tests updated.

* fix(im): address design-review findings (auto-infer post, dedup set, doc routing)

- --attachment/--set-attachments/--clear-attachments now infer msg_type=post
  automatically; only an explicit incompatible --msg-type conflicts.
  --text is rejected with attachments (text is a standalone message, not a
  post body) with a hint to use --markdown or --content.
- --set-attachments deduplicates repeated keys (docs promised this; the
  replace helper now enforces it).
- Shortcut Description no longer leaks the HTTP path or the raw server
  error phrase; it describes the command semantically.
- affordance/im.md +messages-edit now routes WHEN: interactive cards go to
  messages.patch, corrected messages go to +messages-send, and attachment
  tri-state tips are listed.
- mget doc no longer claims --format json exposes raw wire fields (the
  output is the rendered content); download eligibility clarified.
2026-08-28 16:55:28 +08:00
xiongyuanwen-byted 603d13b7eb fix(sheets): suppress multipart stderr noise and tighten e2e boundary test (#2550)
Add a Quiet flag to DriveMediaMultipartUploadConfig so callers whose
success contract forbids non-empty stderr (the sheets shortcuts) can
suppress the chunk-plan and per-block progress lines the multipart
path writes unconditionally on success. Both uploadSheetImage and the
deprecated uploadSheetMediaFile pass Quiet: true.

Also simplify isLocallyOpenedOfficeToken to two HasPrefix calls
instead of a loop over an inline slice, so the two-prefix invariant
stays in one expression rather than drifting from common.

Finally, fix the e2e "at the ceiling stays single-part" test case:
the small.png fixture was 9 bytes, not the 20 MB the name implied.
Truncate it to singlePartCeiling so the boundary is actually pinned.
2026-08-28 16:33:59 +08:00
zhengzhijiej-tech 62be9cf20e feat(sheets): add +cond-format-result-get and --include conditional_format (#2502)
* feat(sheets): add +cond-format-result-get shortcut and --conditional-format flag

- lark_sheet_read_data.go: add CondFormatResultGet shortcut with include_conditional_format_style hardcoded to true
- cellsGetInput(): add --conditional-format flag mapping
- shortcuts.go: register CondFormatResultGet alongside existing cond-format shortcuts
- lark_sheet_read_data_test.go: add dry-run test cases covering new shortcut and --conditional-format
- flag-defs.json / flag_defs_gen.go: sync from sheet-skill-spec

* fix(sheets): fold conditional format into include flag

* refactor(sheets): isolate conditional format result output

* fix(sheets): satisfy nested slice lint
2026-08-28 15:40:21 +08:00
xiongyuanwen-byted 2f8d816512 fix(sheets): make image-upload previews match what Execute sends (#2537)
* fix(sheets): repair the build after the local-office detection move

main does not compile: shortcuts/sheets/lark_sheet_workbook.go references
isOfficeSpreadsheet and officePrefixes, which no longer exist in the package.

Neither PR was wrong on its own. #2531 (merged 10:39) moved local-office token
detection into common.IsLocalOfficeToken and deleted the sheets-local copies;
#2533 (merged 12:48) added errLocalOfficeExportUnsupported, which calls them.
#2533's branch predated the move, so its CI was green against a base that still
had the symbols, and merging it left main broken.

Repoints both references at the moved API. Behaviour is unchanged:
common.IsLocalOfficeToken is the same predicate #2531 moved, and the prefix pair
is the exported form of the same two constants. #2533's own coverage —
TestWorkbookExport_LocalOfficeTokenRejected across the local_office_ prefix, the
fake_office_ prefix, and an interleaved OFL0X token, plus the wiki-node and
dry-run cases — passes unchanged, which is what pins the equivalence.

* fix(sheets): derive dry-run image parent_type from the ref kind, not the token

A `/wiki/` URL reaches a DryRun hook as the wiki node_token: resolving it to the
backing spreadsheet needs the get_node call a preview must not make. Both
image-upload previews fed that node_token straight to sheetMediaParentType, so
the parent_type they showed was derived from a token that is not the one Execute
uploads against. A node_token shaped like an imported office token previewed
office_sheet_file for a spreadsheet that will upload as sheet_image.

sheetsDryRunParentType decides from the ref's kind instead. A wiki ref is native
by construction, not by default: resolveWikiNodeToSpreadsheetToken rejects any
node whose obj_type is not "sheet", and a spreadsheet backed by an imported
office file sits in drive as a "file" node, so it never survives that gate to
reach an upload. Execute is unaffected either way — it derives from the resolved
token. This mirrors slidesDryRunParentType, which the slides domain already
applies for the same reason.

The hooks now hold the parsed ref rather than re-deriving the token from it, so
the kind is visible where the preview is built.

Also records, at uploadSheetMediaFile, that office_sheet_file survives the
multipart path. Slides caps image uploads at 20 MB because upload_prepare
rejects its parent types outright, which raised the question for sheets, whose
deprecated +media-upload has no such cap. Verified against the live API:
upload_prepare accepts both sheet_image and office_sheet_file, and a 20.6 MB
file uploaded with office_sheet_file completes prepare -> 6 x upload_part ->
upload_finish and returns a file_token a float image then accepts.

Tests: sheetsDryRunParentType over both ref kinds, including wiki refs carrying
office-shaped and office-prefixed node tokens; dry-run coverage through the
public flags for +cells-set-image and +float-image-create, each checked against
the identical token as a /wiki/ URL and as a raw spreadsheet token so the two
rows differ only in kind; the same pair added to the e2e dry-run lane. All four
fail against the previous behaviour.

* fix(sheets): send oversized images through the chunked upload, not upload_all

uploadSheetImage always used the single-part endpoint, so an image past the
20 MB ceiling failed with a bare 1061002 "upload media failed: params error"
naming neither the size nor the limit. The capability was already in the domain:
the deprecated sheets +media-upload has dispatched by size since it was written
(backward.uploadSheetMediaFile), which left the same image succeeding through
the old shortcut and failing through +cells-set-image and +float-image-create,
the ones meant to replace it.

uploadSheetImage now picks the endpoint by size the way doc and the deprecated
shortcut already do. The parent_type is unchanged and still comes from
sheetMediaParentType, so the office/native split rides along either branch.

The preview follows the same branch. appendSheetImageUploadDryRun renders one
upload_all under the ceiling and the upload_prepare / upload_part /
upload_finish trio above it, and both image-write hooks now build their upload
step through it rather than each spelling out an upload_all. A preview that
promised a single-part upload for a file the CLI will send in chunks is a
preview of a different request.

Verified against the live API with a 20.6 MB PNG: +cells-set-image and
+float-image-create both complete, the cell reads back holding the uploaded
image_token at the file's real 3000x2400 dimensions, and the dry-run shows the
three chunked steps Execute hits. The same file failed with 1061002 before.

Tests: the chunked branch at exactly one byte past the ceiling, asserted through
upload_prepare's parent_type with upload_all deliberately left unstubbed so a
regression fails loudly; the preview's step list on both sides of the boundary,
as a unit test and in the e2e dry-run lane. All fail against the previous
behaviour.
2026-08-28 14:51:14 +08:00
xiongyuanwen-byted decc9549b5 refactor(sheets): keep the success path off stderr (#2533)
* feat(sheets): reject local-office tokens in +workbook-export

A locally opened Office file (a local_office_ / fake_office_ token, or an
interleaved OFL0X one) names a file the Lark client is showing, not a cloud
document, so the drive export task can only fail on the backend -- and it
fails late, after the create and poll round trips, with an opaque message.

Refuse it up front with a typed failed_precondition that says the workbook is
already a file on disk, and points at +workbook-import for callers who want a
cloud spreadsheet they can export later. The check runs in Validate (so
--dry-run is covered too) and again after the wiki hop in Execute, where the
real spreadsheet token is first known.

* refactor(sheets): report success-path advisories in the result, not on stderr

Every sheets shortcut that had something to say on a successful run said it on
stderr: ignored sub-op locators, emulated dimension semantics, the deprecated
--dimension/--count and +cells-batch-set-style spellings, the dropdown
option-error steer, and the upload/export stage lines in the compatibility
layer. PowerShell's native-command handling and most agent harnesses read
non-empty stderr as failure, so a working call reported itself as an error --
and the facts a caller actually needed sat outside the JSON they parse.

Pure stage text ("Writing image", "Waiting for export task") is deleted: it
duplicates what the result already proves. Everything decision-relevant moves
into the payload:

  - data.warnings           ignored locators, colliding freezes, the dropdown
                            option-error steer (also shown in --dry-run now)
  - data.effective_operation +dim-insert's anchor shift under --inherit-style
                            before, and the whole (rows, cols) state a freeze
                            leaves behind
  - data.deprecation        +cells-batch-set-style and +dim-freeze's legacy
                            flag pair, under a key of its own rather than
                            mixed into warnings
  - data.upload             how +media-upload sent the file

Clean calls keep their exact previous payload shape: every field above is
added only when it has something to report.

Scope is shortcuts/sheets/** on purpose. The remaining success-path stderr in
this domain comes from shared code (the drive export/import core behind
+workbook-export / +workbook-import, the multipart media helper, the auto-grant
helper), which other domains share; cleaning those up belongs to their own
change. The one sheets-owned exception is +workbook-import's extension
correction, which has no slot in the import core's output envelope -- it is
documented at the call site and allowlisted in the guard test.

Tests pin the contract (a successful run leaves stderr empty) and each new
field, plus a source scan that stops new direct ErrOut writes from appearing.

* docs(sheets): point the dropdown option-error warning at data.warnings

The --source-range flag help still told callers the option-error steer arrives
on stderr; it now rides in the result. Mirrors the same edit in the upstream
spec (canonical-spec/spec-tables/flags.json), so the next sync is a no-op.

* fix(sheets): keep export identifiers in +export output, tighten the stderr guard

Review follow-ups on the success-path stderr change:

- +export --output-path lost file_token: on the download branch the token
  reached the caller only through the deleted "Export complete: file_token=…"
  stderr line, and the payload carried just saved_path and size_bytes. Both
  file_token and ticket now ride in the download result, so a caller can
  re-download or resume without re-running the export.

- The stderr guard allowlisted a whole file, hiding any future write in it.
  It now matches one exact statement in one file and asserts that write still
  exists, so both a new write and a stale exception fail the test.

- The contract comments claimed more than the tests prove. They now state
  that only sheets-OWNED code is silent, name the three commands whose noise
  comes from shared implementations (+workbook-export, +workbook-import,
  +media-upload over 20MB), and a new test pins that the shared export core
  does still write -- failing, by design, once that core is cleaned up.

* fix(drive): keep the export and import cores off stderr on success

+workbook-export and +workbook-import delegate to drive.RunExport /
drive.RunImport, so the sheets success-path contract could not hold while
those cores narrated every step: task creation, each poll attempt, completion,
"still in progress", and the import's media upload. Callers that read
non-empty stderr as failure saw a finished export report itself as an error.

The stage text is deleted -- ticket, ready, status, file_token, token and
next_command are all already in the payload. What the narration alone carried
moves into the result:

  - poll               attempts / transient_failures / last_error, added only
                       when a poll actually had to be retried, so a caller can
                       tell a clean run from one that limped to the finish
  - warnings           markdown export falling back to the token as file name
                       after a failed title lookup
  - input_corrections  a caller-supplied record of inputs the CLI rewrote
                       before the request ran; sheets +workbook-import uses it
                       for a mislabeled .xls that is really an .xlsx, which was
                       its last stderr write

drive +export / +import get the same treatment, since they share these cores.
Clean runs keep their exact previous payload shape.

With this, the sheets stderr guard needs no allowlist, and the contract test
covers both workbook commands end to end. Two shared paths a sheets caller can
still reach stay noisy and are named in the contract comment: multipart media
upload over 20MB, and the bot-identity auto-grant warning.

* test(sheets): cover the annotation shapes and both guard call sites

Review follow-ups, all test-side except one comment:

- +dim-insert's effective_operation had no test: a regression could drop the
  emulated-anchor block and still keep stderr empty. Now asserted field by
  field, plus the negative case (--inherit-style after rewrites nothing, so it
  must not gain the block).

- The local-office guard's second call site had no test. A /wiki/ URL only
  reveals its backing token after get_node runs in Execute, so that branch is
  now covered, asserting both the typed rejection and that no export task was
  created.

- annotateSheetsResult's three payload shapes are pinned: object annotated in
  place, array/scalar preserved under `result`, and an empty tool result left
  without an invented `result: null`. The doc comment now spells out that last
  case instead of lumping it in with non-object output.

- The export poll summary test asserted transient_failures but not attempts,
  so a wrong or missing count would have passed.

* fix: preserve recovery state on failure paths and TTY liveness during polls

Review round 2. Removing the success-path narration also removed information
from paths that fail after remote work has started, and removed the only
liveness signal an interactive user had:

- drive +import / sheets +workbook-import: once the import task exists, the
  ticket is the only handle back to it. A poll failure returned bare, so the
  ticket -- previously visible through the polling line -- was lost. It now
  rides on the typed error together with the +task_result command.

- sheets +export --output-path: a download or save failure happens after the
  artifact is ready, so the error now carries ticket, file_token and the
  +export-download command; re-running the whole export is not the recovery.
  A poll timeout carries the ticket for the same reason.

- sheets +batch-update / +batch-chart-*: batch_update is fail-fast without
  rollback, so the ignored-locator and colliding-freeze advisories matter most
  exactly when the call fails part-way -- they decide the safe retry set. They
  are now attached to the typed error's hint as well as the success payload.

- Bounded polls and the import upload are wrapped in RuntimeContext.StartSpinner,
  which is gated on StderrIsTerminal and is a strict no-op for pipes, CI and
  captured output. A human terminal gets liveness back; a machine caller's
  stderr stays empty (the contract tests, which capture stderr, still pass).

+workbook-export's rejection of Office tokens also stopped assuming the caller
holds the file: a local_office_ / fake_office_ prefix means the workbook is
already on their disk, but an interleaved OFL0X token is a file stored in Lark
that may never have been downloaded, so that class is now pointed at
drive +download (then +workbook-import if they want a Lark spreadsheet).

Each behaviour above has a regression test; httpmock's CapturedBodies doc
comment is corrected, since it is appended on every match, not only for
Reusable stubs.
2026-08-28 12:48:19 +08:00
R0bynZhu 0f60fbfbdd fix(slides): relax office token length check from 28 to >=25 (#2531)
* fix(slides): relax office token length check from 28 to >=25

The interleaved "OFL0X" product/region marker is read at fixed positions
(1-based 5/10/15/20/25), so a token only has to be long enough to hold
it. Pinning the total length to exactly 28 silently reclassified every
other length as native.

28 is already stale: per #2509 the local-office format is "OFL0X + 21
random + 1 office type enum" = 27 characters, and sheets relaxed the
identical guard to >= 25 in that PR. Slides was missed, so an imported
office deck at the current length uploaded with parent_type "slide_file"
instead of "office_slide_file".

The marker positions are unchanged. Relaxing the length is only safe
because of them: a false positive is the dangerous direction, since the
drive backend does not validate that parent_node actually names an office
file, so a misclassified native deck uploads successfully and only
surfaces later as an image that will not render. The interleaved native
token cases (same length, different marker) are what keep the floor
honest.

Tests: replaced two mislabelled rows with five verified ones covering 27,
29, 25, 24 characters and a 28-character token carrying the ppt office
type enum. #2509's own labels were off by one or two characters and it
never covered the 25/24 boundary; this does.

* refactor(common): extract local-office token detection into common

The office token shape existed in three identical copies:
shortcuts/sheets/helpers.go, shortcuts/sheets/backward, and
shortcuts/slides. Every copy is somewhere a format change has to be found
again, and that is not hypothetical — #2509 had to apply the same
28-to->=25 relaxation twice inside sheets, and missed slides entirely.

Moved the shape to common.IsLocalOfficeToken. It belongs there because
recognising a local-office document is a drive-level property, not a
per-domain one: an imported office file is an imported office file whether
it backs a spreadsheet or a deck. What genuinely differs per domain is the
parent_type the answer selects — office_sheet_file vs office_slide_file —
so those mappings stay with each domain.

The name deliberately matches the vocabulary #2509 already used
("local-office format"). Its doc comment calls out that "local office" is
the whole category and not the LocalOfficeTokenPrefix case, since the two
now share a word stem while the predicate also accepts
FakeOfficeTokenPrefix and the interleaved marker.

Only slides is rewired here. The two sheets copies are left alone on
purpose to keep this reviewable as a pure no-op for them; they can follow
separately.

The marker offsets are now an array whose length is tied to the marker
string, so adding a character to one without the other stops compiling,
and TestOfficeTokenMinLenMatchesMarkerOffsets pins the length floor to one
past the last offset rather than letting the two merely agree by
coincidence.

Behaviour is unchanged, verified by diffing dry-run parent_type between
the pre-refactor and post-refactor binaries across 13 tokens covering both
prefixes, the 24/25 boundary, 27/28/29 characters, interleaved native
pptcn/shtcn markers, a leading-but-misaligned OFL0X, and an off-by-one
offset: 13/13 identical.

* refactor(sheets): route local-office detection through common

Deletes the last two copies of the token shape, both byte-identical to the
one now in common: shortcuts/sheets/helpers.go and
shortcuts/sheets/backward/lark_sheets_float_images.go. sheetMediaParentType
keeps owning the sheets half of the decision — which parent_type the answer
selects — and only the shape moves.

Equivalence was not assumed from reading. A throwaway fuzz test compared
isOfficeSpreadsheet against common.IsLocalOfficeToken in both packages over
an alphabet biased toward the characters that can actually disagree
(OFL0X plus the native product markers), every single-byte mutation of a
known office token at all 28 positions, and 400k fixed-seed random tokens
of length 0-33. Zero disagreements in either package. Dry-run parent_type
was then diffed binary-to-binary against origin/main across 11 tokens
covering the 24/25 boundary, 27/28/29 characters, interleaved native
shtcn/pptcn markers, a leading-but-misaligned OFL0X and an off-by-one
offset: sheets identical on all 11.

Two comment fixes that the extraction made unavoidable:

The const-block doc in both files still described "a 28-character token".
That was already wrong on main — #2509 relaxed the guard to >= 25 and left
the comment behind — and the shape is no longer described here at all now,
so both defer to common.IsLocalOfficeToken.

Four rows in TestSheetMediaParentType were mislabelled: "25 char, at
boundary" held a 27-character token and the three "new 27-char" rows held
28-character ones, so the floor those labels claimed to cover was never
tested. Relabelled by measured length, and the real 25/24 boundary added.
2026-08-28 10:39:04 +08:00
lark-cli-external-pr-digest[bot] 62eae36008 chore: release v1.0.91 (#2542)
Co-authored-by: lark-cli-external-pr-digest[bot] <305837809+lark-cli-external-pr-digest[bot]@users.noreply.github.com>
v1.0.91
2026-08-27 21:17:13 +08:00
douran-dev a68984f549 docs(im): document chat.join_requests in the lark-im skill (#2395)
Add the chat.join_requests resource (list / handle) and its two scope
rows, matching the registry-side whitelist. Both methods are user
identity only and require the caller to be the chat owner or an admin.

The list entry records a pagination trap verified against the live API:
page_token is returned even when has_more is false, so an agent that
pages while page_token is present never terminates. The handle entry
records that results[] mirrors items[] in count and order and that exit
0 does not mean every item succeeded.

Written by hand rather than via gen-skills. skill-template/domains/im.md
last changed in 7675185f, while this file has five later commits that
edited it directly (#2319, #2223, #2194, #2146, #1906), so a full
regeneration silently reverts them. Reconciling the template with this
file is left as separate work.
2026-08-27 21:03:33 +08:00