くらげnote

"ぼやき" by くらげ(fal)

「異次元の少子化対策」を本当に異次元にしてみる

「異次元の少子化対策」って聞かなくなりました。 しかし、少子化そのものは着実に進行しています。 そこで、真に「異次元」な2つのアプローチを検討しました。

まずは現状の前提を確認します。 日本の再生産年齢女性人口(15〜49歳)は約2,300万人、年間出生数は約70万人で、 一人の女性が生涯に生む子どもの数である合計特殊出生率は約1.1にとどまります[B23]。 人口置換水準とされる2.07を大きく割り込んでおり、人口の自然減が止まりません。

少子化対策を定義の変更だけで解決したらどうなるか、思考実験していきましょう。

案1:国民の定義を拡張する

現行法では、国民は生物学的にホモ・サピエンスのみを想定しています。 この人間中心的な前提を改め、マンボウを国民として法的に認める方針をとります。

マンボウ1人の生涯産卵数は諸説あるものの、ここでは俗説を採用し3億個とします*1。 国内の水族館飼育個体数(常設5館・季節展示4館の計9個体と仮定 *2 )を「在住者」とし、 男女比については先行研究[N07]の値を参考に、 極端な偏りがなく僅かに女性が多いことから9人中5人を女性と定義します。

これにより日本の合計特殊出生率を試算すると、次のとおりです。

 
\displaystyle 
\frac{(2.3\times 10^{7})\times 1.1 + 5 \times (3\times 10^{8})}{2.3\times 10^{7}+5}
\approx 66

世界トップレベルの合計特殊出生率を持つニジェールで6.7とされていますから*3、 これは快挙ですね。 異次元の少子化対策成功と言えそうです。

副作用と課題

しかしながら、重大な副作用があります。 年間数億単位で誕生する新生児に対する保育インフラの不足です。 水温・水質管理を備えた専用保育施設の整備は到底追いつかず、国内の待機児童数は瞬く間に数億人規模へと跳ね上がります。 出生率向上と同時に「異次元の保育水槽増設」が必須となります。

案2:年齢の定義を変更する

干支が一巡することを「還暦」と呼ぶ習俗は、 60による剰余類 \mathbb{Z}/60\mathbb{Z}を適用していると解釈できます。 この思想を戸籍制度に導入し、満60歳を迎えた国民を「0歳」とみなします。 これにより、従来の60歳人口(約150万人/2020年国勢調査)が毎年「新生児」として出生数に算入されます。 年間出生数は従来の70万人から約220万人へと急増します。

ただし、母数となる再生産年齢女性人口も変動します。 現在75歳以上の女性(約1,130万人)は新たに「15歳以上」となるため、 再び再生産年齢(15〜49歳)へ編入されます。 その結果、再生産年齢女性人口は約2,300万人から約3,630万人(約1.45倍)に拡大します。

出生数は約2.79倍、再生産年齢女性人口は約1.45倍となるため、修正後の合計特殊出生率は以下の通りです。

 
\displaystyle 
1.1 \times \frac{2.79}{1.45}
=2.1

めでたく自然減ラインを超えることができました。

副作用と課題

さらになんと、副作用として年金制度も持続可能となります。 これは年金受給開始年齢である65歳に到達する人がいなくなるため、誰にも支払う必要がなくなるためです。

他方で、60歳を超えたベテラン社員は0歳児になり、当然ながら定年延長や再雇用は児童労働とみなされ違法です。 彼らが義務教育を終え15歳になったら、再雇用することにしましょう。

さいごに

本稿で示した試算はフィクションであり、事実でない数値が含まれる可能性があります。 筆者はマンボウに対する戸籍付与や、還暦を迎えた方への児童福祉法適用を推進しておりません。

参考文献

[B23] 別府志海「全国人口の再生産に関する主要指標:2022年」人口問題研究 79-4, 2023年.

[N07] Nakatsubo, Toshiyuki & Kawachi, Masahiro & Mano, Nobuhiro & Hirose, Hitomi. (2007). 飼育下および自然環境下におけるマンボウ Mola mola の成熟評価 (in Japanese with English summary). Aquaculture Science. 55. 259-264. 10.11233/aquaculturesci1953.55.259.

*1:https://ja.wikipedia.org/wiki/マンボウ

*2:https://manboumuseum.com/manbou-aquarium/より。正確な個体数は分からなかったため、各館1個体と仮定。複数個体いれば出生数/率は大幅上昇します

*3:https://ja.wikipedia.org/wiki/国の合計特殊出生率順リスト

Windows 11 + WSL2 + Dockerで構築する安全なAIエージェント開発環境

Windows内の特定ディレクトリのみをAIエージェントに参照させ、それ以外から隔離した開発環境をゼロから構築する。 特定のツールに依存しない汎用的な隔離アーキテクチャをベースとしつつ、具体的な実装例として

  • 適用例1: WSLネイティブGUI型 (Antigravity IDE)
  • 適用例2: ホスト接続型 (VS Code + Codex)

の2通りの接続パターンを解説する。

全体構成

Windowsホストの個人領域(Cドライブ全体)をWSL2から不可視にし、特定ディレクトリのみをホワイトリスト形式でマウントする。その上でDockerコンテナを立ち上げ、AIエージェントの作業領域および参照領域(読取専用)を完全隔離する。

適用例1

[ Windows 11 (ホスト) ]
  ├── C:\Users\... (完全遮断・アクセス不可)
  └──  C:\Users\<Windowsユーザー>\AI-shared (マウントする共有フォルダ)

[ WSL2 (Ubuntu) ] ── (automount=false / Dockerホスト)
  └── Antigravity IDE (GUI)
        └── [ Docker コンテナ ]
              ├── AIエージェントの作業領域 (/workspace)
              └── 参照フォルダ (/shared:ro)

適用例2

[ Windows 11 (ホスト) ]
  ├── C:\Users\... (完全遮断・アクセス不可)
  ├── C:\Users\<Windowsユーザー>\AI-shared (マウントする共有フォルダ)
  └── VS Code (本体 + Dev Containers拡張機能 + Codex拡張機能)
        ↓
