Skill: 2403861767/word-typewriter-demo (version 1.0.0)

--- name: word-typewriter-demo description: "在 Word 中逐字打字显示内容，支持翻页、对齐控制、字体字号自定义，适合现场演示 AI 操作 Word 的场景。格式由内容文件驱动，不拘泥于任何固定模板。" metadata: { "openclaw": { "repository": "https://github.com/你的用户名/word-typewriter-demo", "clawhub": "https://clawhub.com/skill/word-typewriter-demo" } } --- # Word 打字机演示技能 在 Microsoft Word 中逐字逐句地写入内容，展示 AI 实时操作 Word 的视觉效果。 ## 核心原则 **格式由内容文件说了算**，脚本只负责执行。要什么格式，在内容文件里用标记控制。 ## 功能 - **逐字打字** — 一个字一个字地出现在 Word 中，有真实的打字节奏 - **自动滚屏** — 每写一个字自动滚动页面，确保最新内容始终在视野内 - **分页支持** — `---page---` 标记插入分页符 - **对齐控制** — `---left---` / `---center---` / `---right---` 标记切换对齐 - **标题层级** — `# ` 黑体 / `## ` 楷体 / `### ` 仿宋加粗 - **格式校正** — 写完自动扫描全文，校正字体/字号/粗体/对齐/缩进等偏差 - **速度可调** — 标题快、正文慢，节奏感强 ## 使用方式 ```bash python {baseDir}/scripts/word_demo.py --file "C:\path\content.txt" ``` ## 参数说明 | 参数 | 说明 | 默认值 | | ---------- | ------------------------------------------- | --------------------- | | `--file` | 从文本文件读取内容（每行为一段） | 使用内置示例 | | `--speed` | 打字速度，0.01=极快，0.15=很慢 | 0.06 | | `--title` | 文档标题（默认居中显示） | "OpenClaw · 实时文档生成演示" | | `--format` | 格式类型：`default` 或 `gov`（gov 启用国标页边距/行距/字体校正） | `default` | | `--save` | 保存路径 | 桌面 | ## 内容文件标记语法 内容文件是纯文本，每行一个段落。支持以下标记： ### 标题层级（仅 `--format gov` 时有效） | 标记 | 效果 | | ------------ | -------------------- | | `# 一、xxx` | 一级标题 → 黑体（不加粗），左对齐 | | `## （一）xxx` | 二级标题 → 楷体（加粗），左对齐 | | `### 1. xxx` | 三级标题 → 仿宋（加粗），左对齐 | | 无标记的文本 | 正文 → 仿宋（不加粗），左对齐首行缩进 | ### 对齐控制（通用，所有格式都支持） | 标记 | 效果 | | -------------- | ---------- | | `---left---` | 切换为左对齐（默认） | | `---center---` | 切换为居中 | | `---right---` | 切换为右对齐 | ### 分页控制 | 标记 | 效果 | | ------------ | ----- | | `---page---` | 插入分页符 | ## 示例 ### 政府公文格式日报 ```bash python {baseDir}/scripts/word_demo.py --file report.txt --format gov --title "报告标题" --speed 0.06 ``` ### 普通通用文档 ```bash python {baseDir}/scripts/word_demo.py --file notes.txt --title "会议纪要" --speed 0.08 ``` ## 内容文件示例 ``` # 一、工作完成情况 ## （一）Spring 生态调研 这是正文内容，自动左对齐首行缩进。 ---right--- 报告人：张三 日 期：二零二六年五月十九日 ``` ## 自检与校正 - 写完内容后自动扫描全篇 - 逐段比对字体、字号、粗体、对齐、缩进、行距 - 发现偏差自动修正，终端输出修正数量 - 非 gov 格式仅做基本字号检查 ## 依赖 - Windows 系统 - Microsoft Word 或 WPS Writer - Python + pywin32 (`python -m pip install pywin32`)

path: content_demo.txt
content: # 🦞 小龙虾 AI 助手 · 演示场景手册

---center---
版本：v1.0　　日期：2026年5月20日
---left---

---page---

# 场景一：手机上发消息找电脑上的文件

---left---

演示者通过手机QQ或者微信发语音或者打字让小龙虾找到指定文件并发送过来。

等待几秒钟，小龙虾成功找到并发送过来。

可能失败结果

1. 语音描述不准确，小龙虾找错了，或者没听清找了好几个，但不确定发哪个。

解决办法

