wintersdragon-c/office-docx-skills

Habilidade de agente para editar arquivos .docx com marcação de Controlo de Alterações do Microsoft Word.

O que é office-docx-skills?

Esta habilidade permite que agentes de IA (como Claude Code, Cursor ou Codex) editem programaticamente arquivos .docx gerando revisões rastreáveis no Microsoft Word. As alterações aparecem como riscado vermelho para exclusões, sublinhado vermelho para inserções, com metadados de autor e data nas margens — exatamente como o Controlo de Alterações nativo do Word. Isto é alcançado através da manipulação direta do XML OOXML (elementos `w:ins` e `w:del`) usando a biblioteca `lxml`, em vez da API de alto nível do `python-docx`, que não suporta revisões rastreáveis. A habilidade inclui um módulo Python reutilizável (`tracked_change_editor.py`) que gere IDs de revisão únicos, preserva a formatação dos parágrafos e lida com segurança com edições em lote, evitando armadilhas comuns como colisões de IDs, perda de citações académicas e inserção de parágrafos em secções erradas. É especialmente útil para fluxos de trabalho de revisão de manuscritos, documentos legais ou qualquer cenário onde o histórico de alterações precise ser preservado para aceitação ou rejeição por revisores.

Funciona com✓Claude Code✓Codex CLI~Cursor
npx skills add wintersdragon-c/office-docx-skills

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Editar .docx com Controlo de Alterações do Word (Modo de Revisão)

Visão Geral

Editar programaticamente ficheiros .docx para que as alterações apareçam como revisões rastreáveis no Microsoft Word — exclusões mostram-se como riscado vermelho, inserções como sublinhado vermelho, com metadados de autor/data na margem. Isto utiliza manipulação direta do XML OOXML dos elementos w:ins / w:del via lxml, NÃO a API de alto nível do python-docx (que não suporta revisões rastreáveis).

Dependências

pip install python-docx lxml
  • python-docx >= 1.2.0 — usado apenas para inspeção inicial (estilos de parágrafo, formatação de segmentos). NÃO usado para as escritas de revisões rastreáveis.
  • lxml >= 5.0 — o verdadeiro motor; analisa e modifica word/document.xml dentro do zip .docx.

Ambos são instalações Python puras, sem dependências de sistema.

Quando Usar

  • O utilizador pede para editar um .docx e quer VER as alterações no Word (Modo de Revisão / Track Changes)
  • O utilizador precisa enviar um manuscrito revisto onde revisores possam aceitar/rejeitar edições individuais
  • Qualquer cenário onde a substituição direta de texto no .docx seja insuficiente porque o histórico de alterações é importante

Quando NÃO Usar

  • O utilizador quer apenas o texto final substituído (sem marcas de revisão) — use python-docx diretamente
  • O ficheiro é .doc (formato binário antigo) — não suportado
  • O utilizador quer editar cabeçalhos/rodapés/notas de rodapé com revisões rastreáveis — esta técnica cobre apenas parágrafos do corpo

Funciona Com

Esta habilidade pode ser usada sozinha ou em conjunto com outras habilidades em office-docx-skills.

  • Use com word-default-formatting quando o texto revisto também deve seguir o perfil de formatação padrão do pacote Word.
  • Use com word-formula-writing quando edições relacionadas a fórmulas devem ser representadas com equações editáveis nativas do Word e marcas de revisão visíveis.
  • Se tanto formatação quanto fórmulas importam, use as três habilidades juntas.

Estrutura de Revisões Rastreáveis OOXML

Um .docx é um zip contendo word/document.xml. Revisões rastreáveis usam dois elementos wrapper em torno de elementos w:r (segmento) dentro de w:p (parágrafo):

<!-- Exclusão: envolve segmentos originais, a tag de texto torna-se w:delText -->
<w:del w:id="102" w:author="Claude" w:date="2026-04-17T10:00:00Z">
  <w:r>
    <w:rPr><!-- formatação original preservada --></w:rPr>
    <w:delText xml:space="preserve">texto antigo aqui</w:delText>
  </w:r>
</w:del>

<!-- Inserção: envolve novos segmentos com formatação clonada -->
<w:ins w:id="103" w:author="Claude" w:date="2026-04-17T10:00:00Z">
  <w:r>
    <w:rPr><!-- clonado do segmento original --></w:rPr>
    <w:t xml:space="preserve">novo texto aqui</w:t>
  </w:r>