[ WSL2 (Ubuntu) ] ── (automount=false / Dockerホスト)
  └── [ Docker コンテナ ]
        ├── AIエージェントの作業領域 (/workspace)
        └── 参照フォルダ (/shared:ro)

共通基盤編:ホスト完全隔離環境の構築

まずは特定の開発ツールに依存しない、安全なWSL2・Dockerサンドボックス基底環境を構築する。

Step 1. [Windows] WSL2 (Ubuntu) のセットアップ

1. WSL2のインストール

PowerShellを管理者として起動し、WSLおよびUbuntuをインストールする。

wsl --install

※環境によっては、一度の実行でUbuntuまでインストールされず、再起動後に再度 wsl --install を実行する必要がある場合がある。

2. Windows側でのWSLネットワーク設定

Windowsホスト側の C:\Users\<ユーザー名>\.wslconfig を新規作成(または編集)し、以下を記述して保存する。

[wsl2]
networkingMode=NAT
dnsTunneling=true
autoProxy=true

Step 2. [WSL(Ubuntu)] WSL自動マウント制限と共有フォルダ設定

WindowsのCドライブ全体がWSL2へ自動マウントされる動作を無効化する。((本環境では、WSL2からWindowsホストへの実行パス自体は有効であるものの(Interop)、Cドライブ全体のマウントを無効化している。これにより、WSL2側から実質的にマウントした対象以外のホストコマンドやファイルは実行・参照できない「ホワイトリスト状態」が作られる。すなわち、ホスト側の個人フォルダやpowershell.exeなどのパスはWSL2から不可視である。※ただし、万が一コンテナからWSL2(Ubuntu)へエスケープされ、さらにUbuntu側で管理者権限(sudo)を奪取された場合、攻撃者が手動でCドライブを強制マウントする可能性がある。そのため、WSL2側では sudo 実行時に必ずパスワードを要求する設定(デフォルト状態)を維持するべきである。))

1. パッケージの更新

sudo apt update
sudo apt upgrade -y

2. 自動マウントの制限設定

設定ファイル /etc/wsl.conf を編集する。

sudo nano /etc/wsl.conf

以下を追記する。

[automount]
enabled = false # Cドライブ全体の自動マウントを無効化
mountFsTab = true
options = "metadata,uid=1000,gid=1000,umask=022,fmode=11,dmode=755"

[interop]
appendWindowsPath = false # Windows側の環境変数を展開しない

[network]
generateResolvConf = false

3. DNSサーバーの固定

DNS名前解決を固定するためのDNSサーバー情報を書き込む。

# パブリックDNSを上書き
sudo rm -f /etc/resolv.conf
sudo bash -c 'cat << EOF > /etc/resolv.conf
nameserver 8.8.8.8
nameserver 1.1.1.1
EOF'

4. 共有フォルダのマウント

ホワイトリスト方式で、AIに共有するフォルダとアプリをマウントする。 ここでは例としてAIに参照を許可する AI-shared ディレクトリのみをマウントする。 マウント先ディレクトリを作成し、/etc/fstab に定義を追加する。

# Windowsユーザー名を確認し、マウント先を作成
sudo mkdir -p /mnt/c/Users/<Windowsユーザー名>/AI-shared

sudo nano /etc/fstab

/etc/fstab の末尾に追記:

C:/Users/<Windowsユーザー名>/AI-shared /mnt/c/Users/<Windowsユーザー名>/AI-shared drvfs metadata,noatime,gid=1000,uid=1000,defaults 0 0

※マウントオプションで使用するユーザーID uid とグループID gid は、以下で確認できる。初期登録ユーザーは通常どちらも 1000 。

$ id <WSLのユーザー名>

5. 反映と隔離の確認

WindowsのPowerShellでWSLを再起動する。

wsl --shutdown

再度WSLターミナルを開き、指定フォルダのみが見えているか確認する。

