ms-teams-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Microsoft Teams Apps Skill

Microsoft Teams 应用技能

Load with: base.md
Purpose: Build AI-powered agents and apps for Microsoft Teams. Create conversational bots, message extensions, and intelligent assistants that integrate with LLMs like OpenAI and Claude.

加载方式:base.md
用途:为Microsoft Teams构建基于AI的代理和应用。创建对话机器人、消息扩展和智能助手,集成OpenAI和Claude等大语言模型(LLM)。

Architecture Overview

架构概述

┌─────────────────────────────────────────────────────────────────┐
│  TEAMS APP TYPES                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  1. AI AGENTS (Bots)                                            │
│     Conversational apps powered by LLMs                         │
│     Handle messages, commands, and actions                      │
│                                                                 │
│  2. MESSAGE EXTENSIONS                                          │
│     Search external systems, insert cards into messages         │
│     Action commands with modal dialogs                          │
│                                                                 │
│  3. TABS                                                        │
│     Embedded web applications inside Teams                      │
│     Personal, channel, or meeting tabs                          │
│                                                                 │
│  4. WEBHOOKS & CONNECTORS                                       │
│     Incoming: Post messages to channels                         │
│     Outgoing: Respond to @mentions                              │
├─────────────────────────────────────────────────────────────────┤
│  SDK LANDSCAPE (2025)                                           │
│  ─────────────────────────────────────────────────────────────  │
│  Teams SDK v2: Primary SDK for Teams-only apps                  │
│  M365 Agents SDK: Multi-channel (Teams, Outlook, Copilot)       │
│  Teams Toolkit: VS Code extension for development               │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  TEAMS APP TYPES                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  1. AI AGENTS (Bots)                                            │
│     Conversational apps powered by LLMs                         │
│     Handle messages, commands, and actions                      │
│                                                                 │
│  2. MESSAGE EXTENSIONS                                          │
│     Search external systems, insert cards into messages         │
│     Action commands with modal dialogs                          │
│                                                                 │
│  3. TABS                                                        │
│     Embedded web applications inside Teams                      │
│     Personal, channel, or meeting tabs                          │
│                                                                 │
│  4. WEBHOOKS & CONNECTORS                                       │
│     Incoming: Post messages to channels                         │
│     Outgoing: Respond to @mentions                              │
├─────────────────────────────────────────────────────────────────┤
│  SDK LANDSCAPE (2025)                                           │
│  ─────────────────────────────────────────────────────────────  │
│  Teams SDK v2: Primary SDK for Teams-only apps                  │
│  M365 Agents SDK: Multi-channel (Teams, Outlook, Copilot)       │
│  Teams Toolkit: VS Code extension for development               │
└─────────────────────────────────────────────────────────────────┘

Quick Start

快速开始

Install Teams CLI

安装Teams CLI

bash
npm install -g @microsoft/teams.cli
bash
npm install -g @microsoft/teams.cli

Create New Project

创建新项目

bash
undefined
bash
undefined

TypeScript (Recommended)

TypeScript (推荐)

npx @microsoft/teams.cli new typescript my-agent --template echo
npx @microsoft/teams.cli new typescript my-agent --template echo

Python

Python

npx @microsoft/teams.cli new python my-agent --template echo
npx @microsoft/teams.cli new python my-agent --template echo

C#

C#

npx @microsoft/teams.cli new csharp my-agent --template echo
undefined
npx @microsoft/teams.cli new csharp my-agent --template echo
undefined

Project Structure

项目结构

my-agent/
├── src/
│   ├── index.ts              # Entry point
│   ├── app.ts                # App configuration
│   └── handlers/
│       ├── message.ts        # Message handlers
│       └── commands.ts       # Command handlers
├── appPackage/
│   ├── manifest.json         # App manifest
│   ├── color.png             # App icon (192x192)
│   └── outline.png           # Outline icon (32x32)
├── .env                      # Environment variables
├── teamsapp.yml              # Teams Toolkit config
└── package.json

my-agent/
├── src/
│   ├── index.ts              # Entry point
│   ├── app.ts                # App configuration
│   └── handlers/
│       ├── message.ts        # Message handlers
│       └── commands.ts       # Command handlers
├── appPackage/
│   ├── manifest.json         # App manifest
│   ├── color.png             # App icon (192x192)
│   └── outline.png           # Outline icon (32x32)
├── .env                      # Environment variables
├── teamsapp.yml              # Teams Toolkit config
└── package.json

App Manifest

应用清单

Basic Manifest Structure

基本清单结构

json
{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
  "manifestVersion": "1.17",
  "version": "1.0.0",
  "id": "{{APP_ID}}",
  "developer": {
    "name": "Your Company",
    "websiteUrl": "https://yourcompany.com",
    "privacyUrl": "https://yourcompany.com/privacy",
    "termsOfUseUrl": "https://yourcompany.com/terms"
  },
  "name": {
    "short": "AI Assistant",
    "full": "AI Assistant for Teams"
  },
  "description": {
    "short": "Your AI-powered assistant",
    "full": "An intelligent assistant that helps you with tasks using AI."
  },
  "icons": {
    "color": "color.png",
    "outline": "outline.png"
  },
  "accentColor": "#5558AF",
  "bots": [
    {
      "botId": "{{BOT_ID}}",
      "scopes": ["personal", "team", "groupChat"],
      "supportsFiles": false,
      "isNotificationOnly": false,
      "commandLists": [
        {
          "scopes": ["personal", "team", "groupChat"],
          "commands": [
            {
              "title": "help",
              "description": "Show available commands"
            },
            {
              "title": "ask",
              "description": "Ask the AI a question"
            }
          ]
        }
      ]
    }
  ],
  "permissions": ["identity", "messageTeamMembers"],
  "validDomains": ["*.azurewebsites.net"]
}
json
{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
  "manifestVersion": "1.17",
  "version": "1.0.0",
  "id": "{{APP_ID}}",
  "developer": {
    "name": "Your Company",
    "websiteUrl": "https://yourcompany.com",
    "privacyUrl": "https://yourcompany.com/privacy",
    "termsOfUseUrl": "https://yourcompany.com/terms"
  },
  "name": {
    "short": "AI Assistant",
    "full": "AI Assistant for Teams"
  },
  "description": {
    "short": "Your AI-powered assistant",
    "full": "An intelligent assistant that helps you with tasks using AI."
  },
  "icons": {
    "color": "color.png",
    "outline": "outline.png"
  },
  "accentColor": "#5558AF",
  "bots": [
    {
      "botId": "{{BOT_ID}}",
      "scopes": ["personal", "team", "groupChat"],
      "supportsFiles": false,
      "isNotificationOnly": false,
      "commandLists": [
        {
          "scopes": ["personal", "team", "groupChat"],
          "commands": [
            {
              "title": "help",
              "description": "Show available commands"
            },
            {
              "title": "ask",
              "description": "Ask the AI a question"
            }
          ]
        }
      ]
    }
  ],
  "permissions": ["identity", "messageTeamMembers"],
  "validDomains": ["*.azurewebsites.net"]
}

