🐕

外部からTeamsのチャネルに投稿する方法

に公開
2

はじめに


紹介する仕組みの構成図

企業のコミュニケーションツールとして、SlackとMicrosoft Teamsは二大巨頭として君臨しています。Slackへの外部連携は比較的オープンで、多くの開発者がBot開発やAPI連携を活用していますが、Teamsはどうでしょうか?

実は、多くの企業(特に大企業)では、セキュリティポリシーによりTeamsへの外部連携が厳しく制限されています。アプリのインストールには管理者承認が必要だったり、そもそも外部アプリの利用が禁止されていたり...そんな経験はありませんか?

しかし、Incoming Webhookなら話は別です。これはTeamsの標準機能として提供されており、多くの企業で利用が許可されています。本記事では、このWebhookを活用して、最新のAI技術(OpenAI o4-mini)と組み合わせることで、毎朝自動でAIニュースをTeamsチャネルに投稿する仕組みを構築します。

TeamsのURL取得

投稿したいチャネルの「チャネルの管理」をクリックします。

「チャネルの管理」をクリック

「編集」ボタンをクリックします。

「編集」ボタンをクリック

「Incoming Webhook」の「Add」ボタンをクリックします。

「Incoming Webhook」の「Add」ボタンをクリック

「追加」ボタンをクリックします。

「追加」ボタンをクリック

テキストボックス「名称」を入力したうえで、「Create」ボタンをクリックします。

テキストボックス「名称」を入力したうえで、「Create」ボタンをクリック

URLが生成されるので控えておく。

「URL」を控える

OpenAI APIのAPIキー取得

下記記事の前半にOpenAI APIキーの取得方法が記載されています!
DifyでLLMを使えるようにする(OpenAI GPT-4o)

Teamsのチャネルに連携してHello Worldを投稿

下記Pythonを作りました。これは、Teamsの特定のチャネルに「Hello World」を投稿するものとなります。

Hello Worldを出力するPython
Hello Worldを出力するPython
import requests
import json

# Webhook URL
url = "取得したTeamsのURL"

# メッセージ(シンプルなテキスト形式を試す)
msg = {
    "text": "Hello world! 🌍"
}

# 送信
response = requests.post(url, json=msg)
print(f"ステータスコード: {response.status_code}")
print(f"レスポンス内容: {response.text}")

実行した結果、下記のとおりTeamsのチャネルに投稿されました!

Hello Worldが投稿された様子

o4-miniにWeb検索させてTeamsのチャネルに投稿

端末上のPythonで手動実行

OpenAI o4-miniで最新のAI情報をWeb検索で収集して、GPT-4.1-miniで7000文字程度にサマリします。その上で、Teamsのチャネルへ投稿します。

事前の環境変数設定
事前の環境変数設定
$env:OPENAI_API_KEY="取得したOpenAIのAPIキー"
$env:TEAMS_WEBHOOK_URL="取得したTeamsのURL"
o4-miniにWeb検索させてTeamsのチャネルに投稿するPython
o4-miniにWeb検索させてTeamsのチャネルに投稿するPython
"""
AIニュースを収集・要約して Microsoft Teams に投稿するスクリプト
"""

from openai import OpenAI
import requests
from datetime import datetime
import os
import warnings

