用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/alinaqi/maggy --skill ms-teams-apps命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | ms-teams-apps |
| description | Microsoft Teams bots and AI agents - Claude/OpenAI, Adaptive Cards, Graph API |
| when-to-use | When building Microsoft Teams bots, tabs, or message extensions |
| user-invocable | false |
| effort | medium |
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.
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘
npm install -g @microsoft/teams.cli
# TypeScript (Recommended)
npx @microsoft/teams.cli new typescript my-agent --template echo
# Python
npx @microsoft/teams.cli new python my-agent --template echo
# C#
npx @microsoft/teams.cli new csharp my-agent --template echo
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
{
"$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"
{
"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"
}
]
// 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 ()
],
: {
planner
}
});
app.(, (context, state) => {
});
app.(, (context, state) => {
context.({
: ,
:
});
});
app.();
# 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
}
}
# 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:
// 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', : userMessage });
context.({ : });
{
response = anthropic..({
: ,
: ,
: ,
: history
});
assistantMessage = response.[]. ===
? response.[].
: ;
history.({ : , : assistantMessage });
(history. > ) {
history.(, history. - );
}
context.({
: ,
: assistantMessage
});
} (error) {
.(, error);
context.({
: ,
:
});
}
});
app.(, (context, state) => {
userId = context...;
conversations.(userId);
context.();
});
app.();
// 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' },
: { : , : },
: { : , : }
},
: []
}
},
{
: ,
: ,
: {
: ,
: {
: { : , : },
: { : , : }
},
: []
}
}
];
(): <> {
(toolName) {
:
;
:
;
:
;
:
;
}
}
(): <> {
: .[] = [
{ : , : userMessage }
];
() {
response = anthropic..({
: ,
: ,
: ,
tools,
messages
});
(response. === ) {
: .[] = [];
( content response.) {
(content. === ) {
result = (content., content.);
toolResults.({
: ,
: [{
: ,
: content.,
: result
}]
});
}
}
messages.({ : , : response. });
messages.(...toolResults);
;
}
textContent = response..( c. === );
textContent?. || ;
}
}
// 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',
: { : }
}
]
}
]
});
}
// 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',
: answer,
:
}
]
},
...(sources && sources. > ? [{
: ,
: [
{
: ,
: ,
: ,
:
},
...sources.( ({
: ,
: ,
:
}))
]
}] : [])
],
: [
{
: ,
: ,
: { : , : }
},
{
: ,
: ,
: { : , : }
},
{
: ,
: ,
: { : }
}
]
};
}
// 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: },
{ : , : },
{ : , : }
],
:
},
{
: ,
: ,
:
}
],
: [
{
: ,
: ,
: { : }
},
{
: ,
: ,
: { : }
}
]
};
}
// 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 });
}
// 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()
.({
: startDate,
: endDate
})
.()
.()
.();
}
() {
..().({
: {
subject,
: { : , : body },
: [{ : { : to } }]
}
});
}
() {
..().({
subject,
: startTime,
: endTime,
: {
: attendees.( ({
: email,
:
}))
}
});
}
() {
.
.()
.({
: { : message }
});
}
}
// 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}!`);
});
// 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;
}
// 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..title}: `);
}
documents;
}
// 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}`
}
]
});
response.[]. === ? response.[]. : ;
}
# Create resource group
az group create --name rg-teams-bot --location eastus
# Create App Service plan
az appservice plan create \
--name asp-teams-bot \
--resource-group rg-teams-bot \
--sku B1 \
--is-linux
# Create Web App
az webapp create \
--name my-teams-bot \
--resource-group rg-teams-bot \
--plan asp-teams-bot \
--runtime "NODE:18-lts"
# Create Bot Channels Registration
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
az bot msteams create \
--name my-teams-bot \
--resource-group rg-teams-bot
# .env
# Azure Bot
BOT_ID=your-bot-id
BOT_PASSWORD=your-bot-password
BOT_TENANT_ID=your-tenant-id
# Azure OpenAI
AZURE_OPENAI_API_KEY=your-key
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_DEPLOYMENT=gpt-4
# Or OpenAI
OPENAI_API_KEY=sk-xxx
# Or Anthropic
ANTHROPIC_API_KEY=sk-ant-xxx
# Microsoft Graph
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_TENANT_ID=your-tenant-id
# Azure AI Search (for RAG)
AZURE_SEARCH_ENDPOINT=https://your-search.search.windows.net
AZURE_SEARCH_KEY=your-key
AZURE_SEARCH_INDEX=knowledge-base
# 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"]
# 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
# Login to Azure
npx teamsfx account login azure
# Provision resources
npx teamsfx provision --env dev
# Deploy
npx teamsfx deploy --env dev
# Publish to Teams
npx teamsfx publish --env dev
# Start ngrok tunnel
ngrok http 3978
# Update manifest with ngrok URL
# Bot endpoint: https://xxxx.ngrok.io/api/messages
# Start local debugging (opens Teams with your app)
npx teamsfx preview --local
// 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');
});
});
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘
| Tip | Description |
|---|---|
| Cache Graph tokens | Token refresh is expensive |
| Stream long responses | Use typing indicator + chunked responses |
| Index knowledge base | Pre-embed documents for RAG |
| Use connection pooling | Reuse HTTP connections |
| Compress payloads | Gzip large card responses |
// 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 || )]
});
}
}
});
app.(, (context) => {
userId = context...;
userMessage = context..;
(!conversations.(userId)) {
conversations.(userId, []);
}
history = conversations.(userId)!;
history.({ : , : userMessage });
context.({ : });
response = anthropic..({
: ,
: ,
: ,
: history
});
answer = response.[]. ===
? response.[].
: ;
history.({ : , : answer });
context.({
: [{
: ,
: (userMessage, answer)
}]
});
});
app.(, (context) => {
action = context..?.;
(action) {
:
.(, context..);
context.();
;
:
context.();
;
}
});
app.();
| Issue | Cause | Fix |
|---|---|---|
| Bot not responding | Endpoint unreachable | Check ngrok/Azure URL in manifest |
| Auth failures | Token expired/invalid | Refresh OAuth connection |
| Cards not rendering | Invalid schema | Validate at adaptivecards.io/designer |
| Graph 403 errors | Missing permissions | Check app registration permissions |
| Slow responses | API latency | Add typing indicator, consider streaming |
基于 SOC 职业分类