Примеры кода в этом разделе основаны на версии 4.6 и более поздних версиях пакета SDK Bot Framework. Если вы ищете документацию по более ранним версиям, см. раздел пакет SDK для ботов версии 3 в папке Устаревшие пакеты SDK документации.
При создании чат-ботов для Microsoft Teams можно работать с событиями беседы. Microsoft Teams отправляет вашему боту уведомления о событиях в тех областях, где этот бот активен. Эти события можно обрабатывать программно и выполнять с ними те или иные действия, например:
отправлять приветственное сообщение при добавлении бота в команду;
отправлять приветственное сообщение при добавлении пользователя в команду или его удалении;
отправлять уведомление при создании, переименовании или удалении канала;
отправлять уведомление, когда пользователь ставит сообщению бота отметку «Нравится».
Определите канал по умолчанию для бота из пользовательского ввода (выбора) во время установки.
В следующем видео показано, как бот беседы может улучшить взаимодействие с клиентами за счет плавного и интеллектуального взаимодействия:
События обновления беседы
События обновления беседы можно использовать для предоставления более качественных уведомлений и эффективных действий бота.
Важно!
Вы можете добавлять новые события в любое время, и бот начнет их получать.
Разрабатывая бот, учитывайте возможность получения непредвиденных событий.
Если вы используете пакет SDK Bot Framework, бот автоматически отвечает на любые события, которые вы решили не обрабатывать, отправкой 200 - OK.
Когда клиент Службы коммуникации Azure (ACS) присоединяется к собранию Teams или покидает его, события обновления диалога не активируются.
Бот получает событие conversationUpdate во всех перечисленных ниже случаях:
При добавлении бота в беседу.
При добавлении в беседу или удалении из нее других участников.
При изменении метаданных беседы.
Событие conversationUpdate отправляется боту, когда поступают сведения об обновлении членства в командах, куда он добавлен. Бот также получает обновление при первом добавлении в личную беседу.
В следующей таблице приведен список событий Teams обновления беседы с дополнительными сведениями:
protected override async Task OnTeamsChannelCreatedAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var heroCard = new HeroCard(text: $"{channelInfo.Name} is the Channel created");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
async def on_teams_channel_created(
self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
):
# Sends a message activity to the sender of the incoming activity.
return await turn_context.send_activity(
MessageFactory.text(
f"The new channel is {channel_info.name}. The channel id is {channel_info.id}"
)
)
Канал переименован
Событие channelRenamed отправляется боту при каждом переименовании канала в команде, в которой установлен бот.
В следующем коде показан пример события переименования канала:
protected override async Task OnTeamsChannelRenamedAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var heroCard = new HeroCard(text: $"{channelInfo.Name} is the new Channel name");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
async def on_teams_channel_restored(
self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
):
# Sends a message activity to the sender of the incoming activity.
return await turn_context.send_activity(
MessageFactory.text(
f"The restored channel is {channel_info.name}. The channel id is {channel_info.id}"
)
)
Участники добавлены
Добавленное событие участника отправляется боту в следующих сценариях:
При установке самого бота и добавлении в беседу
В контексте команды в качестве conversation.id действия задается id канал, выбранный пользователем во время установки приложения, или канал, в котором был установлен бот.
При добавлении пользователя в беседу, в которой установлен бот
Идентификаторы пользователей, полученные в полезных данных события, являются уникальными для бота и могут кэшироваться для использования в будущем, например для непосредственного обмена сообщениями с пользователем.
При отправке события из контекста команды для добавленного участника действия eventType задается teamMemberAdded значение . Чтобы определить, является ли добавленный новый элемент самим ботом или пользователем, проверка Activity объект объекта turnContext. Если список MembersAdded содержит объект , где id совпадает с полем idRecipient объекта, то добавленный элемент является ботом, в противном случае это пользователь. Бот имеет id формат 28:<MicrosoftAppId>.
protected override async Task OnTeamsMembersAddedAsync(IList<TeamsChannelAccount> teamsMembersAdded , TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (TeamsChannelAccount member in teamsMembersAdded)
{
if (member.Id == turnContext.Activity.Recipient.Id)
{
// Send a message to introduce the bot to the team.
var heroCard = new HeroCard(text: $"The {member.Name} bot has joined {teamInfo.Name}");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
else
{
var heroCard = new HeroCard(text: $"{member.Name} joined {teamInfo.Name}");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
}
}
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
this.onTeamsMembersAddedEvent(async (membersAdded: ChannelAccount[], teamInfo: TeamInfo, turnContext: TurnContext, next: () => Promise<void>): Promise<void> => {
let newMembers: string = '';
console.log(JSON.stringify(membersAdded));
membersAdded.forEach((account) => {
newMembers += account.id + ' ';
});
const name = !teamInfo ? 'not in team' : teamInfo.name;
const card = CardFactory.heroCard('Account Added', `${newMembers} joined ${name}.`);
const message = MessageFactory.attachment(card);
// Sends a message activity to the sender of the incoming activity.
await turnContext.sendActivity(message);
await next();
});
}
}
Сообщение, которое бот получает при добавлении бота в команду.
Примечание.
В этой полезных данных и channelData.settings.selectedChannel.id находятся идентификаторы канала, conversation.id выбранного пользователем во время установки приложения или из которого была активирована установка.
async def on_teams_members_added(
self, teams_members_added: [TeamsChannelAccount], turn_context: TurnContext
):
for member in teams_members_added:
.. # Sends a message activity to the sender of the incoming activity.
await turn_context.send_activity(
MessageFactory.text(f"Welcome your new team member {member.id}")
)
return
Элементы удалены
Событие удаления участника отправляется боту в следующих сценариях:
Когда бот удаляется и удаляется из беседы.
При удалении пользователя из беседы, в которой установлен бот.
При отправке события из контекста команды для eventType действия, удаленного участника, задается teamMemberRemoved значение . Чтобы определить, был ли только что удаленный участник самим ботом или пользователем, проверьте объект Activity из turnContext. Если список MembersRemoved содержит объект , где id совпадает с полем idRecipient объекта, то добавленный элемент является ботом, в противном случае это пользователь. Идентификатор бота форматируется как 28:<MicrosoftAppId>.
Примечание.
При окончательном удалении пользователя из клиента инициируется событие membersRemoved conversationUpdate.
В следующем коде показан пример удаленного события участников команды:
protected override async Task OnTeamsMembersRemovedAsync(IList<ChannelAccount> membersRemoved, TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (TeamsChannelAccount member in membersRemoved)
{
if (member.Id == turnContext.Activity.Recipient.Id)
{
// The bot was removed.
// You should clear any cached data you have for this team.
}
else
{
var heroCard = new HeroCard(text: $"{member.Name} was removed from {teamInfo.Name}");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
}
}
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
this.onTeamsMembersRemovedEvent(async (membersRemoved: ChannelAccount[], teamInfo: TeamInfo, turnContext: TurnContext, next: () => Promise<void>): Promise<void> => {
let removedMembers: string = '';
console.log(JSON.stringify(membersRemoved));
membersRemoved.forEach((account) => {
removedMembers += account.id + ' ';
});
const name = !teamInfo ? 'not in team' : teamInfo.name;
const card = CardFactory.heroCard('Account Removed', `${removedMembers} removed from ${teamInfo.name}.`);
const message = MessageFactory.attachment(card);
// Sends a message activity to the sender of the incoming activity.
await turnContext.sendActivity(message);
await next();
});
}
}
Объект channelData в следующем примере полезных данных основан на добавлении участника в команду, а не в групповой чат и не запуск новой беседы "один к одному":
async def on_teams_members_removed(
self, teams_members_removed: [TeamsChannelAccount], turn_context: TurnContext
):
for member in teams_members_removed:
..# Sends a message activity to the sender of the incoming activity.
await turn_context.send_activity(
MessageFactory.text(f"Say goodbye to {member.id}")
)
return
Команда переименована
Бот получает уведомление при переименовании команды. Он получает событие conversationUpdate с eventType.teamRenamed в объекте channelData.
В следующем коде показан пример события, переименованного командой:
protected override async Task OnTeamsTeamRenamedAsync(TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var heroCard = new HeroCard(text: $"{teamInfo.Name} is the new Team name");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
// Bot is notified when the team is renamed.
this.onTeamsTeamRenamedEvent(async (teamInfo: TeamInfo, turnContext: TurnContext, next: () => Promise<void>): Promise<void> => {
const card = CardFactory.heroCard('Team Renamed', `${teamInfo.name} is the new Team name`);
const message = MessageFactory.attachment(card);
// Sends an activity to the sender of the incoming activity.
await turnContext.sendActivity(message);
await next();
});
}
}
# Bot is notified when the team is renamed.
async def on_teams_team_renamed(
self, team_info: TeamInfo, turn_context: TurnContext
):
# Sends an activity to the sender of the incoming activity.
return await turn_context.send_activity(
MessageFactory.text(f"The new team name is {team_info.name}")
)
Команда удалена
При удалении команды бот получает уведомление. Он получает событие conversationUpdate с eventType.teamDeleted в объекте channelData.
В следующем коде показан пример события, удаленного командой:
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
// Invoked when a Team Deleted event activity is received from the connector. Team Deleted corresponds to the user deleting a team.
this.onTeamsTeamDeletedEvent(async (teamInfo: TeamInfo, turnContext: TurnContext, next: () => Promise<void>): Promise<void> => {
// Handle delete event.
await next();
});
}
}
# Invoked when a Team Deleted event activity is received from the connector. Team Deleted corresponds to the user deleting a team.
async def on_teams_team_deleted(
self, team_info: TeamInfo, turn_context: TurnContext
):
# Handle delete event.
)
Команда восстановлена
Бот получает уведомление о восстановлении команды после удаления. Он получает событие conversationUpdate с eventType.teamrestored в объекте channelData.
В следующем коде показан пример события восстановления команды:
protected override async Task OnTeamsTeamrestoredAsync(TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var heroCard = new HeroCard(text: $"{teamInfo.Name} is the team name");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
// Invoked when a Team Restored event activity is received from the connector. Team Restored corresponds to the user restoring a team.
this.onTeamsTeamrestoredEvent(async (teamInfo: TeamInfo, turnContext: TurnContext, next: () => Promise<void>): Promise<void> => {
const card = CardFactory.heroCard('Team restored', `${teamInfo.name} is the team name`);
const message = MessageFactory.attachment(card);
// Sends an activity to the sender of the incoming activity.
await turnContext.sendActivity(message);
await next();
});
}
}
# Invoked when a Team Restored event activity is received from the connector. Team Restored corresponds to the user restoring a team.
async def on_teams_team_restored(
self, team_info: TeamInfo, turn_context: TurnContext
):
# Sends an activity to the sender of the incoming activity.
return await turn_context.send_activity(
MessageFactory.text(f"The team name is {team_info.name}")
)
Команда архивирована
Бот получает уведомление о том, что команда установлена и архивирована. Он получает событие conversationUpdate с eventType.teamarchived в объекте channelData.
В следующем фрагменте программного кода показан пример события "команда архивирована".
protected override async Task OnTeamsTeamArchivedAsync(TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var heroCard = new HeroCard(text: $"{teamInfo.Name} is the team name");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
// Invoked when a Team Archived event activity is received from the connector. Team Archived.
this.onTeamsTeamArchivedEvent(async (teamInfo: TeamInfo, turnContext: TurnContext, next: () => Promise<void>): Promise<void> => {
const card = CardFactory.heroCard('Team archived', `${teamInfo.name} is the team name`);
const message = MessageFactory.attachment(card);
// Sends an activity to the sender of the incoming activity.
await turnContext.sendActivity(message);
await next();
});
}
}
# Invoked when a Team Archived event activity is received from the connector. Team Archived correspond to the user archiving a team.
async def on_teams_team_archived(
self, team_info: TeamInfo, turn_context: TurnContext
):
# Sends an activity to the sender of the incoming activity.
return await turn_context.send_activity(
MessageFactory.text(f"The team name is {team_info.name}")
)
Архивация команды отменена
Бот получает уведомление об установке команды и восстановлении ее из архива. Он получает событие conversationUpdate с eventType.teamUnarchived в объекте channelData.
В следующем коде показан пример неархивированного события команды:
protected override async Task OnTeamsTeamUnarchivedAsync(TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var heroCard = new HeroCard(text: $"{teamInfo.Name} is the team name");
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Attachment(heroCard.ToAttachment()), cancellationToken);
}
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
// Invoked when a Team Unarchived event activity is received from the connector. Team.
this.onTeamsTeamUnarchivedEvent(async (teamInfo: TeamInfo, turnContext: TurnContext, next: () => Promise<void>): Promise<void> => {
const card = CardFactory.heroCard('Team archived', `${teamInfo.name} is the team name`);
const message = MessageFactory.attachment(card);
// Sends an activity to the sender of the incoming activity.
await turnContext.sendActivity(message);
await next();
});
}
}
# Invoked when a Team Unarchived event activity is received from the connector. Team Unarchived correspond to the user unarchiving a team.
async def on_teams_team_unarchived(
self, team_info: TeamInfo, turn_context: TurnContext
):
# Sends an activity to the sender of the incoming activity.
return await turn_context.send_activity(
MessageFactory.text(f"The team name is {team_info.name}")
)
Теперь, когда вы работали с событиями обновления беседы, вы можете понять события реакции сообщения, которые происходят для различных реакций на сообщение.
События реакции на сообщение
Событие messageReaction отправляется, когда пользователь добавляет или удаляет реакции на сообщение, отправленное ботом.
replyToId содержит идентификатор сообщения, а Type - тип реакции в текстовом формате. В список типов реакций входят: гнев, любовь, смех, "нравится", печаль и удивление. Это событие не содержит содержимое исходного сообщения. Если обработка реакций на сообщения - важная функция бота, сохраняйте их при отправке. В следующей таблице приведены дополнительные сведения о типе события и объектах полезных данных:
protected override async Task OnReactionsAddedAsync(IList<MessageReaction> messageReactions, ITurnContext<IMessageReactionActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var reaction in messageReactions)
{
var newReaction = $"You reacted with '{reaction.Type}' to the following message: '{turnContext.Activity.ReplyToId}'";
var replyActivity = MessageFactory.Text(newReaction);
// Sends an activity to the sender of the incoming activity.
var resourceResponse = await turnContext.SendActivityAsync(replyActivity, cancellationToken);
}
}
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
// Override this in a derived class to provide logic for when reactions to a previous activity.
this.onReactionsAdded(async (context, next) => {
const reactionsAdded = context.activity.reactionsAdded;
if (reactionsAdded && reactionsAdded.length > 0) {
for (let i = 0; i < reactionsAdded.length; i++) {
const reaction = reactionsAdded[i];
const newReaction = `You reacted with '${reaction.type}' to the following message: '${context.activity.replyToId}'`;
// Sends an activity to the sender of the incoming activity.
const resourceResponse = context.sendActivity(newReaction);
// Save information about the sent message and its ID (resourceResponse.id).
}
}
});
}
}
# Override this in a derived class to provide logic for when reactions to a previous activity are added to the conversation.
async def on_reactions_added(
self, message_reactions: List[MessageReaction], turn_context: TurnContext
):
for reaction in message_reactions:
activity = await self._log.find(turn_context.activity.reply_to_id)
if not activity:
# Sends an activity to the sender of the incoming activity.
await self._send_message_and_log_activity_id(
turn_context,
f"Activity {turn_context.activity.reply_to_id} not found in log",
)
else:
# Sends an activity to the sender of the incoming activity.
await self._send_message_and_log_activity_id(
turn_context,
f"You added '{reaction.type}' regarding '{activity.text}'",
)
return
Реакции, удаленные из сообщения бота
В следующем фрагменте программного кода показан пример реакций, удаленных из сообщения бота:
protected override async Task OnReactionsRemovedAsync(IList<MessageReaction> messageReactions, ITurnContext<IMessageReactionActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var reaction in messageReactions)
{
var newReaction = $"You removed the reaction '{reaction.Type}' from the following message: '{turnContext.Activity.ReplyToId}'";
var replyActivity = MessageFactory.Text(newReaction);
// Sends an activity to the sender of the incoming activity.
var resourceResponse = await turnContext.SendActivityAsync(replyActivity, cancellationToken);
}
}
export class MyBot extends TeamsActivityHandler {
constructor() {
super();
// Override this in a derived class to provide logic for when reactions to a previous activity.
this.onReactionsRemoved(async(context,next)=>{
const reactionsRemoved = context.activity.reactionsRemoved;
if (reactionsRemoved && reactionsRemoved.length > 0) {
for (let i = 0; i < reactionsRemoved.length; i++) {
const reaction = reactionsRemoved[i];
const newReaction = `You removed the reaction '${reaction.type}' from the message: '${context.activity.replyToId}'`;
// Sends an activity to the sender of the incoming activity.
const resourceResponse = context.sendActivity(newReaction);
// Save information about the sent message and its ID (resourceResponse.id).
}
}
});
}
}
# Override this in a derived class to provide logic specific to removed activities.
async def on_reactions_removed(
self, message_reactions: List[MessageReaction], turn_context: TurnContext
):
for reaction in message_reactions:
activity = await self._log.find(turn_context.activity.reply_to_id)
if not activity:
# Sends an activity to the sender of the incoming activity.
await self._send_message_and_log_activity_id(
turn_context,
f"Activity {turn_context.activity.reply_to_id} not found in log",
)
else:
# Sends an activity to the sender of the incoming activity.
await self._send_message_and_log_activity_id(
turn_context,
f"You removed '{reaction.type}' regarding '{activity.text}'",
)
return
Событие обновления установки
Бот получает событие installationUpdate при установке бота в поток беседы. Удаление бота из потока также генерирует это событие. При установке бота полю действие в событии присваивается значение add, а при удалении полю действие присваивается значение remove.
Примечание.
При обновлении приложения бот получает installationUpdate событие только для добавления или удаления бота из манифеста. Во всех остальных случаях installationUpdate событие не активируется. При добавлении бота полю действие в событии присваивается значение add-upgrade, а при удалении - значение remove-upgrade.
Событие установки обновления
Используйте событие installationUpdate для отправки вводного сообщения от бота при установке. Это событие помогает обеспечить соответствие требованиям к конфиденциальности и хранению данных. При удалении бота можно также очистить и удалить данные пользователя или потока.
Аналогично событию conversationUpdate , которое отправляется при добавлении бота в команду, в качестве conversation.id installationUpdate события устанавливается идентификатор канала, выбранного пользователем во время установки приложения или канала, в котором произошла установка. Идентификатор представляет канал, в котором пользователь намерен работать с ботом, и должен использоваться ботом при отправке приветственного сообщения. В случаях, когда идентификатор канала "Общий" явно требуется, его можно получить в team.idchannelData.
В этом примере conversation.idconversationUpdate для действий и installationUpdate задается идентификатор канала ответа в команде Daves Demo.
Примечание.
Выбранный идентификатор канала устанавливается только для installationUpdateсобытий добавления , которые отправляются при установке приложения в команду.
async onInstallationUpdateActivity(context: TurnContext) {
var activity = context.activity.action;
if(activity == "Add") {
// Sends an activity to the sender of the incoming activity to add.
await context.sendActivity(MessageFactory.text("Added"));
}
else {
// Sends an activity to the sender of the incoming activity to uninstalled.
await context.sendActivity(MessageFactory.text("Uninstalled"));
}
}
# Override this in a derived class to provide logic specific to InstallationUpdate activities.
async def on_installation_update(self, turn_context: TurnContext):
if turn_context.activity.action == "add":
# Sends an activity to the sender of the incoming activity to add.
await turn_context.send_activity(MessageFactory.text("Added"))
else:
# Sends an activity to the sender of the incoming activity to uninstalled.
await turn_context.send_activity(MessageFactory.text("Uninstalled"))
Поведение при удалении для личного приложения с ботом
При удалении приложения бот также удаляется. Отправив сообщение вашему приложению, пользователь получает код ответа 403. Бот получает код 403 в качестве ответа на новые сообщения, опубликованные им самим. Поведение после удаления ботов в области личного чата теперь согласовано с поведением в аналогичной ситуации в областях Teams и группового чата. Вы не сможете отправлять или получать сообщения после удаления приложения.
Обработка событий установки и удаления
При использовании событий установки и удаления в некоторых случаях боты выдают исключения при получении непредвиденных событий из Teams, что происходит в следующих случаях:
Вы создаете бот без Microsoft Bot Framework SDK, и в результате бот выдает исключение при получении непредвиденного события.
Вы создаете бот с помощью Microsoft Bot Framework SDK и решаете изменить обработку события по умолчанию путем переопределения базового дескриптора события.
Важно знать, что новые события могут быть добавлены в любое время в будущем, и ваш бот начнет получать их. Поэтому необходимо спроектировать возможность получения непредвиденных событий. Если вы используете пакет SDK Bot Framework, бот автоматически отвечает 200 –ОК на любые события, которые вы не решили обрабатывать.
Обработка ошибок в событиях диалога
Когда бот обнаруживает ошибку при обработке различных событий или действий, не отправляйте сообщения, не имеющие осмысленного контекста, в беседу, как показано на следующем снимке экрана:
На этапе разработки всегда полезно отправлять содержательные сообщения в беседах, которые предоставляют дополнительные сведения о конкретной ошибке для лучшей отладки. Однако в рабочей среде необходимо регистрировать ошибки или события, чтобы приложение Azure Insights. Дополнительные сведения см. в статье Добавление данных телеметрии в бот.
Пример кода
Название примера
Описание
.NET
Node.js
Python
Манифест
Бот беседы
В этом примере показано, как использовать различные события бесед бота, доступные в Bot Framework версии 4, для личных и командных область.
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.