551 lines
23 KiB
JavaScript
551 lines
23 KiB
JavaScript
"use strict";
|
||
|
||
/*
|
||
* originalData配下のExcel(顧客相談履歴など)をシート単位でCSVへ分割し、
|
||
* LINE WORKS用とは別のAnythingLLMワークスペースにRAG登録するスクリプト。
|
||
*
|
||
* 使い方:
|
||
* node csv-anythingllm.js convert … originalData/ 内の全.xlsxをシート別CSV+設定雛形に変換
|
||
* (変換完了後、そのExcelはoriginalData/complete/へ移動)
|
||
* node csv-anythingllm.js sync <シート名> … 指定シートのみ同期
|
||
* node csv-anythingllm.js sync-all … csv-list.csv でflag=1のシートのみ順番に同期
|
||
*
|
||
* csv-list.csv (このスクリプトと同じフォルダに配置、sync-all実行には必須):
|
||
* sheetName,flag の形式。flag列が "1" の行だけが sync-all の対象になる。
|
||
* このファイルが無い、またはflag=1の行が1つも無い場合、sync-allは何も同期せず終了する
|
||
* (lineworks-anythingllm.jsのboard-list.csvと異なり、全件同期へのフォールバックはしない)。
|
||
* convert実行時に新しく見つかったシートは自動で flag=0 として追記登録される
|
||
* (既存行のflag値は上書きされない)。同期したいシートは手動でflagを1に変更すること。
|
||
*
|
||
* 環境変数:
|
||
* ANYTHINGLLM_BASE_URL / ANYTHINGLLM_API_KEY … lineworks-anythingllm.js と共通
|
||
* EMBEDDING_BATCH_SIZE … 既定 50
|
||
* ORIGINAL_DATA_DIR … 既定 ./originalData
|
||
* CSV_ROOT_DIR … 既定 ./csv
|
||
* CSV_CONFIG_DIR … 既定 ./csv-config
|
||
* CSV_CHUNK_MAX_LENGTH … 既定 2000 (相談履歴のチャンク分割の目安文字数)
|
||
* CSV_LOG_DIR … 既定 ./logs (実行ログの保存先)
|
||
*
|
||
* 実行ログ: logs/csv-anythingllm_<コマンド>_<日時>.log に、コンソール出力を
|
||
* 日時([YYYY-MM-DD HH:mm:ss])・レベル(INFO/WARN/ERROR)付きで実行毎に保存する。
|
||
*
|
||
* シート毎の設定ファイル csv-config/<シート名>.json:
|
||
* { "csvDir": "csv/<シート名>", "workspaceSlug": "アップロード先ワークスペースのslug" }
|
||
* workspaceSlug は convert 実行後に手動で入力する。
|
||
* 指定した名前/slugのワークスペースがAnythingLLM側に存在しない場合は、
|
||
* sync実行時にその名前で新規ワークスペースを自動作成し、実際に採番されたslugを
|
||
* この設定ファイルに書き戻す。
|
||
*
|
||
* CSVの列構成は位置ベースで読む(1列目=顧客名, 2列目=日付, 3列目=相談履歴)。
|
||
* 相談履歴は「名前:コメント」形式の発言が連続する形式で、2000文字を超える場合は
|
||
* 発言(名前)の境界で分割する(発言の途中では切らない。1発言が2000文字を超える場合は
|
||
* そのまま1チャンクとする)。
|
||
*
|
||
* 重複防止: 顧客名+日付+相談履歴の全文からSHA-256ハッシュを生成し、
|
||
* csv-config/<シート名>.uploaded-hashes.json に記録済みの行はスキップする。
|
||
* originalDataのExcelには過去分すべてのログが毎回まるごと含まれる想定のため、
|
||
* 日次実行しても既にアップロード済みの行が重複登録されないようにするための仕組み。
|
||
*
|
||
* 取り込み完了ファイルの退避: 処理が完了したファイルは、それぞれ元のディレクトリ配下の
|
||
* complete/ サブフォルダへ移動し、次回実行時に対象外(再変換・再アップロード対象外)とする。
|
||
* - originalData配下のExcel → convertで全シート変換後、originalData/complete/
|
||
* - csv/<シート名>配下のCSV → syncで全行処理後、csv/<シート名>/complete/
|
||
* - csv_chunks_md/<シート名>配下のMD → syncでアップロード成功後、csv_chunks_md/<シート名>/complete/
|
||
* 同名ファイルが既にcomplete側にある場合は上書きせず、タイムスタンプを付けて退避する。
|
||
*/
|
||
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const crypto = require("crypto");
|
||
const XLSX = require("xlsx");
|
||
const { uploadFile, addToWorkspaceEmbeddings, ensureWorkspaceExists } = require("./anythingllm-client");
|
||
|
||
const { ANYTHINGLLM_BASE_URL, ANYTHINGLLM_API_KEY } = process.env;
|
||
const EMBEDDING_BATCH_SIZE = Number(process.env.EMBEDDING_BATCH_SIZE || 50);
|
||
|
||
const ORIGINAL_DATA_DIR = process.env.ORIGINAL_DATA_DIR
|
||
? path.resolve(process.env.ORIGINAL_DATA_DIR)
|
||
: path.join(__dirname, "originalData");
|
||
const ORIGINAL_DATA_COMPLETE_DIR = path.join(ORIGINAL_DATA_DIR, "complete");
|
||
const CSV_ROOT_DIR = process.env.CSV_ROOT_DIR
|
||
? path.resolve(process.env.CSV_ROOT_DIR)
|
||
: path.join(__dirname, "csv");
|
||
const CSV_CONFIG_DIR = process.env.CSV_CONFIG_DIR
|
||
? path.resolve(process.env.CSV_CONFIG_DIR)
|
||
: path.join(__dirname, "csv-config");
|
||
const CSV_CHUNK_DIR = path.join(__dirname, "csv_chunks_md");
|
||
const CSV_CHUNK_MAX_LENGTH = Number(process.env.CSV_CHUNK_MAX_LENGTH || 2000);
|
||
const LOG_DIR = process.env.CSV_LOG_DIR ? path.resolve(process.env.CSV_LOG_DIR) : path.join(__dirname, "logs");
|
||
|
||
// csv-list.csv (sheetName,flag) を読み込み、flag列が "1" の行だけを同期対象とする。
|
||
// lineworks-anythingllm.js の board-list.csv/loadTargetBoardsFromCsv と同じ流儀。
|
||
// このファイルが無い、またはflag=1の行が1つも無い場合は [] を返す(全件同期へのフォールバックはしない)。
|
||
const CSV_LIST_CSV_PATH = path.join(__dirname, "csv-list.csv");
|
||
|
||
function loadTargetSheetsFromCsv(csvPath = CSV_LIST_CSV_PATH) {
|
||
if (!fs.existsSync(csvPath)) {
|
||
return [];
|
||
}
|
||
const lines = fs
|
||
.readFileSync(csvPath, "utf8")
|
||
.split(/\r?\n/)
|
||
.map((line) => line.trim())
|
||
.filter(Boolean);
|
||
|
||
// 先頭行は "sheetName,flag" のヘッダーなのでスキップ
|
||
return lines
|
||
.slice(1)
|
||
.map((line) => line.split(",").map((s) => s.trim()))
|
||
.filter(([, flag]) => flag === "1")
|
||
.map(([sheetName]) => sheetName);
|
||
}
|
||
|
||
// ファイル名として安全な形に変換 (パス区切り文字等を除去)
|
||
function sanitizeFileName(name) {
|
||
return String(name || "file").replace(/[\\/:*?"<>|]/g, "_");
|
||
}
|
||
|
||
function pad2(n) {
|
||
return String(n).padStart(2, "0");
|
||
}
|
||
|
||
function timestampForFileName(d = new Date()) {
|
||
return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}-${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
|
||
}
|
||
|
||
function formatTimestamp(d = new Date()) {
|
||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
||
}
|
||
|
||
// console.log/warn/error の出力を、実行毎に日時付きでログファイルへも書き出す。
|
||
// 1行ずつ同期書き込みするため、途中で異常終了してもそこまでのログは残る。
|
||
function startLogging(commandLabel) {
|
||
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||
const logFilePath = path.join(LOG_DIR, `csv-anythingllm_${commandLabel || "run"}_${timestampForFileName()}.log`);
|
||
|
||
const writeLine = (level, args) => {
|
||
const message = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ");
|
||
fs.appendFileSync(logFilePath, `[${formatTimestamp()}] [${level}] ${message}\n`, "utf8");
|
||
};
|
||
|
||
const original = {
|
||
log: console.log.bind(console),
|
||
warn: console.warn.bind(console),
|
||
error: console.error.bind(console),
|
||
};
|
||
console.log = (...args) => {
|
||
original.log(...args);
|
||
writeLine("INFO", args);
|
||
};
|
||
console.warn = (...args) => {
|
||
original.warn(...args);
|
||
writeLine("WARN", args);
|
||
};
|
||
console.error = (...args) => {
|
||
original.error(...args);
|
||
writeLine("ERROR", args);
|
||
};
|
||
|
||
return logFilePath;
|
||
}
|
||
|
||
// 処理が完了したファイルを、そのファイルがあったディレクトリ配下の complete/ サブフォルダへ移動する。
|
||
// 同名ファイルが既にcomplete側にある場合は上書きせず、タイムスタンプを付けて退避する。
|
||
// (originalDataのExcel、csv/配下のCSV、csv_chunks_md/配下のチャンクMDファイルで共通利用)
|
||
function moveFileToCompleteDir(srcPath, completeDir) {
|
||
fs.mkdirSync(completeDir, { recursive: true });
|
||
const filename = path.basename(srcPath);
|
||
const ext = path.extname(filename);
|
||
const base = path.basename(filename, ext);
|
||
let destPath = path.join(completeDir, filename);
|
||
if (fs.existsSync(destPath)) {
|
||
destPath = path.join(completeDir, `${base}_${timestampForFileName()}${ext}`);
|
||
}
|
||
fs.renameSync(srcPath, destPath);
|
||
return destPath;
|
||
}
|
||
|
||
// 変換が完了したExcelファイルを originalData/complete/ へ移動する。
|
||
function moveToCompleteDir(xlsxFileName) {
|
||
const destPath = moveFileToCompleteDir(path.join(ORIGINAL_DATA_DIR, xlsxFileName), ORIGINAL_DATA_COMPLETE_DIR);
|
||
console.log(` 取り込み完了、completeフォルダへ移動しました: ${destPath}`);
|
||
}
|
||
|
||
function configPath(sheetName) {
|
||
return path.join(CSV_CONFIG_DIR, `${sheetName}.json`);
|
||
}
|
||
|
||
function hashesPath(sheetName) {
|
||
return path.join(CSV_CONFIG_DIR, `${sheetName}.uploaded-hashes.json`);
|
||
}
|
||
|
||
// ====================================================================
|
||
// Excel → シート別CSV変換 + 設定雛形の生成
|
||
// ====================================================================
|
||
|
||
// csv-list.csv に未登録のシート名を flag=0 で追記する (convert実行時の自動登録)。
|
||
// 既に登録済みの行(flagの値含む)は一切変更しない。手動でflagを立てた行を壊さないための措置。
|
||
function ensureSheetRegisteredInCsvList(sheetName, csvPath = CSV_LIST_CSV_PATH) {
|
||
const header = "sheetName,flag";
|
||
let lines = [];
|
||
if (fs.existsSync(csvPath)) {
|
||
lines = fs
|
||
.readFileSync(csvPath, "utf8")
|
||
.split(/\r?\n/)
|
||
.map((line) => line.trim())
|
||
.filter(Boolean);
|
||
}
|
||
if (lines.length === 0) {
|
||
lines = [header];
|
||
}
|
||
|
||
const alreadyListed = lines.slice(1).some((line) => line.split(",")[0].trim() === sheetName);
|
||
if (alreadyListed) {
|
||
return;
|
||
}
|
||
|
||
lines.push(`${sheetName},0`);
|
||
// board-list.csv と同じくBOM付きUTF-8・LF改行で出力
|
||
fs.writeFileSync(csvPath, "" + lines.join("\n") + "\n", "utf8");
|
||
console.log(` ${csvPath} に新規シートを登録しました(flag=0): ${sheetName}`);
|
||
}
|
||
|
||
async function convertCommand() {
|
||
if (!fs.existsSync(ORIGINAL_DATA_DIR)) {
|
||
throw new Error(`originalDataフォルダが見つかりません: ${ORIGINAL_DATA_DIR}`);
|
||
}
|
||
|
||
const xlsxFiles = fs.readdirSync(ORIGINAL_DATA_DIR).filter((f) => f.toLowerCase().endsWith(".xlsx"));
|
||
if (xlsxFiles.length === 0) {
|
||
console.log(`${ORIGINAL_DATA_DIR} に.xlsxファイルが見つかりませんでした。`);
|
||
return;
|
||
}
|
||
|
||
for (const xlsxFileName of xlsxFiles) {
|
||
const xlsxPath = path.join(ORIGINAL_DATA_DIR, xlsxFileName);
|
||
const xlsxBaseName = path.basename(xlsxFileName, path.extname(xlsxFileName));
|
||
console.log(`変換中: ${xlsxFileName}`);
|
||
|
||
const workbook = XLSX.readFile(xlsxPath);
|
||
for (const sheetName of workbook.SheetNames) {
|
||
const sheet = workbook.Sheets[sheetName];
|
||
const csvText = XLSX.utils.sheet_to_csv(sheet);
|
||
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 });
|
||
|
||
const safeSheetName = sanitizeFileName(sheetName);
|
||
const sheetCsvDir = path.join(CSV_ROOT_DIR, safeSheetName);
|
||
fs.mkdirSync(sheetCsvDir, { recursive: true });
|
||
|
||
const csvFileName = `${safeSheetName}__${sanitizeFileName(xlsxBaseName)}.csv`;
|
||
const csvPath = path.join(sheetCsvDir, csvFileName);
|
||
// ExcelでUTF-8を正しく開けるようBOM付きで出力(lineworks-anythingllm.jsのlistCommandと同じ流儀)
|
||
fs.writeFileSync(csvPath, "" + csvText, "utf8");
|
||
|
||
console.log(` シート「${sheetName}」: ${Math.max(rows.length - 1, 0)}行 → ${csvPath}`);
|
||
if (rows.length > 0) {
|
||
console.log(` 1行目(ヘッダー想定): ${JSON.stringify(rows[0])}`);
|
||
}
|
||
|
||
const cfgPath = configPath(safeSheetName);
|
||
if (!fs.existsSync(cfgPath)) {
|
||
fs.mkdirSync(CSV_CONFIG_DIR, { recursive: true });
|
||
const relativeCsvDir = path.relative(__dirname, sheetCsvDir).split(path.sep).join("/");
|
||
fs.writeFileSync(
|
||
cfgPath,
|
||
JSON.stringify({ csvDir: relativeCsvDir, workspaceSlug: "" }, null, 2),
|
||
"utf8"
|
||
);
|
||
console.log(` 設定ファイルを新規作成: ${cfgPath} (workspaceSlugを設定してください)`);
|
||
}
|
||
|
||
ensureSheetRegisteredInCsvList(safeSheetName);
|
||
}
|
||
|
||
try {
|
||
moveToCompleteDir(xlsxFileName);
|
||
} catch (moveErr) {
|
||
console.warn(` [警告] ${xlsxFileName} のcompleteフォルダへの移動に失敗しました: ${moveErr.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ====================================================================
|
||
// 相談履歴のチャンク分割 (発言=「名前:コメント」の境界でのみ区切る)
|
||
// ====================================================================
|
||
|
||
// 行頭が「名前:」「名前:」の形式にマッチする行を新しい発言の開始とみなす。
|
||
// "https://..." のようなURLの先頭を誤って名前と判定しないよう除外する。
|
||
const TURN_START_PATTERN = /^([^::\r\n]{1,30})[::](?!\/\/)/;
|
||
|
||
function parseTurns(historyText) {
|
||
const lines = String(historyText || "").split(/\r\n|\r|\n/);
|
||
const turns = [];
|
||
|
||
for (const line of lines) {
|
||
if (TURN_START_PATTERN.test(line) || turns.length === 0) {
|
||
turns.push(line);
|
||
} else {
|
||
turns[turns.length - 1] += "\n" + line;
|
||
}
|
||
}
|
||
|
||
return turns;
|
||
}
|
||
|
||
// 発言を順に貪欲へ積み上げ、次の発言を足すと上限を超える場合はチャンクを区切る。
|
||
// 1発言自体が上限を超える場合はそのまま単独チャンクとする(発言の途中では切らない)。
|
||
function splitIntoChunks(turns, maxLen) {
|
||
const chunks = [];
|
||
let current = "";
|
||
|
||
for (const turnText of turns) {
|
||
if (current.length === 0) {
|
||
current = turnText;
|
||
} else if (current.length + 1 + turnText.length <= maxLen) {
|
||
current += "\n" + turnText;
|
||
} else {
|
||
chunks.push(current);
|
||
current = turnText;
|
||
}
|
||
}
|
||
if (current.length > 0) {
|
||
chunks.push(current);
|
||
}
|
||
|
||
return chunks;
|
||
}
|
||
|
||
function buildChunkMarkdown(sheetName, customerName, date, chunkText, partNumber, totalParts) {
|
||
const lines = [
|
||
`# ${customerName || "(顧客名不明)"}`,
|
||
"",
|
||
`- 顧客名: ${customerName || ""}`,
|
||
`- 日付: ${date || ""}`,
|
||
`- シート: ${sheetName}`,
|
||
];
|
||
if (totalParts > 1) {
|
||
lines.push(`- 分割: ${partNumber}/${totalParts}`);
|
||
}
|
||
lines.push("", chunkText);
|
||
return lines.join("\n");
|
||
}
|
||
|
||
// ====================================================================
|
||
// 重複防止 (顧客名+日付+相談履歴 のハッシュで既アップロード分をスキップ)
|
||
// ====================================================================
|
||
|
||
function rowHash(customerName, date, history) {
|
||
return crypto.createHash("sha256").update(`${customerName}|${date}|${history}`).digest("hex");
|
||
}
|
||
|
||
function loadUploadedHashes(sheetName) {
|
||
const p = hashesPath(sheetName);
|
||
if (!fs.existsSync(p)) {
|
||
return new Set();
|
||
}
|
||
try {
|
||
const arr = JSON.parse(fs.readFileSync(p, "utf8"));
|
||
return new Set(Array.isArray(arr) ? arr : []);
|
||
} catch {
|
||
return new Set();
|
||
}
|
||
}
|
||
|
||
function saveUploadedHashes(sheetName, hashSet) {
|
||
fs.mkdirSync(CSV_CONFIG_DIR, { recursive: true });
|
||
fs.writeFileSync(hashesPath(sheetName), JSON.stringify([...hashSet]), "utf8");
|
||
}
|
||
|
||
// ====================================================================
|
||
// CSV → チャンク分割 → アップロード (シート単位)
|
||
// ====================================================================
|
||
|
||
function loadSheetConfig(sheetName) {
|
||
const cfgPath = configPath(sheetName);
|
||
if (!fs.existsSync(cfgPath)) {
|
||
throw new Error(`設定ファイルが見つかりません: ${cfgPath} (先に convert を実行してください)`);
|
||
}
|
||
return JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
||
}
|
||
|
||
function resolveCsvDir(csvDir) {
|
||
return path.isAbsolute(csvDir) ? csvDir : path.join(__dirname, csvDir);
|
||
}
|
||
|
||
async function syncSheet(sheetName) {
|
||
const config = loadSheetConfig(sheetName);
|
||
if (!config.workspaceSlug) {
|
||
throw new Error(
|
||
`シート「${sheetName}」の workspaceSlug が未設定です。${configPath(sheetName)} を編集してください。`
|
||
);
|
||
}
|
||
|
||
const actualSlug = await ensureWorkspaceExists(config.workspaceSlug, {
|
||
baseUrl: ANYTHINGLLM_BASE_URL,
|
||
apiKey: ANYTHINGLLM_API_KEY,
|
||
});
|
||
if (actualSlug !== config.workspaceSlug) {
|
||
config.workspaceSlug = actualSlug;
|
||
fs.writeFileSync(configPath(sheetName), JSON.stringify(config, null, 2), "utf8");
|
||
console.log(` 設定ファイルのworkspaceSlugを実際のslugに更新しました: ${actualSlug}`);
|
||
}
|
||
|
||
const csvDir = resolveCsvDir(config.csvDir);
|
||
if (!fs.existsSync(csvDir)) {
|
||
throw new Error(`CSVフォルダが見つかりません: ${csvDir}`);
|
||
}
|
||
|
||
const csvFiles = fs.readdirSync(csvDir).filter((f) => f.toLowerCase().endsWith(".csv"));
|
||
if (csvFiles.length === 0) {
|
||
console.log(`シート「${sheetName}」: CSVファイルが見つかりませんでした (${csvDir})`);
|
||
return;
|
||
}
|
||
|
||
const uploadedHashes = loadUploadedHashes(sheetName);
|
||
const chunkDir = path.join(CSV_CHUNK_DIR, sanitizeFileName(sheetName));
|
||
const chunkCompleteDir = path.join(chunkDir, "complete");
|
||
const csvCompleteDir = path.join(csvDir, "complete");
|
||
fs.mkdirSync(chunkDir, { recursive: true });
|
||
|
||
let newCount = 0;
|
||
let skippedCount = 0;
|
||
const locations = [];
|
||
|
||
for (const csvFileName of csvFiles) {
|
||
const csvPath = path.join(csvDir, csvFileName);
|
||
const workbook = XLSX.readFile(csvPath);
|
||
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 });
|
||
|
||
for (let i = 1; i < rows.length; i++) {
|
||
const row = rows[i] || [];
|
||
const customerName = String(row[0] ?? "").trim();
|
||
const date = String(row[1] ?? "").trim();
|
||
const history = String(row[2] ?? "").trim();
|
||
|
||
if (!customerName && !history) {
|
||
continue; // 空行はスキップ
|
||
}
|
||
|
||
const hash = rowHash(customerName, date, history);
|
||
if (uploadedHashes.has(hash)) {
|
||
skippedCount++;
|
||
continue;
|
||
}
|
||
|
||
const chunks = splitIntoChunks(parseTurns(history), CSV_CHUNK_MAX_LENGTH);
|
||
const rowLocations = [];
|
||
|
||
for (let partIndex = 0; partIndex < chunks.length; partIndex++) {
|
||
const partNumber = partIndex + 1;
|
||
const markdown = buildChunkMarkdown(
|
||
sheetName,
|
||
customerName,
|
||
date,
|
||
chunks[partIndex],
|
||
partNumber,
|
||
chunks.length
|
||
);
|
||
const filename = `${sanitizeFileName(csvFileName.replace(/\.csv$/i, ""))}_row${i}_${sanitizeFileName(
|
||
customerName || "unknown"
|
||
)}_part${partNumber}.md`;
|
||
const filepath = path.join(chunkDir, filename);
|
||
fs.writeFileSync(filepath, markdown, "utf8");
|
||
|
||
const location = await uploadFile(filepath, {
|
||
baseUrl: ANYTHINGLLM_BASE_URL,
|
||
apiKey: ANYTHINGLLM_API_KEY,
|
||
mimeType: "text/markdown",
|
||
});
|
||
if (location) {
|
||
rowLocations.push(location);
|
||
moveFileToCompleteDir(filepath, chunkCompleteDir);
|
||
}
|
||
}
|
||
|
||
locations.push(...rowLocations);
|
||
uploadedHashes.add(hash);
|
||
saveUploadedHashes(sheetName, uploadedHashes);
|
||
newCount++;
|
||
}
|
||
|
||
// このCSVの全行を処理し終えた(新規アップロード or 重複スキップ)ので、
|
||
// 元のCSVファイルもcomplete/へ移動して次回convert/syncの対象から外す。
|
||
moveFileToCompleteDir(csvPath, csvCompleteDir);
|
||
}
|
||
|
||
console.log(`シート「${sheetName}」: 新規アップロード ${newCount}件 / スキップ(重複) ${skippedCount}件`);
|
||
|
||
if (locations.length > 0) {
|
||
await addToWorkspaceEmbeddings(locations, {
|
||
baseUrl: ANYTHINGLLM_BASE_URL,
|
||
apiKey: ANYTHINGLLM_API_KEY,
|
||
workspaceSlug: config.workspaceSlug,
|
||
batchSize: EMBEDDING_BATCH_SIZE,
|
||
});
|
||
} else {
|
||
console.log(" 埋め込み対象の新規ドキュメントはありませんでした。");
|
||
}
|
||
}
|
||
|
||
async function syncAllCommand() {
|
||
if (!fs.existsSync(CSV_CONFIG_DIR)) {
|
||
console.log(`設定フォルダが見つかりません: ${CSV_CONFIG_DIR} (先に convert を実行してください)`);
|
||
return;
|
||
}
|
||
|
||
const targetSheetNames = loadTargetSheetsFromCsv();
|
||
if (targetSheetNames.length === 0) {
|
||
console.log(
|
||
`${CSV_LIST_CSV_PATH} に同期対象(flag=1)のシートが見つからないため、何も同期せず終了します。` +
|
||
`(csv-list.csv の対象行の flag を 1 にしてください)`
|
||
);
|
||
return;
|
||
}
|
||
|
||
console.log(`${CSV_LIST_CSV_PATH} から対象シートを読み込みました: ${targetSheetNames.length} 件`);
|
||
targetSheetNames.forEach((name) => console.log(` - ${name}`));
|
||
|
||
for (const sheetName of targetSheetNames) {
|
||
console.log(`=== シート「${sheetName}」を同期します ===`);
|
||
try {
|
||
await syncSheet(sheetName);
|
||
} catch (err) {
|
||
console.error(`シート「${sheetName}」の同期に失敗: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ====================================================================
|
||
// コマンド
|
||
// ====================================================================
|
||
|
||
async function main() {
|
||
const [, , command, ...rest] = process.argv;
|
||
|
||
const logFilePath = startLogging(command || "run");
|
||
console.log(`ログファイル: ${logFilePath}`);
|
||
|
||
if (command === "convert") {
|
||
await convertCommand();
|
||
} else if (command === "sync") {
|
||
const sheetName = rest[0];
|
||
if (!sheetName) {
|
||
console.error("使い方: node csv-anythingllm.js sync <シート名>");
|
||
process.exitCode = 1;
|
||
return;
|
||
}
|
||
await syncSheet(sheetName);
|
||
} else if (command === "sync-all") {
|
||
await syncAllCommand();
|
||
} else {
|
||
console.log("使い方: node csv-anythingllm.js [convert|sync <シート名>|sync-all]");
|
||
}
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error("処理に失敗しました。");
|
||
console.error(err.message);
|
||
process.exitCode = 1;
|
||
});
|