2026年2月13日金曜日

サーバから Chrome ブラウザプッシュする実装の流れ

サーバから Chrome ブラウザプッシュする実装の流れ

概要

実装の流れを紹介します

環境

  • macOS 26.2
  • Python 3.12.11

鍵の作成

こちらを参考に作成します
鍵ファイルと JavaScript 用の鍵文字列を作成すれば OK です

サブスクリプション情報の保存 (Python)

JavaScrpt 側で通知の許可をした場合に通知先の情報をサーバ側に保存しておきます
今回は Redis で紹介しますが永続化できるものであれば何でも OK です

@app.route("/subscribe", methods=["POST"])
def subscribe():
    """フロントから送られてくる Subscription 情報を Redis に保存"""
    data = request.get_json()
    client_id = data.get("client_id")
    subscription_info = data.get("subscription")

    redis_conn = get_kv()
    # client_id ごとに購読情報を保存
    redis_conn.set(f"push_sub:{client_id}", json.dumps(subscription_info))
    return jsonify({"ok": True})

プッシュ送信処理 (Python)

あとで使いますが実際にプッシュを送信する処理です
メールアドレス部分はダミーでも問題ないですが一応ちゃんと設定した方がいいです (公開アプリの場合)

VAPID_PRIVATE_KEY_PATH = os.getenv("VAPID_PRIVATE_KEY_PATH", "./private_key.pem")
VAPID_CLAIMS = {"sub": "mailto:user@mail"}

def send_web_push(subscription_info, message_body):
    try:
        webpush(
            subscription_info=json.loads(subscription_info),
            data=json.dumps(message_body),
            vapid_private_key=VAPID_PRIVATE_KEY_PATH,
            vapid_claims=VAPID_CLAIMS,  # type: ignore
        )
    except WebPushException as ex:
        print("Push failed:", repr(ex))

ServiceWorker (JavaScript)

リモートプッシュはこの ServiceWorker がないと動きません
Python からのプッシュをこの ServiceWorker が受け取り通知を表示する感じです

通知のスタイルなどはシンプルにしているので必要であればアイコンなど設定してください

self.addEventListener("push", function (event) {
  console.log("Push received");
  let data = { title: "新着", body: "メッセージがあります" };

  try {
    if (event.data) {
      data = event.data.json(); // ここでエラーが起きると通知が出ない
    }
  } catch (e) {
    console.error("JSON parse error:", e);
  }

  const options = {
    body: data.body,
  };

  event.waitUntil(self.registration.showNotification(data.title, options));
});

ブラウザ側のサブスクリプション登録 (JavaScript)

ブラウザで特定のサイトなどにアクセスすると通知を許可しますかなど出るサイトがありますがそれを実装します
許可するをクリックしたら必要な情報を Python 側に送ります

urlBase64ToUint8Array に鍵文字列は冒頭で作成したものを使ってください

function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

async function setupPush() {
  const register = await navigator.serviceWorker.register("/static/js/sw.js");

  // すでに購読済みかチェック
  let subscription = await register.pushManager.getSubscription();

  // 未購読なら新規作成
  if (!subscription) {
    subscription = await register.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        "xxx",
      ),
    });
  }

  // サーバーへ送信(常に送ることで client_id との紐付けを確実に更新する)
  await fetch("/subscribe", {
    method: "POST",
    body: JSON.stringify({
      client_id: CLIENT_ID,
      subscription: subscription,
    }),
    headers: { "Content-Type": "application/json" },
  });
}

通知許可ダイアログ表示 (JavaScript)

ページが読み込まれた場合に自動で出してもいいですが嫌がられるケースもあるので特定のボタンを押したら許可する流れにします

document.addEventListener("DOMContentLoaded", () => {
  // Click title to set client_id and request notification permission
  const title = document.getElementById("notification");
  if (title) {
    title.addEventListener("click", async () => {
      // 通知の許可を求める
      if ("Notification" in window) {
        const permission = await Notification.requestPermission();
        if (permission === "granted") {
          // ここで実行!
          await setupPush();
        }
      }
    });
  }
  // 追加: すでに許可済みなら、ページ読み込み時に購読情報を同期する
  if ("Notification" in window && Notification.permission === "granted") {
    setupPush().catch((err) => console.error("Auto-setup failed:", err));
  }
});

プッシュ通知送信タイミング (Python)