Manifest with Message Extensions

包含消息扩展的清单

json
{
  "composeExtensions": [
    {
      "botId": "{{BOT_ID}}",
      "commands": [
        {
          "id": "searchQuery",
          "type": "query",
          "title": "Search",
          "description": "Search for information",
          "initialRun": true,
          "parameters": [
            {
              "name": "query",
              "title": "Search query",
              "description": "Enter your search terms",
              "inputType": "text"
            }
          ]
        },
        {
          "id": "createTask",
          "type": "action",
          "title": "Create Task",
          "description": "Create a new task",
          "fetchTask": true,
          "context": ["compose", "commandBox", "message"]
        }
      ]
    }
  ]
}

json
{
  "composeExtensions": [
    {
      "botId": "{{BOT_ID}}",
      "commands": [
        {
          "id": "searchQuery",
          "type": "query",
          "title": "Search",
          "description": "Search for information",
          "initialRun": true,
          "parameters": [
            {
              "name": "query",
              "title": "Search query",
              "description": "Enter your search terms",
              "inputType": "text"
            }
          ]
        },
        {
          "id": "createTask",
          "type": "action",
          "title": "Create Task",
          "description": "Create a new task",
          "fetchTask": true,
          "context": ["compose", "commandBox", "message"]
        }
      ]
    }
  ]
}

AI Agent Development

AI代理开发

Basic Bot with Teams SDK v2

基于Teams SDK v2的基础机器人

typescript
// src/app.ts
import { App, HttpPlugin, DevtoolsPlugin } from '@microsoft/teams.ai';
import { OpenAIModel, ActionPlanner, PromptManager } from '@microsoft/teams.ai';

// Configure the AI model
const model = new OpenAIModel({
  azureApiKey: process.env.AZURE_OPENAI_API_KEY!,
  azureDefaultDeployment: process.env.AZURE_OPENAI_DEPLOYMENT!,
  azureEndpoint: process.env.AZURE_OPENAI_ENDPOINT!,
  // Or use OpenAI directly:
  // apiKey: process.env.OPENAI_API_KEY!,
  // defaultModel: 'gpt-4'
});

// Configure prompts
const prompts = new PromptManager({
  promptsFolder: './src/prompts'
});

// Create action planner
const planner = new ActionPlanner({
  model,
  prompts,
  defaultPrompt: 'chat'
});

// Create the app
const app = new App({
  plugins: [
    new HttpPlugin(),
    new DevtoolsPlugin()
  ],
  ai: {
    planner
  }
});

// Handle messages
app.on('message', async (context, state) => {
  // AI automatically handles the conversation
  // The planner uses the 'chat' prompt to generate responses
});

// Handle specific commands
app.message('/help', async (context, state) => {
  await context.sendActivity({
    type: 'message',
    text: 'Available commands:\n- /help - Show this message\n- /ask [question] - Ask me anything'
  });
});

// Start the app
app.start();
typescript
// src/app.ts
import { App, HttpPlugin, DevtoolsPlugin } from '@microsoft/teams.ai';
import { OpenAIModel, ActionPlanner, PromptManager } from '@microsoft/teams.ai';

// Configure the AI model
const model = new OpenAIModel({
  azureApiKey: process.env.AZURE_OPENAI_API_KEY!,
  azureDefaultDeployment: process.env.AZURE_OPENAI_DEPLOYMENT!,
  azureEndpoint: process.env.AZURE_OPENAI_ENDPOINT!,
  // Or use OpenAI directly:
  // apiKey: process.env.OPENAI_API_KEY!,
  // defaultModel: 'gpt-4'
});

// Configure prompts
const prompts = new PromptManager({
  promptsFolder: './src/prompts'
});

// Create action planner
const planner = new ActionPlanner({
  model,
  prompts,
  defaultPrompt: 'chat'
});

// Create the app
const app = new App({
  plugins: [
    new HttpPlugin(),
    new DevtoolsPlugin()
  ],
  ai: {
    planner
  }
});

// Handle messages
app.on('message', async (context, state) => {
  // AI automatically handles the conversation
  // The planner uses the 'chat' prompt to generate responses
});

// Handle specific commands
app.message('/help', async (context, state) => {
  await context.sendActivity({
    type: 'message',
    text: 'Available commands:\n- /help - Show this message\n- /ask [question] - Ask me anything'
  });
});

// Start the app
app.start();

Prompt Configuration

提示词配置

yaml
undefined
yaml
undefined

src/prompts/chat/config.json

src/prompts/chat/config.json

{ "schema": 1.1, "description": "AI Assistant for Teams", "type": "completion", "completion": { "model": "gpt-4", "max_tokens": 1000, "temperature": 0.7, "top_p": 1 } }

```text
{ "schema": 1.1, "description": "AI Assistant for Teams", "type": "completion", "completion": { "model": "gpt-4", "max_tokens": 1000, "temperature": 0.7, "top_p": 1 } }

```text

src/prompts/chat/skprompt.txt

src/prompts/chat/skprompt.txt

You are an AI assistant for Microsoft Teams. You help users with their questions and tasks.
Current conversation: {{$history}}
User: {{$input}} Assistant:

---
You are an AI assistant for Microsoft Teams. You help users with their questions and tasks.
Current conversation: {{$history}}
User: {{$input}} Assistant:

---

Integrating Claude/Anthropic

集成Claude/Anthropic

Claude-Powered Teams Bot

基于Claude的Teams机器人

typescript
// src/claude-bot.ts
import { App, HttpPlugin } from '@microsoft/teams.ai';
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!
});

const app = new App({
  plugins: [new HttpPlugin()]
});

// Conversation history store
const conversations = new Map<string, Anthropic.MessageParam[]>();

app.on('message', async (context, state) => {
  const userId = context.activity.from.id;
  const userMessage = context.activity.text;

  // Get or initialize conversation history
  if (!conversations.has(userId)) {
    conversations.set(userId, []);
  }
  const history = conversations.get(userId)!;

  // Add user message to history
  history.push({ role: 'user', content: userMessage });

  // Show typing indicator
  await context.sendActivity({ type: 'typing' });

  try {
    // Call Claude API
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      system: `You are an AI assistant integrated into Microsoft Teams.
        Help users with their questions and tasks.
        Be concise and helpful. Use markdown formatting when appropriate.
        Current user: ${context.activity.from.name}`,
      messages: history
    });

    const assistantMessage = response.content[0].type === 'text'
      ? response.content[0].text
      : '';

    // Add assistant response to history
    history.push({ role: 'assistant', content: assistantMessage });

    // Keep history manageable (last 20 messages)
    if (history.length > 20) {
      history.splice(0, history.length - 20);
    }

    // Send response
    await context.sendActivity({
      type: 'message',
      text: assistantMessage
    });

  } catch (error) {
    console.error('Claude API error:', error);
    await context.sendActivity({
      type: 'message',
      text: 'Sorry, I encountered an error processing your request.'
    });
  }
});

