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()