Agentive
AIエージェント活用

AIエージェントのリモートトリガー — スマホからPCのAIを起動する

約5分で読めます

外出先からスマホ一つでPCのAIエージェントを起動し、タスクを実行させる。Discord Webhookとファイル同期を組み合わせたリモートトリガーシステムにより、物理的にPCの前にいなくても自律的なAIワークフローを開始できる。本記事では、リモートトリガーの設計から実装、セキュリティ対策まで実践的に解説する。

リモートトリガーの全体アーキテクチャ

リモートトリガーシステムは、外部からの信号をPCが検知し、Claude Codeを自動起動する仕組みである。信号の伝達経路は複数のパターンが考えられる。

  1. Discord経由: スマホでDiscordにメッセージ送信 → Bot検知 → PC上のスクリプトが起動
  2. OneDrive経由: スマホでファイルを作成 → クラウド同期 → PC上のWatcherが検知
  3. GitHub経由: スマホでIssueを作成 → Webhookが発火 → PC上のリスナーが起動
// リモートトリガーの基本インターフェース
interface TriggerSignal {
  source: "discord" | "onedrive" | "github" | "webhook";
  timestamp: string;
  taskType: string;
  payload: Record<string, unknown>;
  priority: "low" | "normal" | "high" | "urgent";
  requester: string;
}

interface TriggerHandler {
  watch(): Promise<void>;
  onSignal(signal: TriggerSignal): Promise<void>;
  acknowledge(signalId: string): Promise<void>;
}

Discord Bot経由のトリガー実装

Discord Botを使ったリモートトリガーが最も手軽で信頼性が高い。スマホのDiscordアプリから特定のチャンネルにコマンドを送信するだけで、PCのAIエージェントが起動する。

import { Client, GatewayIntentBits, Message } from "discord.js";
import { execSync, spawn } from "child_process";

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

const TRIGGER_CHANNEL = "ai-trigger";
const ALLOWED_USERS = ["your-discord-user-id"];

client.on("messageCreate", async (message: Message) => {
  // セキュリティチェック
  if (message.channel.name !== TRIGGER_CHANNEL) return;
  if (!ALLOWED_USERS.includes(message.author.id)) return;
  if (message.author.bot) return;

  // コマンドパース
  const content = message.content.trim();
  if (!content.startsWith("!ai")) return;

  const task = content.slice(4).trim();
  await message.reply("タスクを受信しました: " + task);

  try {
    // Claude Codeを非対話モードで実行
    const process = spawn("claude", ["-p", task], {
      cwd: "/path/to/project",
      stdio: "pipe",
    });

    let output = "";
    process.stdout.on("data", (data) => { output += data.toString(); });

    process.on("close", async (code) => {
      const summary = output.length > 1900
        ? output.slice(0, 1900) + "...(truncated)"
        : output;
      await message.reply("完了 (exit: " + code + ")\n```\n" + summary + "\n```");
    });
  } catch (error) {
    await message.reply("エラーが発生しました: " + String(error));
  }
});

client.login(process.env.DISCORD_BOT_TOKEN);

コマンド体系の設計

const COMMANDS: Record<string, {
  description: string;
  handler: (args: string) => Promise<string>;
}> = {
  "!ai run": {
    description: "Claude Codeでタスクを実行",
    handler: async (task) => {
      return execSync("claude -p " + JSON.stringify(task)).toString();
    },
  },
  "!ai status": {
    description: "現在の稼働状態を確認",
    handler: async () => {
      const status = JSON.parse(
        require("fs").readFileSync("runtime_status.json", "utf-8")
      );
      return "Status: " + status.state + "\nUptime: " + status.uptime;
    },
  },
  "!ai wake": {
    description: "スリープ状態のPCを起動",
    handler: async () => {
      return "Wake signal sent";
    },
  },
};

OneDrive/ファイル同期経由のトリガー

クラウドストレージのファイル同期を利用したトリガーは、Discord Botが動作していない場合のフォールバックとして有用である。

import { watch } from "fs";
import { readFile, unlink } from "fs/promises";
import { join } from "path";

const TRIGGER_DIR = "C:/Users/user/OneDrive/ai-triggers";
const SIGNAL_FILE = "wakeup_signal.json";

