ken_nogi/lineworks-sync/lineworks-anythingllm.spec.md
Kenichiro NOGI 88a402ce0f up
2026-07-10 18:13:30 +09:00

181 lines
16 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.

# lineworks-anythingllm.js 仕様書 (v3)
## 1. 概要
LINE WORKS の掲示板(board)投稿と添付ファイルを取得し、Markdown 化した上で AnythingLLM にアップロード・embedding 登録する RAG 連携スクリプト。認証は `getGroupList.js` と同じ、Node.js 標準の `crypto` モジュールによる JWT 自前署名方式を利用する(追加の npm install 不要)。
v3 では投稿本文に加え、PDF・画像等の添付ファイルも個別にダウンロードして AnythingLLM にアップロードする(AnythingLLM 側で OCR/テキスト抽出される)。
参考: LINE WORKS 掲示板 API 仕様書 https://developers.worksmobile.com/jp/docs/board
## 2. 実行環境・依存パッケージ
- Node.js 18 以上(標準 `fetch` / `FormData` / `Blob` を使用)
- 追加の npm パッケージは不要
- 実行例: `node lineworks-anythingllm.js <コマンド>`
- ダブルクリック実行用: [run-lineworks-sync.bat](run-lineworks-sync.bat)(`sync` を実行して自動終了)
## 3. コマンド一覧
| コマンド | 内容 |
|---|---|
| `node lineworks-anythingllm.js sync` | `board-list.csv` の対象掲示板(または全掲示板)の投稿・添付ファイルを取得し、AnythingLLM に同期する。 |
| `node lineworks-anythingllm.js list` | アクセス可能な全掲示板一覧を取得し、`board-list_<日時>.csv` に出力する(sync対象の選定用の一覧出し)。 |
| `node lineworks-anythingllm.js ask "問い合わせ内容"` | AnythingLLM ワークスペースに質問し、回答と参照元投稿を表示する。 |
| `node lineworks-anythingllm.js test-recent` | 動作確認用。掲示板を指定せず直近の新着投稿を横断取得できるか確認する。 |
## 4. 環境変数
### LINE WORKS 認証系(`getGroupList.js` と共通の命名)
| 変数名 | 既定値 | 用途 |
|---|---|---|
| `LW_CLIENT_ID` | (コード内既定値あり) | LINE WORKS API クライアントID |
| `LW_CLIENT_SECRET` | (コード内既定値あり) | クライアントシークレット |
| `LW_SERVICE_ACCOUNT` | (コード内既定値あり) | Service Account のメールアドレス |
| `LW_PRIVATE_KEY` | — | 秘密鍵の中身を直接渡す場合に使用(未設定なら `LW_PRIVATE_KEY_FILE` を使用) |
| `LW_PRIVATE_KEY_FILE` | `./private_20260307184804.key` | 秘密鍵ファイルのパス |
| `LW_SCOPE` | `board board.read` | 取得スコープ。掲示板APIには `board`、投稿本文の読み取りには `board.read` が別途必要な場合がある |
| `LW_REQUEST_DELAY_MS` | `250` | API呼び出し間のスロットリング間隔(ms) |
| `LW_MAX_RETRIES` | `5` | 429(Rate Limit)発生時の最大リトライ回数 |
### AnythingLLM 系
| 変数名 | 既定値 | 用途 |
|---|---|---|
| `ANYTHINGLLM_BASE_URL` | — | 例: `http://localhost:3001` |
| `ANYTHINGLLM_API_KEY` | — | APIキー |
| `ANYTHINGLLM_WORKSPACE_SLUG` | — | 例: `lineworks-board` |
| `EMBEDDING_BATCH_SIZE` | `50` | embedding登録のバッチサイズ |
### AI分割(LM Studio)系
| 変数名 | 既定値 | 用途 |
|---|---|---|
| `AI_SPLIT_MIN_LENGTH` | `3000` | この文字数を超える非条文形式の投稿はAI分割を試みる |
| `LMSTUDIO_CHAT_URL` | `http://localhost:1234/v1/chat/completions` | LM Studio のチャット補完エンドポイント |
| `LMSTUDIO_MODEL_NAME` | `qwen2.5-14b-instruct` | 使用モデル名 |
## 5. board-list.csv(同期対象の管理)
```
boardId,boardName,flag
4080000000758656266,全社掲示板(通達),1
4080000000768413725,全社掲示板(お知らせ)
```
- スクリプトと同じフォルダに配置。
- `flag` 列が `"1"` の行だけが `sync` の対象になる。
- **該当行が1件も無い場合(ファイルが無い場合を含む)は、アクセス可能な全掲示板を自動取得して同期する**(csv-anythingllm.js の `csv-list.csv` とは異なり、こちらは全件フォールバックする仕様)。
- `list` コマンドはこのファイルを直接更新せず、`board-list_<実行日時>.csv` という別ファイルに現在アクセス可能な全掲示板を出力する。同期対象を追加/変更したい場合は、その内容を見ながら手動で `board-list.csv``flag` 列を編集する。
## 6. 認証・トークン管理
- JWTアサーション(RS256)を `crypto.createSign` で自前署名し、`urn:ietf:params:oauth:grant-type:jwt-bearer` グラントで LINE WORKS のトークンエンドポイントからアクセストークンを取得する(`getAccessToken`)。
- 19桁の `boardId`/`postId` は JSON.parse の数値精度落ちを避けるため、パース前に該当キーの数値を文字列化してから `JSON.parse` する(`parseJsonWithQuotedInt64`)。
- アクセストークンの有効期限は1時間。`sync` 実行中は45分経過ごとに自動的に再取得する(`ensureFreshToken`/`TOKEN_REFRESH_INTERVAL_MS`)。長時間の同期処理中に401で失敗するのを防ぐための仕組み。
## 7. Rate Limit対策
- 全ての LINE WORKS API 呼び出しは共通ラッパー `lwFetch` を経由する。
- 呼び出し前に `LW_REQUEST_DELAY_MS`(既定250ms)のウェイトを入れてスロットリングする。
- 429(Rate Limit)応答時は `Retry-After` ヘッダー、無ければ指数バックオフで待機し、`LW_MAX_RETRIES`(既定5回)まで自動リトライする。
## 8. `sync` の処理フロー
1. アクセストークンを取得。
2. `board-list.csv` から `flag=1` の掲示板一覧を取得(`loadTargetBoardsFromCsv`)。0件ならアクセス可能な全掲示板を取得(`fetchAllBoards`)。対象が0件ならエラー終了。
3. 対象掲示板を1件ずつ処理:
1. `fetchAllPosts` でその掲示板の全投稿を取得(カーソルページング)。
2. 投稿ごとに `fetchPostDetail` で詳細取得。`fileCount > 0` なら `fetchPostAttachments` で添付ファイル一覧を取得。
3. 投稿本文のハッシュ(10節)が `board-uploaded-hashes.json` に記録済みならアップロード処理をスキップする。未記録の場合のみ、本文(HTMLタグ除去後)を以下の優先順で処理:
- **条文形式**(「第◯条」が3件以上かつ本文3000文字以上、8.1節): 条ごとに個別 Markdown ファイルに分割してアップロード。
- **非条文の長文**(3000文字超、8.2節): LM Studio に区切り位置だけ判断させ、区切り記号の位置でプログラム側が機械的に分割してアップロード(本文の改変は許容誤差±10%を超えたら破棄し通常アップロードにフォールバック)。
- **通常の投稿**: 1投稿1 Markdown ファイルとしてアップロード。
アップロード成功後、そのファイルを `board_posts_md/complete/` へ移動し(9.1節)、投稿本文のハッシュを記録する。
4. 添付ファイルは1件ずつ、そのハッシュ(10節)が未記録の場合のみ、リダイレクトを手動追跡しつつ本体をダウンロードし、ローカル保存後に個別に AnythingLLM へアップロードする。アップロード成功後、`board_attachments/complete/` へ移動し(9.1節)、添付ファイルのハッシュを記録する。記録済みの場合はダウンロード自体を行わずスキップする。
5. 投稿単位・掲示板単位でエラーが起きても、警告ログを出して次の投稿/掲示板の処理を継続する(スキップ処理)。
4. アップロードした全ドキュメントの location をまとめて `addToWorkspaceEmbeddings` で embedding 登録(`EMBEDDING_BATCH_SIZE` 件ずつバッチ処理、失敗したバッチは警告ログに残し処理は継続)。
5. 処理に失敗した掲示板があれば、最後に一覧として出力する(権限不足の可能性を案内)。
### 8.1 条文形式の分割ロジック
- `第([0-9-]+)条` を条区切りとして正規表現で検出(全角/半角数字対応)。見出しが括弧内にあれば取得。
- 本文が3000文字以上、かつ条文パターンが3件以上マッチした場合のみ分割対象とする(`ARTICLE_SPLIT_MIN_LENGTH` / `ARTICLE_SPLIT_MIN_COUNT`)。それ未満は通常の1投稿1ファイル扱い。
- 最初の条文より前に50文字を超える前文がある場合は「前文」として別ファイルに分離する。
- LLMは使用せず、正規表現による機械的な分割のみ(本文の書き換え・要約は行わない)。
### 8.2 非条文長文のAI分割ロジック
- 対象: 条文形式に該当せず、本文が `AI_SPLIT_MIN_LENGTH`(既定3000文字)を超える投稿。
- LM Studio(OpenAI互換のchat completions API)に対し、「本文は一切変更せず、分割したい箇所にのみ `===SPLIT===` を挿入して返す」プロンプトを送信。
- 返却された本文を `===SPLIT===` で分割し、2件未満(=分割されなかった)場合は通常の1ファイル扱いにフォールバック。
- 改変チェック: 分割後の合計文字数が原文の90%〜110%の範囲を外れた場合は分割結果を破棄し、通常アップロードにフォールバックする(LLMによる意図しない要約・改変を防ぐガード)。
## 9. Markdown/添付ファイルの出力先
| 種別 | 出力先 | ファイル名規則 |
|---|---|---|
| 投稿(通常) | `board_posts_md/` | `board<boardId>_post<postId>.md` |
| 投稿(条文分割) | `board_posts_md/` | `board<boardId>_post<postId>_art<条番号>.md` |
| 投稿(AI分割) | `board_posts_md/` | `board<boardId>_post<postId>_part<連番>.md` |
| 添付ファイル | `board_attachments/` | `board<boardId>_post<postId>_<attachmentId>_<安全化したファイル名>` |
MIMEタイプは拡張子から簡易判定(`guessMimeType`。pdf/png/jpg/gif/webp/txt/md/docx/xlsx/pptx/csv に対応、それ以外は `application/octet-stream`)。
### 9.1 取り込み完了ファイルの退避(complete フォルダ)
AnythingLLMへのアップロードが成功したファイルは、共通関数 `moveFileToCompleteDir` で元のディレクトリ配下の `complete/` サブフォルダへ移動する(次回 `sync` での重複アップロード・重複整理を避けるため)。
| 対象 | 移動先 |
|---|---|
| 投稿Markdown(通常/条文分割/AI分割いずれも) | `board_posts_md/complete/` |
| 添付ファイル | `board_attachments/complete/` |
- 同名ファイルが移動先に既に存在する場合は上書きせず、`<ファイル名>_<タイムスタンプ>.<拡張子>` の形式で退避する。
- アップロードが失敗した(location が取得できなかった)ファイルは移動されず、`board_posts_md/`・`board_attachments/` 直下に残る。
## 10. 重複防止の仕組み
`board-uploaded-hashes.json`(スクリプトと同じフォルダ)に、アップロード済みの投稿・添付ファイルのSHA-256ハッシュを記録し、次回以降の `sync` で同一内容の再アップロードをスキップする(`csv-anythingllm.js` の行ハッシュ方式と同じ考え方)。
| 対象 | ハッシュの元になる値 | 備考 |
|---|---|---|
| 投稿本文 | `boardId + postId + 更新日時(updatedTime、無ければcreatedTime) + タイトル + 本文` | 本文が編集され更新日時が進むと別ハッシュになり、再アップロードされる。 |
| 添付ファイル | `boardId + postId + attachmentId` | `attachmentId` はLINE WORKS側で不変のため、内容ハッシュではなくID自体で同一性を判定する。ダウンロード自体もスキップされる。 |
- 投稿本文と添付ファイルの重複判定は独立している。投稿本文が重複でスキップされても、その投稿に新しく追加された未アップロードの添付ファイルは個別に取り込まれる。
- ハッシュはアップロード成功の都度、即座に `board-uploaded-hashes.json` へ保存する(途中で異常終了しても再実行時に重複しないようにするため)。
- 実行末尾に「重複スキップ: 投稿n件 / 添付ファイルn件」件数をログ出力する。
- 本機能導入前にアップロード済みだった投稿・添付ファイル(初回導入時に `complete/` へ手動退避したものを含む)はハッシュが未記録のため、導入後最初の `sync` では新規として再アップロードされる(13節も参照)。
## 11. `list` コマンド
- アクセス可能な全掲示板を取得し、`board-list_<実行日時(YYYYMMDD-HHMMSS)>.csv` として出力(UTF-8 BOM付き)。
- 値にカンマ・改行・ダブルクォートを含む場合は `csvEscape` によりダブルクォートで囲みエスケープする(`board-list.csv` を読み込む側の `loadTargetBoardsFromCsv` は単純な `split(",")` のためエスケープに対応していない点に注意 — 掲示板名にカンマ等が含まれる場合は `board-list.csv` 側で手動調整が必要)。
## 12. `ask` コマンド
- 指定した質問文を AnythingLLM ワークスペースの `/chat`(`mode: "query"`)に送信し、回答本文(`textResponse`)と参照元(`sources`)のタイトル/ファイル名を表示する。
## 13. ログ・エラーハンドリング
- 専用のログファイル出力機構は無く、標準出力(`console.log`/`warn`/`error`)にのみ出力する(`csv-anythingllm.js` のようなファイルロギングは無い)。
- 投稿単位・掲示板単位のエラーは警告として記録しつつ処理を継続する(スキップ)。
- アクセストークン取得やLINE WORKS API呼び出し自体の失敗(認証情報不備、権限不足など)は例外として送出され、`main()` の `catch` でエラーメッセージを表示して終了コード1で終了する。
## 14. 制限事項・注意点
- `board-list.csv` が無い/対象0件の場合は全掲示板を自動取得するため、想定外の掲示板まで同期されないよう、定常運用では `board-list.csv` を用意し明示的に対象を絞ることを推奨する。
- AI分割機能はローカルの LM Studio(既定 `http://localhost:1234`)が起動している前提。未起動の場合はAI分割時にエラーとなり、警告ログを出した上で通常アップロードにフォールバックする。
- 添付ファイルのダウンロードは LINE WORKS のリダイレクト先(`apis-storage.worksmobile.com`)へも同じBearerトークンを引き継ぐ必要があるため、`fetch` の自動リダイレクトではなく手動でリダイレクトを処理している(`redirect: "manual"`)。
- `board_posts_md/`・`board_attachments/` の `complete/` サブフォルダは、`fetchAllPosts`/`fetchPostAttachments` の取得結果とは無関係にローカルファイルの整理のみを行うものであり、`board-uploaded-hashes.json` への記録とは別軸の仕組みである。`complete/` へ移動済みでもハッシュが未記録であれば、次回 `sync` で再度取得・再アップロードされローカルファイルも再生成される。
## 15. 関連ファイル
- [lineworks-anythingllm.js](lineworks-anythingllm.js) — 本体
- [board-list.csv](board-list.csv) — sync のマスターリスト(flag=1が対象)
- [board-uploaded-hashes.json](board-uploaded-hashes.json) — 重複防止用のアップロード済みハッシュ一覧(10節)
- [run-lineworks-sync.bat](run-lineworks-sync.bat) — `sync` のダブルクリック実行用バッチファイル
- `board_posts_md/`, `board_attachments/` — 生成物の出力先(取り込み完了分は `complete/` 配下)