あとは任意のタイミングでサーバ側で送信すれば OK です
当然ですが送信しまくると大変なことになるので条件など設けて適切なタイミングで送るようにしましょう

@app.route("/send", methods=["POST"])
def redis_chat_send():
    # 1... 既存の処理
    # 2. プッシュ通知の送信 (自分以外の購読者へ)
    try:
        # push_sub: で始まるキーをすべて取得
        # ※ ユーザー数が多い場合はメンバーリストを別途管理するのが理想ですが、
        # 現状のチャットルーム運用であれば keys で十分です
        sub_keys = redis_conn.keys("push_sub:*")

        for sub_key in sub_keys:  # type: ignore
            target_id = sub_key.decode().split(":")[-1]

            # 送信者(自分)以外にのみ送る
            if target_id != client_id:
                sub_info = redis_conn.get(sub_key)
                if sub_info:
                    logger.info(f"Sending push to {target_id}")
                    # 非同期処理にするのが理想ですが、まずは直列で実装
                    send_web_push(
                        sub_info,
                        {"title": "新着メッセージ", "body": f"{client_id}: {msg}"},
                    )
    except Exception as e:
        logger.error(f"Push notification error: {e}")

    return jsonify({"ok": True})

サブスクリプション確認

  • Redis 側にキーが登録されていること
  • 開発者コンソールで ServiceWorker が登録されていること

プッシュが届かない場合

  • Mac であれば通知の設定を確認しましょう
    • Mac の Chrome はデフォルトでオフになっている
    • Chrome で許可していても Mac 側で拒否していることが多い
  • サイトが https 化されていない
    • localhost ならテストできるようです
    • IP ベースのサイトや http のサイトでは動作しません

スマホ or タブレット版でプッシュ通知が届かない場合

  • ラップトップ版と違いかなり作りが違う
  • 単純に通知を許可することができない
  • iOS 側で当然許可する必要があるがそれでもサブスクリプションできないことがある
  • サイトを PWA 化して上げる必要などがあるかも

最後に

参考サイト

2026年2月12日木曜日

Windows のルーティング情報を可視化する Powershell

Windows のルーティング情報を可視化する Powershell

概要

タイトル通り

環境

  • Windows 11

スクリプト

# Visualize-Routes.ps1 (vis.jsを使用したHTMLベースの可視化)

param (
    [string]$OutputPath = "$($PSScriptRoot)\network_routes.html",
    [switch]$IncludeDefaultRoutes = $false,
    [string[]]$ExcludeNextHops = @()
)