1. 找错了 → 给更完整的提示词重新找。"请在我电脑所有盘中找xxx"，或记得位置的话直接说"请在xxx位置找xxx"。
2. 找到了但不确定发哪个 → 如果只有两三个可自己选让小龙虾发；文件太多则优化提示词（格式、日期），或让他全部发过来。

---page---

# 场景二：生成一个海报

---left---

演示者通过手机QQ或者微信发送指令，让他生成一个海报。

主要问题：海报内容可能和描述有点偏差，需要比较清晰的指令。

解决办法

1. 先去免费的对话AI问一个prompt，复制进来发给龙虾。
2. 或者直接在小龙虾里面下指令："我要生成一个什么样子的海报，大概什么分辨率，要不要有文字，主要风格是什么样子，然后你去自我优化我的prompt，再生成出来发送过来。"

---page---

# 场景三：写程序能力

---left---

演示者通过手机QQ或者微信发送指令，让他写一个计算器之类的工具类型的代码，并运行出来。

小龙虾成功跑出来并且无误可以使用。

可能失败的结果：程序报错。

解决办法：让小龙虾自己解决报错重新跑出来。

---page---

# 场景四：操作浏览器

---left---

演示者通过手机QQ或者微信发送指令，让他去操作浏览器。

演示效果方面不是很好，操作流程慢，且显示不了流程，因为是通过控制浏览器来实现的。

---page---

# 场景五：每日日报

---left---

演示者通过手机QQ或者微信给小龙虾发指令。

准备内容：邮箱里面有各个部门今日发来的邮件。

操作流程

1. 你现在阅读我的邮件，今日是不是有各部门发来的邮件，你将其汇总一下。
2. 汇总以后填入桌面日报方案文件夹里的每日汇总文件夹里，填入汇总表中，原始模板还放在那里，生成今日汇总表后面接日期。
3. 汇总表填完以后，生成一份今日报告后面接日期放到文件夹每日报告中，要求是中国政府公文格式。
4. 写完以后保存并关闭该报告发送到邮箱 xxxxx。

可能失败结果 1：发送邮箱给别人以后，点开文件是空的。

解决办法：
- 本身内容就是空的，让他重新写。
- 可能是因为打开了该文档，让小龙虾关闭文档后再发送。

可能失败结果 2：小龙虾因为每日日报写太久了，导致小龙虾自己生成了一份word发送，两个文件内容不一致。

解决办法：
- 给他优化指令，强行让两个一样。
- 让他记住自己检查有没有写完然后关闭文档以后再发送。

---page---

---center---
本手册仅供内部演示参考使用
---left---

path: demo_style.json
content: {
  "title": {
    "font": "微软雅黑",
    "size": 22,
    "bold": true,
    "align": 1
  },
  "h1": {
    "font": "微软雅黑",
    "size": 16,
    "bold": true,
    "align": 0
  },
  "h2": {
    "font": "微软雅黑",
    "size": 13,
    "bold": true,
    "align": 0
  },
  "h3": {
    "font": "微软雅黑",
    "size": 12,
    "bold": true,
    "align": 0
  },
  "body": {
    "font": "微软雅黑",
    "size": 11,
    "bold": false,
    "align": 0,
    "indent": false
  }
}


path: metadata.json
content: {
  "name": "word-typewriter-demo",
  "version": "1.0.0",
  "description": "在 Word 中逐字打字显示内容，支持翻页、对齐控制、字体字号自定义，适合现场演示 AI 操作 Word 的场景。格式由内容文件驱动，不拘泥于任何固定模板。",
  "author": "凉风辞叶",
  "tags": ["Word", "打字机", "演示", "WPS", "逐字打字", "公文", "自动化"],
  "repository": "https://github.com/2403861767/word-typewriter-demo",
  "license": "MIT"
}


path: scripts/word_demo.py
content: # -*- coding: utf-8 -*-
"""
Word 打字机演示脚本（v9）
- 逐字打字 + 智能滚屏
- 格式完全由 --style JSON 驱动，脚本不做硬编码假设
- --format gov 仅设页边距/行距等页面属性
- 所有字体/字号/粗体/对齐/缩进由 --style 决定
- 写完自动对照 --style 进行格式校正
- 对齐标记: ---right--- / ---left--- / ---center---
"""

import win32com.client, time, os, argparse, json, signal, sys
import pythoncom

DESKTOP = os.path.join(os.path.expanduser('~'), 'Desktop')