// Clear conversation command
app.message('/clear', async (context, state) => {
  const userId = context.activity.from.id;
  conversations.delete(userId);
  await context.sendActivity('Conversation cleared. Starting fresh!');
});

app.start();
typescript
// src/claude-bot.ts
import { App, HttpPlugin } from '@microsoft/teams.ai';
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!
});

const app = new App({
  plugins: [new HttpPlugin()]
});

// Conversation history store
const conversations = new Map<string, Anthropic.MessageParam[]>();

app.on('message', async (context, state) => {
  const userId = context.activity.from.id;
  const userMessage = context.activity.text;

  // Get or initialize conversation history
  if (!conversations.has(userId)) {
    conversations.set(userId, []);
  }
  const history = conversations.get(userId)!;

  // Add user message to history
  history.push({ role: 'user', content: userMessage });

  // Show typing indicator
  await context.sendActivity({ type: 'typing' });

  try {
    // Call Claude API
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      system: `You are an AI assistant integrated into Microsoft Teams.
        Help users with their questions and tasks.
        Be concise and helpful. Use markdown formatting when appropriate.
        Current user: ${context.activity.from.name}`,
      messages: history
    });

    const assistantMessage = response.content[0].type === 'text'
      ? response.content[0].text
      : '';

    // Add assistant response to history
    history.push({ role: 'assistant', content: assistantMessage });

    // Keep history manageable (last 20 messages)
    if (history.length > 20) {
      history.splice(0, history.length - 20);
    }

    // Send response
    await context.sendActivity({
      type: 'message',
      text: assistantMessage
    });

  } catch (error) {
    console.error('Claude API error:', error);
    await context.sendActivity({
      type: 'message',
      text: 'Sorry, I encountered an error processing your request.'
    });
  }
});

// Clear conversation command
app.message('/clear', async (context, state) => {
  const userId = context.activity.from.id;
  conversations.delete(userId);
  await context.sendActivity('Conversation cleared. Starting fresh!');
});

app.start();

Claude with Tools/Function Calling

带工具/函数调用的Claude

typescript
// src/claude-agent.ts
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic();

// Define tools the agent can use
const tools: Anthropic.Tool[] = [
  {
    name: 'search_knowledge_base',
    description: 'Search the company knowledge base for information',
    input_schema: {
      type: 'object' as const,
      properties: {
        query: {
          type: 'string',
          description: 'The search query'
        }
      },
      required: ['query']
    }
  },
  {
    name: 'create_task',
    description: 'Create a new task in the task management system',
    input_schema: {
      type: 'object' as const,
      properties: {
        title: { type: 'string', description: 'Task title' },
        description: { type: 'string', description: 'Task description' },
        assignee: { type: 'string', description: 'Person to assign the task to' },
        due_date: { type: 'string', description: 'Due date in YYYY-MM-DD format' }
      },
      required: ['title']
    }
  },
  {
    name: 'get_calendar',
    description: 'Get calendar events for a user',
    input_schema: {
      type: 'object' as const,
      properties: {
        user: { type: 'string', description: 'User email or name' },
        days: { type: 'number', description: 'Number of days to look ahead' }
      },
      required: ['user']
    }
  }
];

// Tool implementations
async function executeTools(toolName: string, toolInput: any): Promise<string> {
  switch (toolName) {
    case 'search_knowledge_base':
      // Implement your search logic
      return `Found 3 results for "${toolInput.query}":\n1. Document A\n2. Document B\n3. Document C`;

    case 'create_task':
      // Implement task creation (e.g., call Microsoft Graph API)
      return `Task created: "${toolInput.title}"`;

    case 'get_calendar':
      // Implement calendar lookup
      return `Calendar for ${toolInput.user}: 2 meetings today`;

    default:
      return 'Unknown tool';
  }
}

// Agent loop with tool use
async function runAgent(userMessage: string): Promise<string> {
  let messages: Anthropic.MessageParam[] = [
    { role: 'user', content: userMessage }
  ];

  while (true) {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      system: 'You are a helpful Teams assistant. Use tools when needed to help users.',
      tools,
      messages
    });

    // Check if we need to use tools
    if (response.stop_reason === 'tool_use') {
      const toolResults: Anthropic.MessageParam[] = [];

      for (const content of response.content) {
        if (content.type === 'tool_use') {
          const result = await executeTools(content.name, content.input);
          toolResults.push({
            role: 'user',
            content: [{
              type: 'tool_result',
              tool_use_id: content.id,
              content: result
            }]
          });
        }
      }

      messages.push({ role: 'assistant', content: response.content });
      messages.push(...toolResults);
      continue;
    }

    // Return final text response
    const textContent = response.content.find(c => c.type === 'text');
    return textContent?.text || 'No response';
  }
}

typescript
// src/claude-agent.ts
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic();

// Define tools the agent can use
const tools: Anthropic.Tool[] = [
  {
    name: 'search_knowledge_base',
    description: 'Search the company knowledge base for information',
    input_schema: {
      type: 'object' as const,
      properties: {
        query: {
          type: 'string',
          description: 'The search query'
        }
      },
      required: ['query']
    }
  },
  {
    name: 'create_task',
    description: 'Create a new task in the task management system',
    input_schema: {
      type: 'object' as const,
      properties: {
        title: { type: 'string', description: 'Task title' },
        description: { type: 'string', description: 'Task description' },
        assignee: { type: 'string', description: 'Person to assign the task to' },
        due_date: { type: 'string', description: 'Due date in YYYY-MM-DD format' }
      },
      required: ['title']
    }
  },
  {
    name: 'get_calendar',
    description: 'Get calendar events for a user',
    input_schema: {
      type: 'object' as const,
      properties: {
        user: { type: 'string', description: 'User email or name' },
        days: { type: 'number', description: 'Number of days to look ahead' }
      },
      required: ['user']
    }
  }
];