class AINewsSummarizer:
    """AI ニュースを収集・要約して Teams に投稿するクラス"""

    def __init__(self, openai_api_key: str, teams_webhook_url: str) -> None:
        self.openai_api_key = openai_api_key
        self.teams_webhook_url = teams_webhook_url
        self.client = OpenAI(api_key=openai_api_key)

        # ニュースソース
        self.news_sources = [
            "TechCrunch AI news",
            "VentureBeat AI",
            "The Verge artificial intelligence",
            "Wired AI",
            "MIT Technology Review AI",
            "Bloomberg Technology AI",
            "Ars Technica AI",
        ]

    # ------------------------------------------------------------------
    # 第1段階: Web検索を使用して詳細な AI ニュース分析を取得
    # ------------------------------------------------------------------
    def get_ai_news_analysis(self) -> str:
        search_prompt = (
            f"Search for the latest AI news from {', '.join(self.news_sources)} "
            "Search TechCrunch, VentureBeat, The Verge, Wired, MIT Technology Review, Bloomberg Technology, and Ars Technica each morning for AI news published in the last 24 hours. Select the 5 most recent and relevant stories. For each article: 1) verify the URL returns HTTP 200 before sharing; if not, discard and choose another, 2) provide the working link, 3) craft a Japanese headline using proper nouns and clear wording, 4) add concise background/context on why this news matters"
        )

        print("🔍 Web検索を使用してAIニュースを収集中...")
        try:
            response = self.client.responses.create(
                model="o4-mini",
                input=search_prompt,
                tools=[{"type": "web_search"}],
            )
            content = response.output_text
            print("✅ 第1段階完了 (Web検索使用)")
            return content
        except Exception as e:
            print(f"❌ 第1段階でエラー: {e}")
            return "記事の取得に失敗しました。"

    # ------------------------------------------------------------------
    # 第2段階: Teams 投稿用に要約
    # ------------------------------------------------------------------
    def summarize_for_teams(self, detailed_analysis: str) -> str:
        prompt = (
            "以下のAIニュース分析をTeamsチャネル投稿用に要約してください。\n\n"
            "要件:\n"
            "- 25KB以下(約7,000文字)に収める\n"
            "- テキストのみ(画像なし)\n"
            "- 5つのニュース全てを含めるが、簡潔に\n"
            "- IT専門家向けのプロフェッショナルなトーン\n"
            "- 読みやすいセクションと箇条書きでフォーマット\n"
            "- 根拠となるURLを含める\n"
            "- 各ニュースに番号を付ける(1️⃣, 2️⃣, 3️⃣, 4️⃣, 5️⃣)\n\n"
            f"元の分析:\n{detailed_analysis}"
        )

        try:
            response = self.client.chat.completions.create(
                model="gpt-4.1-mini",
                messages=[
                    {"role": "system", "content": "あなたはTeams投稿用の簡潔なメッセージを作成する専門家です。"},
                    {"role": "user", "content": prompt},
                ],
                max_tokens=4000,
                temperature=0.5,
            )
            summary = response.choices[0].message.content
            size_kb = len(summary.encode("utf-8")) / 1024
            print(f"✅ 第2段階完了 (サイズ: {size_kb:.2f}KB)")
            return self._further_summarize(summary) if size_kb > 25 else summary
        except Exception as e:
            print(f"❌ 第2段階でエラー: {e}")
            return "要約の生成に失敗しました。"

    def _further_summarize(self, text: str) -> str:
        max_chars = 6500
        return text[:max_chars] + "\n\n[注: サイズ制限のため一部省略]" if len(text) > max_chars else text

    # ------------------------------------------------------------------
    # Teams へ投稿
    # ------------------------------------------------------------------
    def send_to_teams(self, message: str) -> bool:
        current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M")
        title = f"🤖 AIニュースダイジェスト - {current_time}"

        card = {
            "@type": "MessageCard",
            "@context": "https://schema.org/extensions",
            "summary": title,
            "themeColor": "0078D7",
            "title": title,
            "text": message,
            "sections": [
                {
                    "activityTitle": "生成時刻",
                    "activitySubtitle": current_time,
                    "activityImage": "https://cdn-icons-png.flaticon.com/512/2693/2693507.png",
                }
            ],
        }

        try:
            resp = requests.post(
                self.teams_webhook_url,
                json=card,
                headers={"Content-Type": "application/json"},
                timeout=30,
            )
            if resp.status_code in (200, 202):
                print("✅ Teamsへの投稿が完了しました")
                return True
            print(f"❌ Teams投稿エラー: {resp.status_code}\n詳細: {resp.text}")
            return False
        except Exception as e:
            print(f"❌ Teams投稿中にエラー: {e}")
            return False

    # ------------------------------------------------------------------
    # メイン実行メソッド
    # ------------------------------------------------------------------
    def run(self):
        """メイン実行メソッド"""
        print("🚀 AIニュースサマライザーを開始します...")
        print("📌 Web検索機能を使用して最新のAIニュースを収集します")
        
        try:
            # 第1段階: Web検索を使用した詳細な分析
            print("\n📊 第1段階: Web検索でAIニュースを収集・分析中...")
            detailed_analysis = self.get_ai_news_analysis()
            
            # 第2段階: Teams用に要約
            print("\n📝 第2段階: Teams投稿用に要約中...")
            teams_summary = self.summarize_for_teams(detailed_analysis)
            
            # Teams投稿
            print("\n📤 Teamsへ投稿中...")
            success = self.send_to_teams(teams_summary)
            
            if success:
                print("\n✨ 処理が正常に完了しました!")
            else:
                print("\n⚠️ 処理は完了しましたが、Teams投稿に失敗しました")
                
            return success
            
        except Exception as e:
            print(f"\n❌ 予期しないエラー: {str(e)}")
            return False


