实战教程展示如何集成 AI 助手和 Twitter API,适合学习 Copilotkit 框架和 AI 应用集成。
在本教程中,你将学习如何构建一个由 AI 驱动的社交媒体内容生成与调度应用,从而高效地安排帖子发布时间并生成内容。
我们将介绍如何:
为 Next.js 应用添加 Twitter 身份验证;
从零开始创建类似日历的界面;
使用 CopilotKit 将 AI 助手集成到软件应用中;
创建针对特定操作的 AI 副驾,以处理应用中的各种任务;以及
构建帖子生成与调度应用。
这个项目非常适合用来学习如何构建 AI 驱动的应用、掌握社交媒体 API,以及提升社交媒体运营能力。
CopilotKit 是一个开源 AI 副驾平台。它让你能够轻松地将强大的 AI 集成到 React 应用中。
ChatBot:能够在应用内执行操作、感知上下文的应用内聊天机器人 💬
CopilotTextArea:支持上下文感知型自动补全和内容插入的 AI 文本字段 📝
Co-Agents:能够与你的应用和用户交互的应用内 AI 智能体 🤖
要充分理解本教程,你需要具备 React 或 Next.js 的基础知识。
我们还将使用以下工具:
CopilotKit:一个开源副驾框架,用于构建自定义 AI 聊天机器人、应用内 AI 智能体和文本区域。
Redis:用于存储帖子发布计划的内存数据库。
BullMQ:一个用于管理和处理队列中任务的 Node.js 库。
Node Cron:一个用于按特定时间间隔调度和运行任务(作业)的 Node.js 库。
Headless UI:用于为应用创建无障碍 UI 组件。
X Client ID 和 Secret:用于验证用户身份,并代表用户创建帖子。
OpenAI API Key:让我们能够使用 GPT 模型执行各种任务。
首先,在终端中运行以下代码片段来创建一个 Next.js 应用:
npx create-next-app social-media-scheduler
选择你偏好的配置。本教程将使用 TypeScript 和 Next.js App Router。
接下来,安装项目依赖:
npm install @headlessui/react lodash bullmq ioredis node-cron
最后,安装所需的 CopilotKit 软件包。这些软件包使我们能够在应用中使用 AI 自动补全,让 AI 副驾从 React 状态中获取数据,并在应用中做出决策。
npm install @copilotkit/react-ui @copilotkit/react-textarea @copilotkit/react-core @copilotkit/backend
恭喜!现在你已经可以开始构建应用了。
在本节中,你将学习如何为调度应用创建用户界面。该应用分为两个页面:登录页面和仪表盘页面,用户可以在仪表盘页面中创建和调度帖子。
登录页面使用用户的 X(Twitter)个人资料进行身份验证,而仪表盘页面允许用户创建、删除和调度帖子。
登录页面是应用的首页。用户需要使用 Twitter 账户登录才能访问仪表盘。
要实现这一功能,请更新 page.tsx 文件,使其显示如下所示的登录按钮:
import Link from "next/link";
import { getTwitterOauthUrl } from "@/app/util";
export default function Home() {
return (
<main className='w-full min-h-screen flex flex-col items-center justify-center p-8'>
<h2 className='font-semibold text-2xl mb-4'>Your AI Post Scheduler</h2>
<Link
href={getTwitterOauthUrl()}
className='bg-black py-3 px-6 hover:bg-gray-700 text-gray-50 rounded-lg'
>
Sign in with Twitter
</Link>
</main>
);
}
上述代码片段显示了一个“Sign in with Twitter”按钮,它会将用户重定向到 Twitter OAuth2 页面。稍后你将学习如何设置 Twitter 身份验证。
在继续之前,请在 Next.js 项目根目录中创建一个 types.d.ts 文件。该文件将包含应用内变量的类型声明。
interface DelSelectedCell {
content?: string;
day_id?: number;
day?: string;
time_id?: number;
time?: string;
published?: boolean;
minutes?: number;
}
interface SelectedCell {
day_id?: number;
day?: string;
time_id?: number;
time?: string;
minutes?: number;
}
interface Content {
minutes?: number;
content?: string;
published?: boolean;
day?: number;
}
interface AvailableScheduleItem {
time: number;
schedule: Content[][];
}
在 Next.js 的 app 文件夹中创建一个工具文件,并将 GitHub 仓库中的这段代码复制进去。它包含在应用中执行各种数据处理所需的函数。
接下来,在 Next.js 的 app 目录中创建一个 dashboard 文件夹,并在其中创建 page.tsx 文件。
cd app
mkdir dashboard && cd dashboard
touch page.tsx
将下面的代码片段复制到 dashboard/page.tsx 文件中。它会渲染一个 App 组件,该组件通过 props 接收应用的调度数据,并将其显示在表格中:
"use client";
import _ from "lodash";
import { useState } from "react";
import App from "@/app/components/App";
import { availableSchedule } from "../util";
export default function Dashboard() {
//👇🏻 saves a deep copy of the availableSchedule array into the React state
const [yourSchedule, updateYourSchedule] = useState<AvailableScheduleItem[]>(
_.cloneDeep(availableSchedule)
);
return (
<App yourSchedule={yourSchedule} updateYourSchedule={updateYourSchedule} />
);
}
下面是上述表格的数据结构:
export const tableHeadings: string[] = [
"Time",
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
export const availableSchedule: AvailableScheduleItem[] = [
{
time: 0,
schedule: [[], [], [], [], [], [], []],
},
{
time: 1,
schedule: [[], [], [], [], [], [], []],
},
{
time: 2,
schedule: [[], [], [], [], [], [], []],
},
{
time: 3,
schedule: [[], [], [], [], [], [], []],
},
{
time: 4,
schedule: [[], [], [], [], [], [], []],
},
{
time: 5,
schedule: [[], [], [], [], [], [], []],
},
{
time: 6,
schedule: [[], [], [], [], [], [], []],
},
{
time: 7,
schedule: [[], [], [], [], [], [], []],
},
{
time: 8,
schedule: [[], [], [], [], [], [], []],
},
{
time: 9,
schedule: [[], [], [], [], [], [], []],
},
{
time: 10,
schedule: [[], [], [], [], [], [], []],
},
{
time: 11,
schedule: [[], [], [], [], [], [], []],
},
{
time: 12,
schedule: [[], [], [], [], [], [], []],
},
{
time: 13,
schedule: [[], [], [], [], [], [], []],
},
{
time: 14,
schedule: [[], [], [], [], [], [], []],
},
{
time: 15,
schedule: [[], [], [], [], [], [], []],
},
{
time: 16,
schedule: [[], [], [], [], [], [], []],
},
{
time: 17,
schedule: [[], [], [], [], [], [], []],
},
{
time: 18,
schedule: [[], [], [], [], [], [], []],
},
{
time: 19,
schedule: [[], [], [], [], [], [], []],
},
{
time: 20,
schedule: [[], [], [], [], [], [], []],
},
{
time: 21,
schedule: [[], [], [], [], [], [], []],
},
{
time: 22,
schedule: [[], [], [], [], [], [], []],
},
{
time: 23,
schedule: [[], [], [], [], [], [], []],
},
];
tableHeadings 数组包含表格各列的标题,而 availableSchedule 数组则包含一组对象。每个对象都有一个 time 属性,表示一天中的每个小时;还有一个 schedule 属性,其中包含一个嵌套数组,每个元素分别表示一周中的某一天。
例如,当用户将发布时间安排在星期三上午 8 点时,应用会查找 time 属性为 8 的对象,并通过把调度内容插入嵌套数组的第四个位置来更新其 schedule 属性。
你可以从该项目的 GitHub 仓库中复制仪表盘页面剩余的 UI 元素。
在接下来的章节中,你将学习如何向应用添加 Twitter OAuth 和 CopilotKit。
在本节中,你将学习如何创建 X Developer 项目,以及如何为 Next.js 应用添加 X 身份验证。
请确保你拥有 X 账户,然后访问 X Developers' Portal 创建一个新项目。
输入项目名称并回答必填问题,以创建新项目和应用。
设置用户身份验证设置,允许你代表用户读写帖子。
最后,相应地填写应用信息部分。
设置完身份验证流程后,将 OAuth 2.0 客户端 ID 和密钥保存到 .env.local 文件中。
TWITTER_CLIENT_ID=<your_client_ID>
NEXT_PUBLIC_TWITTER_CLIENT_ID=<your_client_ID>
TWITTER_CLIENT_SECRET=<your_client_Secret>
在 Next.js 应用文件夹内创建一个 api 文件夹。在 api 文件夹内,创建一个包含 route.ts 文件的 twitter 目录。这将创建一个 API 端点 (/api/twitter),使我们能够认证用户。
cd app
mkdir api && cd api
mkdir twitter && cd twitter
touch route.ts
将以下代码片段复制到 route.ts 文件中:
import { NextRequest, NextResponse } from "next/server";
const BasicAuthToken = Buffer.from(
`${process.env.TWITTER_CLIENT_ID!}:${process.env.TWITTER_CLIENT_SECRET!}`,
"utf8"
).toString("base64");
const twitterOauthTokenParams = {
client_id: process.env.TWITTER_CLIENT_ID!,
code_verifier: "8KxxO-RPl0bLSxX5AWwgdiFbMnry_VOKzFeIlVA7NoA",
redirect_uri: `http://www.localhost:3000/dashboard`,
grant_type: "authorization_code",
};
//👇🏻 获取用户访问令牌
export const fetchUserToken = async (code: string) => {
try {
const formatData = new URLSearchParams({
...twitterOauthTokenParams,
code,
});
const getTokenRequest = await fetch(
"https://api.twitter.com/2/oauth2/token",
{
method: "POST",
body: formatData.toString(),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${BasicAuthToken}`,
},
}
);
const getTokenResponse = await getTokenRequest.json();
return getTokenResponse;
} catch (err) {
return null;
}
};
//👇🏻 从访问令牌获取用户数据
export const fetchUserData = async (accessToken: string) => {
try {
const getUserRequest = await fetch("https://api.twitter.com/2/users/me", {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
});
const getUserProfile = await getUserRequest.json();
return getUserProfile;
} catch (err) {
return null;
}
};
//👉🏻 利用上述函数的 API 端点
从上面的代码片段中,BasicAuthToken 变量包含令牌的编码版本。twitterOauthTokenParams 包含获取用户访问令牌所需的参数。fetchUserToken 函数向 Twitter 的端点发送包含代码的请求,并返回用户的访问令牌。fetchUserData 函数使用该令牌检索用户的 X 个人资料。
BasicAuthToken 变量包含令牌的编码版本。
twitterOauthTokenParams 包含获取用户访问令牌所需的参数。
fetchUserToken 函数向 Twitter 的端点发送包含代码的请求,并返回用户的访问令牌。
fetchUserData 函数使用该令牌检索用户的 X 个人资料。
在函数下方添加此端点。当用户登录时,它从前端接收代码,并将用户 ID、用户名和访问令牌存储在一个文件中,该文件可在服务器上执行作业时访问。
import { writeFile } from "fs";
export async function POST(req: NextRequest) {
const { code } = await req.json();
try {
//👇🏻 获取访问令牌和整个响应
const tokenResponse = await fetchUserToken(code);
const accessToken = await tokenResponse.access_token;
//👇🏻 获取用户数据
const userDataResponse = await fetchUserData(accessToken);
const userCredentials = { ...tokenResponse, ...userDataResponse };
//👇🏻 将用户的访问令牌、id 和用户名合并到一个对象中
const userData = {
accessToken: userCredentials.access_token,
_id: userCredentials.data.id,
username: userCredentials.data.username,
};
//👇🏻 将其存储在 JSON 文件中(供服务器使用)
writeFile("./src/user.json", JSON.stringify(userData, null, 2), (error) => {
if (error) {
console.log("An error has occurred ", error);
throw error;
}
console.log("Data written successfully to disk");
});
//👇🏻 返回成功响应
return NextResponse.json(
{
data: "User data stored successfully",
},
{ status: 200 }
);
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 });
}
}
更新 dashboard/page.tsx,在认证用户后向 API 端点发送代码。
import { useSearchParams } from 'next/navigation'
const searchParams = useSearchParams()
const code = searchParams.get('code')
const fetchToken = useCallback(async () => {
const res = await fetch("/api/twitter", {
method: "POST",
body: JSON.stringify({ code }),
headers: {
"Content-Type": "application/json",
},
});
if (res.ok) {
const data = await res.json();
console.log(data);
}
}, [code]);
useEffect(() => {
fetchToken();
}, [fetchToken]);
恭喜!当用户点击"使用 Twitter 登录"按钮时,它会将其重定向到 Twitter 授权页面,以使其能够访问应用程序。
在本部分,你将学习如何将 CopilotKit 添加到应用程序中,以使用户能够使用 AI 智能体自动安排帖子,并在创建帖子内容时添加自动完成。
继续之前,请访问 OpenAI 开发者平台并创建一个新的密钥。
创建一个 .env.local 文件并将新创建的密钥复制到该文件中。
OPENAI_API_KEY=<YOUR_OPENAI_SECRET_KEY>
OPENAI_MODEL=gpt-4-1106-preview
接下来,你需要为 CopilotKit 创建一个 API 端点。在 Next.js 应用文件夹内,创建一个包含 route.ts 文件的 api/copilotkit 文件夹。
cd app
mkdir api && cd api
mkdir copilotkit && cd copilotkit
touch route.ts
将以下代码片段复制到 route.ts 文件中。CopilotKit 后端接受用户请求,并使用 OpenAI 模型做出决策。
import { CopilotRuntime, OpenAIAdapter } from "@copilotkit/backend";
export const runtime = "edge";
export async function POST(req: Request): Promise<Response> {
const copilotKit = new CopilotRuntime({});
const openaiModel = process.env["OPENAI_MODEL"];
return copilotKit.response(req, new OpenAIAdapter({model: openaiModel}));
}
要将应用程序连接到后端 API 路由,将以下代码片段复制到 dashboard/page.tsx 文件中。
"use client";
import App from "@/app/components/App";
import _ from "lodash";
import { useState } from "react";
import { availableSchedule } from "../util";
//👇🏻 CopilotKit 组件
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotPopup } from "@copilotkit/react-ui";
//👇🏻 CopilotKit 组件的 CSS 样式
import "@copilotkit/react-ui/styles.css";
import "@copilotkit/react-textarea/styles.css";
export default function Dashboard() {
const [yourSchedule, updateYourSchedule] = useState<AvailableScheduleItem[]>(
_.cloneDeep(availableSchedule)
);
//👉🏻 其他 UI 状态和函数
return (
<CopilotKit runtimeUrl='/api/copilotkit/'>
<App
yourSchedule={yourSchedule}
updateYourSchedule={updateYourSchedule}
/>
<CopilotPopup
instructions='Help the user create and manage ad campaigns.'
defaultOpen={true}
labels={{
title: "Posts Scheduler Copilot",
initial:
"Hello there! I can help you manage your schedule. What do you want to do? You can generate posts, add, and delete scheduled posts.",
}}
clickOutsideToClose={false}
></CopilotPopup>
</CopilotKit>
);
}
CopilotKit 组件包装整个应用程序,并接受一个 runtimeUrl 属性,其中包含指向 API 端点的链接。CopilotPopup 组件向应用程序添加一个聊天机器人侧边栏面板,使我们能够为 CopilotKit 提供各种指令。
CopilotKit 提供了两个 hook,使我们能够处理用户请求并接入应用程序状态:useCopilotAction 和 useCopilotReadable。
useCopilotAction Hook 允许你定义由 CopilotKit 执行的操作。它接受一个包含以下参数的对象:
name - 操作的名称。
description - 操作的描述。
parameters - 包含所需参数列表的数组。
render - 默认的自定义函数或字符串。
handler - 操作触发时执行的函数。
useCopilotAction({
name: "sayHello",
description: "Say hello to someone.",
parameters: [
{
name: "name",
type: "string",
description: "name of the person to say greet",
},
],
render: "Process greeting message...",
handler: async ({ name }) => {
alert(`Hello, ${name}!`);
},
});
useCopilotReadable Hook 将应用状态提供给 CopilotKit。
import { useCopilotReadable } from "@copilotkit/react-core";
const myAppState = "...";
useCopilotReadable({
description: "The current state of the app",
value: myAppState
});
现在,让我们将应用状态接入 CopilotKit,并创建一个帮助我们安排帖子发布时间的操作。
在 App 组件中,将 schedule 状态传递给 CopilotKit。你还可以提供额外信息(上下文),让 CopilotKit 能够作出恰当且精准的决策。
//👇🏻 Application state
useCopilotReadable({
description: "The user's Twitter post schedule",
value: yourSchedule,
});
//👇🏻 Application context
useCopilotReadable({
description: "Guidelines for the user's Twitter post schedule",
value:
"Your schedule is displayed in a table format. Each row represents an hour of the day, and each column represents a day of the week. You can add a post by clicking on an empty cell, and delete a post by clicking on a filled cell. Sunday is the first day of the week and has a day_id of 0.",
});
创建一个 CopilotKit 操作,根据用户的提示安排帖子发布时间:
useCopilotAction({
name: "updatePostSchedule",
description: "Update the user's Twitter post schedule",
parameters: [
{
name: "update_schedule",
type: "object",
description: "The user's updated post schedule",
attributes: [
{
name: "time",
type: "number",
description: "The time of the post",
},
{
name: "schedule",
type: "object[]",
description: "The schedule for the time",
attributes: [
{
name: "content",
type: "string",
description: "The content of the post",
},
{
name: "minutes",
type: "number",
description: "The minutes past the hour",
},
{
name: "published",
type: "boolean",
description: "Whether the post is published",
},
{
name: "day",
type: "number",
description: "The day of the week",
},
],
},
],
},
],
handler: ({ update_schedule }) => {
setAddEventModal(true);
setSelectedCell({
day_id: update_schedule.schedule[0].day + 1,
day: tableHeadings[update_schedule.schedule[0].day + 1],
time_id: update_schedule.time,
time: formatTime(update_schedule.time),
});
setContent(update_schedule.schedule[0].content);
setMinute(update_schedule.schedule[0].minutes);
},
render: "Updating schedule...",
});
上面的代码片段展示了 useCopilotAction Hook 的实际用法。它接受一个包含 name、description、parameters、handler 和 render 属性的对象。
name 属性表示操作的名称。
description 属性简要说明该函数的作用。
parameters 数组包含一个 update_schedule 对象,该对象具有 time 和 schedule 属性。schedule 对象包含 content、minutes、published 和 day 属性。
handler 函数描述了触发时要执行的操作。在上面的示例中,handler 函数会打开 AddPost 模态框,使用 AI 生成的输入更新其中的值,并允许用户相应地调整发布计划。
在本节中,你将学习如何将帖子发布计划存储到 Redis 数据库中,以及如何创建一个定期检查发布计划并将内容发布到 X(Twitter)的任务。
首先,你需要在计算机上安装 Redis。如果你使用的是 macOS,并且已经安装了 Homebrew,请在终端中运行以下代码片段来安装 Redis:
brew --version
brew install redis
安装过程完成后,可以在终端中运行以下代码片段来测试 Redis 服务器:
redis-server
现在,你可以在应用中使用 Node.js Redis 客户端了。
在服务器上创建一个 /api/schedule API 路由,当用户添加或删除计划发布的帖子时,该路由接收整个发布计划表。
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const { schedule } = await req.json();
try {
console.log({schedule})
return NextResponse.json(
{ message: "Schedule updated!", schedule },
{