// Tool implementations
async function executeTools(toolName: string, toolInput: any): Promise<string> {
  switch (toolName) {
    case 'search_knowledge_base':
      // Implement your search logic
      return `Found 3 results for "${toolInput.query}":\n1. Document A\n2. Document B\n3. Document C`;

    case 'create_task':
      // Implement task creation (e.g., call Microsoft Graph API)
      return `Task created: "${toolInput.title}"`;

    case 'get_calendar':
      // Implement calendar lookup
      return `Calendar for ${toolInput.user}: 2 meetings today`;

    default:
      return 'Unknown tool';
  }
}

// Agent loop with tool use
async function runAgent(userMessage: string): Promise<string> {
  let messages: Anthropic.MessageParam[] = [
    { role: 'user', content: userMessage }
  ];

  while (true) {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      system: 'You are a helpful Teams assistant. Use tools when needed to help users.',
      tools,
      messages
    });

    // Check if we need to use tools
    if (response.stop_reason === 'tool_use') {
      const toolResults: Anthropic.MessageParam[] = [];

      for (const content of response.content) {
        if (content.type === 'tool_use') {
          const result = await executeTools(content.name, content.input);
          toolResults.push({
            role: 'user',
            content: [{
              type: 'tool_result',
              tool_use_id: content.id,
              content: result
            }]
          });
        }
      }

      messages.push({ role: 'assistant', content: response.content });
      messages.push(...toolResults);
      continue;
    }

    // Return final text response
    const textContent = response.content.find(c => c.type === 'text');
    return textContent?.text || 'No response';
  }
}

Adaptive Cards

Adaptive Cards

Basic Adaptive Card

基础Adaptive Card

typescript
// src/cards/welcome-card.ts
import { CardFactory } from 'botbuilder';

export function createWelcomeCard(userName: string) {
  return CardFactory.adaptiveCard({
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'TextBlock',
        text: `Welcome, ${userName}!`,
        size: 'Large',
        weight: 'Bolder'
      },
      {
        type: 'TextBlock',
        text: 'I\'m your AI assistant. How can I help you today?',
        wrap: true
      },
      {
        type: 'ActionSet',
        actions: [
          {
            type: 'Action.Submit',
            title: 'Get Started',
            data: { action: 'getStarted' }
          },
          {
            type: 'Action.Submit',
            title: 'View Help',
            data: { action: 'help' }
          }
        ]
      }
    ]
  });
}
typescript
// src/cards/welcome-card.ts
import { CardFactory } from 'botbuilder';

export function createWelcomeCard(userName: string) {
  return CardFactory.adaptiveCard({
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'TextBlock',
        text: `Welcome, ${userName}!`,
        size: 'Large',
        weight: 'Bolder'
      },
      {
        type: 'TextBlock',
        text: 'I\'m your AI assistant. How can I help you today?',
        wrap: true
      },
      {
        type: 'ActionSet',
        actions: [
          {
            type: 'Action.Submit',
            title: 'Get Started',
            data: { action: 'getStarted' }
          },
          {
            type: 'Action.Submit',
            title: 'View Help',
            data: { action: 'help' }
          }
        ]
      }
    ]
  });
}

AI Response Card with Actions

带操作的AI响应卡片

typescript
// src/cards/ai-response-card.ts
export function createAIResponseCard(
  question: string,
  answer: string,
  sources?: string[]
) {
  return {
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'Container',
        style: 'emphasis',
        items: [
          {
            type: 'TextBlock',
            text: 'Your Question',
            size: 'Small',
            weight: 'Bolder'
          },
          {
            type: 'TextBlock',
            text: question,
            wrap: true
          }
        ]
      },
      {
        type: 'Container',
        items: [
          {
            type: 'TextBlock',
            text: 'AI Response',
            size: 'Small',
            weight: 'Bolder'
          },
          {
            type: 'TextBlock',
            text: answer,
            wrap: true
          }
        ]
      },
      ...(sources && sources.length > 0 ? [{
        type: 'Container',
        items: [
          {
            type: 'TextBlock',
            text: 'Sources',
            size: 'Small',
            weight: 'Bolder'
          },
          ...sources.map(source => ({
            type: 'TextBlock',
            text: `${source}`,
            size: 'Small'
          }))
        ]
      }] : [])
    ],
    actions: [
      {
        type: 'Action.Submit',
        title: '👍 Helpful',
        data: { action: 'feedback', value: 'positive' }
      },
      {
        type: 'Action.Submit',
        title: '👎 Not Helpful',
        data: { action: 'feedback', value: 'negative' }
      },
      {
        type: 'Action.Submit',
        title: 'Ask Follow-up',
        data: { action: 'followUp' }
      }
    ]
  };
}
typescript
// src/cards/ai-response-card.ts
export function createAIResponseCard(
  question: string,
  answer: string,
  sources?: string[]
) {
  return {
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'Container',
        style: 'emphasis',
        items: [
          {
            type: 'TextBlock',
            text: 'Your Question',
            size: 'Small',
            weight: 'Bolder'
          },
          {
            type: 'TextBlock',
            text: question,
            wrap: true
          }
        ]
      },
      {
        type: 'Container',
        items: [
          {
            type: 'TextBlock',
            text: 'AI Response',
            size: 'Small',
            weight: 'Bolder'
          },
          {
            type: 'TextBlock',
            text: answer,
            wrap: true
          }
        ]
      },
      ...(sources && sources.length > 0 ? [{
        type: 'Container',
        items: [
          {
            type: 'TextBlock',
            text: 'Sources',
            size: 'Small',
            weight: 'Bolder'
          },
          ...sources.map(source => ({
            type: 'TextBlock',
            text: `${source}`,
            size: 'Small'
          }))
        ]
      }] : [])
    ],
    actions: [
      {
        type: 'Action.Submit',
        title: '👍 Helpful',
        data: { action: 'feedback', value: 'positive' }
      },
      {
        type: 'Action.Submit',
        title: '👎 Not Helpful',
        data: { action: 'feedback', value: 'negative' }
      },
      {
        type: 'Action.Submit',
        title: 'Ask Follow-up',
        data: { action: 'followUp' }
      }
    ]
  };
}

Form Card for User Input

用户输入表单卡片

typescript
// src/cards/task-form-card.ts
export function createTaskFormCard() {
  return {
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'TextBlock',
        text: 'Create New Task',
        size: 'Large',
        weight: 'Bolder'
      },
      {
        type: 'Input.Text',
        id: 'taskTitle',
        label: 'Task Title',
        isRequired: true,
        placeholder: 'Enter task title'
      },
      {
        type: 'Input.Text',
        id: 'taskDescription',
        label: 'Description',
        isMultiline: true,
        placeholder: 'Enter task description'
      },
      {
        type: 'Input.ChoiceSet',
        id: 'priority',
        label: 'Priority',
        choices: [
          { title: 'High', value: 'high' },
          { title: 'Medium', value: 'medium' },
          { title: 'Low', value: 'low' }
        ],
        value: 'medium'
      },
      {
        type: 'Input.Date',
        id: 'dueDate',
        label: 'Due Date'
      }
    ],
    actions: [
      {
        type: 'Action.Submit',
        title: 'Create Task',
        data: { action: 'createTask' }
      },
      {
        type: 'Action.Submit',
        title: 'Cancel',
        data: { action: 'cancel' }
      }
    ]
  };
}