# =====================================================================
# メイン実行部分
# =====================================================================
if __name__ == "__main__":
    # 環境変数から設定を読み込み(APIキーは必ず環境変数で管理)
    OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
    TEAMS_WEBHOOK_URL = os.getenv("TEAMS_WEBHOOK_URL")
    
    if not OPENAI_API_KEY:
        print("❌ エラー: OPENAI_API_KEY環境変数が設定されていません")
        print("以下のコマンドで設定してください:")
        print("Windows (PowerShell): $env:OPENAI_API_KEY='your-api-key'")
        print("Windows (CMD): set OPENAI_API_KEY=your-api-key")
        print("Mac/Linux: export OPENAI_API_KEY=your-api-key")
        exit(1)
    
    if not TEAMS_WEBHOOK_URL:
        print("❌ エラー: TEAMS_WEBHOOK_URL環境変数が設定されていません")
        print("以下のコマンドで設定してください:")
        print("Windows (PowerShell): $env:TEAMS_WEBHOOK_URL='your-webhook-url'")
        print("Windows (CMD): set TEAMS_WEBHOOK_URL=your-webhook-url")
        print("Mac/Linux: export TEAMS_WEBHOOK_URL=your-webhook-url")
        exit(1)
    
    # インスタンス作成と実行
    summarizer = AINewsSummarizer(OPENAI_API_KEY, TEAMS_WEBHOOK_URL)
    summarizer.run()

実行した結果、下記のとおりTeamsのチャネルに投稿されました!

最新のAI情報を投稿された様子

deno deploy上で定期実行

deno deployのようなCloudサービスを活用して定期実行をすることもできます。
https://dash.deno.com/


deno deployの画面

事前の環境変数設定
denoでProjectを作成して、Settingで環境変数を設定します。

Settingで環境変数を設定

details o4-miniにWeb検索させてTeamsのチャネルに投稿するTypeScript
details details o4-miniにWeb検索させてTeamsのチャネルに投稿するTypeScript
// deno run --allow-net --allow-env main.ts
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY") ?? "";
const TEAMS_WEBHOOK_URL = Deno.env.get("TEAMS_WEBHOOK_URL") ?? "";

/* ---------------------------------------------------------
 * ①-A: Responses API Helper (for Web Search)
 * ------------------------------------------------------- */
interface ResponsesApiOptions {
  model: string;
  input: string;
  tools: { type: "web_search" }[];
}
async function responsesApi(options: ResponsesApiOptions): Promise<string> {
  // タイムアウトコントローラーをセット
  const controller = new AbortController();
  // タイムアウトを3分に設定 (3 * 60 * 1000 = 180,000ミリ秒)
  const timeoutId = setTimeout(() => controller.abort(), 180000);

  try {
    console.log("Fetching response from OpenAI with a 3-minute timeout...");
    const res = await fetch("https://api.openai.com/v1/responses", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${OPENAI_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(options),
      signal: controller.signal, // タイムアウトをfetchに伝える
    });

    if (!res.ok) {
      throw new Error(`API request failed: ${await res.text()}`);
    }
    
    const data = await res.json();
    const messageObject = data.output.find((item: any) => item.type === 'message');
    const content = messageObject?.content?.[0]?.text;

    if (!content) {
      console.log("Full API Response (for debugging):");
      console.log(JSON.stringify(data, null, 2));
      throw new Error("Could not find a 'message' object with text content in the API response.");
    }

    return content as string;

  } catch (error) {
    // タイムアウトによって中断された場合のエラーを検知
    if (error.name === 'AbortError') {
      console.error("Fetch timed out after 30 minutes.");
      throw new Error("OpenAI API call timed out.");
    }
    // その他のエラーを再スロー
    throw error;
  } finally {
    // 処理が完了または失敗したら、必ずタイマーを解除する
    clearTimeout(timeoutId);
  }
}