ls -ld /mnt/c/Users/<Windowsユーザー名>/*
# 出力に AI-shared のみが表示され、他の個人フォルダが見えなければ遮断成功

Step 3. [WSL(Ubuntu)] Docker Engineの導入

WSL2内部に直接 Docker Engine を導入する。

# Docker 本体の導入
sudo apt install -y docker.io docker-compose-v2
sudo usermod -aG docker $USER
sudo apt update
sudo apt install -y docker-buildx

# デーモン設定
sudo mkdir -p /etc/docker
sudo bash -c 'cat << "EOF" > /etc/docker/daemon.json
{
  "dns": ["8.8.8.8", "1.1.1.1"],
  "max-concurrent-downloads": 1,
  "iptables": true
}
EOF'
sudo service docker restart

※ターミナルを開き直し、docker run hello-world が sudo なしで動作することを確認する。


適用例 1:Antigravity IDE(WSLネイティブGUI型)

WSL2内にGUI IDE本体を配置し、LinuxネイティブのGUIアプリとして動かす構成。

追加設定:OAuth用Chromeのマウントとブラウザ連携

WSL2上のIDEからホスト側のGoogle Chromeを呼び出せるよう、Chromeのパスのみをホワイトリストマウントする。

# マウントポイント作成と fstab への追記
sudo mkdir -p /mnt/win-chrome
sudo nano /etc/fstab

/etc/fstab に追記:

C:/Program\040Files/Google/Chrome/Application /mnt/win-chrome drvfs defaults,uid=1000,gid=1000,umask=022 0 0

設定を再読み込み後、ブラウザ呼び出し用ラッパーを作成する。

# 呼び出しスクリプトを作成
mkdir -p ~/.local/bin
cat << 'EOF' > ~/.local/bin/win-chrome-wrapper
#!/bin/bash
if [ -n "$1" ]; then
    /mnt/win-chrome/chrome.exe "$1" &
fi
EOF
chmod +x ~/.local/bin/win-chrome-wrapper

# ラッパースクリプト作成
mkdir -p ~/.local/bin ~/.local/share/applications
cat << 'EOF' > ~/.local/bin/win-chrome
#!/bin/bash
if [ -n "$1" ]; then
    /mnt/win-chrome/chrome.exe "$1" &
fi
EOF
chmod +x ~/.local/bin/win-chrome

# デスクトップエントリ作成
cat << 'EOF' > ~/.local/share/applications/win-chrome.desktop
[Desktop Entry]
Version=1.0
Name=Windows Chrome
Exec=/home/<Ubuntuユーザ>/.local/bin/win-chrome %u
Terminal=false
Type=Application
MimeType=text/html;text/xml;application/xhtml_xml;x-scheme-handler/http;x-scheme-handler/https;
EOF

# XDG既定ハンドラに設定
xdg-mime default win-chrome.desktop x-scheme-handler/http
xdg-mime default win-chrome.desktop x-scheme-handler/https
xdg-settings set default-web-browser win-chrome.desktop

IDEのインストールと起動

sudo apt install -y libgtk-3-0 libnss3 libasound2t64 xdg-utils
cd /tmp

# 公式サイトから Linux版 (x64) アーカイブ (tar.gz)をダウンロード。URLは最新のものに書き換える
curl -fSL -o antigravity-ide.tar.gz "<公式サイトのLinux版(x64)ダウンロードURL>"
sudo mkdir -p /opt/antigravity-ide
sudo tar -xzf antigravity-ide.tar.gz -C /opt/antigravity-ide --strip-components=1
sudo ln -sf /opt/antigravity-ide/antigravity-ide /usr/local/bin/antigravity-ide

# 起動
antigravity-ide

起動後、指示に従いブラウザ経由でライセンスなどの認証を行う。

サンドボックスコンテナの作成とアタッチ

mkdir -p ~/workspace/antigravity-app
cd ~/workspace/antigravity-app

docker-compose.yml を作成:

services:
  agent-sandbox:
    container_name: agent-sandbox
    image: mcr.microsoft.com/devcontainers/base:ubuntu
    working_dir: /workspace
    environment:
      - HOME=/workspace
    volumes:
      - .:/workspace
      - /mnt/c/Users/<Windowsユーザー名>/AI-shared:/shared:ro
    command: sleep infinity

サンドボックスコンテナを起動する。

docker compose up -d

起動した Antigravity IDE 上で Ctrl + Shift + P を押し、 Dev Containers: Attach to Running Container... を選択し、agent-sandbox にアタッチする。


適用例 2:VS Code + Codex(ホスト接続型)

Windowsホスト側で起動しているVS Codeから、WSL2上の隔離サンドボックスコンテナへ直接接続し、Codex等のCLIエージェントを動かす構成。

追加設定:VS Code実行パスのホワイトリストマウント

Windowsホスト側のDev Containers拡張機能がWSL2内部と通信する際、VS Code自身のインストールパスを参照する。 automount = false の環境下では参照が遮断されるため、VS CodeのプログラムディレクトリのみをWSL2側へホワイトリストマウントする。

sudo mkdir -p /mnt/c/Users/<Windowsユーザー名>/AppData/Local/Programs/Microsoft\ VS\ Code
sudo nano /etc/fstab

/etc/fstab に追記:

C:/Users/<Windowsユーザー名>/AppData/Local/Programs/Microsoft\040VS\040Code /mnt/c/Users/<Windowsユーザー名>/AppData/Local/Programs/Microsoft\040VS\040Code drvfs defaults,uid=1000,gid=1000,ro 0 0

WindowsのPowerShellで wsl --shutdown を実行して反映させる。

VS Code(Windows側)の設定

  1. Windows側VS Codeの拡張機能で'Dev Containers'および'WSL'をインストールする。
  2. Ctrl + Shift + P → Preferences: Open User Settings (JSON) を開き、以下を追加する。
"dev.containers.executeInWSL": true,
"dev.containers.dockerSocketPath": "/var/run/docker.sock"

サンドボックスコンテナの作成とCodexの導入

WSL2側でプロジェクトを作成する。

mkdir -p ~/workspace/codex-app
cd ~/workspace/codex-app

docker-compose.yml を作成:

services:
  codex-sandbox:
    container_name: codex-sandbox
    image: mcr.microsoft.com/devcontainers/javascript-node:20
    working_dir: /workspace
    environment:
      - HOME=/workspace
    volumes:
      - .:/workspace
      - /mnt/c/Users/<Windowsユーザー名>/AI-shared:/shared:ro
    command: sleep infinity

コンテナを起動し、Codex CLIを導入する。

docker compose up -d
docker compose exec -it codex-sandbox npm install -g @openai/codex

VS Codeからのアタッチ

  1. Windows側でVS Codeを起動する。
  2. Ctrl + Shift + P → Dev Containers: Attach to Running Container... を選択する。
    1. 一覧から codex-sandbox を選択する。

隔離状態の検証(共通確認手順)

どちらの構成でも、アタッチしたコンテナ内の統合ターミナルで以下を実行し、ファイルシステムが適切に制限されていることを検証する。

# 1. カレントディレクトリの確認
pwd
# -> /workspace

# 2. 共有フォルダの参照確認(正常に読めること)
ls /shared

# 3. 共有フォルダへの書き込み遮断確認
touch /shared/test.txt
# -> Read-only file system エラーとなり書き込みが拒否されること

# 4. ホスト領域の完全遮断確認
ls /mnt/c
ls /home
# -> ホストのCドライブ全体やWSLホスト領域へアクセス不能であることを確認

編集履歴

2026-10-06: VS Code + Codexの構成を追加し、全体をリライト 2026-08-23: 公開(Antigravity構成)

定型文を送るLINE Botの作成

やりたいこと

LINEの特定のグループトーク(あるいは特定の個人)に, 定型文を送る.

目次

前提条件

  • LINE公式アカウントは持っているものとします.
  • GAS(Google Apps Script)が使える環境

流れ

  1. 送るべきリクエストの簡単な説明
  2. チャンネルアクセストークンの取得
  3. グループID取得
    • グループID取得のためのBOTが必要
  4. 定型文を送信するBOTの作成
  5. スケジューリングの設定

LINE Messaging APIについて

メッセージはLINE側のサーバー(API)に指定された形式で送る必要がある. 複数人とのグループトークに「Hello World!」というメッセージを送りたい場合, 次のようなリクエストになる.

curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {CHANNEL_ACCESS_TOKEN}' \
-H 'X-Line-Retry-Key: {UUID}' \
-d '{
    "to": "{GROUP_ID}",
    "messages":[
        {
            "type":"text",
            "text":"Hello World!"
        }
    ]
}'
  • UUID : 開発者自身が一意のリトライキーを生成する必要がある[1].
  • CHANNEL_ACCESS_TOKEN : 次節で取得する.
  • GROUP_ID: 次々節で取得する.
    • USER_ID: 個人宛の場合はユーザIDになる.

チャンネルアクセストークンの取得

その際に認証として使われる,チャンネルアクセストークンを取得する.

  • LINE Official Account Managerにログインする.
    • 利用したいアカウントを選択
    • 「設定」→「Messaging API」を選択
    • 「Messaging APIを利用する」をクリック
    • プロバイダーを作成する
      • LINE Official Account Manager にログインするときに使っているアカウントで、LINE Developersコンソールにログインしたことがない場合は、操作の途中で、開発者情報を登録する画面が表示されます。名前とメールアドレスを入力して開発者アカウントを作成します。[2]
      • プラポリや規約は必要なければ空欄でよい
  • LINE Developersにログインする.
    • コンソールに移動する.
    • 対象のプロバイダーとチャネル(Messaging APIを利用するボット)を選択.
    • 「Messaging API」タブに移動
    • 「チャネルアクセストークン(長期)」がある
      • 必要に応じて「発行」ボタンをクリックしてアクセストークンを生成し、その値をコピーする.

グループID(ユーザID)の取得

グループIDの取得するにはサーバが(一時的に)必要になる. Webhookという仕組みを使っているとのこと. 専用のサーバを一から用意するのは大変なので, GAS(Google Apps Script)で行う.

GAS スクリプトの作成

  • Google Driveを開き, 「新規」→「その他」→「Google Apps Script」を選択
  • 次のコードを入力
    • 取得したグループID(ユーザID)を返信する方法として,次の2種類を用意
      1. LINEへIDを返す(返信メッセージを送信)sendReplyToLine
      2. スプレッドシートにIDと現在時刻を保存する saveIdToSpreadsheet
// LINEのBearer認証用アクセストークン。実際のトークンに置き換えてください。
// Access token for LINE Bearer Authorization. Replace with your actual token.
const LINE_CHANNEL_ACCESS_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXX";

// スプレッドシートのID。実際のIDに置き換えてください。
// Spreadsheet ID. Replace with your actual ID.
const SPREADSHEET_ID = "XXXXXXXXXXXXXXXXXXXXXXX";

/**
 * POSTリクエストを処理する関数。
 * Handles POST requests from LINE.
 *
 * @param {Object} eventData - POSTデータを含むイベントオブジェクト / Event object containing the POST data.
 */