typescript
// src/cards/task-form-card.ts
export function createTaskFormCard() {
  return {
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'TextBlock',
        text: 'Create New Task',
        size: 'Large',
        weight: 'Bolder'
      },
      {
        type: 'Input.Text',
        id: 'taskTitle',
        label: 'Task Title',
        isRequired: true,
        placeholder: 'Enter task title'
      },
      {
        type: 'Input.Text',
        id: 'taskDescription',
        label: 'Description',
        isMultiline: true,
        placeholder: 'Enter task description'
      },
      {
        type: 'Input.ChoiceSet',
        id: 'priority',
        label: 'Priority',
        choices: [
          { title: 'High', value: 'high' },
          { title: 'Medium', value: 'medium' },
          { title: 'Low', value: 'low' }
        ],
        value: 'medium'
      },
      {
        type: 'Input.Date',
        id: 'dueDate',
        label: 'Due Date'
      }
    ],
    actions: [
      {
        type: 'Action.Submit',
        title: 'Create Task',
        data: { action: 'createTask' }
      },
      {
        type: 'Action.Submit',
        title: 'Cancel',
        data: { action: 'cancel' }
      }
    ]
  };
}

Microsoft Graph Integration

Microsoft Graph集成

Setup Graph Client

设置Graph客户端

typescript
// src/graph/client.ts
import { Client } from '@microsoft/microsoft-graph-client';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import { ClientSecretCredential } from '@azure/identity';

export function createGraphClient() {
  const credential = new ClientSecretCredential(
    process.env.AZURE_TENANT_ID!,
    process.env.AZURE_CLIENT_ID!,
    process.env.AZURE_CLIENT_SECRET!
  );

  const authProvider = new TokenCredentialAuthenticationProvider(credential, {
    scopes: ['https://graph.microsoft.com/.default']
  });

  return Client.initWithMiddleware({ authProvider });
}
typescript
// src/graph/client.ts
import { Client } from '@microsoft/microsoft-graph-client';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import { ClientSecretCredential } from '@azure/identity';

export function createGraphClient() {
  const credential = new ClientSecretCredential(
    process.env.AZURE_TENANT_ID!,
    process.env.AZURE_CLIENT_ID!,
    process.env.AZURE_CLIENT_SECRET!
  );

  const authProvider = new TokenCredentialAuthenticationProvider(credential, {
    scopes: ['https://graph.microsoft.com/.default']
  });

  return Client.initWithMiddleware({ authProvider });
}

Common Graph Operations

常见Graph操作

typescript
// src/graph/operations.ts
import { Client } from '@microsoft/microsoft-graph-client';

export class GraphOperations {
  constructor(private client: Client) {}

  // Get user profile
  async getUserProfile(userId: string) {
    return this.client.api(`/users/${userId}`).get();
  }

  // Get user's calendar events
  async getCalendarEvents(userId: string, days: number = 7) {
    const startDate = new Date().toISOString();
    const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();

    return this.client
      .api(`/users/${userId}/calendarView`)
      .query({
        startDateTime: startDate,
        endDateTime: endDate
      })
      .select('subject,start,end,location')
      .orderby('start/dateTime')
      .get();
  }

  // Send email
  async sendEmail(
    fromUserId: string,
    to: string,
    subject: string,
    body: string
  ) {
    return this.client.api(`/users/${fromUserId}/sendMail`).post({
      message: {
        subject,
        body: { contentType: 'HTML', content: body },
        toRecipients: [{ emailAddress: { address: to } }]
      }
    });
  }

  // Create Teams meeting
  async createMeeting(
    userId: string,
    subject: string,
    startTime: string,
    endTime: string,
    attendees: string[]
  ) {
    return this.client.api(`/users/${userId}/onlineMeetings`).post({
      subject,
      startDateTime: startTime,
      endDateTime: endTime,
      participants: {
        attendees: attendees.map(email => ({
          upn: email,
          role: 'attendee'
        }))
      }
    });
  }

  // Post message to channel
  async postToChannel(teamId: string, channelId: string, message: string) {
    return this.client
      .api(`/teams/${teamId}/channels/${channelId}/messages`)
      .post({
        body: { content: message }
      });
  }
}

typescript
// src/graph/operations.ts
import { Client } from '@microsoft/microsoft-graph-client';

export class GraphOperations {
  constructor(private client: Client) {}

  // Get user profile
  async getUserProfile(userId: string) {
    return this.client.api(`/users/${userId}`).get();
  }

  // Get user's calendar events
  async getCalendarEvents(userId: string, days: number = 7) {
    const startDate = new Date().toISOString();
    const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();

    return this.client
      .api(`/users/${userId}/calendarView`)
      .query({
        startDateTime: startDate,
        endDateTime: endDate
      })
      .select('subject,start,end,location')
      .orderby('start/dateTime')
      .get();
  }

  // Send email
  async sendEmail(
    fromUserId: string,
    to: string,
    subject: string,
    body: string
  ) {
    return this.client.api(`/users/${fromUserId}/sendMail`).post({
      message: {
        subject,
        body: { contentType: 'HTML', content: body },
        toRecipients: [{ emailAddress: { address: to } }]
      }
    });
  }

  // Create Teams meeting
  async createMeeting(
    userId: string,
    subject: string,
    startTime: string,
    endTime: string,
    attendees: string[]
  ) {
    return this.client.api(`/users/${userId}/onlineMeetings`).post({
      subject,
      startDateTime: startTime,
      endDateTime: endTime,
      participants: {
        attendees: attendees.map(email => ({
          upn: email,
          role: 'attendee'
        }))
      }
    });
  }

  // Post message to channel
  async postToChannel(teamId: string, channelId: string, message: string) {
    return this.client
      .api(`/teams/${teamId}/channels/${channelId}/messages`)
      .post({
        body: { content: message }
      });
  }
}

Authentication

身份验证

SSO with Teams SDK

使用Teams SDK实现SSO

typescript
// src/auth.ts
import { App } from '@microsoft/teams.ai';

const app = new App({
  // ... other config
});

app.on('message', async ({ userGraph, isSignedIn, send, signin }) => {
  // Check if user is signed in
  if (!isSignedIn) {
    // Initiate sign-in flow
    await signin();
    return;
  }

  // User is signed in, access Graph API
  const me = await userGraph.call({
    method: 'GET',
    path: '/me'
  });

  await send(`Hello, ${me.displayName}!`);
});
typescript
// src/auth.ts
import { App } from '@microsoft/teams.ai';

