docs: align create workflow word count check

This commit is contained in:
fangshuyu
2026-07-02 12:11:29 +08:00
parent 4579a5713d
commit e5b9f7c8c5
4 changed files with 8 additions and 142 deletions

View File

@@ -39,7 +39,6 @@ lark-cli docs +update --doc "文档URL或token" --command append --content '<p>
- 连续执行多个文档写操作时,必须按 [`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 直接插入** `<whiteboard type="mermaid">…</whiteboard>`,无需 SubAgent其他图表隔离到 SubAgent——简单图由 SubAgent 直接插入 `<whiteboard type="svg">完整 SVG</whiteboard>`(不读 `lark-whiteboard`),复杂图由主 Agent 先建 `<whiteboard type="blank"></whiteboard>`,再启动 SubAgent 读取 `lark-whiteboard` 写入
- 用户说"看一下文档里的图片/附件/素材""预览素材" → 用 `lark-cli docs +media-preview`
- 用户明确说"下载素材" → 用 `lark-cli docs +media-download`

View File

@@ -48,11 +48,11 @@
### 步骤四:字数校验(无明确字数要求则跳过)
**仅当**用户给了明确字数要求(写 N 字 / x-y 字 / x 字左右 / 上下浮动)时执行;否则**跳过本步**。字数必须用脚本,不要自己估。
**仅当**用户给了明确字数要求(写 N 字 / x-y 字 / x 字左右 / 上下浮动)时执行;否则**跳过本步**。字数必须用脚本统计,不要自己估。
1. 把要求归一成参数`>x``--min x``<y``--max y``x-y``--min x --max y``x 字左右``--approx x`(自动 ±10%
2. 量实际字数(对齐飞书「总字数」):`uv run scripts/count_chars.py --doc <document_id> <上面的目标参数>`(脚本在 lark-doc skill 根的 `scripts/` 下)
3. 看输出 `verdict``pass` 即通过;`under` → 在最该展开的节补**实质内容**(非注水);`over` → 从最长/最冗余处删减。改完**重新跑脚本复测**
1. 把要求归一成目标区间`>x``[x, +∞)``<y``(-∞, y]``x-y``[x, y]``x 字左右``[round(0.9x), round(1.1x)]`
2. 按 [`lark-doc-word-stat.md`](../lark-doc-word-stat.md) 统计文档 URL 或 token 对应文档的实际字数,读取输出里的 `word_count`
3. 对比 `word_count` 与目标区间:区间内即通过;低于下限 → 在最该展开的节补**实质内容**(非注水);高于上限 → 从最长 / 最冗余处删减。改完**重新按同一流程统计**
4. **最多 2 轮**。2 轮后仍不达标:停止,不得为达标而注水或删关键内容;如实汇报【目标区间 / 当前字数 / 差值与方向 / 已试 2 轮 / 未达原因】并交付文档链接,**禁止谎称达标**
## Agent 子任务要求

View File

@@ -50,11 +50,11 @@
### 步骤四:字数校验(无明确字数要求则跳过)
**仅当**用户给了明确字数要求(写 N 字 / x-y 字 / x 字左右 / 上下浮动)时执行;否则**跳过本步**。字数必须用脚本,不要自己估。
**仅当**用户给了明确字数要求(写 N 字 / x-y 字 / x 字左右 / 上下浮动)时执行;否则**跳过本步**。字数必须用脚本统计,不要自己估。
1. 把要求归一成参数`>x``--min x``<y``--max y``x-y``--min x --max y``x 字左右``--approx x`(自动 ±10%
2. 量实际字数(对齐飞书「总字数」):`uv run scripts/count_chars.py --doc <document_id> <上面的目标参数>`(脚本在 lark-doc skill 根的 `scripts/` 下)
3. 看输出 `verdict``pass` 即通过;`under` → 在最该展开处补**实质内容**(非注水);`over` → 从最长/最冗余处删减。改完**重新跑脚本复测**
1. 把要求归一成目标区间`>x``[x, +∞)``<y``(-∞, y]``x-y``[x, y]``x 字左右``[round(0.9x), round(1.1x)]`
2. 按 [`lark-doc-word-stat.md`](../lark-doc-word-stat.md) 统计文档 URL 或 token 对应文档的实际字数,读取输出里的 `word_count`
3. 对比 `word_count` 与目标区间:区间内即通过;低于下限 → 在最该展开处补**实质内容**(非注水);高于上限 → 从最长 / 最冗余处删减。改完**重新按同一流程统计**
4. **最多 2 轮**。2 轮后仍不达标:停止,不得为达标而注水或删关键内容;如实汇报【目标区间 / 当前字数 / 差值与方向 / 已试 2 轮 / 未达原因】并交付文档链接,**禁止谎称达标**
## 画板 SubAgent 子任务要求

View File

@@ -1,133 +0,0 @@
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""
统计飞书文档「总字数」,对齐飞书官方字数统计规则,用于字数遵循校验。
飞书官方规则(总字数):汉字 + 中文标点 + 英文单词 + 数字;英文标点、空格不计。
计数源:文档 raw_content飞书已抽取的纯文本标签已剥离
⚠️ raw_content 会包含 @文档 / @人 / 卡片等飞书「不计入字数」的嵌入内容文字。
对「生成的散文」无影响(不含这些嵌入),与飞书一致;
读取嵌入密集的已有文档会偏高,那不是本功能的目标场景。
说明:数字按「位」计(每个数字算 1与飞书总字符数口径一致
总字数含标题(飞书亦含标题)。
用法:
# 给文档 token自动取 raw_content 并计数
uv run scripts/count_chars.py --doc <document_id>
# 直接数一段文本stdin / 文件)
echo "文本" | uv run scripts/count_chars.py
uv run scripts/count_chars.py --file draft.txt
# 带目标校验(任选其一):
uv run scripts/count_chars.py --doc <id> --min 380 --max 420 # 区间 [x,y]
uv run scripts/count_chars.py --doc <id> --min 100 # >=x>x
uv run scripts/count_chars.py --doc <id> --max 100 # <=y<y
uv run scripts/count_chars.py --doc <id> --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-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()