</w:ins>

Regras-chave:

  • w:del e w:ins são filhos diretos de w:p, irmãos de w:pPr
  • Cada um precisa de um w:id único (inteiro, não deve colidir com IDs existentes no documento)
  • w:author e w:date preenchem a anotação marginal no Word
  • Dentro de w:del, elementos de texto DEVEM ser w:delText (não w:t)
  • Dentro de w:ins, elementos de texto são normais w:t
  • Propriedades do segmento (w:rPr) devem ser clonadas dos segmentos originais para preservar fonte/tamanho/negrito

Implementação Central

A implementação reutilizável reside em tracked_change_editor.py junto a esta habilidade. Copie esse helper para o seu diretório de trabalho quando quiser código que pode importar diretamente.

from tracked_change_editor import TrackedChangeEditor

editor = TrackedChangeEditor('input.docx', author='O seu Nome')

for i, p in enumerate(editor.body_paras):
    if 'texto alvo' in editor._get_para_text(p):
        editor.replace_paragraph_text(i, 'texto substituto')
        break

editor.save('output.docx')

tracked_change_editor.py trata dos mecanismos importantes:

  • verifica IDs de revisão existentes antes de criar novos nós w:ins / w:del
  • preserva a formatação do parágrafo clonando o w:rPr padrão do parágrafo
  • escreve através de um ficheiro temporário seguro antes de mover para o .docx final

Fluxo de Utilização

editor = TrackedChangeEditor('input.docx', author='O seu Nome')

# Encontrar parágrafo alvo pelo conteúdo de texto
for i, p in enumerate(editor.body_paras):
    if 'texto alvo' in editor._get_para_text(p):
        editor.replace_paragraph_text(i, 'texto substituto')
        break

editor.save('output.docx')  # Guardar sempre primeiro para um NOVO ficheiro

Armadilhas Críticas — DEVE LER ANTES DE EDITAR

0. TODAS as edições num SCRIPT, ou encadeie a partir do último ficheiro guardado

Se precisar de fazer 7 edições, faça-as TODAS numa única execução de script contra uma única instância de TrackedChangeEditor, depois guarde uma vez. NÃO:

  • Execute script A → guarde para _revised.docx
  • Execute script B contra o ficheiro ORIGINAL → guarde para _revised.docx (SOBRESCREVE o trabalho do script A)

Se precisar de dividir em múltiplas execuções de script, cada execução subsequente DEVE carregar a partir da saída anterior:

# Ronda 1
editor = TrackedChangeEditor('original.docx')
# ... edições ...
editor.save('revised.docx')

# Ronda 2 — carregar de revised, NÃO original
editor2 = TrackedChangeEditor('revised.docx')
# ... mais edições ...
editor2.save('revised.docx')

Por que isto importa: Carregar do ficheiro original descarta todas as revisões rastreáveis de rondas anteriores. Este é o erro mais comum — descarta silenciosamente o trabalho sem qualquer aviso.

0b. Inserir novos parágrafos: encontrar a SECÇÃO, não apenas o texto

Ao inserir um novo parágrafo (ex.: uma entrada de referência), NUNCA procure em todo o documento por uma correspondência de texto. As citações no corpo do texto contêm os mesmos nomes de autor que as entradas de referência. Uma pesquisa ingénua como if 'Acharya' in text corresponderá primeiro a um parágrafo do corpo e inserirá a referência no meio da Secção 2.

Abordagem correta — encontrar o limite da secção primeiro:

# Encontrar onde a secção de Referências começa
refs_start_idx = None
for i, p in enumerate(editor.body_paras):
    t = editor._get_para_text(p)
    if t.strip() == 'Referências':
        refs_start_idx = i
        break

# Pesquisar APENAS dentro de Referências
for i in range(refs_start_idx + 1, len(editor.body_paras)):
    t = editor._get_para_text(editor.body_paras[i])
    if t.startswith('Acharya') and 'Johnson' in t:
        # Inserir após este parágrafo
        break

Por que isto importa: Nomes de autor aparecem tanto em citações no texto quanto em entradas de referência. Sem pesquisa consciente da secção, novos parágrafos de referência acabam no corpo do texto — uma corrupção difícil de detetar até o utilizador abrir o Word.

0c. Reescrever parágrafos em artigos académicos: preservar TODAS as citações