function Visualize-NetRouteGraph {
    [CmdletBinding()]
    param (
        [string]$OutputPath = "$($PSScriptRoot)\network_routes.html",
        [switch]$IncludeDefaultRoutes = $false,
        [string[]]$ExcludeNextHops = @()
    )

    # ルーティング情報を取得
    $routes = Get-NetRoute
    
    # デフォルトルートを除外する場合
    if (-not $IncludeDefaultRoutes) {
        $routes = $routes | Where-Object { $_.NextHop -ne "::" -and $_.NextHop -ne "0.0.0.0" }
    }
    
    # 指定されたNextHopを除外する場合
    if ($ExcludeNextHops.Count -gt 0) {
        foreach ($excludeHop in $ExcludeNextHops) {
            $routes = $routes | Where-Object { $_.NextHop -ne $excludeHop }
        }
    }

    if (-not $routes) {
        Write-Warning "ルーティング情報が見つかりませんでした。"
        return
    }

    # ノードとエッジの定義
    $nodes = @{}
    $edges = @()
    $nodeId = 0
    $nodeMap = @{}
    $edgeMap = @{}  # エッジの重複を管理

    foreach ($route in $routes) {
        $destination = $route.DestinationPrefix
        $nextHop = $route.NextHop
        $ifIndex = $route.IfIndex
        $interface = $null
        
        # ネットワークアダプタ名を取得(エラーハンドリング付き)
        try {
            $interface = (Get-NetAdapter -IfIndex $ifIndex -ErrorAction Stop).Name
        }
        catch {
            $interface = $null
        }

        # ノードの作成(重複なし)
        if (-not $nodeMap.ContainsKey($destination)) {
            $nodeMap[$destination] = @{
                id    = $nodeId
                label = $destination
                title = "Destination: $destination"
                color = "lightblue"
                shape = "box"
            }
            $nodeId++
        }

        if (-not $nodeMap.ContainsKey($nextHop)) {
            $nodeMap[$nextHop] = @{
                id    = $nodeId
                label = $nextHop
                title = "NextHop: $nextHop"
                color = "lightgreen"
                shape = "ellipse"
            }
            $nodeId++
        }

        # エッジのキーを作成(重複チェック用)
        # NextHop → Destination の向きにすることで、トラフィックフローを表現
        $edgeKey = "$($nodeMap[$nextHop].id)->$($nodeMap[$destination].id)"
        
        # エッジラベルの作成
        $edgeInfo = "Metric: $($route.RouteMetric)"
        if ($interface) {
            $edgeInfo += " (via $interface)"
        }
        
        # 既存のエッジがあれば統合、なければ新規作成
        if ($edgeMap.ContainsKey($edgeKey)) {
            # 既存のエッジにラベルを追加
            $edgeMap[$edgeKey].labels += $edgeInfo
        }
        else {
            # 新規エッジを作成
            $edgeMap[$edgeKey] = @{
                from   = $nodeMap[$nextHop].id
                to     = $nodeMap[$destination].id
                labels = @($edgeInfo)
            }
        }
    }
    
    # エッジマップからエッジ配列を作成(ラベルを結合)
    foreach ($edgeKey in $edgeMap.Keys) {
        $edge = $edgeMap[$edgeKey]
        $edges += @{
            from  = $edge.from
            to    = $edge.to
            label = $edge.labels -join "`n"
        }
    }

    # JSON形式でノードとエッジを作成
    $nodesJson = $nodeMap.Values | ConvertTo-Json
    $edgesJson = $edges | ConvertTo-Json

    # HTMLテンプレートを生成
    $htmlContent = @"
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ネットワークルーティンググラフ</title>
    <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        html, body {
            width: 100%;
            height: 100%;
            font-family: Arial, sans-serif;
        }
        #network {
            width: 100%;
            height: 100%;
            border: 1px solid lightgray;
        }
        .controls {
            position: absolute;
            top: 10px;
            left: 10px;
            background: white;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            z-index: 100;
        }
        button {
            padding: 5px 10px;
            margin: 5px 2px;
            cursor: pointer;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <div class="controls">
        <button onclick="resetView()">リセット</button>
        <button onclick="fitToScreen()">画面に合わせる</button>
    </div>
    <div id="network"></div>

    <script type="text/javascript">
        var nodes = new vis.DataSet($nodesJson);
        var edges = new vis.DataSet($edgesJson);

        var container = document.getElementById('network');
        var data = {
            nodes: nodes,
            edges: edges
        };

        var options = {
            physics: {
                enabled: true,
                stabilization: {
                    iterations: 200
                },
                barnesHut: {
                    gravitationalConstant: -26000,
                    centralGravity: 0.3,
                    springLength: 300,
                    springConstant: 0.04,
                    damping: 0.3
                }
            },
            nodes: {
                font: {
                    size: 14,
                    color: 'black'
                },
                borderWidth: 2,
                shadow: true
            },
            edges: {
                arrows: 'to',
                font: {
                    size: 12,
                    align: 'middle'
                },
                shadow: true,
                smooth: {
                    type: 'continuous'
                }
            },
            interaction: {
                navigationButtons: true,
                keyboard: true,
                zoomView: true,
                dragView: true
            }
        };

        var network = new vis.Network(container, data, options);

        function resetView() {
            network.fit();
        }

        function fitToScreen() {
            network.fit({
                animation: {
                    duration: 1000,
                    easingFunction: 'easeInOutQuad'
                }
            });
        }

        window.addEventListener('resize', function() {
            network.redraw();
        });

        network.fit();
    </script>
</body>
</html>
"@

    # HTMLファイルを保存
    $htmlContent | Set-Content $OutputPath -Encoding UTF8

    Write-Host "ネットワークルーティンググラフをHTMLで生成しました: $OutputPath"
    Write-Host "ブラウザで表示しています..."

    # ブラウザで開く
    Invoke-Item $OutputPath
}

# 関数を実行(スクリプトの引数を渡す)
$params = @{
    OutputPath = $OutputPath
}
if ($IncludeDefaultRoutes) {
    $params['IncludeDefaultRoutes'] = $IncludeDefaultRoutes
}
if ($ExcludeNextHops.Count -gt 0) {
    $params['ExcludeNextHops'] = $ExcludeNextHops
}
Visualize-NetRouteGraph @params

