1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

rex0220 アプリ情報取得の chrome 拡張機能化

Last updated at Posted at 2025-11-27

kintone アプリ情報取得を簡単に実行するため、chrome 拡張機能化します。

概要

rex0220 アプリマイスター・レイ アプリ情報の取得 で、作成したアプリ情報取得コードをchrome 拡張機能化します。

  • アプリ情報取得の拡張機能の作り方
  • インストール方法

2025-11-27_17h00_07.png

関連記事

フォルダー構成

こんな感じで、フォルダーとファイルを準備

rex0220-app-exporter/
  ├─ manifest.json   (設定ファイル)
  ├─ background.js   (起動スクリプト)
  ├─ app_export.js   (設計書出力ロジック)
  └─ icon.png        (アイコン画像:キツネのレイ)

作成された Chrome 拡張機能 「rex0220-app-exporter」 の構成ファイル解説です。
このフォルダ構成一式をそのまま保存しておけば、いつでも再利用・配布が可能です。


📂 フォルダ: rex0220-app-exporter

このフォルダ全体が「1つの拡張機能パッケージ」となります。

1. 🖼️ icon.png (レイ / キツネ)

icon.png

  • 役割: 拡張機能の「顔」となるアイコン画像。
  • 詳細: マスコットキャラクター「アプリマイスター・レイ(キツネ)」の画像です。
  • 表示場所: Chromeブラウザ右上のツールバーや、拡張機能管理画面に表示されます。ユーザーがこれをクリックすることで処理が始まります。

2. ⚙️ manifest.json

  • 役割: 拡張機能の「設計図・設定ファイル」。
  • 詳細:
    • 名前: rex0220 アプリ情報取得
    • 権限: Kintoneの画面(*.cybozu.com 等)へのアクセス権や、スクリプトを実行する権限(scripting)を定義しています。
    • 構成: 「裏で background.js を動かしてね」「アイコンは icon.png を使ってね」という指示が書かれています。

3. 🧠 background.js

  • 役割: イベントを監視する「司令塔」(Service Worker)。
  • 詳細:
    • 常に裏側で待機しており、ユーザーが**「レイのアイコンをクリックした瞬間」**に動作します。
    • 現在開いているタブが Kintone かどうかを判断します。
    • OKなら、**「メインの空間(MAIN World)」**に対して app_export.js を送り込み(注入し)、実行命令を出します。

4. 📝 app_export.js

  • 役割: 実際に仕事をする「実行部隊」。
  • 詳細:
    • Kintone のページ内部で実行されるスクリプト本体です。
    • kintone.app.getId() や REST API を使ってアプリ情報を収集します。
    • 収集したデータを Markdown に整形し、ファイルとしてダウンロードさせます。
    • ポイント: manifest.jsonbackground.js の設定により、Kintone のグローバル変数に直接アクセスできる特別な権限(Main World)で動いています。

処理の流れ(まとめ)

  1. ユーザーが 「レイ(icon.png)」 をクリック。
  2. 「司令塔(background.js)」 がクリックを感知。
  3. 司令塔が 「実行部隊(app_export.js)」 を Kintone の画面内に投入。
  4. 「実行部隊」 がアプリ情報を吸い出し、設計書をダウンロード。

この4つのファイルがあれば、どのPCのChromeでも「デベロッパーモード」から読み込むだけで動作します。
もし必要であれば、この解説文を README.txt としてフォルダの中に一緒に保存しておくと、後で見返した時に便利です。

manifest.json

{
  "manifest_version": 3,
  "name": "rex0220 アプリ情報取得",
  "version": "2.9",
  "description": "現在開いているKintoneアプリの設計情報をMarkdownで出力します。",
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "host_permissions": [
    "https://*.cybozu.com/*",
    "https://*.cybozu.cn/*",
    "https://*.kintone.com/*"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_title": "設計書を出力",
    "default_icon": {
      "16": "icon.png",
      "48": "icon.png",
      "128": "icon.png"
    }
  },
  "icons": {
    "16": "icon.png",
    "48": "icon.png",
    "128": "icon.png"
  }
}

background.js

chrome.action.onClicked.addListener(async (tab) => {
  console.log("アイコンがクリックされました: ", tab.url);

  // 対象ドメインチェック
  // .cybozu.cn を追加しました
  const isKintone = tab.url.includes("cybozu.com") || 
                    tab.url.includes("kintone.com") || 
                    tab.url.includes("cybozu.cn");

  if (!isKintone) {
      console.warn("対象外のドメインです。");
      return;
  }

  try {
    await chrome.scripting.executeScript({
      target: { tabId: tab.id },
      files: ["app_export.js"],
      world: "MAIN" 
    });
    console.log("スクリプトの注入に成功しました。");
  } catch (err) {
    console.error("スクリプトの実行に失敗しました: ", err);
  }
});

app_export.js

/**
 * 🦊 kintone App Meister Ray's Work
 * Project: kintoneアプリ設計書出力(Markdown版)
 * Version: 2.9
 * Author: rex0220
 * Description: 現在開いているアプリの設定情報を取得し、Markdownファイルとしてダウンロードします。
 * Update: カスタマイズビューのHTMLソース表示を修正(エスケープ処理・余計な装飾の削除)。
 */
