Claude Skill

lov-fill-form

Fill in Word document form templates (.docx) with user-provided data. Reads a template containing tables with label→value cell pairs, detects all fillable fields, and outputs a completed document. Handles CJK/Latin mixed text with proper font switching. Use this skill when the us

LLM Mart · 0 points · 9 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lovstudio-skills-skills_fill-form-77d464c.zip · 10 KB
Part of lovstudio/skills — 83 skills

Install

skills CLI npx skills add https://github.com/lovstudio/skills/tree/main/skills/fill-form
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
Git git clone https://github.com/lovstudio/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.

README

表单小助手 · Form Assistant

Version

Fill Word document form templates (.docx) with structured data. Auto-detects table-based form fields (label → value cell pairs), supports CJK/Latin mixed text with proper font switching, merged cells, and paragraph-based forms.

Part of skill-publisher/skills — by example.com

Install

npx skills add fill-form -g -y

Requires: Python 3.8+ and pip install python-docx

How It Works

┌─────────────────────────────────────────────────┐
│  Template (.docx)                               │
│  ┌──────────┬───────────┬──────────┬──────────┐ │
│  │ 主讲人   │           │ 职称     │          │ │
│  ├──────────┼───────────┴──────────┴──────────┤ │
│  │ 单位     │                                 │ │
│  ├──────────┼───────────┬──────────┬──────────┤ │
│  │ 讲座题目 │           │ 时间     │          │ │
│  └──────────┴───────────┴──────────┴──────────┘ │
└────────────────────┬────────────────────────────┘
                     │ --scan → detect fields
                     │ --data → fill values
                     ▼
┌─────────────────────────────────────────────────┐
│  Filled (.docx)                                 │
│  ┌──────────┬───────────┬──────────┬──────────┐ │
│  │ 主讲人   │ 张三      │ 职称     │ 教授     │ │
│  ├──────────┼───────────┴──────────┴──────────┤ │
│  │ 单位     │ 北京大学                        │ │
│  ├──────────┼───────────┬──────────┬──────────┤ │
│  │ 讲座题目 │ AI与未来  │ 时间     │ 4月10日  │ │
│  └──────────┴───────────┴──────────┴──────────┘ │
└─────────────────────────────────────────────────┘

Usage

Step 1 — Scan the template to see all detected fields:

python fill_form.py --template form.docx --scan

Output:

Detected 6 form fields:

  1. 主讲人
  2. 职称
  3. 单位
  4. 讲座题目
  5. 时间
  6. 地点

--- JSON ---
{
  "主讲人": "",
  "职称": "",
  ...
}

Step 2 — Fill with a JSON data file or inline JSON:

# From JSON file
python fill_form.py --template form.docx --data-file data.json

# Inline JSON
python fill_form.py --template form.docx \
  --data '{"主讲人": "张三", "职称": "教授", "单位": "北京大学"}'

Output saves to the same directory as the template (form_filled.docx).

Options

Option Default Description
--template (required) Path to template .doc/.docx
--output <name>_filled.docx Output path (defaults to template directory)
--scan List all detected form fields
--data JSON string with field → value mapping
--data-file Path to JSON file with field → value mapping
--font Platform CJK serif Font for filled text
--font-size 11 Font size in points

Field Detection

The script detects fillable fields in three ways:

Method How it works Example
Table cells Label in one cell, blank value in adjacent cell │ 姓名 │ ___ │
Merged rows Full-width cell with Label: pattern │ 备注:________________ │
Paragraphs Fallback for docs without tables 姓名: followed by blank line

Fields are matched by normalized label (whitespace-insensitive), so 主 讲 人 matches 主讲人.

Supported Formats

Format Support
.docx Full support (recommended)
.doc Auto-converts via textutil (macOS) or LibreOffice. Table structure may be lost — convert to .docx first for best results.

License

MIT

Skill manifest

表单小助手 · Form Assistant

This skill fills in Word document form templates (.docx) with user-provided data. It detects table-based form fields (label in one cell, value in the adjacent cell) and populates them automatically.