Ao substituir o texto de um parágrafo, DEVE preservar todas as citações (Autor Ano) do original. Artigos académicos têm uma lista de referências que deve corresponder às citações no texto — perder uma citação orfana a entrada de referência ou enfraquece o argumento.

Abordagem correta — extrair antes de reescrever:

import re
texto_antigo = editor._get_para_text(editor.body_paras[idx])
citas_antigas = re.findall(r'\([^)]*\d{4}[a-z]?\)', texto_antigo)
# Escrever novo_texto garantindo que todos os itens em citas_antigas aparecem
# Após escrever, verificar:
for c in citas_antigas:
    assert c in novo_texto, f'CITAÇÃO PERDIDA: {c}'

Por que isto importa: Uma introdução reescrita que perde (Malmendier e Nagel 2011; Choi, Gao e Jiang 2020) quebra silenciosamente a integridade das citações do artigo. A lista de referências ainda contém estas entradas, mas nada no texto aponta para elas. Revisores e copidesques irão sinalizar isto.

1. Parágrafos com revisões rastreáveis existentes

Se um parágrafo já contém w:ins/w:del de edições anteriores, _collect_direct_runs apenas captura filhos w:r diretos — segmentos dentro de wrappers w:ins/w:del existentes são ignorados. Isto evita aninhar revisões, mas significa que a substituição cobre apenas a porção "aceite" do texto. Para parágrafos com revisões anteriores extensas, aceite todas as alterações no Word primeiro, depois re-edite.

2. Guardar sempre para um novo ficheiro

Nunca sobrescreva o original. Guarde como _revised.docx, deixe o utilizador verificar no Word, depois substitua manualmente.

3. O índice não será atualizado automaticamente

python-docx / lxml não podem atualizar o Índice do Word. Após editar, o utilizador deve abrir no Word → clique direito no índice → Atualizar Campo.

4. Unicidade do ID de revisão

O método _find_max_id() verifica todos os elementos w:ins, w:del, w:rPrChange, w:pPrChange, w:sectPrChange existentes. Novos IDs incrementam a partir do máximo. Se IDs colidirem, o Word pode corromper o histórico de revisões.

5. Preservação da formatação

_get_default_rpr clona o w:rPr do primeiro segmento no parágrafo. Isto funciona para parágrafos com formatação uniforme. Para parágrafos com formatação mista (ex.: negrito "Palavras-chave:" seguido de texto normal), faça substituição cirúrgica por segmento em vez de substituição de parágrafo inteiro.

6. Fidelidade de ida e volta

Guardar via lxml produz pequenas diferenças cosméticas no XML (estilo de aspas " vs ', finais de linha \r\n vs \n). Conteúdo e formatação são preservados. O tamanho do ficheiro pode diferir devido ao nível de compressão zip — isto é inofensivo.

Lista de Verificação

Após guardar, abra no Word e confirme:

  • Texto eliminado mostra riscado vermelho
  • Texto inserido mostra sublinhado vermelho
  • Nome do autor aparece nas anotações marginais
  • Parágrafos circundantes mantêm a formatação original
  • Sem marcas de revisão duplicadas ou aninhadas
  • Contagem de inserções para o seu autor escolhido corresponde ao esperado — execute um script de verificação para contar w:ins com o seu nome de autor e imprimir os primeiros 100 caracteres de cada um. Se a contagem for menor que o esperado, edições anteriores foram perdidas (ver Armadilha 0).
  • Sem entradas de referência no corpo do texto — verifique parágrafos antes do cabeçalho Referências para qualquer parágrafo que pareça uma entrada bibliográfica completa (autor, ano, jornal, volume, páginas). Se encontrado, uma referência foi inserida no local errado (ver Armadilha 0b).
# Snippet de verificação pós-guarda
target_author = 'Codex'
with zipfile.ZipFile('output.docx') as z:
    xml = z.read('word/document.xml')
root = etree.fromstring(xml)
author_ins = [e for e in root.findall('.//w:ins', nsmap)
              if e.get(qn('w:author')) == target_author]
print(f'{target_author} inserções: {len(author_ins)}')
for i, ins in enumerate(author_ins):
    texts = ''.join(t.text for t in ins.findall('.//w:t', nsmap) if t.text)
    print(f'  [{i+1}] {texts[:100]}')

Habilidades Relacionadas