(async () => {
  // --- 設定 ---
  const IS_PREVIEW = false; 
  const APP_ID = kintone.app.getId();
  const NOW = new Date();
  const SCRIPT_VER = '2.9';
  const AUTHOR = 'rex0220';
  const BASE_URL = location.origin;

  if (!APP_ID) {
    console.error('🦊 レイ: アプリの画面を開いてから実行してください!');
    alert('アプリの画面を開いてから実行してください。');
    return;
  }

  console.log(`🦊 レイ: アプリ(ID: ${APP_ID}) の情報を収集中です...`);

  try {
    // -------------------------------------------------------
    // 1. APIパス & ヘルパー関数定義
    // -------------------------------------------------------
    const pathApp = (endpoint) => kintone.api.url(`/k/v1/${IS_PREVIEW ? 'preview/' : ''}app/${endpoint}`, true);
    const pathRoot = (endpoint) => kintone.api.url(`/k/v1/${IS_PREVIEW ? 'preview/' : ''}${endpoint}`, true);
    const pathBase = (endpoint) => kintone.api.url(`/k/v1/${endpoint}`, true);

    const pad = (n) => n.toString().padStart(2, '0');
    const formatDate = (dateObjOrIsoString) => {
        if (!dateObjOrIsoString) return '-';
        const d = new Date(dateObjOrIsoString);
        return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
    };
    const formatDateForFile = (d) => `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;

    const formatEntity = (entity) => {
        if (!entity) return '-';
        const typeMap = { 'USER': 'ユーザー', 'GROUP': 'グループ', 'ORGANIZATION': '組織', 'CREATOR': '作成者', 'FIELD_ENTITY': 'フィールド値', 'CUSTOM_FIELD': 'カスタムフィールド' };
        const typeStr = typeMap[entity.type] || entity.type;
        const codeStr = entity.code ? ` (${entity.code})` : '';
        return `${typeStr}${codeStr}`;
    };

    const formatTargets = (targets) => {
        if (!targets || targets.length === 0) return '設定なし';
        return targets.map(t => {
            const ent = formatEntity(t.entity);
            return t.includeSubs ? `${ent} (下部組織含む)` : ent;
        }).join('<br>');
    };

    // 【追加】HTMLエスケープ関数
    const escapeHtml = (str) => {
        if (!str) return '';
        return str.replace(/[&<>"']/g, (m) => ({ 
            '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' 
        })[m]);
    };

    // -------------------------------------------------------
    // 2. 先行データ取得 (アクション & フィールド定義)
    // -------------------------------------------------------
    const [actionsResp, fieldsResp] = await Promise.all([
        kintone.api(pathApp('actions'), 'GET', { app: APP_ID }),
        kintone.api(pathApp('form/fields'), 'GET', { app: APP_ID })
    ]);

    const fieldProps = fieldsResp.properties;

    let updatedTimeCode = '更新日時'; 
    let createdTimeCode = '作成日時'; 
    Object.values(fieldProps).forEach(f => {
        if (f.type === 'UPDATED_TIME') updatedTimeCode = f.code;
        if (f.type === 'CREATED_TIME') createdTimeCode = f.code;
    });

    // -------------------------------------------------------
    // 3. 関連アプリIDの収集
    // -------------------------------------------------------
    const relatedAppIds = new Set();
    if (actionsResp.actions) {
        Object.values(actionsResp.actions).forEach(a => {
            if (a.destApp && a.destApp.app) relatedAppIds.add(Number(a.destApp.app));
        });
    }
    Object.values(fieldProps).forEach(f => {
        if (f.lookup && f.lookup.relatedApp && f.lookup.relatedApp.app) {
            relatedAppIds.add(Number(f.lookup.relatedApp.app));
        }
        if (f.type === 'REFERENCE_TABLE' && f.referenceTable && f.referenceTable.relatedApp && f.referenceTable.relatedApp.app) {
            relatedAppIds.add(Number(f.referenceTable.relatedApp.app));
        }
        if (f.type === 'SUBTABLE' && f.fields) {
            Object.values(f.fields).forEach(sf => {
                if (sf.lookup && sf.lookup.relatedApp && sf.lookup.relatedApp.app) {
                    relatedAppIds.add(Number(sf.lookup.relatedApp.app));
                }
            });
        }
    });
    const allAppIds = [...new Set([Number(APP_ID), ...relatedAppIds])];

    // -------------------------------------------------------
    // 4. 残りのデータ取得 (APIコール)
    // -------------------------------------------------------
    const fetchSystemPlugins = async () => {
        try { return (await kintone.api(kintone.api.url('/k/v1/plugins', true), 'GET', {})).plugins || []; } 
        catch (e) { return []; }
    };
    const fetchCategoriesJsApi = async () => {
        if (kintone.app.getCategories) {
            try { return await kintone.app.getCategories(); } 
            catch (e) { return { enabled: false, categories: [] }; }
        }
        return { enabled: false, categories: [] };
    };
    const fetchRecordStats = async () => {
        try {
            const [updateResp, createResp] = await Promise.all([
                kintone.api(kintone.api.url('/k/v1/records', true), 'GET', { app: APP_ID, query: `order by ${updatedTimeCode} desc limit 1`, totalCount: true }),
                kintone.api(kintone.api.url('/k/v1/records', true), 'GET', { app: APP_ID, query: 'order by $id desc limit 1', fields: ['$id', createdTimeCode] })
            ]);
            return {
                totalCount: updateResp.totalCount,
                lastUpdate: (updateResp.records[0]) ? updateResp.records[0][updatedTimeCode].value : null,
                lastRecordId: (createResp.records[0]) ? createResp.records[0]['$id'].value : null,
                lastCreate: (createResp.records[0]) ? createResp.records[0][createdTimeCode].value : null
            };
        } catch (e) { return null; }
    };

    const externalFieldMap = {};
    if(relatedAppIds.size > 0){
        await Promise.all([...relatedAppIds].map(async (rid) => {
             if(rid === Number(APP_ID)) { externalFieldMap[rid] = fieldProps; return; }
             try {
                 const res = await kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', { app: rid });
                 externalFieldMap[rid] = res.properties;
             } catch(e){ externalFieldMap[rid] = {}; }
        }));
    }

    const [
      appsResp, settings, formLayout, views, reports, status, 
      appPlugins, systemPlugins, notifyGeneral, notifyRecord, notifyReminder, 
      customize, aclApp, aclRecord, aclField, recordStats, categoriesResp
    ] = await Promise.all([
      kintone.api(pathBase('apps'), 'GET', { ids: allAppIds }),
      kintone.api(pathApp('settings'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('form/layout'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('views'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('reports'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('status'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('plugins'), 'GET', { app: APP_ID }),
      fetchSystemPlugins(),
      kintone.api(pathApp('notifications/general'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('notifications/perRecord'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('notifications/reminder'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('customize'), 'GET', { app: APP_ID }),
      kintone.api(pathApp('acl'), 'GET', { app: APP_ID }),
      kintone.api(kintone.api.url('/k/v1/preview/record/acl.json', true), 'GET', { app: APP_ID }),
      kintone.api(pathRoot('field/acl'), 'GET', { app: APP_ID }),
      fetchRecordStats(),
      fetchCategoriesJsApi()
    ]);

    // -------------------------------------------------------
    // 5. 変数定義と前処理
    // -------------------------------------------------------
    const appNameMap = {};
    if (appsResp.apps) {
        appsResp.apps.forEach(app => { appNameMap[app.appId] = app; });
    }
    
    const appInfo = appNameMap[APP_ID] || {};
    const appName = settings.name || appInfo.name || '名称不明';
    const appCode = appInfo.code || '(なし)';
    
    const isGuest = location.pathname.includes('/guest/');
    let appUrl = `${BASE_URL}/k/${APP_ID}/`;
    if (isGuest && appInfo.spaceId) {
        appUrl = `${BASE_URL}/k/guest/${appInfo.spaceId}/${APP_ID}/`;
    }
    const spaceIdStr = appInfo.spaceId ? `${appInfo.spaceId}${isGuest ? ' (ゲスト)' : ''}` : '-';

    let spaceName = 'なし (ポータル)';
    if (appInfo.spaceId) {
        try {
            const spaceResp = await kintone.api(pathBase('space'), 'GET', { id: appInfo.spaceId });
            spaceName = spaceResp.name;
        } catch (e) { spaceName = `(ID: ${appInfo.spaceId} / 取得失敗)`; }
    }

    let iconStr = '-';
    if (settings.icon) {
        if (settings.icon.type === 'PRESET') iconStr = `Preset: ${settings.icon.key}`;
        else if (settings.icon.type === 'FILE') iconStr = `File: ${settings.icon.file.name}`;
    }

    const formatCategories = (catsObj, depth = 0) => {
        if (!catsObj) return '';
        const catsArray = Object.values(catsObj).sort((a, b) => Number(a.index) - Number(b.index));
        let result = '';
        catsArray.forEach(c => {
            const indent = '&nbsp;&nbsp;'.repeat(depth);
            const icon = depth > 0 ? '' : '';
            result += `<br>${indent}${icon}${c.name || c.label}`;
            if (c.children && Object.keys(c.children).length > 0) result += formatCategories(c.children, depth + 1);
        });
        return result;
    };
    const categoryTreeStr = categoriesResp.categories ? formatCategories(categoriesResp.categories) : '';

    // -------------------------------------------------------
    // 6. レイアウト解析 (POS計算)
    // -------------------------------------------------------
    const orderedFields = [];
    const processedCodes = new Set();
    let topLevelCounter = 0;

    const traverseLayout = (layoutList, parentPrefix = '', parentGroupCode = null) => {
      let groupInnerRowCounter = 0;
      layoutList.forEach(row => {
        let currentPosPrefix = '';
        if (!parentPrefix) {
            topLevelCounter++;
            currentPosPrefix = `${topLevelCounter}`;
        } else {
            groupInnerRowCounter++;
            currentPosPrefix = `${parentPrefix}-${groupInnerRowCounter}`;
        }
        let locationStr = 'row';
        if (parentGroupCode) locationStr = `group (${parentGroupCode})`;

        if (row.type === 'ROW') {
            row.fields.forEach((f, idx) => {
                const pos = `${currentPosPrefix}-${idx + 1}`;
                const sizeInfo = f.size || {};
                if (fieldProps[f.code]) {
                    const fieldProp = fieldProps[f.code];
                    if (fieldProp.type === 'REFERENCE_TABLE') {
                        orderedFields.push({ ...fieldProp, _pos: pos, _location: 'refer', _size: sizeInfo });
                        processedCodes.add(f.code);
                        if (fieldProp.referenceTable && fieldProp.referenceTable.displayFields) {
                            fieldProp.referenceTable.displayFields.forEach((df, dfIdx) => {
                                orderedFields.push({
                                    type: '(Display)', code: df, label: df, _pos: `${pos}-${dfIdx + 1}`, _location: `refer (${f.code})`,
                                    _isReferenceChild: true, _parentRef: fieldProp
                                });
                            });
                        }
                    } else {
                        orderedFields.push({ ...fieldProp, _pos: pos, _location: locationStr, _size: sizeInfo });
                        processedCodes.add(f.code);
                    }
                } else if (f.elementId) {
                    orderedFields.push({ type: 'SPACER', code: f.elementId, label: '(スペース)', _pos: pos, _location: locationStr, _size: sizeInfo, _isSpacer: true });
                } else if (f.type === 'LABEL') {
                    orderedFields.push({ type: 'LABEL', code: '-', label: f.label || '(ラベル)', _pos: pos, _location: locationStr, _size: sizeInfo, _isLayoutItem: true });
                } else if (f.type === 'HR') {
                    orderedFields.push({ type: 'HR', code: '-', label: '(罫線)', _pos: pos, _location: locationStr, _size: sizeInfo, _isLayoutItem: true });
                }
            });
        } else if (row.type === 'SUBTABLE') {
            if (fieldProps[row.code]) {
                orderedFields.push({ ...fieldProps[row.code], _pos: currentPosPrefix, _location: 'table' });
                processedCodes.add(row.code);
            }
            row.fields.forEach((f, idx) => {
                const pos = `${currentPosPrefix}-${idx + 1}`;
                const parentProp = fieldProps[row.code];
                const sizeInfo = f.size || {};
                if (parentProp && parentProp.fields && parentProp.fields[f.code]) {
                    orderedFields.push({ ...parentProp.fields[f.code], _pos: pos, _location: `table (${row.code})`, _inTable: true, _size: sizeInfo });
                }
            });
        } else if (row.type === 'GROUP') {
            if (fieldProps[row.code]) {
                orderedFields.push({ ...fieldProps[row.code], _pos: currentPosPrefix, _location: 'group' });
                processedCodes.add(row.code);
            }
            if (row.layout) traverseLayout(row.layout, currentPosPrefix, row.code);
        }
      });
    };
    traverseLayout(formLayout.layout);

    const unplacedFields = [];
    const lookupDestinations = {};
    const collectLookupDest = (props) => {
        Object.keys(props).forEach(key => {
            const f = props[key];
            if (f.lookup && f.lookup.fieldMappings) {
                f.lookup.fieldMappings.forEach(mapping => {
                    lookupDestinations[mapping.field] = {
                        lookupCode: f.code,
                        relatedField: mapping.relatedField
                    };
                });
            }
            if (f.type === 'SUBTABLE' && f.fields) collectLookupDest(f.fields);
        });
    };
    collectLookupDest(fieldProps);

    Object.keys(fieldProps).forEach(key => {
        if (!processedCodes.has(key)) {
            const f = fieldProps[key];
            if (f.type !== 'SUBTABLE') {
                let loc = '(未配置)';
                if (f.type === 'CATEGORY') loc = '(カテゴリー)';
                else if (['STATUS', 'STATUS_ASSIGNEE'].includes(f.type)) loc = '(プロセス管理)';
                else if (['RECORD_NUMBER', 'CREATOR', 'MODIFIER', 'CREATED_TIME', 'UPDATED_TIME'].includes(f.type)) loc = '(システム)';
                unplacedFields.push({ ...f, _pos: '-', _location: loc });
            }
        }
    });
    const definedOrder = ['STATUS', 'STATUS_ASSIGNEE', 'CATEGORY'];
    unplacedFields.sort((a, b) => {
        const idxA = definedOrder.indexOf(a.type);
        const idxB = definedOrder.indexOf(b.type);
        if (idxA !== -1 && idxB !== -1) return idxA - idxB;
        if (idxA !== -1) return -1;
        if (idxB !== -1) return 1;
        return 0;
    });

    // -------------------------------------------------------
    // 7. Markdown生成処理
    // -------------------------------------------------------
    const md = [];
    const statusMap = { 'ACTIVATED': '運用中', 'DEACTIVATED': '停止中', 'FLUSHED': '削除済み' };
    let appStatus = statusMap[appInfo.status] || appInfo.status || '運用中 (推定)';
    const check = (bool) => bool ? '✅ 有効' : 'ー 無効';
    const roundMap = { 'HALF_EVEN': '最近接偶数への丸め', 'UP': '切り上げ', 'DOWN': '切り捨て' };
    const recCountStr = recordStats ? `${recordStats.totalCount} 件` : '(取得不可)';
    const lastRecUpdateStr = (recordStats && recordStats.lastUpdate) ? formatDate(recordStats.lastUpdate) : '-';
    const lastRecIdStr = (recordStats && recordStats.lastRecordId) ? recordStats.lastRecordId : '-';
    const lastRecCreateStr = (recordStats && recordStats.lastCreate) ? formatDate(recordStats.lastCreate) : '-';

    // --- ヘッダー ---
    md.push(`# アプリ設計書: ${appName}`);
    md.push(`> Ver.${SCRIPT_VER} / By ${AUTHOR}`);
    md.push(`\n**基本情報**\n`);
    md.push(`| 項目 | 内容 |`);
    md.push(`| --- | --- |`);
    md.push(`| 環境URL | ${BASE_URL} |`);
    md.push(`| アプリID | ${APP_ID} |`);
    md.push(`| アプリコード | ${appCode} |`);
    md.push(`| アプリ名 | [${appName}](${appUrl}) |`);
    md.push(`| ステータス | ${appStatus} |`);
    md.push(`| テーマ | ${settings.theme || 'Default'} |`);
    md.push(`| アイコン | ${iconStr} |`);
    md.push(`| リビジョン | ${settings.revision || '-'} |`);
    md.push(`| 所属スペース | ${spaceName} (ID: ${spaceIdStr}) |`);
    md.push(`| スレッドID | ${appInfo.threadId || '-'} |`);
    md.push(`| 作成者 | ${appInfo.creator ? appInfo.creator.name : '-'} (${appInfo.creator ? appInfo.creator.code : '-'}) |`);
    md.push(`| 作成日時 | ${formatDate(appInfo.createdAt)} |`);
    md.push(`| 最終更新者 | ${appInfo.modifier ? appInfo.modifier.name : '-'} (${appInfo.modifier ? appInfo.modifier.code : '-'}) |`);
    md.push(`| 最終更新日時 | ${formatDate(appInfo.modifiedAt)} |`);
    md.push(`| 情報取得日時 | ${formatDate(NOW)} |`);
    md.push(`| レコード数 | ${recCountStr} |`);
    md.push(`| 最終レコードID | ${lastRecIdStr} |`);
    md.push(`| レコード最終追加日時 | ${lastRecCreateStr} |`);
    md.push(`| レコード最終更新日時 | ${lastRecUpdateStr} |`);
    
    // --- 詳細設定 ---
    md.push(`\n**詳細設定**\n`);
    md.push(`| 項目 | 設定値 |`);
    md.push(`| --- | --- |`);
    md.push(`| レコードタイトル | ${settings.titleField ? `\`${settings.titleField.code}\`` : 'なし'} |`);
    md.push(`| 年度開始月 | ${settings.firstMonthOfFiscalYear}月 |`);
    md.push(`| サムネイル表示 | ${check(settings.enableThumbnails)} |`);
    md.push(`| レコード一括削除 | ${check(settings.enableBulkDeletion)} |`);
    md.push(`| コメント機能 | ${check(settings.enableComments)} |`);
    md.push(`| レコード再利用 | ${check(settings.enableDuplicateRecord)} |`);
    md.push(`| インライン編集 | ${check(settings.enableInlineRecordEditing)} |`);
    if (settings.numberPrecision) {
        md.push(`\n**数値と計算の精度**\n`);
        md.push(`| 項目 | 設定値 |`);
        md.push(`| --- | --- |`);
        md.push(`| 全体の桁数 | ${settings.numberPrecision.digits} |`);
        md.push(`| 小数部の桁数 | ${settings.numberPrecision.decimalPlaces} |`);
        md.push(`| 丸め方 | ${roundMap[settings.numberPrecision.roundingMode] || settings.numberPrecision.roundingMode} |`);
    }
    md.push(`\n**アプリメモ (説明)**\n`);
    if (settings.description) md.push('```html\n' + settings.description + '\n```'); else md.push('(なし)');
    md.push('\n');

    // --- フィールド一覧 ---
    md.push(`## 1. フィールド一覧`);
    md.push(`| No. | POS | 配置 | ラベル | フィールドコード | タイプ | サイズ | 必須 | 重複 | 初期値 | 備考 |`);
    md.push(`| :---: | :---: | --- | --- | --- | --- | :---: | :---: | :---: | --- | --- |`);
    let fieldCounter = 1;
    const createFieldRow = (f) => {
        const no = fieldCounter++;
        let sizeStr = '-';
        if (f._size) {
            const p = [];
            if (f._size.width) p.push(`W:${f._size.width}`);
            if (f._size.height) p.push(`H:${f._size.height}`);
            if (f._size.innerHeight) p.push(`IH:${f._size.innerHeight}`);
            if (p.length > 0) sizeStr = p.join(', ');
        }

        if (f._isReferenceChild) {
            let realType = '(Display)';
            const refAppId = f._parentRef.referenceTable.relatedApp.app;
            if (externalFieldMap[refAppId] && externalFieldMap[refAppId][f.code]) {
                realType = externalFieldMap[refAppId][f.code].type;
            }
            return `| ${no} | ${f._pos} | ${f._location} | └ ${f.label} | \`${f.code}\` | ${realType} | - | - | - | - | in ${f._parentRef.code} |`;
        }

        if (f._isSpacer) return `| ${no} | ${f._pos} | ${f._location} | (スペース) | \`${f.code}\` | SPACER | ${sizeStr} | - | - | - | Element ID |`;
        if (f._isLayoutItem) return `| ${no} | ${f._pos} | ${f._location} | ${f.label} | \`${f.code}\` | ${f.type} | ${sizeStr} | - | - | - | - |`;
        
        const isTableParent = f.type === 'SUBTABLE';
        const labelPrefix = f._inTable ? '' : '';
        const req = f.required ? '' : '';
        const uniq = f.unique ? '' : '';
        let def = f.defaultValue !== undefined ? JSON.stringify(f.defaultValue) : '';
        let typeDisplay = f.type;
        let note = '';

        if (['STATUS', 'STATUS_ASSIGNEE'].includes(f.type) && status && !status.enable) note += ' (機能無効)';
        if (f.type === 'CATEGORY') {
            if (categoryTreeStr) note += `設定値: ${categoryTreeStr}`;
            if (categoriesResp && categoriesResp.enabled === false) note += ' (機能無効)';
        }
        if (f.lookup) {
            typeDisplay += '<br>Lookup';
            const relatedAppId = f.lookup.relatedApp ? f.lookup.relatedApp.app : '不明';
            const relatedAppName = appNameMap[relatedAppId] ? appNameMap[relatedAppId].name : '';
            note += `LookUp: ${relatedAppName} (ID: ${relatedAppId})<br>キー: ${f.lookup.relatedKeyField} `;
            if (f.lookup.fieldMappings && f.lookup.fieldMappings.length > 0) {
                const copyFields = f.lookup.fieldMappings.map(m => m.field).join(', ');
                note += `<br>コピー先: [${copyFields}]`;
            }
        }
        if (lookupDestinations[f.code]) {
            const info = lookupDestinations[f.code];
            typeDisplay += `<br>Copy (${info.relatedField})`;
            note += `Lookup: ${info.lookupCode} `;
        }
        if (f.type === 'REFERENCE_TABLE') {
            const ref = f.referenceTable;
            const rId = ref.relatedApp.app;
            const rName = appNameMap[rId] ? appNameMap[rId].name : '';
            note += `参照アプリ: ${rName} (ID: ${rId})<br>`;
            note += `条件: ${ref.condition.field} = ${ref.condition.relatedField}<br>`;
            if (ref.filterCond) note += `絞り込み: ${ref.filterCond}`;
        }
        if (f.expression) note += `式: \`${f.expression}\` `;
        if (f.options) {
            const opts = Object.values(f.options).sort((a, b) => Number(a.index) - Number(b.index));
            const optStr = opts.map(o => o.label).join(', ');
            note += `選択肢: [${optStr}] `;
        }
        if (f.hideLabel) note += 'ラベル非表示 ';
        if (f.minLength || f.maxLength) {
            const min = f.minLength ? `${f.minLength}文字` : '';
            const max = f.maxLength ? `${f.maxLength}文字` : '';
            const range = (min && max) ? `${min}${max}` : (min ? `${min}以上` : `${max}以下`);
            note += `文字数: ${range} `;
        }
        if (f.minValue !== undefined || f.maxValue !== undefined) {
             const min = (f.minValue !== null && f.minValue !== '') ? f.minValue : null;
             const max = (f.maxValue !== null && f.maxValue !== '') ? f.maxValue : null;
             if (min !== null || max !== null) {
                 const limitStr = (min !== null && max !== null) ? `${min}${max}` : (min !== null ? `${min}以上` : `${max}以下`);
                 note += `値制限: ${limitStr} `;
             }
        }
        if (f.unit) {
            const pos = f.unitPosition === 'BEFORE' ? '' : '';
            note += `単位: ${f.unit} (${pos}) `;
        }
        if (isTableParent) note += 'サブテーブル';
        return `| ${no} | ${f._pos} | ${f._location} | ${labelPrefix}**${f.label}** | \`${f.code}\` | ${typeDisplay} | ${sizeStr} | ${req} | ${uniq} | ${def} | ${note} |`;
    };
    orderedFields.forEach(f => md.push(createFieldRow(f)));
    if (unplacedFields.length > 0) {
        md.push(`| - | - | (未配置) | **(未配置フィールド)** | | | | | | | |`);
        unplacedFields.forEach(f => md.push(createFieldRow(f)));
    }
    md.push('\n');

    // --- 一覧 (Views) ---
    md.push(`## 2. 一覧設定`);
    const viewNames = Object.keys(views.views);
    if (viewNames.length === 0) { md.push(`- 設定なし`); } else {
      md.push(`| No. | ID | 一覧名 | タイプ | 絞り込み | ソート | 表示フィールド | HTMLソース (CUSTOMのみ) |`);
      md.push(`| :---: | :---: | --- | :---: | --- | --- | --- | --- |`);
      const sortedViews = Object.values(views.views).sort((a, b) => Number(a.index) - Number(b.index));
      sortedViews.forEach((v, index) => {
        const filterStr = v.filterCond ? `\`${v.filterCond}\`` : '-';
        const sortStr = v.sort ? `\`${v.sort}\`` : '-';
        let fieldsContent = '-';
        let htmlContent = '-';
        if (v.type === 'LIST' && v.fields) fieldsContent = v.fields.join(', ');
        else if (v.type === 'CALENDAR') fieldsContent = `日付: ${v.date || '-'} / タイトル: ${v.title || '-'}`;
        else if (v.type === 'CUSTOM') {
            fieldsContent = '(HTMLカスタマイズ)';
            if (v.html) htmlContent = `<code>${escapeHtml(v.html).replace(/\r?\n/g, '<br>')}</code>`;
        }
        md.push(`| ${index + 1} | ${v.id} | **${v.name}** | ${v.type} | ${filterStr} | ${sortStr} | ${fieldsContent} | ${htmlContent} |`);
      });
    }
    md.push('\n');

    // --- グラフ (Reports) ---
    md.push(`## 3. グラフ設定`);
    const reportNames = Object.keys(reports.reports);
    if (reportNames.length === 0) { md.push(`- 設定なし`); } else {
      md.push(`| No. | ID | グラフ名 | タイプ | 分類 (大/中/小) | 集計 | ソート | 絞り込み |`);
      md.push(`| :---: | :---: | --- | :---: | --- | --- | --- | --- |`);
      const chartModeMap = { 'NORMAL': '集合', 'STACKED': '積み上げ', 'PERCENTAGE': '100%積み上げ' };
      const perMap = { 'YEAR': '', 'QUARTER': '四半期', 'MONTH': '', 'WEEK': '', 'DAY': '', 'HOUR': '', 'MINUTE': '' };
      const sortedReports = Object.values(reports.reports).sort((a, b) => Number(a.index) - Number(b.index));
      sortedReports.forEach((r, index) => {
        let typeStr = r.chartType;
        if (r.chartMode && chartModeMap[r.chartMode]) typeStr += `<br>(${chartModeMap[r.chartMode]})`;
        let groupsStr = '-';
        if (r.groups && r.groups.length > 0) {
            groupsStr = r.groups.map(g => {
                let str = g.code;
                if (g.per && perMap[g.per]) str += ` (${perMap[g.per]})`;
                else if (g.level) str += ` (${g.level})`;
                return str;
            }).join('<br>');
        }
        let aggStr = '-';
        if (r.aggregations && r.aggregations.length > 0) {
            aggStr = r.aggregations.map(a => `${a.type}: ${a.code || '(件数)'}`).join('<br>');
        }
        let sortStr = '-';
        if (r.sorts && r.sorts.length > 0) sortStr = r.sorts.map(s => `${s.by} (${s.order})`).join('<br>');
        else if (r.sort) sortStr = r.sort;
        const filterStr = r.filterCond ? `\`${r.filterCond}\`` : '-';
        md.push(`| ${index + 1} | ${r.id} | **${r.name}** | ${typeStr} | ${groupsStr} | ${aggStr} | ${sortStr} | ${filterStr} |`);
      });
    }
    md.push('\n');

    // --- アプリアクション ---
    md.push(`## 4. アプリアクション設定`);
    if (!actionsResp.actions || Object.keys(actionsResp.actions).length === 0) {
        md.push(`- 設定なし`);
    } else {
        md.push(`| No. | アクション名 | ID | 連携先アプリ | コピー設定 (元 ➡ 先) | 利用条件 |`);
        md.push(`| :---: | --- | --- | --- | --- | --- |`);
        const getAnyValue = (obj) => {
            if (!obj) return '(なし)';
            if (typeof obj === 'string') return obj;
            if (obj.value !== undefined && obj.value !== null) return obj.value;
            if (obj.code) return obj.code;
            if (obj.type) return `[${obj.type}]`;
            try { return JSON.stringify(obj); } catch (e) { return '(不明)'; }
        };
        const sortedActions = Object.values(actionsResp.actions).sort((a, b) => Number(a.index) - Number(b.index));
        sortedActions.forEach((act, idx) => {
            const targetAppInfo = appNameMap[act.destApp.app];
            const targetAppName = targetAppInfo ? targetAppInfo.name : '名称取得不可';
            const destAppStr = `**${targetAppName}** (ID: ${act.destApp.app})`;
            let mappingsStr = '-';
            if (act.mappings && Array.isArray(act.mappings)) {
                mappingsStr = act.mappings.map(m => {
                    let src = '';
                    if (m.srcType === 'FIELD') src = m.srcField;
                    else if (m.srcType === 'RECORD_URL') src = 'レコードURL';
                    else if (m.src) src = getAnyValue(m.src);
                    else src = `[${m.srcType}]`;
                    const dest = m.destField || getAnyValue(m.dest);
                    return `${src}${dest}`;
                }).join('<br>');
            }
            const filter = act.filterCond || '-';
            md.push(`| ${idx + 1} | **${act.name}** | ${act.id} | ${destAppStr} | ${mappingsStr} | \`${filter}\` |`);
        });
    }
    md.push('\n');

    // --- プロセス管理 ---
    md.push(`## 5. プロセス管理`);
    if (!status.enable) {
      md.push(`- 無効`);
    } else {
      md.push(`- **有効**`);
      const states = Object.values(status.states).sort((a, b) => Number(a.index) - Number(b.index));
      const sortedStateNames = states.map(s => s.name);
      const statusAssigneeMap = {};
      states.forEach(s => {
        let assigneesStr = '設定なし';
        if (s.assignee && s.assignee.entities && s.assignee.entities.length > 0) {
            const entities = s.assignee.entities.map(e => formatEntity(e.entity)).join(', ');
            const typeMap = { 'ONE': '作業者が指定', 'ALL': '次のユーザー全員', 'ANY': '次のユーザーのうち1人' };
            const typeLabel = typeMap[s.assignee.type] || s.assignee.type;
            assigneesStr = `[${typeLabel}] ${entities}`;
        }
        statusAssigneeMap[s.name] = assigneesStr;
      });
      if (sortedStateNames.length > 0) {
          md.push(`\n### 5-1. ステータス遷移図 (Mermaid)`);
          md.push('```mermaid');
          md.push('stateDiagram-v2');
          md.push('  direction LR');
          md.push(`  [*] --> ${sortedStateNames[0]}`);
          status.actions.forEach(a => { md.push(`  ${a.from} --> ${a.to} : ${a.name}`); });
          const lastState = sortedStateNames[sortedStateNames.length - 1];
          md.push(`  ${lastState} --> [*]`);
          md.push('```');
      }
      md.push(`\n### 5-2. ステータス一覧`);
      md.push(`| No. | ステータス名 | 担当者設定 |`);
      md.push(`| :---: | --- | --- |`);
      states.forEach((s, idx) => {
        md.push(`| ${idx + 1} | ${s.name} | ${statusAssigneeMap[s.name]} |`);
      });
      md.push(`\n### 5-3. アクション一覧`);
      md.push(`| No. | アクション名 | 作業者 (From担当者) | 実行前 | 実行後 | 利用条件 |`);
      md.push(`| :---: | --- | --- | --- | --- | --- |`);
      status.actions.forEach((a, idx) => {
        const filter = a.filterCond ? `\`${a.filterCond}\`` : '(なし)';
        const worker = statusAssigneeMap[a.from] || '-';
        md.push(`| ${idx + 1} | **${a.name}** | ${worker} | ${a.from} | ${a.to} | ${filter} |`);
      });
    }
    md.push('\n');

    // --- 通知設定 ---
    md.push(`## 6. 通知設定`);
    md.push(`### 一般通知`);
    if (notifyGeneral.notifications.length === 0) md.push(`- 設定なし`);
    else {
        md.push(`| No. | 対象 | レコード追加 |`);
        md.push(`| :---: | --- | :---: |`);
        notifyGeneral.notifications.forEach((n, idx) => {
            const includeSubs = n.includeSubs ? '含む' : '含まない';
            md.push(`| ${idx + 1} | ${formatEntity(n.entity)} | ${includeSubs} |`);
        });
    }
    md.push(`### レコード通知`);
    if (notifyRecord.notifications.length === 0) md.push(`- 設定なし`);
    else {
        md.push(`| No. | 通知の条件 | 通知先 | タイトル |`);
        md.push(`| :---: | --- | --- | --- |`);
        notifyRecord.notifications.forEach((n, idx) => {
            const cond = n.filterCond ? `\`${n.filterCond}\`` : '(すべてのレコード)';
            const targets = formatTargets(n.targets);
            md.push(`| ${idx + 1} | ${cond} | ${targets} | ${n.title} |`);
        });
    }
    md.push(`### リマインダー通知`);
    if (notifyReminder.notifications.length === 0) md.push(`- 設定なし`);
    else {
        md.push(`**基準タイムゾーン**: ${notifyReminder.timezone || 'User Default'}\n`);
        md.push(`| No. | タイトル | 通知のタイミング | 通知先 | 条件 |`);
        md.push(`| :---: | --- | --- | --- | --- |`);
        notifyReminder.notifications.forEach((n, idx) => {
            let timingStr = `${n.timing.code}`;
            const days = Number(n.timing.daysLater);
            const time = n.timing.time;
            if (!isNaN(days)) {
                if (days === 0) timingStr += ` 当日`;
                else if (days < 0) timingStr += ` ${Math.abs(days)}日前`;
                else timingStr += ` ${days}日後`;
            }
            if (time) timingStr += ` ${time}`;
            const targets = formatTargets(n.targets);
            const filter = n.filterCond ? `\`${n.filterCond}\`` : '(すべてのレコード)';
            md.push(`| ${idx + 1} | ${n.title} | ${timingStr} | ${targets} | ${filter} |`);
        });
    }
    md.push('\n');

    // --- アクセス権 ---
    md.push(`## 7. アクセス権設定`);
    md.push(`### 7-1. アプリのアクセス権`);
    if (aclApp.rights.length === 0) md.push(`- 設定なし`);
    else {
        md.push(`| No. | 対象 | 閲覧 | 追加 | 編集 | 削除 | アプリ管理 | File読 | File出 |`);
        md.push(`| :---: | --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |`);
        aclApp.rights.forEach((r, index) => {
            const check = (bool) => bool ? '' : '-';
            md.push(`| ${index + 1} | ${formatEntity(r.entity)} | ${check(r.recordViewable)} | ${check(r.recordAddable)} | ${check(r.recordEditable)} | ${check(r.recordDeletable)} | ${check(r.appEditable)} | ${check(r.recordImportable)} | ${check(r.recordExportable)} |`);
        });
    }
    md.push('\n');
    md.push(`### 7-2. レコードのアクセス権`);
    if (!aclRecord || !aclRecord.rights || aclRecord.rights.length === 0) {
        md.push(`- 設定なし (またはAPI未提供)`);
    } else {
        md.push(`| 条件No. | 条件 | 項番 | 対象 | 閲覧 | 編集 | 削除 |`);
        md.push(`| :---: | --- | :---: | --- | :---: | :---: | :---: |`);
        aclRecord.rights.forEach((r, idx) => {
            const condNo = idx + 1;
            const condition = r.filterCond ? `\`${r.filterCond}\`` : '(すべてのレコード)';
            r.entities.forEach((e, entityIdx) => {
                const itemNo = entityIdx + 1;
                const check = (bool) => bool ? '' : '-';
                md.push(`| ${condNo} | ${condition} | ${itemNo} | ${formatEntity(e.entity)} | ${check(e.viewable)} | ${check(e.editable)} | ${check(e.deletable)} |`);
            });
        });
    }
    md.push('\n');
    md.push(`### 7-3. フィールドのアクセス権`);
    if (aclField.rights.length === 0) md.push(`- 設定なし`);
    else {
        const fieldNameMap = {};
        const extractLabels = (props) => {
            Object.keys(props).forEach(key => {
                const f = props[key];
                fieldNameMap[key] = f.label;
                if (f.type === 'SUBTABLE' && f.fields) extractLabels(f.fields);
            });
        };
        extractLabels(fieldProps);
        md.push(`| Field No. | フィールド | コード | 項番 | 対象 | 権限 |`);
        md.push(`| :---: | --- | --- | :---: | --- | :---: |`);
        aclField.rights.forEach((r, fIdx) => {
            const fieldNo = fIdx + 1;
            const label = fieldNameMap[r.code] || r.code;
            r.entities.forEach((e, eIdx) => {
                const itemNo = eIdx + 1;
                let access = '';
                if (e.accessibility === 'WRITE') access = '閲覧・編集';
                else if (e.accessibility === 'READ') access = '閲覧のみ';
                else access = 'なし';
                md.push(`| ${fieldNo} | ${label} | \`${r.code}\` | ${itemNo} | ${formatEntity(e.entity)} | ${access} |`);
            });
        });
    }
    md.push('\n');

    // --- プラグイン ---
    md.push(`## 8. プラグイン情報`);
    if (appPlugins.plugins.length === 0) { md.push(`- 設定なし`); } else {
      md.push(`| No. | プラグイン名 | ID | バージョン | ステータス |`);
      md.push(`| :---: | --- | --- | --- | --- |`);
      appPlugins.plugins.forEach((p, index) => {
        const sysInfo = systemPlugins.find(sp => sp.id === p.id);
        const version = sysInfo ? sysInfo.version : '(不明)';
        md.push(`| ${index + 1} | ${p.name} | ${p.id} | ${version} | ${p.enabled ? '有効' : '無効'} |`);
      });
    }
    md.push('\n');

    // --- カスタマイズ ---
    md.push(`## 9. JavaScript / CSS カスタマイズ`);
    md.push(`**適用範囲**: ${customize.scope || '-'}`);
    const renderResourceTable = (title, resources) => {
        if (!resources || resources.length === 0) return;
        md.push(`\n**${title}**`);
        md.push(`| No. | タイプ | 内容 | サイズ |`);
        md.push(`| :---: | :---: | --- | :---: |`);
        resources.forEach((r, index) => {
            const type = r.type;
            let content = '-';
            let size = '-';
            if (type === 'URL') { content = `[Link](${r.url})`; } 
            else if (type === 'FILE') { content = r.file.name; size = `${Math.round(r.file.size / 1024)} KB`; }
            md.push(`| ${index + 1} | ${type} | ${content} | ${size} |`);
        });
    };
    let hasCustomize = false;
    if (customize.desktop) {
        if (customize.desktop.js && customize.desktop.js.length > 0) { renderResourceTable('PC用 JavaScript', customize.desktop.js); hasCustomize = true; }
        if (customize.desktop.css && customize.desktop.css.length > 0) { renderResourceTable('PC用 CSS', customize.desktop.css); hasCustomize = true; }
    }
    if (customize.mobile) {
        if (customize.mobile.js && customize.mobile.js.length > 0) { renderResourceTable('スマホ用 JavaScript', customize.mobile.js); hasCustomize = true; }
    }
    if (!hasCustomize) { md.push(`- 設定なし`); }

    // -------------------------------------------------------
    // 8. ファイルダウンロード処理
    // -------------------------------------------------------
    const output = md.join('\n');
    const blob = new Blob([output], { type: 'text/markdown' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `APP_${APP_ID}_${appName.replace(/\s+/g, '_')}-${formatDateForFile(NOW)}.md`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);

    console.log('🎉 レイ: 出力が完了しました!');

  } catch (error) {
    console.error('🦊 レイ: エラーが発生しました。', error);
  }
})();

インストール手順

このツールをChromeにインストールする手順を解説します。
※Chromeウェブストアには公開していない「野良拡張機能(開発者モード用)」としての導入手順になります。

1. 準備するもの

配布された(または作成した)拡張機能のフォルダ一式を手元に用意してください。
フォルダ構成は以下のようになっています。

rex0220-app-exporter/
  ├─ manifest.json   (設定ファイル)
  ├─ background.js   (起動スクリプト)
  ├─ app_export.js   (設計書出力ロジック)
  └─ icon.png        (アイコン画像:キツネのレイ)

このフォルダを、デスクトップやドキュメントなど、削除しない場所に保存してください。
(※場所を移動したり削除すると、拡張機能が動かなくなります)

2. Chromeの設定を開く

  1. Chromeブラウザを起動します。
  2. アドレスバーに chrome://extensions/ と入力してEnterキーを押すか、右上の「︙」メニュー > 「拡張機能」 > 「拡張機能を管理」を開きます。

3. デベロッパーモードをONにする

画面右上にある 「デベロッパーモード」 のスイッチを ON にします。
これをONにすることで、ストア以外の自作拡張機能を読み込めるようになります。

注意
デベロッパーモードをONにすると、Chrome起動時に「デベロッパーモードの拡張機能を無効にしますか?」という警告が出ることがありますが、そのまま利用して問題ありません。

2025-11-27_16h51_17.png

4. パッケージ化されていない拡張機能を読み込む

  1. 画面左上に表示された 「パッケージ化されていない拡張機能を読み込む」 ボタンをクリックします。
  2. 先ほど用意した rex0220-app-exporter フォルダ自体 を選択します。
    • ※フォルダの中身ではなく、フォルダそのものを選択してください。

5. インストール完了!

拡張機能の一覧に 「rex0220 アプリ情報取得」 というカードが表示されればインストール完了です!
アイコンには、マスコットキャラクターの「レイ(キツネ)」が表示されているはずです。

使いやすくするための設定(ピン留め)

ブラウザ右上の「パズルピースのアイコン(拡張機能)」をクリックし、「rex0220 アプリ情報取得」の横にある 画鋲(ピン)マーク をクリックして青色にします。
これで、常にツールバーに「レイ」のアイコンが表示されるようになり、ワンクリックで起動できるようになります。

使い方

  1. kintoneで、設計書を出力したいアプリの画面(一覧画面や詳細画面)を開きます。
  2. ツールバーの レイ(キツネ)のアイコン をクリックします。
  3. 自動的に情報の解析が始まり、完了するとMarkdownファイル(.md)がダウンロードされます。

ダウンロードされたファイルを、VS Codeなどのエディタや、Markdown対応のビューワーで開いて確認してみてください。


トラブルシューティング

Q. アイコンをクリックしても反応しない
A. 一度、kintoneの画面をリロード(F5キー)してから再度クリックしてみてください。拡張機能をインストールした直後は、開いているページに反映されていないことがあります。

Q. エラーが表示される
A. 拡張機能の管理画面で「更新(回転矢印)」ボタンを押すか、一度「削除」してから手順4をやり直してみてください。


1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?