Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 113 additions & 6 deletions skills/lark-doc/references/lark-doc-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

## 安全流程

1. 先用分页接口 `+history-list` 找到目标版本的 `history_version_id`。
1. 先用分页接口 `+history-list` 精确查找目标版本的 `history_version_id`。
2. 如果用户指定的是 `revision_id`,不要假设它唯一,也不要把 `revision_id` 直接传给 `+history-revert`。先拉一页并在 `entries[]` 中筛选 `revision_id` 相同的候选;如果未匹配到且 `has_more=true`,继续用 `page_token` 翻页;如果已匹配到候选,最多额外再拉一页补齐可能跨页的相邻候选。最终优先根据用户目标时间与 `edit_time` 的接近程度选择最合适的一条,取同一条的 `history_version_id`;如果没有目标时间,或多个候选无法可靠区分,再向用户展示候选版本(`history_version_id`、`revision_id`、`edit_time`、`name/description`)并确认后回滚。
3. 如果用户指定的是某一时刻但没有指定 `revision_id`,按 `entries[].edit_time` 匹配;优先选择不晚于目标时刻的最近一条历史记录,无法明确匹配时先向用户确认候选版本。
4. 再用 `+history-revert --history-version-id <history_version_id>` 发起回滚。默认最多等待 30 秒;如果返回 `status: running`,记录 `task_id`。
5. 用 `+history-revert-status` 轮询 `task_id`,直到状态不再是 `running`。
6. 回滚完成后,用 `docs +fetch` 读取文档确认内容。
4. **有精确 history entry**:用该条目的 `history_version_id` 调用 `+history-revert`。不要猜测或使用邻近条目。默认最多等待 30 秒;如果返回 `status: running`,记录 `task_id`,再用 `+history-revert-status` 轮询到终态。
5. **没有精确 history entry**:不要直接停止。若用户给出了目标 `revision_id`,尝试一次 `docs +fetch --revision-id <revision_id> --detail full --format json`:
- 成功返回目标 revision 的真实完整内容:将同一次响应的 `data.document.content` 与 `data.document.reference_map` 配套保存并用于 `docs +update --command overwrite`,不得根据摘要、缓存、模型记忆或硬编码重建正文。
- 目标 revision 明确不可读或返回业务错误:停止并如实说明。权限、网络或临时系统错误保持原分类,不要据此声称 revision 不存在,也不要绕过身份、权限或确认门禁。
6. `overwrite` 会清空并重建正文,旧 block ID 会失效,当前版本中未包含在目标 fetch 响应里的评论等非正文对象可能丢失。用户明确要求整篇版本恢复时可以替换正文;若还要求保留当前评论或其它未包含在 fetch 响应里的对象,先说明该 fallback 无法保证保留并确认,不得宣称无损恢复。
7. 恢复完成后,用 `docs +fetch --detail full --format json` 读取最新完整文档,验证 `content` 和可回放的 `reference_map` 均与目标 revision 的真实返回一致。
Comment on lines +10 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Only treat status: done as a successful history restore.