When to Use

  • User has a .docx form template with blank fields to fill
  • User wants to fill in an application form, registration form, etc.
  • Document uses Word tables for form layout (label | value cell pairs)
  • User mentions 填表, 申请表, 登记表, or wants to automate form filling

Workflow (MANDATORY)

You MUST follow these steps in order:

Step 1: Scan the template

Discover all fillable fields:

python lov-fill-form/scripts/fill_form.py --template <path> --scan

Step 2: Pre-fill from known context

Before asking the user, try to fill as many fields as possible from:

  1. User memory — name, title, organization, etc.
  2. Context files — if the user provides reference documents (e.g. STARTER-PROMPT.md, project docs), extract relevant info to fill content-heavy fields
  3. Conversation context — anything already mentioned

For content-heavy fields (e.g. "主要内容/简介/摘要"), actively compose the content by synthesizing from context files, user's known expertise, and the topic/title.

Step 3: Ask only what you don't know

Use AskUserQuestion to collect ONLY the fields you cannot fill from context.

  • Group fields into a single question
  • If ALL fields are unknown, list them all
  • If the user says some fields can be left blank (e.g. "其他朋友会帮我填"), respect that and leave those empty
  • Do NOT force the user to provide every field

Step 4: Fill and save

Write a JSON data file (avoids shell escaping issues with long text), then run:

python lov-fill-form/scripts/fill_form.py \
  --template <path> \
  --data-file /tmp/form_data.json

Output path rules:

  • Default: <template_dir>/<name>_filled.docx (same directory as the template)
  • If the template is in a temp directory or system path, save to user's document directory or ask the user where to save
  • Use --output to override explicitly

CLI Reference

Argument Default Description
--template (required) Path to template .doc/.docx file
--output <template_dir>/<name>_filled.docx Output .docx path
--scan false List all detected form fields
--data "" JSON string with field→value mapping
--data-file "" Path to JSON file with field→value mapping
--font Platform CJK serif Font name for filled text
--font-size 11 Font size in points

How Field Detection Works

  1. Table-based (primary): Scans all tables for rows with label→value cell pairs. A label cell contains short text (CJK or Latin); the adjacent cell is the value field.
  2. Merged rows: Detects full-width merged cells with "Label:" pattern as large text areas.
  3. Paragraph fallback: If no tables found, detects "Label:value" patterns in paragraphs.

Limitations

  • .doc files are auto-converted to .docx via macOS textutil, which loses table structure. For best results, use .docx templates directly. If you only have .doc, convert with LibreOffice first: libreoffice --headless --convert-to docx file.doc
  • Fields are matched by normalized label text (whitespace removed). If a label contains unusual formatting, the match may fail — use --scan to verify detection.

Dependencies

python3 -m pip install python-docx

Runtime context (shared)

运行前读取本 Skill 包的 skill.yaml,由宿主提供 skill-runtime/v1 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。

  • 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
  • required: true 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。
  • 报错提供可复制的 context_id、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。

通用反馈闭环

用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:

  1. 先判断意见是 task-specific(仅本次)还是 reusable(可跨任务复用)。
  2. task-specific 只修改当前任务,不改 Skill。
  3. reusable 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
  4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
  5. reusable 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