# 默认 gov 格式的 style 配置（仅做默认值，不写死到代码逻辑）
GOV_STYLE_DEFAULTS = {
    "h1": {"font": "黑体", "size": 16, "bold": False, "align": 0},
    "h2": {"font": "楷体", "size": 16, "bold": True, "align": 0},
    "h3": {"font": "仿宋", "size": 16, "bold": True, "align": 0},
    "body": {"font": "仿宋", "size": 16, "bold": False, "align": 0, "indent": True}
}

def pump():
    pythoncom.PumpWaitingMessages()

def need_scroll(win):
    try:
        visible = win.VisibleRange
        sel_end = win.Selection.Range.End
        vis_end = visible.End
        return sel_end > vis_end - 80
    except:
        return False

def scroll_into_view(win):
    try:
        win.ScrollIntoView(win.Selection.Range, False)
    except:
        pass

def typewrite(sel, text, delay, win):
    i = 0
    while i < len(text):
        batch_size = 2 if delay > 0.05 else 4
        chunk = text[i:i+batch_size]
        try:
            sel.TypeText(chunk)
        except:
            for ch in chunk:
                try:
                    sel.TypeText(ch)
                except:
                    pass
        i += batch_size
        time.sleep(delay)
        pump()
        if i % 8 == 0 and need_scroll(win):
            scroll_into_view(win)


def auto_correct_doc(doc, word, paragraphs, style_cfg):
    """写完文档后，逐段检查并校正格式。style_cfg 是用户指定的 --style JSON"""
    print('[校正] 开始自动格式校正...')
    fixes = 0

    h1s = style_cfg.get('h1', GOV_STYLE_DEFAULTS['h1'])
    h2s = style_cfg.get('h2', GOV_STYLE_DEFAULTS['h2'])
    h3s = style_cfg.get('h3', GOV_STYLE_DEFAULTS['h3'])
    bs  = style_cfg.get('body', GOV_STYLE_DEFAULTS['body'])

    # 段落类型对应的目标格式
    def get_target(text, para_idx):
        if para_idx == 0 or text.startswith('报告人') or text.startswith('日　期'):
            return style_cfg.get('title', {'font': '黑体', 'size': 22, 'bold': True, 'align': 1})
        return bs  # default to body

    para_idx = 0
    total_paras = doc.Content.Paragraphs.Count
    for i in range(1, total_paras + 1):
        try:
            para = doc.Content.Paragraphs.Item(i)
            text = para.Range.Text.strip()
            if len(text) < 2 or text.startswith('---'):
                continue
        except:
            continue

        rng = para.Range
        s = rng.Font.Size
        f = rng.Font.Name
        b = rng.Font.Bold
        a = para.Alignment
        indent = para.Format.FirstLineIndent

        # 判断段落类型
        target = {}
        for pt in paragraphs:
            pt = pt.strip()
            if pt.startswith('# ') and (pt[2:] in text or text in pt[2:]):
                target = h1s
                break
            if pt.startswith('## ') and (pt[3:] in text or text in pt[3:]):
                target = h2s
                break
            if pt.startswith('### ') and (pt[4:] in text or text in pt[4:]):
                target = h3s
                break
        if not target:
            target = bs

        # 校正字体
        try:
            tf = target.get('font', '仿宋')
            if f and tf not in str(f):
                rng.Font.Name = tf
                fixes += 1
        except:
            pass

        # 校正在size
        try:
            ts = target.get('size', 16)
            if s and abs(s - ts) > 0.5:
                rng.Font.Size = ts
                fixes += 1
        except:
            pass

        # 校正粗体
        try:
            tb = target.get('bold', False)
            if b is not None and b != tb:
                rng.Font.Bold = tb
                fixes += 1
        except:
            pass

        # 校正对齐
        try:
            ta = target.get('align', 0)
            if a is not None and a != ta:
                para.Alignment = ta
                fixes += 1
        except:
            pass

        # 校正首行缩进
        try:
            need_indent = target.get('indent', False)
            target_indent = word.Application.CentimetersToPoints(0.74) if need_indent else 0
            if indent is not None and abs(indent - target_indent) > 1:
                para.Format.FirstLineIndent = target_indent
                fixes += 1
        except:
            pass

        para_idx += 1

    print(f'[校正] 完成，共修正 {fixes} 处格式问题')
    return fixes


def check_document(doc, paragraphs, gov_format, title):
    issues = []
    if not title or '实时文档' in title:
        issues.append('未指定文档标题')
    if issues:
        print('[自检] 发现以下问题：')
        for iss in issues:
            print(f'  [注意] {iss}')
    else:
        print('[自检] 格式检查全部通过')
    print()