最後に

作成された HTML をブラウザ開けば OK です

2026年2月11日水曜日

GAE 用のバケットやサービスアカウントを削除して deploy できなくなった場合の対処方法

GAE 用のバケットやサービスアカウントを削除して deploy できなくなった場合の対処方法

概要

間違って GAE 用のバケットやサービスアカウントを削除してしまった素直に repair しましょう

環境

  • gcloud 545.0.0

コマンド

  • gcloud beta app repair

最後に

ドメイン入りのバケットは手動では作成できません

参考サイト

2026年2月10日火曜日

Outlook の受信トレイにあるメールを検索する Powershell

Outlook の受信トレイにあるメールを検索する Powershell

概要

タイトル通り

環境

  • Windows 11

スクリプト

param(
    [string]$Keyword = "test",
    [switch]$SearchBody = $false
)

try {
    Write-Host "Connecting to Outlook..."

    $ErrorActionPreference = "Stop"

    $Outlook = New-Object -ComObject Outlook.Application
    $Namespace = $Outlook.GetNamespace("MAPI")
    $Namespace.Logon()

    $Inbox = $Namespace.GetDefaultFolder(6)

    Write-Host "Searching in $($Inbox.Name)"
    Write-Host "Keyword: $Keyword"
    Write-Host "Search Body: $SearchBody"

    $Items = $Inbox.Items
    Write-Host "Total items: $($Items.Count)"

    $FoundItems = @()
    $count = 0
    foreach ($Item in $Items) {
        $count++
        if ($count % 100 -eq 0) {
            Write-Host "Processing item $count..."
        }
        if ($SearchBody) {
            if ($Item.Subject -match $Keyword -or $Item.Body -match $Keyword) {
                $FoundItems += $Item
            }
        }
        else {
            if ($Item.Subject -match $Keyword) {
                $FoundItems += $Item
            }
        }
    }

    Write-Host "Found: $($FoundItems.Count) emails"

    if ($FoundItems.Count -gt 0) {
        foreach ($MailItem in $FoundItems) {
            Write-Host "-----"
            Write-Host "Subject: $($MailItem.Subject)"
            Write-Host "Sender: $($MailItem.SenderName)"
            Write-Host "Received: $($MailItem.ReceivedTime)"
        }
    }
    else {
        Write-Host "No emails found."
    }

    if ($Inbox) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Inbox) | Out-Null }
    if ($Namespace) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Namespace) | Out-Null }
    if ($Outlook) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Outlook) | Out-Null }

    Write-Host "Done."
}
catch {
    Write-Host "Error: $($_.Exception.Message)"
}

最後に

日本語で検索すると文字コードエラーになる場合があるのでその場合は検索キーワードを引数で受け取るようにしましょう
メールの件数が多いとかなり時間がかかるのでタイトルだけで検索するなどの工夫をしましょう
進捗がわかりづらい場合などがあるので 100件ずつ分割して検索してもいいと思います

2026年2月9日月曜日

外部の Redis にアクセスする場合はちゃんと pipeline を使おう

外部の Redis にアクセスする場合はちゃんと pipeline を使おう

概要

ループごとに GET するような処理はネットワークの負荷が重くなり結果としてアプリのレスポンス遅延にもつながるので可能な限り1回でほしい結果はすべて取得するようにしましょう

環境

  • Python 3.12.11
  • redis-py 7.1.0
  • redis 8.4.0

ダメなコード

def get_index_pairs(redis_client, hand_images: dict, ai_images: dict) -> list:
    profiles = get_profiles()
    pairs = []
    for num in sorted(
        set(hand_images.keys()) | set(ai_images.keys()),
        key=lambda x: int(x),
        reverse=True,
    ):
        hand_like_key = f"{num}_hand"
        ai_like_key = f"{num}_ai"
        hand_like = int(redis_client.get(hand_like_key) or 0)  # type: ignore
        ai_like = int(redis_client.get(ai_like_key) or 0)  # type: ignore
        pair = {
            "num": num,
            "hand": hand_images.get(num),
            "ai": ai_images.get(num),
            "hand_like": hand_like,
            "ai_like": ai_like,
            "name_hand": profiles.get(hand_like_key, {}).get("name", "名無し"),
            "character_hand": profiles.get(hand_like_key, {}).get(
                "character", "考え中"
            ),
            "name_ai": profiles.get(ai_like_key, {}).get("name", "名無し"),
            "character_ai": profiles.get(ai_like_key, {}).get("character", "考え中"),
            "winner": (
                "hand" if hand_like > ai_like else "ai" if ai_like > hand_like else None
            ),
        }
        pairs.append(pair)
    return pairs