function doPost(eventData) {
  // POSTデータを解析し、イベント配列の最初のイベントを取得する
  // Parse the POST data and extract the first event from the array.
  const jsonData = JSON.parse(eventData.postData.contents);
  const lineEvent = jsonData.events[0];

  // 返信メッセージを送信するためのreplyTokenを抽出する
  // Extract the reply token used to send a response message.
  const replyToken = lineEvent.replyToken;

  // ソースの種類(ユーザーまたはグループ)を判定し、対応するIDを設定する
  // Determine the source type (user or group) and assign the corresponding id.
  const sourceType = lineEvent.source.type;
  let sourceId;
  if (sourceType === 'user') {
    sourceId = lineEvent.source.userId;
  } else if (sourceType === 'group') {
    sourceId = lineEvent.source.groupId;
  } else {
    throw new Error("`lineEvent.source.type` is not `user` or `group`.");
  }

  // 好みの方(あるいはどちらも)を選ぶ: 以下の2つの関数のどちらか、または両方を呼び出すことが可能です。
  // Choose either (or both) of the following functions as desired.

  // LINEへIDを返す(返信メッセージを送信)
  // Return the id back to LINE by sending a reply message.
  sendReplyToLine(replyToken, sourceId, sourceType);

  // スプレッドシートにIDと現在時刻を保存する
  // Save the id and current timestamp to a spreadsheet.
  saveIdToSpreadsheet(sourceId);
}

/**
 * スプレッドシートにIDとタイムスタンプを保存する関数。
 * Save the id and a timestamp to a spreadsheet.
 *
 * @param {string} sourceId - 保存するソースID / The source id to save.
 */
function saveIdToSpreadsheet(sourceId) {
  // 指定されたIDのスプレッドシートを開く
  // Open the spreadsheet by its ID.
  const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  
  // 「シート1」を取得する
  // Get the sheet named "シート1".
  const sheet = spreadsheet.getSheetByName("シート1");
  
  // 最後に値が入力されている行番号を取得し、次の行に値を書き込む
  // Get the last row with data and write the id in the next row.
  const lastRow = sheet.getLastRow();
  // IDを1列目に保存
  // Save the source id in the first column.
  sheet.getRange(lastRow + 1, 1).setValue(sourceId);
  // 現在のタイムスタンプ(ミリ秒)を2列目に保存
  // Save the current timestamp (in milliseconds) in the second column.
  sheet.getRange(lastRow + 1, 2).setValue(Date.now());
}

/**
 * LINEに返信メッセージを送信する関数。
 * Send a reply message to LINE.
 *
 * @param {string} replyToken - 返信用トークン / Token to reply to the message.
 * @param {string} sourceId - 送信するソースID / The source id to include in the reply.
 * @param {string} sourceType - ソースの種類(user, group, etc.) / The type of the source.
 */