def cleanup_blank_docs(word):
    """关闭所有未保存的空白文档（形如"文字文稿N"或"文档N"），不保存"""
    count = word.Documents.Count
    for i in range(count, 0, -1):  # 倒序遍历，避免删除时索引变化
        try:
            doc = word.Documents.Item(i)
            name = doc.Name
            # 判断是否为未保存的空白文档
            is_blank = False
            for prefix in ['文字文稿', '文档']:
                if name.startswith(prefix):
                    rest = name[len(prefix):].strip().rstrip('.* ')
                    if rest.isdigit() or not rest:
                        is_blank = True
                        break
            if is_blank and not doc.Saved:
                print(f'[清理] 关闭空白文档: {name}')
                doc.Close(SaveChanges=False)
        except:
            pass


def get_or_create_doc(word):
    """获取或创建文档：复用第一个未保存的空白文档，否则新建"""
    try:
        if word.Documents.Count > 0:
            for i in range(1, word.Documents.Count + 1):
                doc = word.Documents.Item(i)
                if not doc.Saved:
                    print(f'[文档] 复用已有文档: {doc.Name}')
                    # 清除内容，准备写入新内容
                    try:
                        doc.Content.Delete()
                    except:
                        pass
                    return doc
    except:
        pass
    return word.Documents.Add()


def run_demo(title, paragraphs, speed, gov_format=False, save_path=None, style_cfg=None):
    # 使用 DispatchEx 创建独立 WPS/Word 实例，避免污染用户已有窗口
    word = win32com.client.DispatchEx("Word.Application")
    word.Visible = True
    word.DisplayAlerts = False

    # 清理已有空白文档
    cleanup_blank_docs(word)

    # 复用空白文档或创建新文档（避免每次新增一个空白文档）
    doc = get_or_create_doc(word)

    sel = word.Selection
    sel.Font.Name = '宋体'
    sel.Font.Size = 12
    win = word.ActiveWindow
    pump()
    time.sleep(0.5)

    try:
        word.Activate()
    except:
        pass

    # gov 格式仅设页面属性
    if gov_format:
        for section in doc.Sections:
            section.PageSetup.TopMargin = word.Application.CentimetersToPoints(3.7)
            section.PageSetup.BottomMargin = word.Application.CentimetersToPoints(3.5)
            section.PageSetup.LeftMargin = word.Application.CentimetersToPoints(2.8)
            section.PageSetup.RightMargin = word.Application.CentimetersToPoints(2.6)
        sel.ParagraphFormat.LineSpacingRule = 4
        sel.ParagraphFormat.LineSpacing = 30

    # 解析样式配置
    fmt = style_cfg or {}

    def style_for(key, default):
        s = fmt.get(key, {})
        merged = default.copy()
        merged.update(s)
        return merged

    def sf(sz, bold, font=None):
        sel.Font.Size = sz
        sel.Font.Bold = bold
        if font:
            sel.Font.Name = font

    def nl():
        sel.TypeParagraph()
        pump()

    def wl(text, d, sz=12, b=False, font=None, align=None, indent=False, indent_chars=0):
        sf(sz, b, font)
        if align is not None:
            sel.ParagraphFormat.Alignment = align
        if indent_chars > 0:
            sel.ParagraphFormat.FirstLineIndent = word.Application.CentimetersToPoints(indent_chars * 0.37)
        else:
            sel.ParagraphFormat.FirstLineIndent = (
                word.Application.CentimetersToPoints(0.74) if indent else 0
            )
        typewrite(sel, text, d, win)
        nl()
        if need_scroll(win):
            scroll_into_view(win)
        try:
            word.Application.ScreenRefresh()
        except:
            pass

    # 获取各层级样式
    h1s = style_for('h1', GOV_STYLE_DEFAULTS['h1'])
    h2s = style_for('h2', GOV_STYLE_DEFAULTS['h2'])
    h3s = style_for('h3', GOV_STYLE_DEFAULTS['h3'])
    bs  = style_for('body', GOV_STYLE_DEFAULTS['body'])
    ts  = style_for('title', GOV_STYLE_DEFAULTS['h1'].copy())
    ts['align'] = 1  # 标题默认居中

    # 输出标题（如果内容第一行不是相同标题）
    skip_title = False
    if paragraphs and paragraphs[0].strip().startswith('# '):
        first_heading = paragraphs[0].strip()[2:].strip()
        if first_heading == title:
            skip_title = True

    if not skip_title:
        wl(title, 0.07, ts['size'], ts['bold'], ts['font'], ts.get('align', 1))
        time.sleep(0.2)
        nl()
        nl()
        time.sleep(0.2)

    # 对齐控制变量
    current_align = 0

    # 正文段落循环
    for para in paragraphs:
        p = para.strip()
        if not p:
            continue

        if p == '---page---':
            sel.InsertBreak(7)
            pump()
            time.sleep(0.4)
            continue
        if p == '---right---':
            current_align = 2
            continue
        if p == '---left---':
            current_align = 0
            continue
        if p == '---center---':
            current_align = 1
            continue

        # 使用 --style 设定的格式
        if p.startswith('# '):
            wl(p[2:], max(0.03, speed * 0.6),
               sz=h1s['size'], b=h1s['bold'], font=h1s['font'],
               align=h1s.get('align', 0))
            time.sleep(0.15)
        elif p.startswith('## '):
            wl(p[3:], max(0.03, speed * 0.7),
               sz=h2s['size'], b=h2s['bold'], font=h2s['font'],
               align=h2s.get('align', 0))
            time.sleep(0.1)
        elif p.startswith('### '):
            wl(p[4:], max(0.03, speed * 0.7),
               sz=h3s['size'], b=h3s['bold'], font=h3s['font'],
               align=h3s.get('align', 0))
            time.sleep(0.1)
        else:
            wl(p, speed,
               sz=bs['size'], b=bs['bold'], font=bs['font'],
               align=current_align if current_align != 0 else bs.get('align', 0),
               indent=(bs.get('indent', False) and current_align == 0))
            time.sleep(0.08)

    # 结尾空行
    nl()
    pump()
    time.sleep(0.3)

    # 写完自动校正
    if style_cfg:
        print('[校正] 正在对照 --style 配置进行格式校正...')
        fixes = auto_correct_doc(doc, word, paragraphs, style_cfg)
        if fixes == 0:
            print('[校正] 所有格式均符合要求，无需修正')
        try:
            word.Application.ScreenRefresh()
        except:
            pass
        time.sleep(0.5)

    # 自检
    check_document(doc, paragraphs, gov_format, title)

    # 保存
    if save_path:
        try:
            abs_path = os.path.abspath(save_path)
            doc.SaveAs(abs_path)
            print(f'[OK] 文档已自动保存到: {abs_path}')
        except Exception as e:
            print(f'[FAIL] 自动保存失败: {e}')

    print('关闭 Word 或按 Ctrl+C 退出。')
    try:
        while True:
            time.sleep(1)
            pump()
    except KeyboardInterrupt:
        pass
    finally:
        # 关闭当前文档（不保存），然后退出，避免下次启动时 WPS 堆积空白文档
        try:
            if doc:
                doc.Close(SaveChanges=False)
        except:
            pass
        try:
            word.Quit()
        except:
            pass