良いコード

def get_index_pairs(redis_client, hand_images: dict, ai_images: dict) -> list:
    profiles = get_profiles()
    sorted_nums = sorted(
        set(hand_images.keys()) | set(ai_images.keys()),
        key=lambda x: int(x),
        reverse=True,
    )

    # --- Pipeline の開始 ---
    pipe = redis_client.pipeline()
    for num in sorted_nums:
        pipe.get(f"{num}_hand")
        pipe.get(f"{num}_ai")

    # 1回の通信で全データを取得
    raw_results = pipe.execute()
    # --- Pipeline の終了 ---

    pairs = []
    # 取得した結果を 2 つずつ取り出す
    for i, num in enumerate(sorted_nums):
        hand_like_bytes = raw_results[i * 2]
        ai_like_bytes = raw_results[i * 2 + 1]

        # decode_responses=False の場合を考慮
        hand_like = int(hand_like_bytes.decode() if hand_like_bytes else 0)
        ai_like = int(ai_like_bytes.decode() if ai_like_bytes else 0)

        pair = {
            "num": num,
            "hand": hand_images.get(num),
            "ai": ai_images.get(num),
            "hand_like": hand_like,
            "ai_like": ai_like,
            "name_hand": profiles.get(f"{num}_hand", {}).get("name", "名無し"),
            "character_hand": profiles.get(f"{num}_hand", {}).get(
                "character", "考え中"
            ),
            "name_ai": profiles.get(f"{num}_ai", {}).get("name", "名無し"),
            "character_ai": profiles.get(f"{num}_ai", {}).get("character", "考え中"),
            "winner": (
                "hand" if hand_like > ai_like else "ai" if ai_like > hand_like else None
            ),
        }
        pairs.append(pair)
    return pairs

比較

ネットワーク・レイテンシの削減

これが最も大きな改善点です。

ダメなコード(逐次処理): ループの中で毎回 redis_client.get() を呼び出しています。もし num が100個あれば、200回の通信(リクエストとレスポンスの往復)が発生します。

良いコード(パイプライン): すべての get コマンドを一旦溜めてから、pipe.execute() で 1回だけ Redisサーバーに送ります。通信回数が1回(往復1回)で済むため、ネットワークの遅延(レイテンシ)の影響を最小限に抑えられます。

スループット(処理能力)の向上

Redisサーバー側の負荷も軽減されます。

ダメなコード: サーバーは「リクエスト受信 → 解析 → 実行 → レスポンス送信」というサイクルを200回繰り返します。

良いコード: サーバーは「大きなリクエストを1つ受信 → まとめて実行 → 大きなレスポンスを1つ送信」という挙動になります。これにより、コンテキストスイッチやパケット処理のオーバーヘッドが減り、全体のスループットが向上します。

型の安全性と堅牢な実装

細かい部分ですが、データの扱いも丁寧になっています。

デコード処理: hand_like_bytes.decode() if hand_like_bytes else 0 のように、Redisから返ってくる値が None(キーが存在しない)場合や、バイト列(bytes型)である可能性を考慮して明示的にデコードしています。

マジックナンバーの排除: i * 2 と i * 2 + 1 を使ってパイプラインの結果から正しくデータを取り出しており、インデックス管理が明確です。

最後に

開発初期ではデータが少なく問題にならないですがデータが増えてくるとアプリの挙動がおかしくなるので注意しましょう

gunicorn などを使っていると Redis の応答よりもワーカーが先に死亡し以下のようなエラーになります