function sendReplyToLine(replyToken, sourceId, sourceType) {
  // LINE APIのエンドポイントURL
  // LINE API endpoint to reply to messages.
  const url = 'https://api.line.me/v2/bot/message/reply';

  // 返信メッセージのペイロードを作成する
  // Build the payload for the reply message.
  const payload = {
    replyToken: replyToken,
    messages: [
      {
        type: 'text',
        text: `${sourceType}_id = ${sourceId}`  // メッセージにソースの種類とIDを表示 / Create a message showing the type and id.
      }
    ]
  };

  // HTTPリクエストのオプションを設定する(メソッド、ヘッダー、ペイロードなど)
  // Setup the HTTP request options, including method, headers, and payload.
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN,
    },
    payload: JSON.stringify(payload)
  };

  // UrlFetchAppを使用してLINEに返信メッセージを送信する
  // Send the reply message to LINE using UrlFetchApp.
  UrlFetchApp.fetch(url, options);
}
  • 右上「デプロイ」→「新しいデプロイ」
    • 種類は「ウェブアプリ」
      • 「実行者」は「自分」
      • 「アクセスできるユーザ」は「全員」
      • 説明文などは任意
    • 諸々の確認を承認する
    • デプロイができたら, ウェブアプリのURLをコピーする
  • LINE Developers コンソールから再び, 使いたいチャネルを選択
    • 「Messaging API設定」の「Webhook URL」に先ほどコピーしたリンクを貼る
    • Webhookの利用をオンにする
  • LINEのグループにBOTを招待する(またはメッセージを送る)ことでIDを入手できる.

定型文を送信するBOTの作成

GAS スクリプトの作成(再び)

  • Google Driveを開き, 「新規」→「その他」→「Google Apps Script」を選択
  • 次のjavascriptを貼る
// LINEのBearer認証用アクセストークン。実際のトークンに置き換えてください。
// Access token for LINE Bearer Authorization. Replace with your actual token.
const LINE_CHANNEL_ACCESS_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXX";

// 送信相手のグループID。実際のIDに置き換えてください。
// Replace with receiver actual ID.
const GROUP_ID = "XXXXXXXXXXXXXXXXXXXXXXX";

function main() {
  const message = "Hello, World!";
  pushTextV2MessageToLine(GROUP_ID, message, null, Utilities.getUuid());
}

/**
 * LINEにプッシュメッセージ(textV2)を送信する関数。
 * Send a push message (textV2) to LINE.
 *
 * @param {string} targetId - 送信先のグループIDまたはユーザーID / The target group or user ID.
 * @param {string} messageText - 送信するメッセージのテキスト / The text of the message to send.
 * @param {Object} substitution - プレースホルダーの置換内容(空の場合は空オブジェクトかnull) / The substitution object for placeholders (empty if none).
 * @param {string} retryKey - X-Line-Retry-Key用のUUID / The UUID for the X-Line-Retry-Key header.
 */
function pushTextV2MessageToLine(targetId, messageText, substitution, retryKey) {
  // LINE APIのプッシュメッセージ用エンドポイントURL
  // LINE API endpoint for push messages.
  const url = 'https://api.line.me/v2/bot/message/push';

  // 送信するペイロードの基本形を作成する
  // Build the base payload for the push message.
  const messagePayload = {
    type: "textV2",
    text: messageText
  };

  // substitutionが有効なオブジェクトの場合のみペイロードに追加する
  // Only add substitution if it is a valid, non-empty object.
  if (substitution && Object.keys(substitution).length > 0) {
    messagePayload.substitution = substitution;
  }

  const payload = {
    to: targetId,
    messages: [messagePayload]
  };

  // HTTPリクエストのオプションを設定する(メソッド、ヘッダー、ペイロードなど)
  // Setup the HTTP request options, including method, headers, and payload.
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN,
      'X-Line-Retry-Key': retryKey
    },
    payload: JSON.stringify(payload)
  };

  // UrlFetchAppを使用してLINEにプッシュメッセージを送信する
  // Send the push message to LINE using UrlFetchApp.
  UrlFetchApp.fetch(url, options);
}
  • main関数を実行することで, "Hello World!"を受け取れるはず.
  • トリガーを設定すれば定時実行できる.

参考文献

  • [1] LINE Messaging API reference (LINE Developers) - テキストメッセージ(v2) link
  • [2] Messaging APIを始めよう(LINE Developers) link

正十二面体の外接球半径

この記事は旧ブログからの移植です.

(各ページ右上の「内接球半径」は誤りで、「外接球半径」が正しいです)