const app = new App({
  // ... other config
});

app.on('message', async ({ userGraph, isSignedIn, send, signin }) => {
  // Check if user is signed in
  if (!isSignedIn) {
    // Initiate sign-in flow
    await signin();
    return;
  }

  // User is signed in, access Graph API
  const me = await userGraph.call({
    method: 'GET',
    path: '/me'
  });

  await send(`Hello, ${me.displayName}!`);
});

Manual OAuth Flow

手动OAuth流程

typescript
// src/auth/oauth.ts
import { OAuthPrompt, OAuthPromptSettings } from 'botbuilder-dialogs';

const oauthSettings: OAuthPromptSettings = {
  connectionName: process.env.OAUTH_CONNECTION_NAME!,
  text: 'Please sign in to continue',
  title: 'Sign In',
  timeout: 300000 // 5 minutes
};

// In your dialog
async function handleAuth(context, state) {
  const tokenResponse = await context.adapter.getUserToken(
    context,
    oauthSettings.connectionName
  );

  if (!tokenResponse?.token) {
    // No token, show sign-in card
    await context.sendActivity({
      attachments: [
        CardFactory.oauthCard(
          oauthSettings.connectionName,
          oauthSettings.title,
          oauthSettings.text
        )
      ]
    });
    return null;
  }

  return tokenResponse.token;
}

typescript
// src/auth/oauth.ts
import { OAuthPrompt, OAuthPromptSettings } from 'botbuilder-dialogs';

const oauthSettings: OAuthPromptSettings = {
  connectionName: process.env.OAUTH_CONNECTION_NAME!,
  text: 'Please sign in to continue',
  title: 'Sign In',
  timeout: 300000 // 5 minutes
};

// In your dialog
async function handleAuth(context, state) {
  const tokenResponse = await context.adapter.getUserToken(
    context,
    oauthSettings.connectionName
  );

  if (!tokenResponse?.token) {
    // No token, show sign-in card
    await context.sendActivity({
      attachments: [
        CardFactory.oauthCard(
          oauthSettings.connectionName,
          oauthSettings.title,
          oauthSettings.text
        )
      ]
    });
    return null;
  }

  return tokenResponse.token;
}

RAG (Retrieval-Augmented Generation)

RAG(检索增强生成)

Vector Search with Azure AI Search

基于Azure AI Search的向量搜索

typescript
// src/rag/azure-search.ts
import { SearchClient, AzureKeyCredential } from '@azure/search-documents';

const searchClient = new SearchClient(
  process.env.AZURE_SEARCH_ENDPOINT!,
  process.env.AZURE_SEARCH_INDEX!,
  new AzureKeyCredential(process.env.AZURE_SEARCH_KEY!)
);

export async function searchKnowledgeBase(
  query: string,
  topK: number = 5
): Promise<string[]> {
  const results = await searchClient.search(query, {
    top: topK,
    select: ['content', 'title', 'source'],
    queryType: 'semantic',
    semanticConfiguration: 'default'
  });

  const documents: string[] = [];
  for await (const result of results.results) {
    documents.push(`${result.document.title}: ${result.document.content}`);
  }

  return documents;
}
typescript
// src/rag/azure-search.ts
import { SearchClient, AzureKeyCredential } from '@azure/search-documents';

const searchClient = new SearchClient(
  process.env.AZURE_SEARCH_ENDPOINT!,
  process.env.AZURE_SEARCH_INDEX!,
  new AzureKeyCredential(process.env.AZURE_SEARCH_KEY!)
);

export async function searchKnowledgeBase(
  query: string,
  topK: number = 5
): Promise<string[]> {
  const results = await searchClient.search(query, {
    top: topK,
    select: ['content', 'title', 'source'],
    queryType: 'semantic',
    semanticConfiguration: 'default'
  });

  const documents: string[] = [];
  for await (const result of results.results) {
    documents.push(`${result.document.title}: ${result.document.content}`);
  }

  return documents;
}

RAG-Enhanced Claude Response

基于RAG增强的Claude响应

typescript
// src/rag/claude-rag.ts
import Anthropic from '@anthropic-ai/sdk';
import { searchKnowledgeBase } from './azure-search';

const anthropic = new Anthropic();

export async function getRAGResponse(userQuery: string): Promise<string> {
  // 1. Search knowledge base
  const relevantDocs = await searchKnowledgeBase(userQuery);

  // 2. Build context
  const context = relevantDocs.join('\n\n---\n\n');

  // 3. Generate response with context
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system: `You are a helpful assistant for Teams. Answer questions based on the provided context.
If the context doesn't contain relevant information, say so and provide a general response.
Always cite your sources when using information from the context.`,
    messages: [
      {
        role: 'user',
        content: `Context:\n${context}\n\nQuestion: ${userQuery}`
      }
    ]
  });

  return response.content[0].type === 'text' ? response.content[0].text : '';
}

typescript
// src/rag/claude-rag.ts
import Anthropic from '@anthropic-ai/sdk';
import { searchKnowledgeBase } from './azure-search';

const anthropic = new Anthropic();

export async function getRAGResponse(userQuery: string): Promise<string> {
  // 1. Search knowledge base
  const relevantDocs = await searchKnowledgeBase(userQuery);

  // 2. Build context
  const context = relevantDocs.join('\n\n---\n\n');

  // 3. Generate response with context
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system: `You are a helpful assistant for Teams. Answer questions based on the provided context.
If the context doesn't contain relevant information, say so and provide a general response.
Always cite your sources when using information from the context.`,
    messages: [
      {
        role: 'user',
        content: `Context:\n${context}\n\nQuestion: ${userQuery}`
      }
    ]
  });

  return response.content[0].type === 'text' ? response.content[0].text : '';
}

Deployment

部署

Azure Bot Service Setup

Azure Bot Service设置

bash
undefined
bash
undefined

Create resource group

创建资源组

az group create --name rg-teams-bot --location eastus
az group create --name rg-teams-bot --location eastus

Create App Service plan

创建App Service计划

az appservice plan create
--name asp-teams-bot
--resource-group rg-teams-bot
--sku B1
--is-linux
az appservice plan create
--name asp-teams-bot
--resource-group rg-teams-bot
--sku B1
--is-linux

Create Web App

创建Web App

az webapp create
--name my-teams-bot
--resource-group rg-teams-bot
--plan asp-teams-bot
--runtime "NODE:18-lts"
az webapp create
--name my-teams-bot
--resource-group rg-teams-bot
--plan asp-teams-bot
--runtime "NODE:18-lts"

Create Bot Channels Registration

创建Bot通道注册

