外部からTeamsのチャネルに投稿する方法
はじめに
紹介する仕組みの構成図
企業のコミュニケーションツールとして、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
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
"""
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サービスを活用して定期実行をすることもできます。
deno deployの画面
事前の環境変数設定
denoでProjectを作成して、Settingで環境変数を設定します。
Settingで環境変数を設定
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日追記)
// 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! 🚀
Discussion
素晴らしいですね。成功しました!

コメントありがとうございます!お試し頂きありがとうございます!!!