def main():
    parser = argparse.ArgumentParser(description='Word 打字机演示')
    parser.add_argument('--file', help='内容文件路径')
    parser.add_argument('--speed', type=float, default=0.06, help='打字速度')
    parser.add_argument('--title', default='OpenClaw 实时文档生成演示', help='文档标题')
    parser.add_argument('--format', choices=['default', 'gov'], default='default',
                        help='文档格式（gov 仅设页边距/行距，不设字体）')
    parser.add_argument('--style', default='',
                        help='样式JSON，如：{"h1":{"font":"黑体","size":26,"bold":true,"align":1},"body":{"font":"楷体","size":16}}')
    parser.add_argument('--save', default='', help='保存路径')
    args = parser.parse_args()

    if args.file and os.path.exists(args.file):
        with open(args.file, 'r', encoding='utf-8') as f:
            paragraphs = [line.rstrip('\n\r') for line in f if line.strip()]
    else:
        paragraphs = ['# 实时打字演示', '展示 AI 操作 Word 的效果。']

    save_path = args.save if args.save else ''
    if not save_path and args.title:
        safe_name = args.title.replace('/', '_').replace('\\', '_').replace(':', '_')
        save_path = os.path.join(DESKTOP, safe_name + '.docx')
    elif not save_path:
        save_path = os.path.join(DESKTOP, 'OpenClaw_文档.docx')

    # 解析样式
    style_cfg = None
    if args.style:
        try:
            style_cfg = json.loads(args.style)
            print(f'[样式] 已加载: {style_cfg}')
        except json.JSONDecodeError as e:
            print(f'[样式] JSON 解析失败: {e}，使用默认样式')

    run_demo(args.title, paragraphs, args.speed, args.format == 'gov', save_path, style_cfg)


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (CRITICAL)