/* ---------------------------------------------------------
 * ②-A: Chat Completions API Helper (for Summarization)
 * ------------------------------------------------------- */
interface ChatCompletionOptions {
  model: string;
  messages: unknown[];
  max_completion_tokens?: number;
  temperature?: number;
}
async function chatCompletion(options: ChatCompletionOptions): Promise<string> {
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(options),
  });
  if (!res.ok) throw new Error(await res.text());
  const data = await res.json();
  return data.choices[0].message.content as string;
}

/* ---------------------------------------------------------
 * ①-B: Web 検索+収集 (o4-mini via Responses API)
 * ------------------------------------------------------- */
async function getAiNewsAnalysis(): Promise<string> {
  const prompt =
    `Search TechCrunch, The Verge, Wired, and Bloomberg Technology for AI news published **in the last 24 hours**. Select the three most recent stories. For each: 1) make sure the URL returns HTTP 200, 2) output a working link, 3) craft a Japanese headline with proper nouns, 4) add concise background on why it matters.`;

  const analysis = await responsesApi({
    model: "o4-mini",
    input: prompt,
    tools: [{ type: "web_search" }],
  });
  return analysis;
}

/* ---------------------------------------------------------
 * ②-B: 要約 (gpt-4.1-mini via Chat Completions API)
 * ------------------------------------------------------- */
async function summarizeForTeams(analysis: string): Promise<string> {
  const userPrompt = `
以下のAIニュース分析を Teams 投稿用に 25KB 以内へ要約してください。
- 3本すべて含める
- プロフェッショナルなトーン
- セクション分けと箇条書き
- 根拠URLを残す
- 番号は 1️⃣~3️⃣

元の分析:
${analysis}`.trim();

  const summary = await chatCompletion({
    model: "gpt-4.1-mini",
    messages: [
      { role: "system", content: "あなたはTeams投稿メッセージを作成するアシスタントです。" },
      { role: "user", content: userPrompt },
    ],
    max_completion_tokens: 4096,
    temperature: 0.5,
  });

  return (new TextEncoder().encode(summary).length > 25 * 1024)
    ? summary.slice(0, 6500) + "\n\n[注: 25KB制限のため一部省略]"
    : summary;
}

/* ---------------------------------------------------------
 * ③ Teams 送信
 * ------------------------------------------------------- */
async function postToTeams(text: string) {
  const now = new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });
  const card = {
    "@type": "MessageCard",
    "@context": "https://schema.org/extensions",
    summary: "AIニュースダイジェスト",
    themeColor: "0078D7",
    title: `🤗 AIニュースダイジェスト - ${now}`,
    text,
    sections: [{ activityTitle: "生成時刻", activitySubtitle: now }],
  };

  const res = await fetch(TEAMS_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(card),
  });
  console.log("Teams status:", res.status);
  return res.ok;
}

/* ---------------------------------------------------------
 * ④ 1 サイクル実行
 * ------------------------------------------------------- */
async function runOnce() {
  console.log("=== AI News summarizer start ===", new Date());
  try {
    const analysis = await getAiNewsAnalysis();
    console.log("✅ Analysis fetched via /v1/responses");
    const summary = await summarizeForTeams(analysis);
    console.log("✅ Summary generated via /v1/chat/completions");
    await postToTeams(summary);
    console.log("✅ Posted to Teams");
  } catch(e) {
    console.error("Error in runOnce:", e);
  }
}