ここから検索用: 上図は一辺の長さが1である正十二面体である. この正十二面体の外接球の半径を求めたい. (1) $\cos 36\Deg$, ACの長さをそれぞれ求めよ. (2) $\bekutoru{AB}=\beku{b}, \bekutoru{AE}=\beku{e}, \bekutoru{AF}=\beku{f}$とする. $\beku{b}\cdot\beku{e}$を求めよ. (3) $\bekutoru{AC}=\beku{u}, \bekutoru{AG}=\beku{v}, \bekutoru{AN}=\beku{w}$とする. $\beku{u}\cdot\beku{v}$を求めよ. (4) 六面体ACLN-GISPが立方体であることを示せ. (5) 一辺の長さが1であるような正十二面体の外接球の半径を求めよ. (1) $\theta = 36 \Deg$とする. \textrm{AC}&=2\cdot\textrm{AB}\cos 36\Deg
= 2\cos\theta\ \textrm{CD}&=2\cdot\textrm{AC}\cos 72\Deg
= 4\cos\theta\cos 2\theta $\textrm{CD}=1$より 4\cos\theta\cos 2\theta &= 1\ 4\cos\theta \left( 2\cos2\theta - 1 \right) -1 &= 0 \tyuu{2倍角の公式}\ 8\cos3\theta -4\cos\theta -1 &=0\ (2\cos\theta + 1)(4 \cos2\theta - 2 \cos\theta - 1) &= 0 \tyuu{因数定理を用いると良い.}\ \therefore \cos\theta &= -\dfrac{1}{2}, \dfrac{1\pm\sqrt{5}}{4} \intertext{$\theta=36\Deg$より, $\cos\theta >0$だから} \therefore \cos \theta &= \dfrac{1 + \sqrt{5}}{4} したがって, $\textrm{AC} = 2\cos\theta = \dfrac{1+\sqrt{5}}{2}$ (2) 内積の定義より \beku{b}\cdot\beku{e} &= |\bekutoru{AB}||\bekutoru{AE}|\cos 108\Deg ここで3倍角の公式を用いると \cos 108 \Deg = \cos 3\theta &= 4\cos3\theta -3\cos\theta\ &= \cos\theta(4\cos2\theta -3\cos\theta)\ &= \dfrac{1 + \sqrt{5}}{4}\cdot \left{ 4\cdot\left( \dfrac{1 + \sqrt{5}}{4} \right)2 - 3 \right} \ &= \dfrac{1 + \sqrt{5}}{4}\cdot\dfrac{- 6 + 2\sqrt{5}}{4} &= \dfrac{1-\sqrt{5}}{4} したがって, \beku{b}\cdot\beku{e} = |\bekutoru{AB}||\bekutoru{AE}|\cos 108\Deg = 1\cdot 1\cdot\dfrac{1-\sqrt{5}}{4} =\dfrac{1-\sqrt{5}}{4} (2)と同様にして \beku{b}\cdot\beku{e} = \beku{e}\cdot\beku{f} = \beku{f}\cdot\beku{b} = \dfrac{1-\sqrt{5}}{4} となることがわかる. (3) $\beku{u}, \beku{v}, \beku{w}$を $\beku{b}, \beku{e}, \beku{f}$で表す. \beku{u}=\bekutoru{AC} &=\bekutoru{AE} + \bekutoru{EC}\ &=\bekutoru{AE} + \dfrac{1 + \sqrt{5}}{2}\bekutoru{AB} \tyuu{$\textrm{CE}\heikou\textrm{AB}$および,\ CEの長さが$\dfrac{1+\sqrt{5}}{2}$より.}\ &=\beku{e} + \dfrac{1 + \sqrt{5}}{2}\beku{b} 同様にして, \beku{u}= \beku{e} + \dfrac{1 + \sqrt{5}}{2}\beku{b}\ \beku{v}= \beku{b} + \dfrac{1 + \sqrt{5}}{2}\beku{f}\ \beku{w}= \beku{f} + \dfrac{1 + \sqrt{5}}{2}\beku{e} \end{empheq} これを用いると \beku{u} \cdot \beku{v} &= \left( \beku{e} + \dfrac{1 + \sqrt{5}}{2}\beku{b} \right) \cdot \left( \beku{b} + \dfrac{1 + \sqrt{5}}{2}\beku{f} \right)\ &= \dfrac{1+\sqrt{5}}{2}|\beku{b}|^2 + \beku{b}\cdot\beku{e} + \dfrac{1+\sqrt{5}}{2}\beku{e}\cdot\beku{f} + \left( \dfrac{1+\sqrt{5}}{2} \right)2 \beku{b}\cdot\beku{f}\ &= \dfrac{1+\sqrt{5}}{2} + \dfrac{1-\sqrt{5}}{4} + \dfrac{1+\sqrt{5}}{2}\cdot\dfrac{1-\sqrt{5}}{4} + \left( \dfrac{1+\sqrt{5}}{2} \right)2\cdot\dfrac{1-\sqrt{5}}{4}\ &= 0 同様にして \beku{u} \cdot \beku{v} = \beku{v} \cdot \beku{w} = \beku{w} \cdot \beku{u} = 0 (4) (3)より, $\textrm{AC}\perp\textrm{AG}, \textrm{AG}\perp\textrm{AN}, \textrm{AN}\perp\textrm{AC}$である. 対称性から,交わる辺はすべて直交することがわかるから, ACLN-GISPは直方体となる. またすべての辺の長さは等しい. したがってACLN-GISPは立方体. (5) 正十二面体の対角線のなかで, 最長のものが外接球の直径$2R$になるが,これはCP間の距離に等しい. (4)より,これはACLN-GISPの対角線の長さに等しく,その長さは 2R &=\sqrt{|\beku{u}|^2 + |\beku{v}|^2 + |\beku{w}|^2}\ &= \sqrt{3}|\beku{u}|\ &= \sqrt{3}\cdot\dfrac{1+\sqrt{5}}{2}\ &= \dfrac{\sqrt{3} + \sqrt{15}}{2}\ \therefore R &= \dfrac{\sqrt{3} + \sqrt{15}}{4}

iPhone標準カレンダーに、共有されたGoogleカレンダーを表示する方法

背景

Google カレンダーを iPhone (Apple) の標準カレンダーに同期する方法については多くの解説がある.

しかし, 「Google カレンダーの共有カレンダー」(以下, 単に共有カレンダー)を iPhone の標準カレンダーへ同期する方法は手順が分かりづらいにもかかわらず, 十分に共有されていない.

他人・別アカウントの Google カレンダーを iPhone カレンダーに同期したい場合の具体的な解決策を示す.

結論

  1. https://calendar.google.com/calendar/syncselectにアクセス
  2. iPhone カレンダーへ登録したい共有カレンダーにチェック
  3. 最後に「保存」を押す

チェックを入れて「保存」を押すだけ

最後に二言

いつの時代のUIやねん.

Googleカレンダーの設定ページにちゃんと入れておいてくれ.

CSVファイルをGoogleカレンダーへインポートする

目次

CSVファイルの作成