async function watchForTriggers(): Promise<void> {
  console.log("Watching for triggers in:", TRIGGER_DIR);

  watch(TRIGGER_DIR, async (eventType, filename) => {
    if (filename !== SIGNAL_FILE || eventType !== "change") return;

    const filePath = join(TRIGGER_DIR, SIGNAL_FILE);

    try {
      const content = await readFile(filePath, "utf-8");
      const signal: TriggerSignal = JSON.parse(content);

      console.log("Trigger received:", signal.taskType);

      // シグナルファイルを削除(処理済みマーク)
      await unlink(filePath);

      // タスクを実行
      await executeTask(signal);
    } catch (error) {
      console.error("Trigger processing failed:", error);
    }
  });
}

シグナルファイルのフォーマット

スマホからOneDriveに保存するシグナルファイルの形式を定義する。

{
  "source": "onedrive",
  "timestamp": "2026-04-10T14:30:00Z",
  "taskType": "code-review",
  "payload": {
    "repository": "my-project",
    "branch": "feature/new-api",
    "instruction": "PRの全変更をレビューして"
  },
  "priority": "normal",
  "requester": "haruk"
}

セキュリティ対策

リモートトリガーはセキュリティリスクを伴うため、以下の対策を必ず実装する。

認証と認可

interface SecurityConfig {
  allowedUsers: string[];
  requireToken: boolean;
  maxRequestsPerHour: number;
  allowedTaskTypes: string[];
  blockedCommands: RegExp[];
}

const securityConfig: SecurityConfig = {
  allowedUsers: ["user-id-1"],
  requireToken: true,
  maxRequestsPerHour: 10,
  allowedTaskTypes: ["code-review", "test-run", "status-check"],
  blockedCommands: [
    /rm\s+-rf/,
    /sudo/,
    /curl.*\|.*sh/,
  ],
};

function validateTrigger(
  signal: TriggerSignal,
  config: SecurityConfig
): { valid: boolean; reason?: string } {
  if (!config.allowedUsers.includes(signal.requester)) {
    return { valid: false, reason: "Unauthorized user" };
  }

  if (!config.allowedTaskTypes.includes(signal.taskType)) {
    return { valid: false, reason: "Task type not allowed" };
  }

  return { valid: true };
}

レート制限

class RateLimiter {
  private requests: Map<string, number[]> = new Map();

  isAllowed(userId: string, maxPerHour: number): boolean {
    const now = Date.now();
    const hourAgo = now - 60 * 60 * 1000;

    const userRequests = this.requests.get(userId) || [];
    const recentRequests = userRequests.filter((t) => t > hourAgo);

    if (recentRequests.length >= maxPerHour) {
      return false;
    }

    recentRequests.push(now);
    this.requests.set(userId, recentRequests);
    return true;
  }
}

Watchdog統合

リモートトリガーとWatchdogパターンを組み合わせることで、エージェントの死活監視とリモート再起動を実現する。

interface WatchdogConfig {
  heartbeatIntervalMs: number;
  maxMissedHeartbeats: number;
  autoRestart: boolean;
  notifyOnRestart: boolean;
}

class AgentWatchdog {
  private lastHeartbeat: number = Date.now();
  private missedCount: number = 0;

  constructor(private config: WatchdogConfig) {
    setInterval(() => this.check(), config.heartbeatIntervalMs);
  }

  heartbeat(): void {
    this.lastHeartbeat = Date.now();
    this.missedCount = 0;
  }

  private async check(): Promise<void> {
    const elapsed = Date.now() - this.lastHeartbeat;
    if (elapsed > this.config.heartbeatIntervalMs * 1.5) {
      this.missedCount++;
      console.warn("Missed heartbeat:", this.missedCount);

      if (this.missedCount >= this.config.maxMissedHeartbeats) {
        if (this.config.autoRestart) {
          await this.restart();
        }
      }
    }
  }

  private async restart(): Promise<void> {
    console.log("Restarting agent...");
    // エージェント再起動ロジック
  }
}

トリガー方式の比較

方式レイテンシ信頼性セットアップコスト
Discord Bot1-3秒無料
OneDrive同期5-30秒無料
GitHub Webhook1-5秒無料
直接SSH即座最高VPN費用
Tailscale1-2秒無料〜

ベストプラクティス

  1. 複数経路を用意する: メイン経路(Discord)とフォールバック経路(OneDrive)を設ける
  2. 認証を必ず実装する: 誰でもトリガーできる状態は絶対に避ける
  3. タスクの範囲を制限する: リモートから実行できるタスクタイプを明確に定義する
  4. 実行結果を通知する: タスク完了時に同じ経路で結果を返す

関連記事

A

Agentive 編集部

AIエージェントを実際に使い倒す個人開発者。サイト制作の自動化を実践しながら、その知見を発信しています。