/* ---------------------------------------------------------
 * ⑤ スケジューリング & エンドポイント
 * ------------------------------------------------------- */
Deno.cron("ai-news-digest", "0 22 * * *", () => { // 毎朝 07:00 JST
    runOnce();
});

Deno.serve(async (req: Request) => {
  const url = new URL(req.url);
  if (url.pathname === "/run" && req.method === "POST") {
    if (!OPENAI_API_KEY || !TEAMS_WEBHOOK_URL) {
      return new Response("環境変数が設定されていません", { status: 500 });
    }
    runOnce();
    return new Response("実行を開始しました。結果はTeamsに投稿されます。", { status: 202 });
  }
  const html = `<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI News Digest</title><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:600px;margin:50px auto;padding:20px;text-align:center}h1{color:#0078D7}button{background:#0078D7;color:white;border:none;padding:12px 30px;font-size:16px;border-radius:5px;cursor:pointer;margin:20px 0}button:hover{background:#106ebe}button:disabled{background:#ccc;cursor:not-allowed}.status{margin:20px 0;padding:15px;background:#f0f0f0;border-radius:5px;font-size:14px;text-align:left}.result{margin:20px 0;color:#666}.loading{color:#0078D7}.success{color:#107c10}.error{color:#d83b01}</style></head><body><h1>🤖 AI News Digest</h1><div class="status"><p>⏰ 自動実行: 毎日朝7時(日本時間)</p><p>🔧 環境変数: ${OPENAI_API_KEY && TEAMS_WEBHOOK_URL ? '✅ 設定済み' : '❌ 未設定'}</p></div><button id="runBtn" onclick="runManually()" ${!OPENAI_API_KEY || !TEAMS_WEBHOOK_URL ? 'disabled' : ''}>今すぐ実行</button><div id="result" class="result"></div><script>async function runManually(){const btn=document.getElementById('runBtn');const result=document.getElementById('result');btn.disabled=true;result.className='result loading';result.textContent='実行中...';try{const res=await fetch('/run',{method:'POST'});if(res.ok){result.className='result success';result.textContent='✅ '+await res.text()}else{result.className='result error';result.textContent='❌ エラー: '+await res.text()}}catch(e){result.className='result error';result.textContent='❌ ネットワークエラー: '+e.message}finally{btn.disabled=false}}</script></body></html>`;
  return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
});

実行した結果、下記のとおりTeamsのチャネルに投稿されました!

最新のAI情報を投稿された様子

精度を向上させた版(2025年7月17日追記)
制度を向上させた版(2025年7月17日追記)
// deno run --allow-net --allow-env main.ts
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY") ?? "";
const TEAMS_WEBHOOK_URL = Deno.env.get("TEAMS_WEBHOOK_URL") ?? "";

/* ---------------------------------------------------------
 * ①-A: Responses API Helper (for Web Search)
 * ------------------------------------------------------- */
interface ResponsesApiOptions {
  model: string;
  input: string;
  tools: { type: "web_search" }[];
}
async function responsesApi(options: ResponsesApiOptions): Promise<string> {
  // タイムアウトコントローラーをセット
  const controller = new AbortController();
  // タイムアウトを3分に設定 (3 * 60 * 1000 = 180,000ミリ秒)
  const timeoutId = setTimeout(() => controller.abort(), 180000);

  try {
    console.log("Fetching response from OpenAI with a 3-minute timeout...");
    const res = await fetch("https://api.openai.com/v1/responses", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${OPENAI_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(options),
      signal: controller.signal, // タイムアウトをfetchに伝える
    });

    if (!res.ok) {
      throw new Error(`API request failed: ${await res.text()}`);
    }
    
    const data = await res.json();
    const messageObject = data.output.find((item: any) => item.type === 'message');
    const content = messageObject?.content?.[0]?.text;

    if (!content) {
      console.log("Full API Response (for debugging):");
      console.log(JSON.stringify(data, null, 2));
      throw new Error("Could not find a 'message' object with text content in the API response.");
    }

    return content as string;

  } catch (error) {
    // タイムアウトによって中断された場合のエラーを検知
    if (error.name === 'AbortError') {
      console.error("Fetch timed out after 30 minutes.");
      throw new Error("OpenAI API call timed out.");
    }
    // その他のエラーを再スロー
    throw error;
  } finally {
    // 処理が完了または失敗したら、必ずタイマーを解除する
    clearTimeout(timeoutId);
  }
}