az bot create
--resource-group rg-teams-bot
--name my-teams-bot
--kind registration
--endpoint https://my-teams-bot.azurewebsites.net/api/messages
--sku F0
az bot create
--resource-group rg-teams-bot
--name my-teams-bot
--kind registration
--endpoint https://my-teams-bot.azurewebsites.net/api/messages
--sku F0

Enable Teams channel

启用Teams通道

az bot msteams create
--name my-teams-bot
--resource-group rg-teams-bot
undefined
az bot msteams create
--name my-teams-bot
--resource-group rg-teams-bot
undefined

Environment Variables

环境变量

bash
undefined
bash
undefined

.env

.env

Azure Bot

Azure Bot

BOT_ID=your-bot-id BOT_PASSWORD=your-bot-password BOT_TENANT_ID=your-tenant-id
BOT_ID=your-bot-id BOT_PASSWORD=your-bot-password BOT_TENANT_ID=your-tenant-id

Azure OpenAI

Azure OpenAI

AZURE_OPENAI_API_KEY=your-key AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com AZURE_OPENAI_DEPLOYMENT=gpt-4
AZURE_OPENAI_API_KEY=your-key AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com AZURE_OPENAI_DEPLOYMENT=gpt-4

Or OpenAI

Or OpenAI

OPENAI_API_KEY=sk-xxx
OPENAI_API_KEY=sk-xxx

Or Anthropic

Or Anthropic

ANTHROPIC_API_KEY=sk-ant-xxx
ANTHROPIC_API_KEY=sk-ant-xxx

Microsoft Graph

Microsoft Graph

AZURE_CLIENT_ID=your-client-id AZURE_CLIENT_SECRET=your-client-secret AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id AZURE_CLIENT_SECRET=your-client-secret AZURE_TENANT_ID=your-tenant-id

Azure AI Search (for RAG)

Azure AI Search (for RAG)

AZURE_SEARCH_ENDPOINT=https://your-search.search.windows.net AZURE_SEARCH_KEY=your-key AZURE_SEARCH_INDEX=knowledge-base
undefined
AZURE_SEARCH_ENDPOINT=https://your-search.search.windows.net AZURE_SEARCH_KEY=your-key AZURE_SEARCH_INDEX=knowledge-base
undefined

Docker Deployment

Docker部署

dockerfile
undefined
dockerfile
undefined

Dockerfile

Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ RUN npm ci --only=production
COPY . . RUN npm run build
EXPOSE 3978
CMD ["node", "dist/index.js"]

```yaml
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ RUN npm ci --only=production
COPY . . RUN npm run build
EXPOSE 3978
CMD ["node", "dist/index.js"]

```yaml

docker-compose.yml

docker-compose.yml

version: '3.8'
services: teams-bot: build: . ports: - "3978:3978" environment: - BOT_ID=${BOT_ID} - BOT_PASSWORD=${BOT_PASSWORD} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} restart: unless-stopped
undefined
version: '3.8'
services: teams-bot: build: . ports: - "3978:3978" environment: - BOT_ID=${BOT_ID} - BOT_PASSWORD=${BOT_PASSWORD} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} restart: unless-stopped
undefined

Teams Toolkit Deployment

使用Teams Toolkit部署

bash
undefined
bash
undefined

Login to Azure

登录Azure

npx teamsfx account login azure
npx teamsfx account login azure

Provision resources

预配资源

npx teamsfx provision --env dev
npx teamsfx provision --env dev

Deploy

部署

npx teamsfx deploy --env dev
npx teamsfx deploy --env dev

Publish to Teams

发布到Teams

npx teamsfx publish --env dev

---
npx teamsfx publish --env dev

---

Testing

测试

Local Testing with ngrok

使用ngrok进行本地测试

bash
undefined
bash
undefined

Start ngrok tunnel

启动ngrok隧道

ngrok http 3978
ngrok http 3978

Update manifest with ngrok URL

更新清单中的ngrok URL

undefined
undefined

Teams Toolkit Local Debug

使用Teams Toolkit本地调试

bash
undefined
bash
undefined

Start local debugging (opens Teams with your app)

启动本地调试(在Teams中打开你的应用)

npx teamsfx preview --local
undefined
npx teamsfx preview --local
undefined

Unit Testing

单元测试

typescript
// tests/bot.test.ts
import { TestAdapter, TurnContext } from 'botbuilder';
import { createWelcomeCard } from '../src/cards/welcome-card';

describe('Bot Tests', () => {
  let adapter: TestAdapter;

  beforeEach(() => {
    adapter = new TestAdapter();
  });

  test('should respond to hello', async () => {
    await adapter
      .send('hello')
      .assertReply((activity) => {
        expect(activity.text).toContain('Hello');
      });
  });

  test('should create welcome card', () => {
    const card = createWelcomeCard('John');
    expect(card.content.body[0].text).toContain('John');
  });
});

typescript
// tests/bot.test.ts
import { TestAdapter, TurnContext } from 'botbuilder';
import { createWelcomeCard } from '../src/cards/welcome-card';

describe('Bot Tests', () => {
  let adapter: TestAdapter;

  beforeEach(() => {
    adapter = new TestAdapter();
  });

  test('should respond to hello', async () => {
    await adapter
      .send('hello')
      .assertReply((activity) => {
        expect(activity.text).toContain('Hello');
      });
  });

  test('should create welcome card', () => {
    const card = createWelcomeCard('John');
    expect(card.content.body[0].text).toContain('John');
  });
});

Best Practices

最佳实践

Conversation Design

对话设计

┌─────────────────────────────────────────────────────────────────┐
│  CONVERSATION UX GUIDELINES                                     │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  1. GREET INTELLIGENTLY                                         │
│     - Welcome new users with onboarding card                    │
│     - Return users get quick access to recent actions           │
│                                                                 │
│  2. HANDLE ERRORS GRACEFULLY                                    │
│     - Never show stack traces to users                          │
│     - Provide clear recovery options                            │
│     - Log errors for debugging                                  │
│                                                                 │
│  3. USE CARDS FOR RICH CONTENT                                  │
│     - Adaptive Cards for forms and structured data              │
│     - Hero Cards for simple actions                             │
│     - Keep cards concise and actionable                         │
│                                                                 │
│  4. TYPING INDICATORS                                           │
│     - Show typing for long operations                           │
│     - Provide progress updates for very long tasks              │
│                                                                 │
│  5. CONTEXT AWARENESS                                           │
│     - Remember conversation history                             │
│     - Personalize based on user preferences                     │
│     - Respect team/channel context                              │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│  对话UX指南                                                     │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  1. 智能问候                                                     │
│     - 向新用户发送带引导卡片的欢迎消息                           │
│     - 向返回用户提供快速访问最近操作的入口                       │
│                                                                 │
│  2. 优雅处理错误                                                 │
│     - 绝不向用户显示堆栈跟踪                                     │
│     - 提供清晰的恢复选项                                         │
│     - 记录错误以便调试                                           │
│                                                                 │
│  3. 使用卡片展示丰富内容                                         │
│     - 用Adaptive Cards展示表单和结构化数据                       │
│     - 用Hero Cards展示简单操作                                   │
│     - 保持卡片简洁且可操作                                       │
│                                                                 │
│  4. 输入指示器                                                   │
│     - 长时间操作时显示输入状态                                   │
│     - 极长时间任务提供进度更新                                   │
│                                                                 │
│  5. 上下文感知                                                   │
│     - 记住对话历史                                               │
│     - 根据用户偏好个性化内容                                     │
│     - 尊重团队/频道上下文                                       │
└─────────────────────────────────────────────────────────────────┘