The flow currently polls any terminal status and then proceeds to post-restore verification. partial_failed and failed must stop the success flow and must not be reported as restored. The exact-entry path also needs a defined target document snapshot if verification must compare content and reference_map with the target revision.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-doc/references/lark-doc-history.md` around lines 10 - 15, Update
the history restore flow around +history-revert and +history-revert-status so
only status: done proceeds to post-restore verification; failed, partial_failed,
and other terminal non-success statuses must stop without reporting restoration.
For the exact-entry path, obtain and retain the target document snapshot,
including content and reference_map, so the final docs +fetch verification can
compare against it.


> 路径是二选一:精确 history entry → `history-revert`;无精确 entry但目标 revision 可读 → `fetch target revision` → `overwrite` → `fetch latest verify`。不要猜邻近版本,也不要用重复 help、重复读取 Skill/Reference 或大型探针脚本替代这条有界流程。

## 按 revision_id 或时间点回滚

Expand All @@ -18,8 +23,110 @@
1. 执行 `docs +history-list --doc <doc>` 获取第一页历史记录;`+history-list` 是分页接口,只有 `has_more=true` 且还需要更多候选时才继续传 `--page-token` 翻页。
2. 如果用户给出 `revision_id`:先筛选当前页中 `entries[].revision_id == 用户给出的 revision_id`。如果未命中且 `has_more=true`,继续拉下一页;如果已经命中候选,最多额外再拉一页,补齐同一个 `revision_id` 可能跨页出现的相邻 `history_version_id`。若用户同时给出目标时间,在候选里选择 `edit_time` 与目标时间最接近的一条;若未给目标时间但候选只有一条,可直接使用;若多个候选无法可靠区分,不要自行取第一条,向用户展示候选并确认。
3. 如果用户只给出时间:用 `entries[].edit_time` 匹配,选择目标时刻之前最近的一条;如果用户表达的是“最接近某时刻”,则选择绝对时间差最小的一条。
4. 从最终匹配条目读取 `history_version_id`。`history_version_id` 对应服务端 `minor_history.version`,这是回滚接口需要的 ID。
5. 执行 `docs +history-revert --doc <doc> --history-version-id <history_version_id>`。
4. 如果存在最终精确匹配条目,从该条目读取 `history_version_id`。`history_version_id` 对应服务端 `minor_history.version`,这是回滚接口需要的 ID。
5. 有精确匹配时,执行 `docs +history-revert --doc <doc> --history-version-id <history_version_id>`,完成后 fetch 最新完整文档核验。
6. 没有精确匹配且用户给出了目标 `revision_id` 时,执行以下 fallback;只有目标 revision 确实可读,才可覆盖当前文档:

```bash
# 在子 shell 中 fail closed,避免任一校验失败后继续执行 overwrite
(
set -eu

# 每个恢复任务使用独占临时目录;target/current/latest 各用独立工作目录隔离 fetch sidecar
doc_restore_tmp=$(mktemp -d "${TMPDIR:-/tmp}/lark-doc-restore.XXXXXX") || exit 1
readonly doc_restore_tmp
trap 'rm -rf -- "$doc_restore_tmp"' EXIT
mkdir "$doc_restore_tmp/target" "$doc_restore_tmp/current" "$doc_restore_tmp/latest"

# 读取目标 revision,并将同一响应的正文与 sidecar 分开保存
target_revision_id="<target_revision_id>"
(
cd "$doc_restore_tmp/target"
lark-cli docs +fetch --doc "<doc>" --revision-id "$target_revision_id" --detail full --format json > target-revision.json
)
jq -e --arg revision_id "$target_revision_id" \
'.data.document.revision_id | tostring == $revision_id' \
"$doc_restore_tmp/target/target-revision.json" > /dev/null
jq -j '.data.document.content' "$doc_restore_tmp/target/target-revision.json" > "$doc_restore_tmp/target/target-content.xml"
jq '.data.document.reference_map // {}' "$doc_restore_tmp/target/target-revision.json" > "$doc_restore_tmp/target/target-reference-map.json"
Comment on lines +47 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject incomplete target responses before overwrite.

The revision check validates only revision_id. jq -j can still succeed when content is missing or null, which can write the literal null into target-content.xml and start a destructive overwrite. Require a string content value before extracting it.

Proposed validation
-jq -j '.data.document.content' "$doc_restore_tmp/target/target-revision.json" > "$doc_restore_tmp/target/target-content.xml"
+jq -er '.data.document.content | select(type == "string")' \
+  "$doc_restore_tmp/target/target-revision.json" > "$doc_restore_tmp/target/target-content.xml"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jq -e --arg revision_id "$target_revision_id" \
'.data.document.revision_id | tostring == $revision_id' \
"$doc_restore_tmp/target/target-revision.json" > /dev/null
jq -j '.data.document.content' "$doc_restore_tmp/target/target-revision.json" > "$doc_restore_tmp/target/target-content.xml"
jq '.data.document.reference_map // {}' "$doc_restore_tmp/target/target-revision.json" > "$doc_restore_tmp/target/target-reference-map.json"
jq -e --arg revision_id "$target_revision_id" \
'.data.document.revision_id | tostring == $revision_id' \
"$doc_restore_tmp/target/target-revision.json" > /dev/null
jq -er '.data.document.content | select(type == "string")' \
"$doc_restore_tmp/target/target-revision.json" > "$doc_restore_tmp/target/target-content.xml"
jq '.data.document.reference_map // {}' "$doc_restore_tmp/target/target-revision.json" > "$doc_restore_tmp/target/target-reference-map.json"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-doc/references/lark-doc-history.md` around lines 47 - 51,
Strengthen target response validation before the extraction commands: in the
revision-check jq expression, require that .data.document.content is a string in
addition to matching target_revision_id. Keep the existing failure behavior so
jq -j does not run when content is missing or null, preventing
target-content.xml from being overwritten with invalid data.


# 写前读取当前 revision_id;将它作为乐观锁,避免静默覆盖并发编辑
(
cd "$doc_restore_tmp/current"
lark-cli docs +fetch --doc "<doc>" --detail full --format json > current-document.json
)
target_document_id=$(jq -er '.data.document.document_id' "$doc_restore_tmp/target/target-revision.json")
current_document_id=$(jq -er '.data.document.document_id' "$doc_restore_tmp/current/current-document.json")
if [ "$target_document_id" != "$current_document_id" ]; then
printf 'target/current document_id mismatch; refusing overwrite\n' >&2
exit 1
fi
current_revision_id=$(jq -er '.data.document.revision_id' "$doc_restore_tmp/current/current-document.json")

# 从 target 工作目录配套回放正文与 sidecar,确保 reference_map 的相对路径解析到目标快照
# 只使用 +update --help 中定义的参数;该命令没有 --yes,不要添加未定义的确认参数
(
cd "$doc_restore_tmp/target"
lark-cli docs +update --doc "<doc>" --command overwrite \
--revision-id "$current_revision_id" \
--content @target-content.xml \
--reference-map @target-reference-map.json
)

# 读取最新完整文档,并与目标 revision 的 content/reference_map 比对
(
cd "$doc_restore_tmp/latest"
lark-cli docs +fetch --doc "<doc>" --detail full --format json > latest-document.json
)

python3 - \
"$doc_restore_tmp/target/target-revision.json" \
"$doc_restore_tmp/latest/latest-document.json" \
"$doc_restore_tmp/target" \
"$doc_restore_tmp/latest" <<'PY'
import copy
import json
import re
import sys
from pathlib import Path

def document(path):
with open(path, encoding="utf-8") as stream:
return json.load(stream)["data"]["document"]

def normalize_content(value):
return re.sub(r'\s+id="[^"]*"', "", value or "")

def materialize_paths(value, base):
value = copy.deepcopy(value or {})
if isinstance(value, dict):
path = value.get("path")
if isinstance(path, str) and path.startswith("@"):
relative = path[1:]
candidate = (base / relative).resolve()
if base.resolve() not in candidate.parents:
raise SystemExit(f"reference path escapes snapshot directory: {relative}")
value["data"] = candidate.read_text(encoding="utf-8")
value["path"] = ""
return {key: materialize_paths(item, base) for key, item in value.items()}
if isinstance(value, list):
return [materialize_paths(item, base) for item in value]
return value

target = document(sys.argv[1])
latest = document(sys.argv[2])
content_matches = normalize_content(target.get("content")) == normalize_content(latest.get("content"))
target_references = materialize_paths(target.get("reference_map"), Path(sys.argv[3]))
latest_references = materialize_paths(latest.get("reference_map"), Path(sys.argv[4]))
references_match = target_references == latest_references
print(json.dumps({"content_matches": content_matches, "reference_map_matches": references_match}))
if not content_matches or not references_match:
raise SystemExit("latest document does not match target revision")
PY
)
```

`mktemp -d` 生成的目录只属于当前恢复任务;退出时只清理该目录,不要删除共享工作区中的固定文件。若目标 revision 的完整内容包含引用或资源块,必须按 `lark-doc-fetch.md` 返回的 `reference_map` 和 `lark-doc-update.md` 的保真规则一并回放,不得降级成纯文本或占位符。若服务端因 revision 冲突拒绝写入,重新检查当前内容并向用户说明,不要自动覆盖。

候选确认时使用类似格式:

Expand Down
Loading