/* ---------------------------------------------------------
 * ②-A: Chat Completions API Helper (for Summarization)
 * ------------------------------------------------------- */
interface ChatCompletionOptions {
  model: string;
  messages: unknown[];
  max_completion_tokens?: number;
  temperature?: number;
}
async function chatCompletion(options: ChatCompletionOptions): Promise<string> {
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(options),
  });
  if (!res.ok) throw new Error(await res.text());
  const data = await res.json();
  return data.choices[0].message.content as string;
}

/* ---------------------------------------------------------
 * ①-B: Web 検索+収集 (o4-mini via Responses API)
 * ------------------------------------------------------- */
async function getAiNewsAnalysis(): Promise<string> {
  const now = new Date();
  const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
  
  const fromDateTime = yesterday.toISOString();
  const toDateTime = now.toISOString();
  
  // 日本時間でもログ出力(運用しやすいように)
  console.log("News search time range:");
  console.log(`  UTC: ${fromDateTime} - ${toDateTime}`);
  console.log(`  JST: ${yesterday.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" })} - ${now.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" })}`);
  
  const prompt =
    `Search TechCrunch, The Verge, Wired, and Bloomberg Technology for AI news published after ${fromDateTime} and before ${toDateTime} (UTC). Select the five most recent stories within this 24-hour period. For each: 1) make sure the URL returns HTTP 200, 2) output a working link, 3) craft a Japanese headline with proper nouns, 4) add concise background on why it matters.`;

  const analysis = await responsesApi({
    model: "o4-mini",
    input: prompt,
    tools: [{ type: "web_search" }],
    reasoning: { effort: "medium" },
  });
  return analysis;
}

/* ---------------------------------------------------------
 * ②-B: 要約 (gpt-4.1-mini via Chat Completions API)
 * ------------------------------------------------------- */
async function summarizeForTeams(analysis: string): Promise<string> {
  const userPrompt = `
以下のAIニュース分析を Teams 投稿用に 25KB 以内へ要約してください。
- 5本すべて含める
- プロフェッショナルなトーン
- セクション分けと箇条書き
- 根拠URLを残す
- 番号は 1️⃣~5️⃣

元の分析:
${analysis}`.trim();

  const summary = await chatCompletion({
    model: "gpt-4.1-mini",
    messages: [
      { role: "system", content: "あなたはTeams投稿メッセージを作成するアシスタントです。" },
      { role: "user", content: userPrompt },
    ],
    max_completion_tokens: 4096,
    temperature: 0.5,
  });

  return (new TextEncoder().encode(summary).length > 25 * 1024)
    ? summary.slice(0, 6500) + "\n\n[注: 25KB制限のため一部省略]"
    : summary;
}

/* ---------------------------------------------------------
 * ③ Teams 送信
 * ------------------------------------------------------- */
async function postToTeams(text: string) {
  const now = new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });
  const card = {
    "@type": "MessageCard",
    "@context": "https://schema.org/extensions",
    summary: "AIニュースダイジェスト",
    themeColor: "0078D7",
    title: `🤗 AIニュースダイジェスト - ${now}`,
    text,
    sections: [{ activityTitle: "生成時刻", activitySubtitle: now }],
  };

  const res = await fetch(TEAMS_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(card),
  });
  console.log("Teams status:", res.status);

  return res.ok;
}

/* ---------------------------------------------------------
 * ④ 1 サイクル実行
 * ------------------------------------------------------- */
