diff --git a/skills/lark-doc/SKILL.md b/skills/lark-doc/SKILL.md index 32c3c5e0..a0b1c70b 100644 --- a/skills/lark-doc/SKILL.md +++ b/skills/lark-doc/SKILL.md @@ -39,7 +39,6 @@ lark-cli docs +update --doc "文档URL或token" --command append --content '

- 连续执行多个文档写操作时,必须按 [`lark-doc-update.md`](references/lark-doc-update.md) 的「Block ID 生命周期」判断旧 block ID 是否还能复用;`overwrite` / `block_replace` / `block_delete` 后不要复用受影响的旧 ID,插入 / 复制后要重新 fetch 才能拿到新 block ID - 用户需要在文档内**创建、复制或移动**资源块(画板、电子表格、多维表格等)时,必须先读取 [`lark-doc-xml.md`](references/lark-doc-xml.md) 的「三、资源块」章节 - 写文档时,由内容和用户意图决定表达形式;流程、架构、路线图、关键指标等信息可以使用画板,但不要默认把重要信息都画板化 -- 用户给了明确字数要求(写 N 字 / x-y 字 / x 字左右 / 上下浮动)→ 生成或改写后用 `scripts/count_chars.py`(lark-doc skill 根的 `scripts/` 下,对齐飞书「总字数」)校验,按 create/update workflow 的「字数校验」闭环处理(最多 2 轮,不达标如实告知) - 新增画板:思维导图/时序图/类图/饼图/甘特图用 Mermaid,由**主 Agent 直接插入** ``,无需 SubAgent;其他图表隔离到 SubAgent——简单图由 SubAgent 直接插入 `完整 SVG`(不读 `lark-whiteboard`),复杂图由主 Agent 先建 ``,再启动 SubAgent 读取 `lark-whiteboard` 写入 - 用户说"看一下文档里的图片/附件/素材""预览素材" → 用 `lark-cli docs +media-preview` - 用户明确说"下载素材" → 用 `lark-cli docs +media-download` diff --git a/skills/lark-doc/references/style/lark-doc-create-workflow.md b/skills/lark-doc/references/style/lark-doc-create-workflow.md index 6450a4e4..21f466f1 100644 --- a/skills/lark-doc/references/style/lark-doc-create-workflow.md +++ b/skills/lark-doc/references/style/lark-doc-create-workflow.md @@ -48,11 +48,11 @@ ### 步骤四:字数校验(无明确字数要求则跳过) -**仅当**用户给了明确字数要求(写 N 字 / x-y 字 / x 字左右 / 上下浮动)时执行;否则**跳过本步**。字数必须用脚本量,不要自己估。 +**仅当**用户给了明确字数要求(写 N 字 / x-y 字 / x 字左右 / 上下浮动)时执行;否则**跳过本步**。字数必须用脚本统计,不要自己估。 -1. 把要求归一成参数:`>x`→`--min x`;` <上面的目标参数>`(脚本在 lark-doc skill 根的 `scripts/` 下) -3. 看输出 `verdict`:`pass` 即通过;`under` → 在最该展开的节补**实质内容**(非注水);`over` → 从最长/最冗余处删减。改完**重新跑脚本复测** +1. 把要求归一成目标区间:`>x`→`[x, +∞)`;`x`→`--min x`;` <上面的目标参数>`(脚本在 lark-doc skill 根的 `scripts/` 下) -3. 看输出 `verdict`:`pass` 即通过;`under` → 在最该展开处补**实质内容**(非注水);`over` → 从最长/最冗余处删减。改完**重新跑脚本复测** +1. 把要求归一成目标区间:`>x`→`[x, +∞)`;` - # 直接数一段文本(stdin / 文件) - echo "文本" | uv run scripts/count_chars.py - uv run scripts/count_chars.py --file draft.txt - # 带目标校验(任选其一): - uv run scripts/count_chars.py --doc --min 380 --max 420 # 区间 [x,y] - uv run scripts/count_chars.py --doc --min 100 # >=x(>x) - uv run scripts/count_chars.py --doc --max 100 # <=y( --approx 400 # x 左右 = ±10% - -输出 JSON:{total_words, total_chars, target:{min,max}, verdict, gap} - verdict: pass | under | over | none(未给目标) - gap: 距区间还差多少(under/over 均为正数,pass=0)——告诉你要 +gap 或 -gap 字 -""" -import sys -import re -import json -import argparse -import subprocess - - -def fetch_raw_content(doc_id, identity): - cmd = ["lark-cli", "api", "GET", - f"/open-apis/docx/v1/documents/{doc_id}/raw_content", "--as", identity] - try: - out = subprocess.run(cmd, capture_output=True, text=True) - except FileNotFoundError: - sys.exit("未找到 lark-cli:请先安装/配置 lark-cli,或改用 --file / stdin 传入文本") - if out.returncode != 0: - sys.exit(f"取 raw_content 失败: {out.stderr or out.stdout}") - try: - return json.loads(out.stdout)["data"]["content"] - except Exception as e: - sys.exit(f"解析 raw_content 失败: {e}\n{out.stdout[:300]}") - - -def is_hanzi(ch): - o = ord(ch) - return (0x4E00 <= o <= 0x9FFF or 0x3400 <= o <= 0x4DBF - or 0xF900 <= o <= 0xFAFF or 0x20000 <= o <= 0x2A6DF) - - -def is_zh_punct(ch): - o = ord(ch) - # CJK 符号与标点 / 兼容形式 - if 0x3000 <= o <= 0x303F or 0xFE10 <= o <= 0xFE1F or 0xFE30 <= o <= 0xFE4F: - return True - # 全角 ASCII 标点(排除全角数字 FF10-FF19、全角字母 FF21-FF3A / FF41-FF5A) - if (0xFF01 <= o <= 0xFF0F or 0xFF1A <= o <= 0xFF20 - or 0xFF3B <= o <= 0xFF40 or 0xFF5B <= o <= 0xFF65 - or 0xFFE0 <= o <= 0xFFEE): # 全角货币 ¥£¢ 等 - return True - return ch in "·—…“”‘’" - - -def count(text): - hanzi = sum(1 for ch in text if is_hanzi(ch)) - zh_punct = sum(1 for ch in text if is_zh_punct(ch)) - en_words = len(re.findall(r"[A-Za-zÀ-ÿĀ-ɏḀ-ỿ]+", text)) - digits = len(re.findall(r"[0-90-9]", text)) # 数字按位计 - total_words = hanzi + zh_punct + en_words + digits - # 总字符数 = 所有非空白、非控制字符(仅供参考) - total_chars = sum(1 for ch in text if (not ch.isspace()) and ord(ch) >= 0x20) - return total_words, total_chars - - -def judge(words, mn, mx): - if mn is None and mx is None: - return "none", 0 - if mn is not None and words < mn: - return "under", mn - words - if mx is not None and words > mx: - return "over", words - mx - return "pass", 0 - - -def main(): - ap = argparse.ArgumentParser(description="飞书文档总字数统计与字数遵循校验") - ap.add_argument("--doc", help="文档 document_id(自动取 raw_content)") - ap.add_argument("--file", help="从文件读取文本") - ap.add_argument("--as", dest="identity", default="user", help="身份:user(默认)|bot|auto") - ap.add_argument("--min", type=int, help="字数下限(>=x)") - ap.add_argument("--max", type=int, help="字数上限(<=y)") - ap.add_argument("--approx", type=int, help="x 左右:自动展开为 [round(0.9x), round(1.1x)]") - args = ap.parse_args() - - if args.doc: - text = fetch_raw_content(args.doc, args.identity) - elif args.file: - try: - text = open(args.file, encoding="utf-8").read() - except OSError as e: - sys.exit(f"读取文件失败: {e}") - elif not sys.stdin.isatty(): - text = sys.stdin.read() - else: - ap.error("需提供 --doc / --file 或从 stdin 传入文本") - - mn, mx = args.min, args.max - if args.approx is not None: - mn, mx = round(args.approx * 0.9), round(args.approx * 1.1) - - total_words, total_chars = count(text) - verdict, gap = judge(total_words, mn, mx) - - print(json.dumps({ - "total_words": total_words, - "total_chars": total_chars, - "target": {"min": mn, "max": mx}, - "verdict": verdict, - "gap": gap, - }, ensure_ascii=False)) - - -if __name__ == "__main__": - main()