Traceback (most recent call last):
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 142, in handle
    self.handle_request(listener, req, client, addr)
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 185, in handle_request
    respiter = self.wsgi(environ, resp.start_response)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 1536, in __call__
    return self.wsgi_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/werkzeug/middleware/proxy_fix.py", line 183, in __call__
    return self.app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/app.py", line 162, in index
    pairs = get_pairs()
            ^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/app.py", line 143, in get_pairs
    pairs = get_index_pairs(redis_client, hand_images, ai_images)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/lib/pair.py", line 45, in get_index_pairs
    ai_like = int(redis_client.get(ai_like_key) or 0)  # type: ignore
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/commands/core.py", line 1923, in get
    return self.execute_command("GET", name, keys=[name])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 657, in execute_command
    return self._execute_command(*args, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 668, in _execute_command
    return conn.retry.call_with_retry(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/retry.py", line 116, in call_with_retry
    return do()
           ^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 669, in <lambda>
    lambda: self._send_command_parse_response(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 640, in _send_command_parse_response
    return self.parse_response(conn, command_name, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 691, in parse_response
    response = connection.read_response()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/connection.py", line 1133, in read_response
    response = self._parser.read_response(disable_decoding=disable_decoding)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/resp2.py", line 15, in read_response
    result = self._read_response(disable_decoding=disable_decoding)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/resp2.py", line 25, in _read_response
    raw = self._buffer.readline()
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/socket.py", line 115, in readline
    self._read_from_socket()
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/socket.py", line 65, in _read_from_socket
    data = self._sock.recv(socket_read_size)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/base.py", line 198, in handle_abort
    sys.exit(1)
SystemExit: 1

2026年2月8日日曜日

VSCode で Powershell をフォーマットする方法

VSCode で Powershell をフォーマットする方法

概要

VSCode を使うのが一番簡単のようです

環境

  • Windows11
  • VSCode 1.108.2

フォーマット

  • ps1 ファイルを開く
  • 右クリックから「Format Document」

リント

lint は CLI でできます

Invoke-ScriptAnalyzer -Path .\test.ps1

最後に

全部 CLI でやりたい

参考サイト

2026年2月7日土曜日

Slack で管理者権限のあるユーザトークンを取得する方法

Slack で管理者権限のあるユーザトークンを取得する方法

概要

ボットトークン (xoxb-) ではなくユーザトークン (xoxp-) を取得する方法を紹介します
ただし無料プランだと管理者権限のあるユーザトークンは設定できないので注意してください

環境

  • Slack 2026/02/04 時点

アプリ作成

権限設定

  • OAuth & Permissions
  • User Token Scopes
  • admin を追加

アプリ再インストール

  • OAuth & Permissions
  • OAuth Tokens
  • Reinstall to xxx

トークン確認

  • xoxp- から始まるトークンが発行されていることを確認します
  • すでにボットトークンがある場合はその上に User OAuth Token が発行されます

最後に

あとはトークンを使うだけ

参考サイト

2026年2月6日金曜日

GCP のクラウドストレージに上書きアップロードしてもオブジェクトが変更されない場合の対応方法

GCP のクラウドストレージに上書きアップロードしてもオブジェクトが変更されない場合の対応方法

概要

おそらくキャッシュされているのが問題です
特に発生しやすいのが「同じような画像を上書きアップロードした場合」で例えば元の画像を90度回転させた画像などは上書きアップロードしても反映されないことが多いです

そんな場合の反映方法を紹介します

環境

  • GCP 2026/02/04 時点

ブラウザのキャッシュを削除する

とりあえず削除して再度アクセスしてみましょう

クエリ付きでアクセスする

クラウドストレージ側でキャッシュしている可能性があるのでクエリ付きでアクセスしましょう

https://storage.googleapis.com/your_bucket_name/file.jpg?nocache=1

Cache-Control を確認し短くする

curl -I https://storage.googleapis.com/your_bucket_name/file.jpg

これで「Cache-Control:」「Age:」「X-Cache:」があればクラウドストレージ側のキャッシュが有効になっています

  • gsutil setmeta -h "Cache-Control:public,max-age=60" gs://your_bucket_name/file.jpg

60秒など短い時間にしましょう
0 だと毎回クラウドストレージにアクセスしネットワーク量が多くなり金額上がるので注意しましょう

別のファイル名にする

これが一番カンタンです
がアプリ側の改修などが必要になるケースがあるので注意しましょう

よくあるパターンはファイル名にハッシュなどを含めるケースです

ダメだったケース

  • 削除 -> 再度アップロード
    • なぜか元の画像がクラウドストレージ側でも利用されてしまう
  • 実はファイルが回転できていない
    • exif の Orientation にまだデータが残っている
    • ビューアの見た目だけが回転している

最後に

まずはキャッシュを疑いましょう

2026年2月5日木曜日

VSCode の git で認証ダイアログが定期的に出てくる場合の対処方法

VSCode の git で認証ダイアログが定期的に出てくる場合の対処方法

概要

タイトルの通りなぜか定期的に出てくるので抑制します

環境

  • Windows 11
  • VScode 1.108.2

方法

  • git config --global credential.helper cache

最後に

ただしこれでも push や pull 時には認証情報が聞かれるので注意してください
また Windows を再起動しても再度入力する必要があります

wincred とか使ってもなぜか毎回聞かれてしまいます

https の認証だとそうなるのかも

2026年2月4日水曜日

Windows 起動時に特定のページで Chrome を起動する方法

Windows 起動時に特定のページで Chrome を起動する方法

概要

ホームページにはしたくないが Windows 起動時に特定ページを開く方法を紹介します

環境

  • Windows11
  • Chrome 144.0.7559.110

shell:startup を開く

エクスプローラで「shell:startup」と入力し起動時に自動で開くフォルダを開きます

ショートカットの作成

  • 右クリック
  • 新規作成
  • ショートカット

ショートカットの設定

  • 項目の場所を入力してくださいに以下を入力
    • "C:\Program Files\Google\Chrome\Application\chrome.exe" "http://xxx.xxx.xxx.xxx"
  • 次へ
  • 適当なショートカットの名前を設定

最後に

ホームページを設定し Chrome のショートカットをスタートアップに追加してもいいですがホームページに設定すると毎回そのページからになるのでそれが嫌な場合は便利です

2026年2月3日火曜日

VSCode + Continue でエージェント開発をする

VSCode + Continue でエージェント開発をする

概要

前回 VSCode + copilot + ollama でローカル開発する方法を紹介しました
Ask モードしか使えないのが残念でした
今回は VSCode + continue + ollama で Agent 開発してみます

環境

  • M2 mac mini macOS 26.2
    • ollama 0.14.3
    • codegemma 7b
    • llama3.1 8b
  • VSCode 1.108.2

Continue のインストール

VSCode の拡張一覧から「continue」で検索しインストールすれば OK です

https://github.com/continuedev/continue

Continue の設定

  • 左メニューから Continue のアイコンを選択
  • 右上の歯車マークを選択
  • Configs
  • Local Config の歯車マークを選択
  • YAML を以下のように編集 (IP部分は適宜変更)
name: Local Config
version: 1.0.0
schema: v1
models:
  - name: codegemma
    provider: ollama
    model: codegemma:latest
    apiBase: http://192.168.1.65:11434
    roles:
      - chat
      - edit
      - apply
      - autocomplete

動作確認

あとは Continue のチャット画面に移動しチャットすれば OK です
「Agent」になっておりかつ先程設定したモデルが選択されていることを確認してからチャットしましょう

M2 mac mini 上では特にストレスなくレスポンスが返ってきました

トラブルシューティング: Agent モードが使えない

「Agent might not work well with this model」という警告が出ている場合は Agent モードが使えません

codegemma は tools に対応しているモデルではないようです
https://ollama.com/library/codegemma に「Tools」というタグが付与されていないと Agent モードは使えません

なのでモデルを変更しましょう
ただ Tools が使えるモデルは比較的大きめなのでマシンスペックが必要になるので注意しましょう

公式のおすすめモデルは https://docs.continue.dev/ide-extensions/agent/model-setup です

  • ollama run llama3.2
name: Local Config
version: 1.0.0
schema: v1
models:
  - name: llama3.2
    provider: ollama
    model: llama3.2:latest
    apiBase: http://192.168.1.65:11434
    roles:
      - chat
      - edit
      - apply
      - autocomplete
    capabilities: 
     - tool_use
     - image_input

最後に

コンテキストの渡し方 (現在選択中のファイルや別のファイル) を送信する方法などは copilot chat プラグインとは少し違うので注意しましょう

また ollama を使う場合エージェントモードが使えるモデルと使えないモデルがあり使えるモデルはそれなりの大きさのモデルが必要になるようです

gemma3 などは使えませんでした
この辺り最適なモデルの選択などは続けていきたいです

エージェントモードで動かすと回答の処理や Continue の挙動がおかしくなるのでその場合は素直に Chat モードを使いましょう

参考サイト