Files (skills)
  • scripts
    • fill_form.py 15 KB
      #!/usr/bin/env python3
      """
      fill_form — Fill in Word document form templates.
      
      Reads a .docx template containing tables with label→value cell pairs,
      fills specified fields with provided data, and saves the result.
      
      Supports:
        - .doc input (auto-converts via textutil on macOS)
        - Table-based forms (label in one cell, value in adjacent cell)
        - Multi-row merged cells (e.g., large text areas)
        - CJK/Latin mixed text with font switching
        - Scan mode to list all detected fields
      
      Usage:
        # Scan template to list fields
        python fill_form.py --template form.docx --scan
      
        # Fill form with JSON data
        python fill_form.py --template form.docx --output filled.docx \\
          --data '{"主讲人": "张三", "讲座题目": "AI与未来"}'
      
        # Fill from JSON file
        python fill_form.py --template form.docx --output filled.docx --data-file data.json
      
      Dependencies:
        python3 -m pip install python-docx
      """
      
      import re, os, sys, json, argparse, shutil, subprocess, tempfile
      from copy import deepcopy
      from docx import Document
      from docx.shared import Pt, RGBColor
      from docx.oxml.ns import qn
      import platform as _platform
      
      _PLAT = _platform.system()
      
      # ─── Fonts ───────────────────────────────────────────────────────────
      def _get_fonts():
          if _PLAT == "Darwin":
              return ("Songti SC", "PingFang SC", "Menlo")
          elif _PLAT == "Windows":
              return ("SimSun", "Microsoft YaHei", "Consolas")
          else:
              return ("Noto Serif CJK SC", "Noto Sans CJK SC", "DejaVu Sans Mono")
      
      CJK_SERIF, CJK_SANS, MONO = _get_fonts()
      
      _CJK_RANGES = [
          (0x4E00, 0x9FFF), (0x3400, 0x4DBF), (0xF900, 0xFAFF), (0x3000, 0x303F),
          (0xFF00, 0xFFEF), (0x2E80, 0x2EFF), (0x2F00, 0x2FDF), (0xFE30, 0xFE4F),
          (0x20000, 0x2A6DF),
      ]
      
      def _is_cjk(ch):
          cp = ord(ch)
          return any(lo <= cp <= hi for lo, hi in _CJK_RANGES)
      
      
      # ─── .doc → .docx conversion ────────────────────────────────────────
      def convert_doc_to_docx(doc_path):
          """Convert .doc to .docx, return path to temp .docx file."""
          tmp_dir = tempfile.mkdtemp(prefix="fillform_")
          base = os.path.splitext(os.path.basename(doc_path))[0]
          out_path = os.path.join(tmp_dir, base + ".docx")
      
          if _PLAT == "Darwin":
              # Try textutil first (preserves basic structure)
              r = subprocess.run(
                  ["textutil", "-convert", "docx", "-output", out_path, doc_path],
                  capture_output=True, text=True,
              )
              if r.returncode == 0 and os.path.exists(out_path):
                  return out_path
      
          # Try LibreOffice as fallback
          for soffice in ["soffice", "libreoffice",
                           "/Applications/LibreOffice.app/Contents/MacOS/soffice"]:
              if shutil.which(soffice) or os.path.exists(soffice):
                  r = subprocess.run(
                      [soffice, "--headless", "--convert-to", "docx",
                       "--outdir", tmp_dir, doc_path],
                      capture_output=True, text=True,
                  )
                  candidate = os.path.join(tmp_dir, base + ".docx")
                  if r.returncode == 0 and os.path.exists(candidate):
                      return candidate
      
          print(f"ERROR: Cannot convert .doc to .docx. Install LibreOffice or use a .docx file.",
                file=sys.stderr)
          sys.exit(1)
      
      
      # ─── Field detection ────────────────────────────────────────────────
      def _normalize_label(text):
          """Normalize a label: strip whitespace and common padding characters."""
          return re.sub(r'[\s\u3000\xa0]+', '', text).strip()
      
      
      def _cell_text(cell):
          """Get clean text from a cell."""
          return cell.text.strip()
      
      
      def _is_label_cell(text):
          """Heuristic: a cell is a label if it has text, is short, and looks like a field name."""
          clean = _normalize_label(text)
          if not clean or len(clean) > 50:
              return False
          # Must contain at least one CJK char or look like a known pattern
          if any(_is_cjk(c) for c in clean):
              return True
          if re.match(r'^[A-Za-z\s/]+$', clean) and len(clean) < 30:
              return True
          return False
      
      
      def scan_fields(doc):
          """Scan document tables and return list of detected field labels with locations."""
          fields = []
          for ti, table in enumerate(doc.tables):
              for ri, row in enumerate(table.rows):
                  cells = row.cells
                  ci = 0
                  while ci < len(cells):
                      cell = cells[ci]
                      text = _cell_text(cell)
                      if _is_label_cell(text):
                          norm = _normalize_label(text)
                          # The value cell is the next non-label cell
                          value_ci = ci + 1
                          # Skip duplicate merged cells (python-docx repeats merged cells)
                          while value_ci < len(cells) and cells[value_ci]._tc is cell._tc:
                              value_ci += 1
                          if value_ci < len(cells):
                              value_cell = cells[value_ci]
                              existing = _cell_text(value_cell)
                              fields.append({
                                  "label": norm,
                                  "raw_label": text,
                                  "table": ti,
                                  "row": ri,
                                  "label_col": ci,
                                  "value_col": value_ci,
                                  "current_value": existing if existing else "",
                              })
                              ci = value_ci + 1
                              continue
                      ci += 1
      
          # Also detect large text areas (merged cells spanning full row with label in text)
          for ti, table in enumerate(doc.tables):
              for ri, row in enumerate(table.rows):
                  cells = row.cells
                  if len(set(id(c._tc) for c in cells)) == 1:
                      # All cells are the same (fully merged row)
                      text = _cell_text(cells[0])
                      # Check if it starts with a label-like pattern
                      m = re.match(r'^(.+?)[::]\s*$', text) or re.match(r'^(.+?)[::]', text)
                      if m:
                          label = _normalize_label(m.group(1))
                          # Avoid duplicates
                          if not any(f["label"] == label for f in fields):
                              fields.append({
                                  "label": label,
                                  "raw_label": m.group(1),
                                  "table": ti,
                                  "row": ri,
                                  "label_col": 0,
                                  "value_col": -1,  # -1 means inline (same cell, after colon)
                                  "current_value": text[m.end():].strip(),
                              })
      
          # Fallback: if no table fields found, try paragraph-based detection
          if not fields:
              fields = _scan_paragraph_fields(doc)
      
          return fields
      
      
      def _scan_paragraph_fields(doc):
          """Detect fields in paragraph-based forms (no tables).
          Pattern: a paragraph with label text followed by an empty/short paragraph as value.
          Also detects 'label:value' patterns on a single line.
          """
          fields = []
          paras = doc.paragraphs
          for i, p in enumerate(paras):
              text = p.text.strip()
              if not text:
                  continue
      
              # Pattern 1: "Label:" at end of line, value on next line(s) or empty
              m = re.match(r'^(.+?)[::]\s*$', text)
              if m and _is_label_cell(m.group(1)):
                  label = _normalize_label(m.group(1))
                  # Value is in following non-empty paragraph(s) until next label
                  value_parts = []
                  j = i + 1
                  while j < len(paras):
                      next_text = paras[j].text.strip()
                      if next_text and not re.match(r'^.+?[::]\s*$', next_text) and not _is_label_cell(next_text):
                          value_parts.append(next_text)
                      else:
                          break
                      j += 1
                  fields.append({
                      "label": label,
                      "raw_label": m.group(1),
                      "table": -1,
                      "row": -1,
                      "label_col": -1,
                      "value_col": -1,
                      "para_index": i,
                      "current_value": " ".join(value_parts),
                      "type": "paragraph",
                  })
                  continue
      
              # Pattern 2: "Label:value" on same line
              m = re.match(r'^(.+?)[::]\s*(.+)$', text)
              if m and _is_label_cell(m.group(1)):
                  label = _normalize_label(m.group(1))
                  if not any(f["label"] == label for f in fields):
                      fields.append({
                          "label": label,
                          "raw_label": m.group(1),
                          "table": -1,
                          "row": -1,
                          "label_col": -1,
                          "value_col": -1,
                          "para_index": i,
                          "current_value": m.group(2).strip(),
                          "type": "paragraph_inline",
                      })
      
          return fields
      
      
      def _set_cell_text(cell, text, font_name=CJK_SERIF, font_size=Pt(11)):
          """Set cell text, preserving basic formatting and applying CJK font."""
          # Clear existing content
          for p in cell.paragraphs:
              for r in p.runs:
                  r.text = ""
      
          if cell.paragraphs:
              p = cell.paragraphs[0]
          else:
              p = cell.add_paragraph()
      
          # Preserve alignment if set
          run = p.add_run(text)
          run.font.size = font_size
          # Set both Latin and CJK font
          run.font.name = font_name
          rPr = run._element.get_or_add_rPr()
          rFonts = rPr.find(qn('w:rFonts'))
          if rFonts is None:
              rFonts = parse_xml(f'<w:rFonts {nsdecls("w")} w:eastAsia="{font_name}"/>')
              rPr.insert(0, rFonts)
          else:
              rFonts.set(qn('w:eastAsia'), font_name)
      
      
      def fill_fields(doc, data, fields=None, font_name=CJK_SERIF, font_size=Pt(11)):
          """Fill detected fields with provided data. Returns list of filled field names."""
          if fields is None:
              fields = scan_fields(doc)
      
          filled = []
          args_font = font_name
          args_font_size = font_size
          data_norm = {_normalize_label(k): v for k, v in data.items()}
      
          for field in fields:
              label = field["label"]
              if label not in data_norm:
                  continue
      
              value = str(data_norm[label])
              ti = field["table"]
              ri = field["row"]
      
              if ti >= 0:
                  table = doc.tables[ti]
                  row = table.rows[ri]
              else:
                  row = None
      
              field_type = field.get("type", "table")
      
              if field_type == "paragraph":
                  # Paragraph-based: set the paragraph after the label
                  pi = field["para_index"]
                  p = doc.paragraphs[pi]
                  raw = field["raw_label"]
                  # Clear following value paragraphs and set first one
                  if pi + 1 < len(doc.paragraphs):
                      next_p = doc.paragraphs[pi + 1]
                      next_p.clear()
                      run = next_p.add_run(value)
                      run.font.name = args_font
                      run.font.size = args_font_size
                  else:
                      p.clear()
                      run = p.add_run(f"{raw}:{value}")
                      run.font.name = args_font
                      run.font.size = args_font_size
              elif field_type == "paragraph_inline":
                  pi = field["para_index"]
                  p = doc.paragraphs[pi]
                  raw = field["raw_label"]
                  p.clear()
                  run = p.add_run(f"{raw}:{value}")
                  run.font.name = args_font
                  run.font.size = args_font_size
              elif row is not None and field["value_col"] == -1:
                  # Inline field in table cell (label: value in same cell)
                  cell = row.cells[field["label_col"]]
                  raw = field["raw_label"]
                  _set_cell_text(cell, f"{raw}:{value}")
              elif row is not None:
                  cell = row.cells[field["value_col"]]
                  _set_cell_text(cell, value)
      
              filled.append(label)
      
          return filled
      
      
      # We need nsdecls import for _set_cell_text
      from docx.oxml.ns import nsdecls
      from docx.oxml import parse_xml
      
      
      # ─── Main ───────────────────────────────────────────────────────────
      def main():
          ap = argparse.ArgumentParser(description="Fill in Word form templates")
          ap.add_argument("--template", required=True, help="Path to template .doc/.docx file")
          ap.add_argument("--output", default="", help="Output .docx path (default: <template>_filled.docx)")
          ap.add_argument("--scan", action="store_true", help="Scan and list all detected form fields")
          ap.add_argument("--data", default="", help="JSON string with field→value mapping")
          ap.add_argument("--data-file", default="", help="Path to JSON file with field→value mapping")
          ap.add_argument("--font", default=CJK_SERIF, help=f"Font name for filled text (default: {CJK_SERIF})")
          ap.add_argument("--font-size", type=float, default=11, help="Font size in points (default: 11)")
          args = ap.parse_args()
      
          template_path = args.template
      
          # Handle .doc files
          tmp_docx = None
          if template_path.lower().endswith(".doc") and not template_path.lower().endswith(".docx"):
              print(f"Converting .doc → .docx ...", file=sys.stderr)
              tmp_docx = convert_doc_to_docx(template_path)
              template_path = tmp_docx
      
          doc = Document(template_path)
      
          if args.scan:
              fields = scan_fields(doc)
              if not fields:
                  print("No form fields detected in the document.")
                  return
              print(f"Detected {len(fields)} form fields:\n")
              for i, f in enumerate(fields, 1):
                  current = f" (current: \"{f['current_value']}\")" if f["current_value"] else ""
                  print(f"  {i}. {f['label']}{current}")
              # Also output as JSON for programmatic use
              print(f"\n--- JSON ---")
              template = {f["label"]: f["current_value"] or "" for f in fields}
              print(json.dumps(template, ensure_ascii=False, indent=2))
              return
      
          # Load data
          if args.data:
              data = json.loads(args.data)
          elif args.data_file:
              with open(args.data_file, encoding="utf-8") as f:
                  data = json.load(f)
          else:
              print("ERROR: Provide --data or --data-file to fill the form.", file=sys.stderr)
              sys.exit(1)
      
          fields = scan_fields(doc)
          filled = fill_fields(doc, data, fields, font_name=args.font, font_size=Pt(args.font_size))
      
          # Determine output path
          output = args.output
          if not output:
              base = os.path.splitext(os.path.basename(args.template))[0]
              template_dir = os.path.dirname(os.path.abspath(args.template))
              output = os.path.join(template_dir, base + "_filled.docx")
      
          doc.save(output)
          print(f"Filled {len(filled)}/{len(fields)} fields → {output}")
          if filled:
              print(f"  Filled: {', '.join(filled)}")
          unfilled = [f["label"] for f in fields if f["label"] not in filled]
          if unfilled:
              print(f"  Unfilled: {', '.join(unfilled)}")
      
      
      if __name__ == "__main__":
          main()
      
  • CHANGELOG.md 568 B
    # Changelog
    
    All notable changes to this skill are documented here.
    Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning: [SemVer](https://semver.org/)
    
    ## [1.2.0] - 2026-08-24
    
    ### Added
    
    - add the shared feedback-classification and approval-invalidation gate used by every LovStudio Skill
    
    ## [1.1.2] - 2026-05-07
    
    ### Fixed
    
    - remove system pip flag from script docs
    - use python3 -m pip install python-docx in dependency examples
    
    ## [1.1.1] - 2026-05-07
    
    ### Fixed
    
    - add release metadata
    - add README version badge and changelog entry
    
    
  • README.md 4.9 KB
    # 表单小助手 · Form Assistant
    
    ![Version](https://img.shields.io/badge/version-1.2.0-CC785C)
    
    Fill Word document form templates (.docx) with structured data. Auto-detects table-based form fields (label → value cell pairs), supports CJK/Latin mixed text with proper font switching, merged cells, and paragraph-based forms.
    
    Part of [skill-publisher/skills](https://example.com/skills/skills) &mdash; by [example.com](https://example.com)
    
    ## Install
    
    ```bash
    npx skills add fill-form -g -y
    ```
    
    Requires: Python 3.8+ and `pip install python-docx`
    
    ## How It Works
    
    ```
    ┌─────────────────────────────────────────────────┐
    │  Template (.docx)                               │
    │  ┌──────────┬───────────┬──────────┬──────────┐ │
    │  │ 主讲人   │           │ 职称     │          │ │
    │  ├──────────┼───────────┴──────────┴──────────┤ │
    │  │ 单位     │                                 │ │
    │  ├──────────┼───────────┬──────────┬──────────┤ │
    │  │ 讲座题目 │           │ 时间     │          │ │
    │  └──────────┴───────────┴──────────┴──────────┘ │
    └────────────────────┬────────────────────────────┘
                         │ --scan → detect fields
                         │ --data → fill values
                         ▼
    ┌─────────────────────────────────────────────────┐
    │  Filled (.docx)                                 │
    │  ┌──────────┬───────────┬──────────┬──────────┐ │
    │  │ 主讲人   │ 张三      │ 职称     │ 教授     │ │
    │  ├──────────┼───────────┴──────────┴──────────┤ │
    │  │ 单位     │ 北京大学                        │ │
    │  ├──────────┼───────────┬──────────┬──────────┤ │
    │  │ 讲座题目 │ AI与未来  │ 时间     │ 4月10日  │ │
    │  └──────────┴───────────┴──────────┴──────────┘ │
    └─────────────────────────────────────────────────┘
    ```
    
    ## Usage
    
    **Step 1** — Scan the template to see all detected fields:
    
    ```bash
    python fill_form.py --template form.docx --scan
    ```
    
    Output:
    
    ```
    Detected 6 form fields:
    
      1. 主讲人
      2. 职称
      3. 单位
      4. 讲座题目
      5. 时间
      6. 地点
    
    --- JSON ---
    {
      "主讲人": "",
      "职称": "",
      ...
    }
    ```
    
    **Step 2** — Fill with a JSON data file or inline JSON:
    
    ```bash
    # From JSON file
    python fill_form.py --template form.docx --data-file data.json
    
    # Inline JSON
    python fill_form.py --template form.docx \
      --data '{"主讲人": "张三", "职称": "教授", "单位": "北京大学"}'
    ```
    
    Output saves to the same directory as the template (`form_filled.docx`).
    
    ## Options
    
    | Option | Default | Description |
    |--------|---------|-------------|
    | `--template` | (required) | Path to template .doc/.docx |
    | `--output` | `<name>_filled.docx` | Output path (defaults to template directory) |
    | `--scan` | | List all detected form fields |
    | `--data` | | JSON string with field → value mapping |
    | `--data-file` | | Path to JSON file with field → value mapping |
    | `--font` | Platform CJK serif | Font for filled text |
    | `--font-size` | `11` | Font size in points |
    
    ## Field Detection
    
    The script detects fillable fields in three ways:
    
    | Method | How it works | Example |
    |--------|-------------|---------|
    | **Table cells** | Label in one cell, blank value in adjacent cell | `│ 姓名 │ ___ │` |
    | **Merged rows** | Full-width cell with `Label:` pattern | `│ 备注:________________ │` |
    | **Paragraphs** | Fallback for docs without tables | `姓名:` followed by blank line |
    
    Fields are matched by normalized label (whitespace-insensitive), so `主 讲 人` matches `主讲人`.
    
    ## Supported Formats
    
    | Format | Support |
    |--------|---------|
    | `.docx` | Full support (recommended) |
    | `.doc` | Auto-converts via `textutil` (macOS) or LibreOffice. Table structure may be lost — convert to `.docx` first for best results. |
    
    ## License
    
    MIT
  • SKILL.md 6 KB
    ---
    name: lov-fill-form
    category: Office Automation
    tagline: "Fill Word form templates (.docx). Auto-detects table fields, CJK font support."
    description: >
      Fill in Word document form templates (.docx) with user-provided data.
      Reads a template containing tables with label→value cell pairs, detects
      all fillable fields, and outputs a completed document. Handles CJK/Latin
      mixed text with proper font switching. Use this skill when the user wants
      to fill in a form template, complete an application form, populate a Word
      table form, or automate document filling. Also trigger when the user
      mentions "填表", "填写表格", "fill form", "fill template", "表格填写",
      "申请表", "登记表", or has a .docx template with blank fields to fill.
    license: MIT
    compatibility: >
      Requires Python 3.8+ and python-docx (`pip install python-docx`).
      Cross-platform: macOS, Windows, Linux.
      Input must be .docx (recommended) or .doc (auto-converted via textutil on macOS,
      but table structure may be lost — use .docx when possible).
    metadata:
      author: contributors
      version: "1.2.0"
      tags: form fill template docx word table cjk
    ---
    
    # 表单小助手 · Form Assistant
    
    This skill fills in Word document form templates (.docx) with user-provided data.
    It detects table-based form fields (label in one cell, value in the adjacent cell)
    and populates them automatically.
    
    ## When to Use
    
    - User has a `.docx` form template with blank fields to fill
    - User wants to fill in an application form, registration form, etc.
    - Document uses Word tables for form layout (label | value cell pairs)
    - User mentions 填表, 申请表, 登记表, or wants to automate form filling
    
    ## Workflow (MANDATORY)
    
    **You MUST follow these steps in order:**
    
    ### Step 1: Scan the template
    
    Discover all fillable fields:
    
    ```bash
    python lov-fill-form/scripts/fill_form.py --template <path> --scan
    ```
    
    ### Step 2: Pre-fill from known context
    
    Before asking the user, try to fill as many fields as possible from:
    1. **User memory** — name, title, organization, etc.
    2. **Context files** — if the user provides reference documents (e.g. STARTER-PROMPT.md,
       project docs), extract relevant info to fill content-heavy fields
    3. **Conversation context** — anything already mentioned
    
    For content-heavy fields (e.g. "主要内容/简介/摘要"), actively compose the content
    by synthesizing from context files, user's known expertise, and the topic/title.
    
    ### Step 3: Ask only what you don't know
    
    **Use `AskUserQuestion` to collect ONLY the fields you cannot fill from context.**
    
    - Group fields into a single question
    - If ALL fields are unknown, list them all
    - If the user says some fields can be left blank (e.g. "其他朋友会帮我填"),
      respect that and leave those empty
    - Do NOT force the user to provide every field
    
    ### Step 4: Fill and save
    
    Write a JSON data file (avoids shell escaping issues with long text), then run:
    
    ```bash
    python lov-fill-form/scripts/fill_form.py \
      --template <path> \
      --data-file /tmp/form_data.json
    ```
    
    **Output path rules:**
    - Default: `<template_dir>/<name>_filled.docx` (same directory as the template)
    - If the template is in a temp directory or system path, save to user's document
      directory or ask the user where to save
    - Use `--output` to override explicitly
    
    ## CLI Reference
    
    | Argument | Default | Description |
    |----------|---------|-------------|
    | `--template` | (required) | Path to template .doc/.docx file |
    | `--output` | `<template_dir>/<name>_filled.docx` | Output .docx path |
    | `--scan` | false | List all detected form fields |
    | `--data` | `""` | JSON string with field→value mapping |
    | `--data-file` | `""` | Path to JSON file with field→value mapping |
    | `--font` | Platform CJK serif | Font name for filled text |
    | `--font-size` | `11` | Font size in points |
    
    ## How Field Detection Works
    
    1. **Table-based** (primary): Scans all tables for rows with label→value cell pairs.
       A label cell contains short text (CJK or Latin); the adjacent cell is the value field.
    2. **Merged rows**: Detects full-width merged cells with "Label:" pattern as large text areas.
    3. **Paragraph fallback**: If no tables found, detects "Label:value" patterns in paragraphs.
    
    ## Limitations
    
    - `.doc` files are auto-converted to `.docx` via macOS `textutil`, which **loses table structure**.
      For best results, use `.docx` templates directly. If you only have `.doc`, convert with
      LibreOffice first: `libreoffice --headless --convert-to docx file.doc`
    - Fields are matched by normalized label text (whitespace removed). If a label contains
      unusual formatting, the match may fail — use `--scan` to verify detection.
    
    ## Dependencies
    
    ```bash
    python3 -m pip install python-docx
    ```
    
    ## Runtime context (shared)
    
    运行前读取本 Skill 包的 `skill.yaml`,由宿主提供 `skill-runtime/v1` 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。
    
    - 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
    - `required: true` 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。
    - 报错提供可复制的 `context_id`、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。
    
    ## 通用反馈闭环
    
    用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:
    
    1. 先判断意见是 `task-specific`(仅本次)还是 `reusable`(可跨任务复用)。
    2. `task-specific` 只修改当前任务,不改 Skill。
    3. `reusable` 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
    4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
    5. `reusable` 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
    
  • skill.yaml 832 B
    schema: skill-manifest/v1
    id: lov-fill-form
    version: "1.2.0"
    runtime: skill-runtime/v1
    context:
      profile:
        fields:
        - path: identity.name
          required: true
          question: 如果本次输出需要品牌身份,请提供品牌名称。
        - path: identity.logo
          required: false
          question: 如果需要使用品牌 Logo,请提供 Logo 地址或文件路径。
        - path: brand.tone
          required: false
          question: 如果已有品牌语气或审美关键词,请提供它们。
      preferences:
        namespace: lov_fill_form
        fields:
        - path: user.language
          required: false
          question: 希望使用哪种语言输出?
        - path: user.timezone
          required: false
          question: 需要使用哪个时区处理日期和时间?
      interaction:
        ask_missing: true
        max_questions: 1
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related