Security Checklist

安全检查清单

  • Validate all incoming messages
  • Use App-Only auth for Graph API when possible
  • Never log sensitive user data
  • Implement rate limiting
  • Use managed identity in Azure
  • Rotate secrets regularly
  • Enable audit logging
  • 验证所有传入消息
  • 尽可能为Graph API使用应用级身份验证
  • 绝不记录敏感用户数据
  • 实现速率限制
  • 在Azure中使用托管标识
  • 定期轮换密钥
  • 启用审计日志

Performance Tips

性能优化技巧

TipDescription
Cache Graph tokensToken refresh is expensive
Stream long responsesUse typing indicator + chunked responses
Index knowledge basePre-embed documents for RAG
Use connection poolingReuse HTTP connections
Compress payloadsGzip large card responses

技巧描述
缓存Graph令牌令牌刷新开销大
流式传输长响应使用输入指示器 + 分块响应
为知识库建立索引预嵌入文档用于RAG
使用连接池复用HTTP连接
压缩负载对大型卡片响应使用Gzip压缩

Project Templates

项目模板

AI Assistant Template

AI助手模板

typescript
// Complete AI assistant with Claude
import { App, HttpPlugin } from '@microsoft/teams.ai';
import Anthropic from '@anthropic-ai/sdk';
import { createWelcomeCard } from './cards/welcome-card';
import { createAIResponseCard } from './cards/ai-response-card';

const anthropic = new Anthropic();
const app = new App({ plugins: [new HttpPlugin()] });
const conversations = new Map<string, Anthropic.MessageParam[]>();

// Welcome new users
app.conversationUpdate('membersAdded', async (context) => {
  for (const member of context.activity.membersAdded || []) {
    if (member.id !== context.activity.recipient.id) {
      await context.sendActivity({
        attachments: [createWelcomeCard(member.name || 'User')]
      });
    }
  }
});

// Handle messages
app.on('message', async (context) => {
  const userId = context.activity.from.id;
  const userMessage = context.activity.text;

  // Initialize or get conversation
  if (!conversations.has(userId)) {
    conversations.set(userId, []);
  }
  const history = conversations.get(userId)!;
  history.push({ role: 'user', content: userMessage });

  // Show typing
  await context.sendActivity({ type: 'typing' });

  // Get AI response
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system: 'You are a helpful Teams assistant.',
    messages: history
  });

  const answer = response.content[0].type === 'text'
    ? response.content[0].text
    : '';

  history.push({ role: 'assistant', content: answer });

  // Send rich card response
  await context.sendActivity({
    attachments: [{
      contentType: 'application/vnd.microsoft.card.adaptive',
      content: createAIResponseCard(userMessage, answer)
    }]
  });
});

// Handle card actions
app.on('adaptiveCard/action', async (context) => {
  const action = context.activity.value?.action;

  switch (action) {
    case 'feedback':
      // Log feedback
      console.log('Feedback:', context.activity.value);
      await context.sendActivity('Thanks for your feedback!');
      break;
    case 'followUp':
      await context.sendActivity('What would you like to know more about?');
      break;
  }
});

app.start();

typescript
// Complete AI assistant with Claude
import { App, HttpPlugin } from '@microsoft/teams.ai';
import Anthropic from '@anthropic-ai/sdk';
import { createWelcomeCard } from './cards/welcome-card';
import { createAIResponseCard } from './cards/ai-response-card';

const anthropic = new Anthropic();
const app = new App({ plugins: [new HttpPlugin()] });
const conversations = new Map<string, Anthropic.MessageParam[]>();

// Welcome new users
app.conversationUpdate('membersAdded', async (context) => {
  for (const member of context.activity.membersAdded || []) {
    if (member.id !== context.activity.recipient.id) {
      await context.sendActivity({
        attachments: [createWelcomeCard(member.name || 'User')]
      });
    }
  }
});

// Handle messages
app.on('message', async (context) => {
  const userId = context.activity.from.id;
  const userMessage = context.activity.text;

  // Initialize or get conversation
  if (!conversations.has(userId)) {
    conversations.set(userId, []);
  }
  const history = conversations.get(userId)!;
  history.push({ role: 'user', content: userMessage });

  // Show typing
  await context.sendActivity({ type: 'typing' });

  // Get AI response
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system: 'You are a helpful Teams assistant.',
    messages: history
  });

  const answer = response.content[0].type === 'text'
    ? response.content[0].text
    : '';

  history.push({ role: 'assistant', content: answer });

  // Send rich card response
  await context.sendActivity({
    attachments: [{
      contentType: 'application/vnd.microsoft.card.adaptive',
      content: createAIResponseCard(userMessage, answer)
    }]
  });
});

// Handle card actions
app.on('adaptiveCard/action', async (context) => {
  const action = context.activity.value?.action;

  switch (action) {
    case 'feedback':
      // Log feedback
      console.log('Feedback:', context.activity.value);
      await context.sendActivity('Thanks for your feedback!');
      break;
    case 'followUp':
      await context.sendActivity('What would you like to know more about?');
      break;
  }
});

app.start();

Troubleshooting

故障排除

IssueCauseFix
Bot not respondingEndpoint unreachableCheck ngrok/Azure URL in manifest
Auth failuresToken expired/invalidRefresh OAuth connection
Cards not renderingInvalid schemaValidate at adaptivecards.io/designer
Graph 403 errorsMissing permissionsCheck app registration permissions
Slow responsesAPI latencyAdd typing indicator, consider streaming

问题原因解决方法
机器人无响应端点不可达检查清单中的ngrok/Azure URL
身份验证失败令牌过期/无效刷新OAuth连接
卡片无法渲染无效的Schema在adaptivecards.io/designer验证
Graph 403错误缺少权限检查应用注册权限
响应缓慢API延迟添加输入指示器,考虑流式传输

Resources

资源