項目 要件 表示
Subject 必須 予定のタイトル
Start Date 必須(yyyy/mm/dd) 開始日
Start Time 任意(hh:mm) 開始時刻
End Date 任意(yyyy/mm/dd) 終了日
End Time 任意(hh:mm) 終了時刻
All Day Event 任意(True/False) 終日の予定か
Descrirption 任意 詳細欄
Location 任意 場所
Private 任意(True/False) 限定公開か

上記の形式に則ったCSVファイルとして保存する.

ヘッダコピー用

Subject Start Date  Start Time  End Date    End Time    All Day Event   Descrirption    Location    Private

csv例

余談

解説記事によっては, Reminder On/Off, Reminder Date などのヘッダが記載されていることがある. これはおそらく古いバージョンのもので, 現在は使えないヘッダのようである.

公式の情報はここにある.

csvヘッダの説明(https://support.google.com/calendar/answer/37118?hl=ja&visit_id=638782121743444702-2902216615&rd=1#zippy=%2Ccsv-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90%E3%81%A8%E7%B7%A8%E9%9B%86 より引用)

Google カレンダーにインポート

Google カレンダーの設定を開く(歯車マーク→設定).

Google カレンダーの設定

左のメニューから、「インポート・エクスポート」を選ぶ. 「パソコンからファイルを選択」から、先程保存したCSVを選び、追加したいカレンダーを指定して,インポートをクリック.

インポート画面

おまけ

競技かるたの大会名から、適切な省略形を手に入れるための正規変換を考える.

不要な情報としては

"第.*回|全国|競技かるた|小倉百人一首|大会|\(.*\)|(.*)|※|\s"

なので、例えばExcelで変換する場合には REGEXREPLACE(A1,"第.*回|全国|競技かるた|小倉百人一首|大会|\(.*\)|(.*)|※|\s","") を用いれば良い.

文書から予定をGoogleカレンダーに追加する【ChatGPT】2025/03

やりたいこと

「大会要項」が書かれた文書(PDFまたはWord形式)をChatGPTに読ませて, ChatGPTに要約させた後, 「申込締切日」と「大会開催日」をGoogleカレンダーに追加する.

完成したものはこれ

具体的なフロー

やり取りはすべてchatGPTとのチャット画面で行う.

  1. ユーザーがPDFまたはドキュメントをアップロード。
  2. 指定フォーマットで文章を作成し、ユーザーへ確認を求める。
  3. ユーザーがOKあるいは修正点を送信。
  4. 「会〆、本〆 *1、 大会当日のスケジュールをGoogleカレンダーに追加するための情報」をユーザーに提示。
  5. ユーザーが確認し、OKを送信。
  6. 「会〆、本〆、大会当日のスケジュール」をGoogleカレンダーに追加(OpenAPI Specificationを用いる)。

目次

必要なもの

  • ChatGPTのmyGPT機能(作成)
    • 記事執筆時点では有料版限定の機能
  • Google Cloud Console

OAuth認証するユーザはGoogle Calendarのユーザと一致しなくても良い. この場合、最後に設定するテストユーザに、OAuth認証するユーザを追加する.

Google Cloud の設定

https://console.cloud.google.com/welcome を開いて、 上部の「プロジェクトの選択」→「新しいプロジェクト」を作成する.

Google Cloudのトップページ

プロジェクト名はわかりやすければ何でも良い.

新しいプロジェクトの作成画面

上部の検索バーから"Google Calendar API"を検索しクリックし、Google Calendar API を「有効にする」をクリック.

検索バー

Google Calendar API

右上「認証情報を作成」.

Google Calendar API の詳細

認証情報を作成する.

  • APIを選択→Google Calendar API
  • アクセスするデータ→ユーザーデータ
  • アプリ名→わかりやすければ何でも(ログイン画面でも出るので分かりやすい名前に)
  • メール→(自分だけが今回のサービス使うなら)使うことはない
  • ロゴ→不要
  • スコープ→Calendar.events (今回は予定の追加権限がほしいため)
  • OAuth クライアントID →ウェブアプリケーション(GPTが使うため)
  • 名前→適当に

これでひとまず作成は完了. 後ほど戻って来る.

myGPTの作成

ここからChatGPT.

myGPTをクリックし「GPTを作成する」を選択、その後左下「新しいアクションを作成する」をクリック.

メニュー

アクションの追加画面に来る. OpenAPI スキーマを入力する必要があるが、書き方がわからなければGPTに聞くと教えてくれる. 一応、例を本記事下部に掲載する. 例を下にGPTに尋ねるとカスタマイズしてくれるだろう.

アクションを追加

GPTとGoogleカレンダーの連携設定

【GPT】OAuth認証の設定

上記の続き, アクションの追加画面上部の「認証」を選択する. 「OAuth」を選択.

OAuth設定画面

この画面を次のように埋める.

Google Cloudに戻り、 「APIとサービス」→「認証情報」→「OAuth2.0 クライアントID」から作ったものを選択. このページ★にはすぐ戻ってくるので、開けたままにする. クライアント IDとクライアントシークレットがそれぞれ表示されるので、コピペする.

APIとサービス→認証情報

【Google Cloud】OAuth認証の設定

ここまで終えたらmyGPTを保存したうえで、再度myGPTの設定を開く. 先程「アクションを追加」を押した場所に、アクションが設定されているはず. その直下にある「コールバックURL」をコピーする.

アクションの下にコールバックURL

開けっ放しにしておいたページ★(「APIとサービス」→「認証情報」→「OAuth2.0 クライアントID」)に戻ってきて、 「承認済みのリダイレクトURI」に先程の「コールバックURL」を貼り付ける.

最後に「APIとサービス」→「OAuth同意画面」→「対象」→「テストユーザー」を追加して、終了. このテストユーザーはカレンダーのユーザと一致しなくても良い.

テストユーザーの追加

あとはGPTのプロンプト

ここまでで連携部分はできたので、あとはGPTのプロンプトを各々で記載すればよい. 今回の作例は本記事下部に.

完成

1- ユーザーがPDFまたはドキュメントをアップロード。

2- 指定フォーマットで文章を作成し、ユーザーへ確認を求める。*2

4- 「会〆、本〆、大会当日のスケジュールをGoogleカレンダーに追加するための情報」をユーザーに提示。

6- 「会〆、本〆、大会当日のスケジュール」をGoogleカレンダーに追加。

OpenAPI スキーマ例

openapi: 3.1.0
info:
  title: 大会スケジュール管理API
  description: "大会の会〆、本〆、大会当日をGoogleカレンダーに登録するAPI"
  version: 1.0.0

servers:
  - url: "https://www.googleapis.com/calendar/v3"

paths:
  /calendars/{calendarId}/events:
    post:
      operationId: createEvent
      summary: "Googleカレンダーに大会情報を追加"
      description: "指定されたカレンダーに大会情報のイベントを登録"
      parameters:
        - name: calendarId
          in: path
          required: true
          schema:
            type: string
          description: "GoogleカレンダーのID(会〆、本〆、大会当日など任意のもの)"
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                summary:
                  type: string
                  description: "イベントのタイトル"
                start:
                  type: object
                  properties:
                    date:
                      type: string
                      format: date
                      example: "2025-03-07"
                end:
                  type: object
                  properties:
                    date:
                      type: string
                      format: date
                      example: "2025-03-07"
                location:
                  type: string
                  nullable: true
                  description: "開催場所(大会当日のみ)"
                description:
                  type: string
                  nullable: true
                  description: "会〆日(大会当日のみ)"
      responses:
        "200":
          description: "イベントが正常に追加された"
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: "イベントID"
        "400":
          description: "リクエストが不正"
        "401":
          description: "認証エラー"
        "500":
          description: "サーバーエラー"

components:
  schemas: {}
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

プロンプト例

このGPTは、ユーザーがアップロードしたPDFファイルから情報を抽出し、特に大会やイベントに関連する情報を指定されたフォーマットに従って整形します。

ワークフロー:
①ユーザーがPDFまたはドキュメントをアップロード。
②「昇級対象」かどうかを尋ねる(それ以外の質問をせず、ただ尋ねる)。
③ユーザーがYes/Noを送信。
④「大会情報フォルダのアドレス」を尋ねる(それ以外の質問をせず、ただ尋ねる)。
⑤ユーザーが「大会情報フォルダのアドレス」を送信。
⑥指定フォーマットで文章を作成し、ユーザーへ確認を求める。
⑦ユーザーがOKあるいは修正点を送信。
⑧「会〆、本〆、大会当日のスケジュールをGoogleカレンダーに追加するための情報」をユーザーに提示。
⑨ユーザーが確認し、OKを送信。
⑩「会〆、本〆、大会当日のスケジュール」をGoogleカレンダーに追加(OpenAPI Specificationを用いる)。

【指定されたフォーマット】
(フォーマットはここから)
----------------------------------------
■■[日付][大会名][級名]・[昇級対象か]■■
□□[計算された締切日]会〆□□

📍場所:[場所] [GoogleマップURL]

参加費:[金額]

📅締切日
会〆:[計算された締切日]
本〆:[申込締切日]

参加資格:[資格]

📁大会情報フォルダ:[ユーザーが提供したアドレス]

(フォーマットはここまで)

フォーマットの改行や順序は完全に遵守し、一切の変更を加えないこと。また、各項目の間に適切な改行を入れ、情報が見やすい形式で出力すること。
PDFに記載されている情報の正確性を優先すること。出力は指定フォーマットの結果のみを表示し、余分な説明や情報は含みません。

さらに、大会名は以下の規則で略記します:
・文字列「全国」「かるた」「競技かるた」「百人一首」「大会」および回数名は取り除く。

本〆は申込締切日を指し、大会開催日ではありません。この点を絶対に間違えないよう、最大限の注意を払います。また、会〆は必ず本〆日の直前の火曜日を計算して記載します。「開催日の直前の火曜日」ではなく、「本〆日の直前の火曜日」であることを厳密に確認します。

すべての日付(会〆日、本〆日、大会開催日)は"mm/dd(曜)"形式に統一し、括弧や中括弧は一切使用しません。
URLを出力する際にはリンク文字列のみを出力し、Markdown形式([title](link))は使用しません。

情報は各「級」ごとに分類して整理され、指定フォーマットに従って級ごとにまとめられます。級については、アルファベット1字で表現できる場合は、アルファベット1字のみで表現してください。「初心者級」のように表現できない場合は、記載の通り出力します。この指示を厳格に守り、各級の情報を漏れなく正確に分類します。

場所情報にはGoogleマップURLを生成して追記します。

【Googleカレンダー追加時のフォーマット】(※summaryには空白文字を含めない)
- **会〆**
  - `summary` : "〆"+[級すべて]+"|"+[大会名]+[開催日]+[級すべて]
  - `start`, `end` : [会〆日](終日の予定)
  - `location` : 空欄
  - `description` : [大会情報フォルダのリンク]

- **本〆**
  - `summary` : "⏰"+[大会名]+[開催日]+[級すべて]
  - `start`, `end` : [本〆日の前日](終日の予定)
  - `location` : 空欄
  - `description` : 空欄

- **大会当日**
  - `summary` : [大会名]+[級すべて]
  - `start`, `end` : [開催日](終日の予定)
  - `location` : [場所]
  - `description` : "会〆日"+[会〆日]+"\n"+[大会情報フォルダのリンク]

【GoogleカレンダーID】
- 会〆: ***@group.calendar.google.com
- 本〆: ***@group.calendar.google.com
- 大会当日: ***@group.calendar.google.com

参考文献

カスタムGPTs完全ガイド:GoogleカレンダーAPIを実装した秘書GPTの作り方 | AIじゃないよ

*1:締切が2種類あるのは 「サークル内での締切→会〆」 「主催者側の締切→本〆」 がユースケース的に必要だから.本質的ではない.

*2:実は締切日をchatGPTが間違えていたため、修正の依頼を出しています