GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。 NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは 別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
205 lines
7.9 KiB
JavaScript
205 lines
7.9 KiB
JavaScript
/**
|
||
* get-site-config.js
|
||
* ------------------------------------------------------------
|
||
* プリザンターの指定サイトの構成情報(SiteSettings等)を取得し、
|
||
* {プロジェクトフォルダ}/configs/{env}/ 配下にタイムスタンプ付きJSONとして保存します。
|
||
* ClaudePleasanter直下に置く共通実装。ClaudePleasanter直下から直接実行する
|
||
* (対象プロジェクトは --project、サーバー環境は --env で指定。詳細は resolve-project.js 参照)。
|
||
*
|
||
* 使い方:
|
||
* node .claude/js/get-site-config.js --project="IC(東京)【引継依頼】" --env=production
|
||
* node .claude/js/get-site-config.js --project=IC東京 --env=development
|
||
*
|
||
* レイアウト(configs/{env}/の保存構造)は環境変数で切り替え可能:
|
||
* PLEASANTER_LAYOUT … "flat" | 未設定(既定=hierarchical)
|
||
*
|
||
* 事前準備:
|
||
* 1. リポジトリルート直下の config_production.json / config_development.json に
|
||
* BaseUrl / ApiKey / ApiVersion(サーバー接続情報)が設定済みであること。
|
||
* 2. 対象プロジェクトフォルダ直下の siteid.json に SiteId を設定してください。
|
||
* SiteId はカンマ区切りで複数指定可能です(例: "480111,438166,96235")。
|
||
* 1件のみの場合は従来通り数値(例: 480111)でも構いません。
|
||
* 3. Node.js 18以降(組み込みfetchを使用)
|
||
*
|
||
* 参照した公式マニュアル:
|
||
* - 開発者向け機能:API:サイト操作:サイト更新
|
||
* https://pleasanter.org/manual/api-site-update
|
||
* - 開発者向け機能:API:サイト操作:サイト設定の更新(部分追加/更新/削除)
|
||
* https://pleasanter.org/ja/manual/api-update-sitesettings
|
||
*
|
||
* 注意:
|
||
* プリザンターのバージョンにより get のレスポンス構造
|
||
* (SiteSettingsが Response.Site 配下にあるか等)が異なる場合があります。
|
||
* 初回実行時はコンソールに出力されるレスポンスのトップレベルキーを確認し、
|
||
* 想定通りの構造か確認してください。
|
||
* ------------------------------------------------------------
|
||
*/
|
||
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const { extractSiteConfig } = require("./extract-site-config");
|
||
const { rawJsonSaveDir } = require("./site-paths");
|
||
const { resolveProjectRoot, loadServerConfig, loadSiteId } = require("./resolve-project");
|
||
|
||
let _resolved = null;
|
||
function getResolved() {
|
||
if (!_resolved) _resolved = resolveProjectRoot();
|
||
return _resolved;
|
||
}
|
||
|
||
function getBaseDir() {
|
||
return getResolved().baseDir;
|
||
}
|
||
|
||
function getEnv() {
|
||
return getResolved().env;
|
||
}
|
||
|
||
function getLayout() {
|
||
const v = process.env.PLEASANTER_LAYOUT;
|
||
return v === "flat" || v === "separate" ? v : "hierarchical";
|
||
}
|
||
|
||
// config.json の SiteId(数値 / カンマ区切り文字列 / 配列)を
|
||
// 個々のSiteId文字列の配列に正規化する。
|
||
function parseSiteIds(rawSiteId) {
|
||
const list = Array.isArray(rawSiteId) ? rawSiteId : String(rawSiteId).split(",");
|
||
return list
|
||
.map((id) => String(id).trim())
|
||
.filter((id) => id.length > 0);
|
||
}
|
||
|
||
// 1サイト分の取得・保存・抽出処理
|
||
async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
|
||
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/getsite`;
|
||
const body = {
|
||
ApiVersion: ApiVersion || "1.1",
|
||
ApiKey: ApiKey,
|
||
};
|
||
|
||
console.log(`[INFO] 取得先: ${url}`);
|
||
console.log(`[INFO] SiteId: ${siteId}`);
|
||
|
||
let response;
|
||
try {
|
||
response = await fetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
} catch (err) {
|
||
console.error("[エラー] リクエスト送信に失敗しました:", err.message);
|
||
console.error("BaseUrlの疎通・ネットワーク設定を確認してください。");
|
||
return { ok: false };
|
||
}
|
||
|
||
const text = await response.text();
|
||
|
||
if (!response.ok) {
|
||
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
|
||
console.error(text);
|
||
return { ok: false };
|
||
}
|
||
|
||
let json;
|
||
try {
|
||
json = JSON.parse(text);
|
||
} catch (err) {
|
||
console.error("[エラー] レスポンスがJSONとして解析できませんでした。");
|
||
console.error(text);
|
||
return { ok: false };
|
||
}
|
||
|
||
// レスポンス構造の確認用ログ(初回はここを見て構造を把握してください)
|
||
console.log("[INFO] レスポンス トップレベルキー:", Object.keys(json));
|
||
if (json.Response) {
|
||
console.log("[INFO] Response配下のキー:", Object.keys(json.Response));
|
||
}
|
||
|
||
const baseDir = getBaseDir();
|
||
const env = getEnv();
|
||
const layout = getLayout();
|
||
const configsDir = path.join(baseDir, "configs", env);
|
||
const title = json?.Response?.Data?.Title;
|
||
const outDir = rawJsonSaveDir(baseDir, configsDir, siteId, title, layout, env);
|
||
|
||
const timestamp = new Date()
|
||
.toISOString()
|
||
.replace(/[:.]/g, "-");
|
||
const outFile = path.join(outDir, `site-${siteId}_${timestamp}.json`);
|
||
|
||
fs.writeFileSync(outFile, JSON.stringify(json, null, 2), "utf-8");
|
||
|
||
// 常に「最新」ファイルも別名で保持(Claudeへのアップロード用に固定名があると便利)
|
||
const latestFile = path.join(outDir, `site-${siteId}_latest.json`);
|
||
fs.writeFileSync(latestFile, JSON.stringify(json, null, 2), "utf-8");
|
||
|
||
console.log(`[OK] 保存しました: ${outFile}`);
|
||
console.log(`[OK] 最新版としても保存しました: ${latestFile}`);
|
||
|
||
// 取得と同時に Scripts/Styles/ServerScripts/Processes/ガイドHTML も抽出する
|
||
console.log("\n[INFO] 続けて設定の個別ファイル抽出を実行します...");
|
||
extractSiteConfig(latestFile);
|
||
|
||
// 権限継承先の追跡: InheritPermission が自サイト以外を指している場合、
|
||
// 実際の権限はそちらのサイトにあるため、そのサイトも同様に取得する。
|
||
const inheritPermission = json?.Response?.Data?.InheritPermission;
|
||
|
||
return { ok: true, inheritPermission };
|
||
}
|
||
|
||
async function main() {
|
||
const { baseDir } = getResolved();
|
||
const env = getEnv();
|
||
const { BaseUrl, ApiKey, ApiVersion } = loadServerConfig(env);
|
||
const SiteId = loadSiteId(baseDir);
|
||
|
||
const siteIds = parseSiteIds(SiteId);
|
||
if (siteIds.length === 0) {
|
||
console.error("[エラー] config.json の SiteId から有効なサイトIDを取得できませんでした。");
|
||
process.exit(1);
|
||
}
|
||
|
||
// 権限継承先の追跡: 指定サイトの取得後、InheritPermission が自サイト以外を
|
||
// 指していれば、そのサイトも同じ要領でキューに追加して取得する
|
||
// (権限の実体は継承元サイド側にあるため)。
|
||
const queue = [...siteIds];
|
||
const visited = new Set();
|
||
const results = [];
|
||
|
||
while (queue.length > 0) {
|
||
const siteId = String(queue.shift());
|
||
if (visited.has(siteId)) continue;
|
||
visited.add(siteId);
|
||
|
||
console.log(`\n==== [${results.length + 1}] SiteId: ${siteId} ====`);
|
||
const { ok, inheritPermission } = await fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion });
|
||
results.push({ siteId, ok });
|
||
|
||
if (
|
||
ok &&
|
||
inheritPermission != null &&
|
||
String(inheritPermission) !== "0" &&
|
||
String(inheritPermission) !== siteId &&
|
||
!visited.has(String(inheritPermission))
|
||
) {
|
||
console.log(`[INFO] SiteId ${siteId} は権限をSiteId ${inheritPermission} から継承しています。継承元も取得します。`);
|
||
queue.push(String(inheritPermission));
|
||
}
|
||
}
|
||
|
||
console.log("\n==== 取得結果まとめ ====");
|
||
for (const { siteId, ok } of results) {
|
||
console.log(` SiteId ${siteId}: ${ok ? "OK" : "失敗"}`);
|
||
}
|
||
|
||
console.log("\n次のステップ: latest.jsonをClaudeのチャットにアップロードして仕様書化を依頼するか、");
|
||
console.log("configs/site-{SiteId}/ 配下のファイルを直接編集・指示して修正してください。");
|
||
|
||
if (results.some((r) => !r.ok)) {
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
main();
|