Files
maestro/docs/tools/office.md
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

548 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Office ファイル系ツール(ReadPdf / ReadExcel / ReadDocx / ReadPPTX / ReadMsg / PdfToImages / SplitExcelSheets / SplitDocxSections / EditExcel / EditDocx / EditPptx)
ローカル workspace の Office 文書・PDF を読み込むツール群。
## 読み取り系
### ReadPdf
```js
// 全文抽出 (page 区切りで markdown)
ReadPdf({ path: "input/manual.pdf" })
// ページ範囲を絞る
ReadPdf({ path: "input/spec.pdf", page_range: "10-25" })
// grep -n 風検索 (query mode)
ReadPdf({ path: "input/manual.pdf", query: "保証期間" })
ReadPdf({ path: "input/manual.pdf", query: "保証期間", context_lines: 5 })
ReadPdf({ path: "input/spec.pdf", query: "第\\d+条", query_mode: "regex" })
```
スキャン PDF(テキストレイヤなし)の場合は **自動で PdfToImages + Vision OCR にフォールバック**。手動で `PdfToImages → ReadImage` を呼ぶ必要は通常ない。query を併用すると OCR 結果にも同じフィルタが適用される。
#### 引数
| 引数 | 型 | デフォルト | 説明 |
|---|---|---|---|
| `path` | string | (required) | workspace 相対の PDF パス |
| `page_range` | `"3"` / `"1-5"` | (全ページ) | 抽出ページ範囲 |
| `max_pages` | number | (無制限) | 抽出ページ数の上限 |
| `max_chars` | number | 50,000 | 返却文字数の上限 |
| `query` | string | (なし) | 一致行のみ grep -n 風で返す。trim 後 empty なら全文 mode |
| `query_mode` | `substring` / `regex` / `iregex` | `substring` | 検索モード。substring は大小無視 + metachar auto-escape |
| `context_lines` | number (0..20) | 2 | query マッチ前後の context 行数 |
#### query mode の出力例
```
# foo.pdf, query: "保証期間"
### Summary
- Total pages: 50
- Pages with match: 3
- Total matches: 5
### Matches
## Page 7 — 2 matches
6: 商品は購入日より
> 7: 保証期間内に故障した場合、無償修理対象…
8: ただし、消耗品は対象外です。
…
> 22: 延長保証期間は最大 3 年まで…
```
`>` が一致行、空白マーカーが context 行。隣接マッチは context window が overlap したら 1 cluster にまとめられ context 重複を回避。
#### gotcha
- **MAX_MATCHES_PER_PAGE = 50**: 1 ページで 50 件超のマッチは打ち切り、page header に `(capped)` を付与。`"the"` のような broad pattern を絞るシグナル
- **`query_mode: "regex"` の invalid pattern** → `isError: true` で friendly メッセージ。substring mode は metachar をエスケープするので絶対に regex error にならない
- **OCR fallback path** でも query 適用。スキャン PDF + キーワード検索 OK
- 上記 PdfToImages の手動呼び出しは **DPI / 出力ファイル個別管理** がしたい時のみ。普通の "PDF を vision で読みたい" は ReadPdf 単発で済む
### ReadExcel
```js
ReadExcel({ path: "input/data.xlsx" })
// → 全シートのセル内容をテキスト形式で返す
```
巨大な Excel は token を食うので、シートを絞る場合は SplitExcelSheets を使う。
#### include_styles — セル装飾の取得(オプション)
```js
ReadExcel({ path: "input/report.xlsx", include_styles: true })
// → 値テーブルの後に ### Styles セクションが追記される
```
デフォルトは `false`(後方互換)。`include_styles: true` を指定すると、値テーブルの直後に各シートの `### Styles` セクションが出力される。
**出力形式 — dedup legend + range map:**
```
### Styles — Sheet1
s1 = fill:#FFF2CC;font:bold
s2 = border:top(thin),bottom(double,#808080);numFmt:"0.00%"
s1: A1:D1,A5:D5
s2: B2
merges: B2:D4
```
- **legend**: スタイル ID (`s1`, `s2`, ...) とそのシグネチャを列挙。シグネチャは `fill` / `font` / `border` / `numFmt` / `align` のうち非デフォルト部分を `;` 区切りで結合した文字列。
- **range map**: 同一スタイルを持つセルを矩形にまとめて `A1:D3` 形式で列挙。単一セルは `A1`。
- **merges**: 結合セル範囲の一覧(`ws.model.merges` から取得)。
- **conditionalFormatting**: 条件付き書式が存在する場合 `present (effective styles not evaluated)` と表示(実際の表示色は評価しない)。
- **装飾のみ・値なしセルも捕捉**: スタイルスキャンは `includeEmpty:true` で実行するため、値がなく書式だけ設定されたセルも対象に含まれる。
- **`sheet`/`range` フィルタを尊重**: `include_styles` は値テーブルと同じ `sheet`/`range` フィルタでスキャン範囲を限定する。指定した範囲外のセルのスタイルと、範囲に交差しない結合セルは結果に含まれない。
- **デフォルト色の省略**: デフォルト色 (theme index 1 = 通常黒) に明示設定したフォント色はデフォルト扱いで省略される。同様に border の theme index 0(自動色)も省略される。
**テーマカラーの解決(ベストエフォート):**
テーマカラーはファイルの `theme1.xml` から読み取り、OOXML の lt/dk スワップを適用して `#RRGGBB` に解決する。解決できた場合は `theme(N,resolved=#RRGGBB)` の形式で表示し、`resolved=` は近似値であることに注意。テーマ XML が解析できない場合は `theme(N,tint=T)` のように生の参照のまま出力する。
**max_style_ranges — range 数の上限:**
`max_style_ranges`(デフォルト 250)で全シートを通じた range 出力数を制限する。上限に達した場合は `[styles truncated: max_style_ranges=N reached]` を表示し、以降のスタイルは省略される。値テーブルが先に出力されるため、スタイルセクションがトークン上限で切り捨てられても値は失われない。
**編集は EditExcel を使う:**
値・数式だけでなく、セル結合、書式、シート追加、ガントの着色にも対応する。独自のPythonスクリプトやopenpyxlより先に `EditExcel` を使うこと。詳細編集では保存前の構造検査と数式再計算まで一括で行う。
### ReadDocx
```js
ReadDocx({ file_path: "input/spec.docx" })
// → 本文 + 表を抽出
```
### ReadPPTX
```js
ReadPPTX({ file_path: "input/slides.pptx" })
// → 各スライドのテキスト・表・スピーカーノートを返す
```
### ReadMsg
Outlook の `.msg`(OLE2 / CFBF 複合バイナリ)メールを読む。
```js
ReadMsg({ file_path: "input/inquiry.msg" })
// → 件名・差出人・宛先・CC・日時・本文(テキスト)と添付一覧を返す
```
返却テキストの構成:
```
Subject: 見積もりのご依頼
From: 山田太郎 <[email protected]>
To: [email protected]
Date: ...
(本文テキスト)
Attachments (2):
- 見積書.pdf (20480 bytes) -> input/見積書.pdf
- data.xlsx (8192 bytes) -> input/data.xlsx
```
ポイント:
- 本文は plain text を優先。HTML メールはタグを除去して整形。どちらも取れない場合は `(no text body)`
- **添付は `input/` に自動保存される**。保存後は種別ごとのツールで開く(PDF → ReadPdf、Excel → ReadExcel、画像 → ReadImage など)
- ファイル名は basename に正規化し、パス区切りや制御文字を除去(ディレクトリトラバーサル防止)。同名衝突時は連番を付与
- 添付に埋め込まれた `.msg`(メール in メール)は保存せず一覧に注記のみ。必要なら個別に扱う
- 不正な `.msg`(CFBF シグネチャ不一致)は中身を読まずエラーを返す。バイナリがそのまま出力に混ざることはない
## 変換・分割系
### PdfToImages
```js
PdfToImages({ file_path: "input/scan.pdf", dpi: 200 })
// → output/ReadPdf/page-001.png, page-002.png, ... に保存
```
スキャン PDF を ReadImage で扱うときの前段。
### SplitExcelSheets
```js
SplitExcelSheets({ file_path: "input/big.xlsx" })
// → output/excel/{sheetname}.md と manifest.json を生成
```
シート単位で別ファイルにすることで、Read で必要なものだけ取り出せる。
### SplitDocxSections
```js
SplitDocxSections({ file_path: "input/long-spec.docx" })
// → 見出しベースで分割した .md と manifest.json を生成
```
長い Word 文書を構造化して Read で取り回しやすくする。
## ファイル選択の指針
| ファイル | 第一選択 | フォールバック |
|---|---|---|
| PDF(テキストあり) | ReadPdf | - |
| PDF(スキャン画像) | ReadPdf(自動 OCR フォールバック) | 手動 PdfToImages → ReadImage |
| PDF 内をキーワード検索 | ReadPdf + `query` | Read → Grep(ReadPdf で出力保存後) |
| Excel(小〜中) | ReadExcel | - |
| Excel(巨大) | SplitExcelSheets → Read | - |
| Word(短〜中) | ReadDocx | - |
| Word(長文・章構成) | SplitDocxSections → Read | - |
| PowerPoint | ReadPPTX | - |
### EditExcel
`.xlsx` を編集する。**`edits` と `operations` は別モードであり、同時に指定しない。**
#### 軽量なセル編集(edits / rows / merges / columns)
値・数式・基本的な書式・セル結合・列幅を直し、複雑なブックの未対応部品を保持したい場合に使う。
```js
EditExcel({
path: "output/sales.xlsx",
edits: [
{ cell: "B2", value: 250 },
{ sheet: "集計", cell: "C10", formula: "SUM(C2:C9)" },
{ cell: "D5", value: null },
// 値はそのまま、書式だけを変える
{ cell: "A1", style: { bold: true, fill_color: "#FFFF00", alignment: { horizontal: "center" } } },
// 表示形式(数値の見せ方)
{ cell: "B3", style: { number_format: "#,##0" } },
{ cell: "C3", style: { number_format: "yyyy/mm/dd" } }
],
rows: [{ row: 1, height: 28 }],
merges: [{ range: "A1:C1", merged: true }],
columns: [{ columns: "A:C", width: 18 }]
})
```
対象シートのOOXMLだけを差し替えるため、書式・条件付き書式・入力規則・結合セル・図・グラフ・ピボット・マクロ・他シートを保持する。数式は `fullCalcOnLoad=1` を付け、古い `calcChain.xml` を除去する。計算結果の検証は行わない。
**書式(`edits[].style`)**
指定した項目だけが、そのセルの今の書式に重なる。書かなかった項目は元のまま残る。
`value` も `formula` も省くと、値には触れず書式だけを変える。
重ねる土台は**セル → 行 → 列**の順に探す。セルに書式が付いていなくても、行(`customFormat="1"` の付いた行の書式)や列(`<col style="...">`)から受け継いだ見た目があればそれを土台にする。どこにも無ければ既定の書式から組み立てる。
| 項目 | 意味 |
|---|---|
| `bold` | 太字。`false` で解除 |
| `font_color` | 文字色 `"#RRGGBB"` または `"#AARRGGBB"`。`null` で既定色に戻す |
| `fill_color` | 背景色 `"#RRGGBB"` または `"#AARRGGBB"`。`null` で塗りなし |
| `border` | 罫線。`{ edges, style, color }`。`edges` 省略時は 4 辺。`style` 省略時は `thin`。`color` 省略時と `null` は既定色。`null` で 4 辺とも消す |
| `alignment` | 揃え。`{ horizontal, vertical, wrap_text }`。`null` で揃えの指定を消す。`{}`(中身なし)は断る |
| `number_format` | 表示形式。Excel の書式コードをそのまま渡す(`"#,##0"` `"0.0%"` `"yyyy/mm/dd"` `'"¥"#,##0'`)。`null` か `"General"` で標準に戻す。255 文字まで |
**行の高さ(`rows`)**
`{ sheet, row, height }`。`row` は 1 始まり、`height` はポイント(0 より大きく 409.5 以下= Excel の上限と同じ)。`null` で自動に戻す。
書式表(`xl/styles.xml`)へは**末尾にだけ**組を足す。既存セルが指す番号は 1 つも動かないので、触っていないセルの見た目は変わらない。同じ書式を何セルに、何回の保存にまたいで掛けても、書式表は増えない(元からある組も突き合わせて使い回す)。
表示形式の番号は、規格が決めた組み込みの席(0〜163)に載っているものはそのまま使い、それ以外は 164 から採番して `numFmts` に載せる。日付・時刻・通貨は組み込みの番号に**寄せない**——組み込みの番号は見え方が表示側の地域設定で変わり、規格と Excel で中身が違う番号もあるため、渡した書式コードをそのまま載せる。
ブックが `numFmts` で組み込みの席を自分の書式コードに**上書きしている**ことがある(変換ツールを通ったファイルなど)。その番号には寄せず、164 以上へ新しく採番する。上書きされた番号へ寄せると、頼んだ見せ方と違うものが読み手に見える。
**セルの結合(`merges`)**
`{ sheet, range, merged }`。`range` は `A1:C1`(左上:右下)、`merged` は `true` で結合・`false` で解除。
| 断るもの | 例 | 理由 |
|---|---|---|
| 範囲になっていない | `A1` | 1 セルの結合は Excel にも無い |
| 逆順 | `C1:A1` | 左上と右下が入れ替わっている |
| シートの外 | `A1:XFE1` / `A1:A1048577` | Excel の列数(16384)・行数(1048576)を超える |
| 重なる結合 | `A1:C1` と `B1:D1` | 重なった結合はブックが開けなくなる。既にある結合との重なりも、同じ呼び出しの中の重なりも断る |
| 二重の結合 | 既に `A1:C1` があるところへ `A1:C1` | 同上 |
| 結合されていない範囲の解除 | 結合の無い `D1:E1` を `merged: false` | 黙って何もしないと、解除できたと誤解する |
| 中身が入ったセルを巻き込む結合 | `B1` に値があるまま `A1:C1` | **Excel は結合すると左上以外の中身を捨てる**。黙って消さず、どのセルが該当するかを挙げて断る |
| 位置を並び順で書いているシートの結合 | `<c>` に `r` が無い行がある | どのセルが範囲に入るか読めない。何が消えるか分からないまま結合するより断る(その行にかかる結合だけ) |
最後の 1 件は、同じ呼び出しの `edits` でそのセルを空にすれば通る(`edits` → `columns` → `merges` の順に当てるため)。
```js
EditExcel({
path: "output/sales.xlsx",
edits: [{ cell: "B1", value: null }, { cell: "C1", value: null }],
merges: [{ range: "A1:C1", merged: true }]
})
```
解除と結合を同じ呼び出しに書いた場合は**解除が先**。`A1:C1` を解いて `A1:B1` に結び直す、が 1 回で書ける。
`<mergeCells count="N">` の N は実際の件数に合わせて書き直す。最後の 1 件を解いたときは `<mergeCells>` という入れ物ごと消す(空の入れ物は ECMA-376 違反)。
**列の幅(`columns`)**
`{ sheet, columns, width }`。`columns` は `"A:C"`(範囲)または `"B"`(1 列)。`width` の単位は**標準書体の「0」の文字幅**で、ピクセルでもポイントでもない(ECMA-376 の `CT_Col/@width`)。0 より大きく 255 以下。`null` で幅の指定を外す(既定幅に戻る)。
`<col>` の `min` / `max` は範囲なので、1 本が複数の列をまとめていることがある。3 列目だけ幅を変える指定は、1-5 をまとめた 1 本を 1-2 / 3-3 / 4-5 の 3 本に割ってから当てる(範囲が重なった `<col>` を並べると Excel の挙動が定まらない)。割った先には列の書式(`style`)・非表示(`hidden`)などの属性をすべて引き継ぐ。
幅を書くときは `customWidth="1"` も一緒に書く。付けないと Excel は幅を無視して既定幅で描く。逆に `bestFit`(自動で決めた幅、という印)は落とす。
**`worksheet` の子要素の並び順**
`<cols>` は `sheetData` の**前**、`<mergeCells>` は**後ろ**にあり、ECMA-376 の `CT_Worksheet` は子要素の順番を決めている(`sheetPr` → `dimension` → `sheetViews` → `sheetFormatPr` → `cols` → `sheetData` → `sheetCalcPr` → `sheetProtection` → `protectedRanges` → `scenarios` → `autoFilter` → `sortState` → `dataConsolidate` → `customSheetViews` → `mergeCells` → `phoneticPr` → `conditionalFormatting` → … → `extLst`)。順番を破ると Excel が修復を求める。要素が無いシートへ足すときは、`<cols>` は `sheetData` の直前、`<mergeCells>` は**後ろに来る要素のうちいちばん手前にあるもの**の直前へ入れる。並びの根拠は `src/office/xlsx-sheet-layout.ts` の冒頭に全 39 個を列挙してある(数えたのは ECMA-376 4th edition の **transitional** 版。`legacyDrawingHF` と `picture` の間に `drawingHF` が入る。strict 版はこれを持たず 38 個)。
数式の計算結果の検証、シート追加、ガントの着色は引き続き `operations` を使う。
**コピーされた式のおおもとを書き換えるとき(#1351 段 2)**
Excel で式を下へコピーした列は、ファイルの中では 1 つのセルだけが式の文面と範囲を持ち(`<f t="shared" ref="C2:C4" si="0">`)、残りはその番号を指すだけになる(`<f t="shared" si="0"/>`)。この**本体**のセルへ値や数式を書くと、そのままでは同じかたまりの残りのセルから式も計算結果も消える。
そこで、本体へ書く前に**追随するセルの式を実体化する**。本体の式を各セルの位置に合わせて訳し(相対参照はずれ、`$` の付いた側は動かない)、`<f>` に全文で書き込んでから本体を書き換える。計算済みの値(`<v>`)はそのまま残す。`edits` と `operations` のどちらから来ても同じ扱いで、扱いは `src/office/shared-formula.ts` の 1 か所に寄せてある。
訳せない式は、段 1 と同じく断ってファイルに手を付けない。
| 断る場面 | 例 |
|---|---|
| 列や行をまるごと指す参照が混じっている | `SUM(A:A)` / `SUM(1:1)` |
| 訳した結果が表の外(行 1 や列 A より手前)へ出る | 本体が範囲の左上に無い書類 |
- `edits`: `Cannot rewrite the copied formula from C2 (the block C2:C4) for C3 ...` を返す
- `operations`: `violations[].code` が `SHARED_FORMULA_MASTER`、`sheet` と `cell` に場所が入る
これとは別に、**壊れた書類**として断る場面が 3 つある。どれも Excel 本体は書かない形で、外から来た書類への歯止め。
| 断る場面 | なぜ決められないか |
|---|---|
| 本体が範囲と番号を持ちながら式の文面を持たず(`<f t="shared" ref="C2:C4" si="0"/>`)、その番号を指すセルが残っている | 配る中身が無い。通すと本体の `ref` と `si` の定義ごと消え、残りのセルの `si` が指す先を失う(規格 ISO/IEC 29500 18.3.1.40 は本体が文面を持つと定めている) |
| 番号を名乗るセルが、その番号のどの本体の `ref` 範囲にも入っていない | どちらのかたまりへ配るのが正しいか決まらない |
| 同じ番号の本体が 2 つあり、`ref` 範囲が重なっている | 重なりの中のセルがどちらの持ち物か決まらない |
**追随するセルの持ち主は、番号(`si`)だけでは決まらない。**番号と `ref` 範囲の両方で決める。番号だけで決めると、番号がぶつかった 2 つのかたまりが混線する——`C2:C3` と `E2:E3` がどちらも `si="0"` の書類で `C2` だけを書き換えると、`E3` に C のかたまりの式が入り、触っていない E のかたまりが黙って壊れる。判定は `src/office/xlsx-sheet-edit.ts` の `assignSharedFormulaFollowers` の 1 か所にある。
**番号がぶつかった書類は `edits` なら通るが、`operations` は 1 マスも書けない。**扱いが分かれるのは、`operations` が exceljs を通すため。exceljs は読み込みの時点で追随するセルを 1 つの本体へ結びつけてしまい、**番号がぶつかっていた事実そのものがモデルから消える**。しかも結び先の選び方が読み手ごとに違う(exceljs は**あとに**見た本体が勝ち、Excel / LibreOffice は**先に**見た本体が勝つ)。`operations` はブックを丸ごと読み込んで丸ごと書き戻すので、**書き換えたマスと壊れるマスは別**になる。コピー式と関係のない `A1` に値を 1 つ入れるだけで、触っていないかたまりの式が別のかたまりの式に置き換わる(実測: `C3` が 33 から 90、`E3` が 40 から 630。別のシートを編集しても同じ)。
そこで `operations` は、**ファイルを開いた時点で**生のシート XML をシートごとに 1 回ずつ見て、下の 5 つのどれかがあれば `SHARED_FORMULA_MASTER` で断る。書き込み先を組み立てる前に断るので、ファイルには手を付けない(#1378 / #1376)。
| 断る並び | どうなるはずだったか | `edits` は |
|---|---|---|
| 同じ番号の本体が 2 つ以上ある | 触っていないかたまりの式が化ける | 通る(`ref` 範囲で持ち主を決める) |
| `ref` 範囲からも持ち主が決まらない(上の表の 2 つ) | 別のかたまりの式が流れ込む | その本体を書く指示だけ断る |
| 番号を名乗るセルが残っているのに、その番号の本体がシートに 1 つも無い | 中身の無い式(`<f/>`)が書かれ、Excel の表示が `Err:520` になる | 通る(触らないセルは書き換えない) |
| 本体が、その番号を名乗るセルより**後ろ**に並んでいる | 同上。exceljs は読みながら採番表を作るので、まだ登録されていない番号を引いたセルの持ち主が `undefined` になる | 通る(`ref` 範囲で持ち主を決めるので並び順に依らない) |
| 本体が式の文面を持たず、その番号を指すセルが残っている | exceljs が式を平行移動しようとして例外を出す(保存できない) | その本体を書く指示だけ断る |
**並び順まで見る。**シートにその番号の本体が「居るか」だけを見ると、後ろに並んだ本体の書類が素通りして値が消える(#1378 レビュー P1-1)。判定は `<c>` を文書順に 1 回歩き、本体を見た時点でその番号を登録済みにして、**その時点で登録済みの番号だけ**を親ありと見なす。
判定は `src/office/xlsx-sheet-edit.ts` の `findSharedFormulaSheetDoubts` の 1 か所。番号のぶつかりと持ち主の判定はさらに `findSharedFormulaOwnershipDoubts` / `assignSharedFormulaFollowers` に相乗りしていて、`edits` 側と同じ判定を使う。断り文句は「`edits` なら通る」のか「本体に触らなければ通る」のかで書き分けている。
**共有式の番号はシートごとに閉じている。**普通のブックは、シートが何枚あっても素で「どのシートも `si="0"`」になる。だから断る判定は**シート 1 枚ぶんの XML**を見る。複数シートのブックがどれも `si="0"` を使っていても通る。
| 経路 | 番号がぶつかった書類 |
|---|---|
| `edits` | 通る。`ref` 範囲で持ち主を決めて実体化する(範囲からも決まらない書き方なら、こちらも断る) |
| `operations` | どのマスを書く指示でも `SHARED_FORMULA_MASTER` で断る。ファイルには手を付けない |
配列数式(`t="array"`)のもとになっているセルは、この扱いの対象外(#1363)。
#### 検証つき詳細編集(operations)
色・レイアウト・シート追加・ガント、または数式の計算結果検証が必要な場合に使う(セルの結合と列の幅は上の `merges` / `columns` で足りる)。既存ファイルを直接上書きせず、まず別名へ出すのを推奨。
```js
EditExcel({
path: "input/plan.xlsx",
output_path: "output/plan-checked.xlsx",
operations: [
{ op: "set_values", sheet: "計画", range: "A2:B3",
values: [["設計", 0.5], ["実装", 0.25]] },
{ op: "set_formula", sheet: "計画", cell: "C2",
formula: "B2*100", expect: { equals: 50, tolerance: 0.001 } },
{ op: "merge", sheet: "計画", range: "A1:C1" },
{ op: "style_range", sheet: "計画", range: "A1:C1",
style: { fill: "#1F4E78", font_color: "#FFFFFF", bold: true } },
{ op: "set_column_width", sheet: "計画", columns: "A:C", width: 18 },
{ op: "freeze_panes", sheet: "計画", rows: 1, columns: 0 }
]
})
```
主な `op`:
| op | 必須項目 | 用途 |
|---|---|---|
| `add_sheet` | `name` | シート追加 |
| `set_values` | `sheet, range, values` | 二次元配列を範囲へ書く |
| `set_formula` | `sheet, cell, formula` | 1セルへ数式。任意の `expect` で結果検証 |
| `fill_formula` | `sheet, range, formula` | 相対参照をずらしながら数式を範囲へコピー |
| `merge` / `unmerge` | `sheet, range` | セル結合・解除(保持したまま直したいときは上の `merges`) |
| `style_range` | `sheet, range, style` | 背景・文字・罫線・太字・表示形式・配置 |
| `set_column_width` | `sheet, columns, width` | 列幅(保持したまま直したいときは上の `columns`。上限 255 は共通) |
| `set_row_height` | `sheet, rows, height` | 行高(`height` は 0 より大きく 409.5 以下。上の `rows` と同じ上限) |
| `freeze_panes` | `sheet, rows, columns` | ウィンドウ枠固定 |
色は `#RRGGBB` / `#AARRGGBB`。数式先頭の `=` は省略可能。`expect` は単一値、または `{ equals, tolerance }`。不一致なら保存しない。
ガントは通常の `operations` で日付・開始日・終了日・進捗を書いたうえで `gantt` を指定する。
```js
gantt: {
sheet: "計画",
timeline_header: "D1:AH1",
task_rows: "2:20",
start_column: "A",
end_column: "B",
progress_column: "C",
progress_scale: "fraction",
holidays: ["2026-08-11"],
roles: {
weekend: { fill: "#EEEEEE" },
holiday: { fill: "#FFD9D9" },
planned: { fill: "#9DC3E6" },
completed: { fill: "#70AD47" }
}
}
```
保存前に、入力OOXMLの危険・非対応機能検査、操作競合、セル数上限、数式の静的検査、LibreOffice再計算、`#VALUE!` 等の数式エラー、期待値、数式と結合構造の保持を検証する。どれかが失敗すると候補を削除し、JSONの `phase` / `violations[].code` / `sheet` / `cell` に理由を返す。`dry_run: true` なら検証のみで保存しない。
`violations[].code` は原因の所在で読み分ける。`SHARED_FORMULA_MASTER` は入力ファイル(コピーされた式のかたまりが扱えない形になっている。おおもとへ書こうとして残りのセルの式を訳せなかった場合と、**書き込み先とは関係なくファイルを開いた時点で断った**場合の両方がある。上記)、`INPUT_INVALID` は渡した引数、`PACKAGE_INVALID` / `UNSUPPORTED_WORKBOOK_FEATURE` は入力ファイル、`INTERNAL_ERROR` は**こちら側の実行時例外**(再計算プロセスが起動できない等)で、ファイルが壊れていることを意味しない。`INTERNAL_ERROR` を「ファイルを直してから再実行」の根拠に使わないこと。ただし、以前はファイル側が原因でも `INTERNAL_ERROR` になる形が 1 つあった——式の文面を持たない共有式の本体(上記)で、exceljs が式を平行移動しようとして例外を出していた。いまはファイルを開いた時点で `SHARED_FORMULA_MASTER` として断るので、この形は `INTERNAL_ERROR` にならない(#1376)。
新規保存では `output_path` が既に存在すると拒否する。同じ入力を上書きする場合だけ `overwrite: true` と読取時点の `expected_source_sha256` が必要。編集中に元ファイルが変わった場合も確定しない。
**詳細編集の制約**: `.xlsx` のみ。マクロ、外部リンク、ピボット、スライサー、グラフ、OLE、Excelテーブル、署名、動的配列などを含むブックはfail closedで拒否する。複雑なブックの値だけを変える場合は `edits` を使う。`readonly/` 配下は常に書き換え不可。
### EditDocx
既存の .docx の**指定した段落だけ**を書き換える。
```
EditDocx({ path: "output/report.docx", list_paragraphs: true }) // まず番号を知る
EditDocx({
path: "output/report.docx",
edits: [{ paragraph: 3, text: "書き直した本文です。" }]
})
```
| 引数 | 意味 |
|---|---|
| `path` | 対象の .docx(workspace 相対) |
| `list_paragraphs` | `true` なら書き換えず、番号つきの段落一覧だけ返す |
| `edits[].paragraph` | 0 始まりの段落番号(空段落も 1 件として数える) |
| `edits[].text` | 差し替える文字列。空文字なら段落を残して中身だけ消す |
| `output_path` | 別名保存先。省略時は上書き |
**何が保たれるか**: 本文の項目(`word/document.xml`)だけを差し替える。スタイル定義・表・画像・ヘッダー / フッター・変更履歴・埋め込みオブジェクトは元のバイト列のまま残る。段落の書式(見出しなど)と、その段落の先頭の文字書式も引き継ぐ。
**制約**: 段落まるごとの差し替えなので、1 段落の中で途中から書式が変わっていた場合、その変化は先頭の書式に統一される。引き継ぐのは**最初の run の書式**で、run が 1 つも無い段落のときだけ段落記号の書式を引き継ぐ(そのとき、段落記号にしか置けない挿入・削除・移動・書式変更の印は落とす)。**書き換えた段落の中にあった変更履歴(挿入・削除・移動の印)・コメントの錨・しおり・ハイパーリンク・コンテンツコントロールは、出力から消える**(コメントや脚注の本体は別ファイルに残るが、本文からの参照は失われる)。校閲中の文書を書き換えるときは、消してよいかを先に確認すること。段落の追加・削除、表の行・列・セルの追加や削除・結合はできない。**表の中の段落は段落番号に入り、書き換えられる**(`list_paragraphs` が返した番号をそのまま指定する)。`.doc`(旧形式)は非対応。
### EditPptx
既存の .pptx の**指定した段落だけ**を書き換える。
```
EditPptx({ path: "output/deck.pptx", list_paragraphs: true }) // まずスライドと段落の番号を知る
EditPptx({
path: "output/deck.pptx",
edits: [{ slide: 2, paragraph: 0, text: "第2四半期の実績" }]
})
```
| 引数 | 意味 |
|---|---|
| `path` | 対象の .pptx(workspace 相対) |
| `list_paragraphs` | `true` なら書き換えず、スライドごとの段落一覧だけ返す |
| `edits[].slide` | 1 始まりのスライド番号(**見た目の並び順**) |
| `edits[].paragraph` | 0 始まりの段落番号(空段落も 1 件として数える) |
| `edits[].text` | 差し替える文字列。空文字なら段落を残して中身だけ消す |
| `output_path` | 別名保存先。省略時は上書き |
**スライド番号は並び順で数える**。項目名の番号(`slide3.xml`)とは一致しないことがある——スライドを並べ替えてもファイル名は変わらず、順序だけが変わるため。`list_paragraphs` が返す番号をそのまま使うこと。
段落番号は図形をまたいだ通し番号。タイトルも本文も表の中の文字も、同じ 1 本の並びに入る。
**何が保たれるか**: 指定したスライドの項目(`ppt/slides/slideN.xml`)だけを差し替える。配置・図形・画像・アニメーション・スライドマスター・ノート・埋め込みオブジェクトは元のバイト列のまま残る。段落の書式と、その段落の先頭の文字書式も引き継ぐ。
**制約**: 段落まるごとの差し替えなので、1 段落の中で途中から書式が変わっていた場合、その変化は先頭の書式に統一される。段落・スライドの追加や削除、図形の移動はできない(新規作成は AddSlide / BuildPptx)。`.ppt`(旧形式)は非対応。
## 注意
- すべて workspace 内のローカルファイル(`input/` または `output/`)が対象
- URL 指定不可 → DownloadFile で先にローカル保存
- EditExcel / EditDocx / EditPptx 以外は read-only(書き込まない)
## ファイルサイズ上限
Read 系ツールは悪意あるファイル / リソース枯渇対策として入力サイズを検証する。デフォルトは以下の通りで、`config.yaml` の `tools` セクション、または Settings UI の「Tools → Office ファイルサイズ上限」から変更可能。
| ツール | デフォルト | config キー |
|---|---|---|
| ReadExcel / EditExcel | 10 MB | `tools.office_excel_max_size_mb` |
| ReadDocx / EditDocx | 10 MB | `tools.office_docx_max_size_mb` |
| ReadPdf | 10 MB | `tools.office_pdf_max_size_mb` |
| ReadPPTX / EditPptx | 50 MB | `tools.office_pptx_max_size_mb` |
| SplitExcelSheets | 10 MB | `tools.office_excel_max_size_mb` |
| SplitDocxSections | 10 MB | `tools.office_docx_max_size_mb` |
| 画面の Excel セル保存 | 10 MB | `tools.office_excel_max_size_mb` |
| 画面の Excel プレビュー | 50 MB | `tools.office_preview_max_size_mb` |
| 展開後(下記 10 経路すべて) | 200 MB | `tools.office_pptx_max_uncompressed_mb` |
「展開後」は ZIP bomb 検知用で、ZIP 内の全エントリの非圧縮合計サイズに対する閾値。超過時は、エージェント向けの経路では `ZIP bomb detected: ...` エラーとなる(画面のプレビューだけは 413 と数値を返し、文言は UI が翻訳して組む)。キー名は ReadPPTX 由来だが、**OOXML を開く経路すべてで共有する**。判定は ZIP ヘッダが申告するサイズの合計で行う。`EditExcel` の `operations` モードだけは共通の関所ではなく preflight を通り、実際に展開して測るが、上限は同じ config キーを読む。
| 展開後の上限が掛かる経路 | 備考 |
|---|---|
| `Read` → ReadExcel / ReadDocx / ReadPPTX | 読む口。ReadExcel / ReadDocx は #1084 段階 2 で追加 |
| SplitExcelSheets / SplitDocxSections | #1084 段階 2 で追加(それまでファイルサイズ上限すら無かった) |
| EditExcel / EditDocx / EditPptx | 書き換える口 |
| 画面の Excel プレビュー(`GET .../files/office-preview`) | #1084 段階 2 で追加(それまで上限が 1 つも無かった) |
| 画面の Excel セル保存(`PUT .../files/xlsx-cells`) | |
以前この関所が無かった 5 経路(読む口 2 つ・分割ツール 2 つ・画面のプレビュー)は、**嘘の申告をしなくても増幅した**——正当なファイルに大きなエントリを正直に足すだけで通っていた(issue #1084 段階 2 で塞いだ)。
**画面のプレビューだけ、ファイルサイズのつまみが別**(`tools.office_preview_max_size_mb`、既定 50MB)。エージェント側の 10MB は「読んで LLM に渡す量」を想定した値で、人が眺めるだけの経路には小さい——この本の前まで上限が無く映せていた実務ブック(30 万行の受注明細で約 20MB)が、共有すると今日から映らなくなる(#1084 レビュー P2-1)。展開後サイズと項目数はエージェントと共有のまま。実メモリを縛っているのはそちらなので、片方だけ動かす理由が無い。
**既定値で残る量**: ファイル 10MB・展開後の申告 61MB・項目 10 件のブックが、全部の関所を通ったうえで常駐を約 588MB 増やす(嘘の申告は無し)。実質的に効いている歯止めはファイルサイズで、展開後 200MB の関所は実メモリを縛れていない(展開後サイズと実メモリの比が約 9.6 倍あるため)。ここを実測で縛るのは #1084 段階 3 の担当。
**上げるときの副作用**: `operations` モードは申告値ではなく実際に展開して測るため、このキーはその経路が**展開に使ってよいメモリの実質的な上限**でもある。値を 200MB から大きく上げると、1 回の編集が確保しうるメモリの上限も同じだけ上がる(以前は経路側の固定値 200MB で頭打ちだった)。同時実行数と積んでいるメモリを見て決めること。
**まだ関所が乗っていない経路**もある。次の 2 つは対象外か、別の守りに任せている。
| 関所の無い経路 | 守り |
|---|---|
| `Read` から呼ばれる PDF / メール(`.msg`)の解析 | ZIP ではないので対象外。ファイルサイズ上限で守る |
| 画面の PowerPoint / Word プレビュー変換 | 変換を別プロセス(LibreOffice)に出すので、本体プロセスは膨らまない |
展開後サイズとあわせて **ZIP エントリ数**も見る。中身が空のエントリを大量に並べたファイルは申告合計が 0 バイトになるため、サイズの閾値だけでは止まらない。超過時は `ZIP bomb detected: N entries exceeds limit of 10000` を返す(画面のプレビューは同じ判定で 413 と件数を返す)。
| 上限 | デフォルト | config キー |
|---|---|---|
| ZIP エントリ数(展開後サイズと同じ 10 経路 + `operations` モードの preflight) | 10,000 件 | `tools.office_max_entries` |
件数の判定は 2 段構えになっている(#1084 段階 1)。
1. **ZIP 末尾の中央ディレクトリが申告するエントリ数**で先に断る。エントリを 1 件も構築せずに済むので、断ると決まっているファイルにメモリを払わない。
2. 申告が上限内なら、**中央ディレクトリを自前で最後まで歩き**(`src/office/zip-central-directory.ts`)、実際に並んでいる件数と申告を突き合わせる。食い違えば `Refusing to open this package: its index lists N entries but the trailer declares M...` で断る。
2 が要るのは、**中央ディレクトリの読み方が層によって非対称**だから。adm-zip は申告件数で止まり(`zipFile.js` の `readEntries` は `new Array(mainHeader.diskEntries)` から始まる)、ExcelJS が使う JSZip はエントリの並びが署名の続く限り読む。申告を小さく偽ると、関所からは 5 項目に見え、読み込み層からは 60,005 項目すべてが見えた——511KB のファイル 1 本で常駐 1.1GB、増幅は約 2,300 倍(issue #1084 の実測)。歩く部品は JSZip と同じ歩き方(末尾の記録はファイル全体の最後の署名、目録は署名が続く限り)に揃えてある。**adm-zip に件数を無視して読む口は無い**ので自前で書いている。
**歩けない形は断る(fail closed)。** 断ると決めた形は次のとおりで、いずれも実在の作り手が書く形ではない。
| 断る形 | 理由 |
|---|---|
| 実際の件数と申告件数が食い違う | 穴の本体。隠したエントリを持つ包み |
| 末尾の記録(EOCD)が見つからない | ZIP ではない。または末尾が壊れている |
| 中央ディレクトリが申告位置に無い(前にデータが付いた形=自己解凍形式など) | adm-zip は開けずに例外、JSZip は位置をずらして読み切る。**層ごとに見えるものが変わる**ので数えられない |
| 中央ディレクトリが途中で切れている | 同上 |
| ZIP64 だと申告しているのに、その記録が無い/表現できない大きさ | 同上 |
ZIP64(件数欄が 0xFFFF に張り付いて別の記録に載る形)は**正しく読んで通す**。ExcelJS / adm-zip / JSZip / LibreOffice / Info-ZIP / Python zipfile が書いた包みと、70,000 件の本物の ZIP64 が通ることを実測済み(`src/office/zip-central-directory.test.ts`)。
**まだ止まらないもの**: エントリの**申告サイズ**を小さく偽る形は、この関所では止まらない(申告値の合計で判定しているため)。そちらは実際に展開して測る形へ上げる段階 3 で塞ぐ(issue #1084)。
マクロ付きファイル(`.xlsm` / `.docm` / `.pptm` / `.xlsb`)は警告付きで読み込まれる(実行はされない)。