async function runOnce() {
  console.log("=== AI News summarizer start ===", new Date());
  try {
    const analysis = await getAiNewsAnalysis();
    console.log("✅ Analysis fetched via /v1/responses");
    const summary = await summarizeForTeams(analysis);
    console.log("✅ Summary generated via /v1/chat/completions");
    await postToTeams(summary);
    console.log("✅ Posted to Teams");
  } catch(e) {
    console.error("Error in runOnce:", e);
  }
}

/* ---------------------------------------------------------
 * ⑤ スケジューリング & エンドポイント
 * ------------------------------------------------------- */
Deno.cron("ai-news-digest", "33 23 * * *", () => { // 毎朝 08:33 JST
    runOnce();
});

Deno.serve(async (req: Request) => {
  const url = new URL(req.url);
  if (url.pathname === "/run" && req.method === "POST") {
    if (!OPENAI_API_KEY || !TEAMS_WEBHOOK_URL || !TEAMS_WEBHOOK_URL_2) {
      return new Response("環境変数が設定されていません", { status: 500 });
    }
    runOnce();
    return new Response("実行を開始しました。結果はTeamsに投稿されます。", { status: 202 });
  }
  const html = `<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI News Digest</title><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:600px;margin:50px auto;padding:20px;text-align:center}h1{color:#0078D7}button{background:#0078D7;color:white;border:none;padding:12px 30px;font-size:16px;border-radius:5px;cursor:pointer;margin:20px 0}button:hover{background:#106ebe}button:disabled{background:#ccc;cursor:not-allowed}.status{margin:20px 0;padding:15px;background:#f0f0f0;border-radius:5px;font-size:14px;text-align:left}.result{margin:20px 0;color:#666}.loading{color:#0078D7}.success{color:#107c10}.error{color:#d83b01}</style></head><body><h1>🤖 AI News Digest</h1><div class="status"><p>⏰ 自動実行: 毎日朝7時(日本時間)</p><p>🔧 環境変数: ${OPENAI_API_KEY && TEAMS_WEBHOOK_URL ? '✅ 設定済み' : '❌ 未設定'}</p></div><button id="runBtn" onclick="runManually()" ${!OPENAI_API_KEY || !TEAMS_WEBHOOK_URL ? 'disabled' : ''}>今すぐ実行</button><div id="result" class="result"></div><script>async function runManually(){const btn=document.getElementById('runBtn');const result=document.getElementById('result');btn.disabled=true;result.className='result loading';result.textContent='実行中...';try{const res=await fetch('/run',{method:'POST'});if(res.ok){result.className='result success';result.textContent='✅ '+await res.text()}else{result.className='result error';result.textContent='❌ エラー: '+await res.text()}}catch(e){result.className='result error';result.textContent='❌ ネットワークエラー: '+e.message}finally{btn.disabled=false}}</script></body></html>`;
  return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
});

おわりに

本記事では、Teams Webhookを活用したAIニュース自動投稿システムの構築方法を紹介しました。

🎯 この方法のメリット

  • 導入が簡単:管理者承認不要で、チャネル管理者なら誰でも設定可能
  • セキュリティ制約をクリア:多くの企業で利用可能な標準機能
  • 最新AI技術との融合:OpenAI o4-miniのWeb検索機能で常に最新情報を取得
  • 運用コストが低い:Deno Deployを使えば無料で定期実行も可能

⚠️ 制限事項と対策

  • 28KB制限:日本語で約7,000〜9,000文字が上限
    • 対策:GPT-4.1-miniで適切なサイズに要約
  • ワークフローアプリへの移行推奨:Microsoftの今後の方針
    • 対策:シンプルな通知用途に限定して利用

💡 活用アイデア

今回はAIニュースを例にしましたが、以下のような用途にも応用できます:

  • 日次の売上レポート自動投稿
  • システム監視アラート通知
  • 競合他社の動向分析レポート
  • 社内Wiki更新情報の配信

Teamsへの外部連携に苦労している方々にとって、この記事が一つの突破口となれば幸いです。まずはHello Worldから始めて、徐々に高度な活用へとステップアップしていきましょう!

Happy Teams Integration! 🚀

Accenture Japan (有志)

Discussion