mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
docs: align create workflow word count check
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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 子任务要求
|
||||
|
||||
@@ -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 子任务要求
|
||||
|
||||
@@ -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-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()
|
||||
Reference in New Issue
Block a user