2023年的12月29日晚上,掘金的年度打榜直播,我作为嘉宾之一,参与了活动,并现场用 ChatGPT 来组织了抽奖。
这是当时的活动截图:
在现场我也承诺了大家,把这个抽奖助手具体如何实现,写一篇详细的文章,所以今天我将这篇文章整理出来,分享给大家。
技术实现
接下来,我来介绍具体如何实现这个抽奖助手。
准备工作
为了实现 ChatGPT 抽奖助手,我们事先准备三个工具:
1. OpenAI ChatGPT Plus 账号
我们直接在 ChatGPT Plus 中开发 GPTs,这个你可以理解为 ChatGPT 官方支持的个性化定制的 ChatGPT 助手。如果你不了解什么是 GPTs,具体介绍详见OpenAI 的官方博客。
2. Google Cloud 账号
在记录参与者、奖项和获奖信息时,我选择使用 Google Sheets,所以我们需要一个 Google Cloud 账号,以便于授权 Google Sheets,让 ChatGPT 能够访问。
3. AirCode.io 账号
AirCode.io 是一个快速开发和免费部署 Node.js 服务的 Serverless 平台,我们用它来实现具体的数据接口。
配置 Google Sheets
首先我们在 Google cloud 的控制台点击 API & Services:
然后在左侧菜单点击 Credentials:
- 点击右侧 CREATE CREDENTIALS 按钮
- 选择创建一个 Service account
创建之后,复制这个 Service account 的 Email 地址以备后续使用。
点击 Service Account 右侧的编辑按钮,进入编辑状态。
切换到 KEYS Tab 页,选择 ADD KEY,创建一个 API KEY。
默认选择创建 JSON Key。
当这个 KEY 被创建成功之后,浏览器会默认下载一个 JSON 文件,将它的内容保存下来。
现在我们到 Google Doc Spreadsheets 里创建任意一个 Sheets,将它的 ID 记录下来,就是 https://docs.google.com/spreadsheets/d/
后的那一串字符串。
然后我们点击右上角的 Share 按钮,将我们创建的这个表格的权限授予刚才记录的 Service account 的 Email 地址。
这样我们就完成了 Google Sheets 的授权。
实现操作 Google Sheets 的 API
接下来,我们要具体实现抽奖功能。
我们在 AirCode 中创建一个项目,在 IDE 中先创建一个 lib 文件夹,然后创建两个文件,credentials.json
和 sheets.js
。
其中 credentials.json
文件的内容就是我们刚才在 Google Cloud 创建 KEY 时下载的 JSON 文件内容。
然后 sheets.js
文件的内容如下:
js
const {google} = require('googleapis');
const auth = new google.auth.GoogleAuth({
keyFile: './lib/credentials.json',
scopes: 'https://www.googleapis.com/auth/spreadsheets',
});
// Instance of Google Sheets API
module.exports = async function getSheets() {
// Create client instance for auth
const client = await auth.getClient();
return google.sheets({version: 'v4', auth: client});
}
注意我们要安装一下依赖包 googleapis
。
接着我们实现三个接口:
- index 抽奖的接口
- prices 奖品信息接口
- winners 获奖人名单接口
首先是奖品信息接口,这个最简单:
js
// @see https://docs.aircode.io/guide/functions/
const aircode = require('aircode');
const getSheets = require('./lib/sheets');
const spreadsheetId = process.env.SHEET_ID;
module.exports = async function () {
const googleSheets = await getSheets();
const getRows = await googleSheets.spreadsheets.values.get({
spreadsheetId,
range: 'Sheet2!A2:C9999',
});
if(getRows.data.values) {
const prices = getRows.data.values.map(v => {
return {
name: v[0],
summary: v[1],
description: v[2],
}
});
return {prices};
}
return {
error: 'Unknow error.',
};
};
上面的代码很简单,就是读取 Google Sheets 的 Sheet2
里的数据,我将奖品信息保存在这里。
注意 spreadsheetId
就是要操作的表格 ID,也就是前面记录下来的那一串字符串。AirCode.io 的 IDE 支持配置环境变量,我将它记录在 process.env.SHEET_ID
里。
这样,prices 接口就能返回奖品信息了。
读取获奖人信息的 winners 接口类似:
js
// @see https://docs.aircode.io/guide/functions/
const aircode = require('aircode');
const getSheets = require('./lib/sheets');
const spreadsheetId = process.env.SHEET_ID;
module.exports = async function () {
const googleSheets = await getSheets();
const getRows = await googleSheets.spreadsheets.values.get({
spreadsheetId,
range: 'Sheet3!A2:C9999',
});
if(getRows.data.values) {
const winners = getRows.data.values.map(v => {
return {
phone: v[0],
prices: v[1],
}
});
return {winners};
}
return {
error: '还没有人中奖',
winners: [],
};
};
这里我们将中奖信息保存在 Sheet3
中。
最后是稍微复杂一些的抽奖接口:
首先我们实现一些依赖的函数。
js
const getPrices = require('./prices');
async function getPriceByType(type) {
const prices = (await getPrices()).prices;
return prices.filter(p => p.name === type)[0];
}
上面这个函数根据抽奖类型返回对应的奖品信息。
接着我们实现具体的抽奖接口:
js
function pick(candidates, winners, count) {
const avaliableCandidates = candidates.filter(c => {
return winners.filter((w) => w.phone !== c.phone)
});
const picked = [];
for(let i = 0; i < count; i++) {
const idx = Math.floor(Math.random() * avaliableCandidates.length);
[avaliableCandidates[idx], avaliableCandidates[avaliableCandidates.length - 1]]
= [avaliableCandidates[avaliableCandidates.length - 1], avaliableCandidates[idx]];
picked.push(avaliableCandidates.pop());
if(avaliableCandidates.length <= 0) break;
}
return picked;
}
上面的代码也不是很复杂,首先我们从参与者名单中过滤一下已经中奖的人,避免重复中奖,接着我们就随机抽出count
个中奖者,将获奖者信息用数组返回。
接着就是具体的抽奖过程:
js
const getSheets = require('./lib/sheets');
const getWinners = require('./winners');
const spreadsheetId = process.env.SHEET_ID;
module.exports = async function (params, context) {
const {type, count} = params;
const price = await getPriceByType(type);
if(!price) {
return {
error: '没有这个奖项',
}
}
// Read rows from spreadsheet
const googleSheets = await getSheets();
const getRows = await googleSheets.spreadsheets.values.get({
spreadsheetId,
range: 'Sheet1!A2:B9999',
});
if(getRows.data.values) {
const candidates = getRows.data.values.map(v => {
return {
phone: v[0],
};
});
const winners = (await getWinners()).winners;
const lottery = pick(candidates, winners, count).map((l) => {
return {
...l,
price,
};
});
const result = await googleSheets.spreadsheets.values.append({
spreadsheetId,
range: 'Sheet3!A2:D9999',
valueInputOption: "USER_ENTERED", // RAW | USER_ENTERED
resource: {
values: lottery.map(l => [l.phone, l.price.name, l.price.summary]),
},
});
return {winners: lottery};
}
return {
error: 'Unknow error.',
};
};
我们先从Sheet1
中获取所有的抽奖者信息,这里主要是手机号,然后是获取奖品信息和已中奖者信息,最后我们进行抽奖,将中奖者和中奖的奖品信息返回。
注意我们将本轮中奖者通过 append
方法添加到 Sheet3
的表格中。
这样我们的接口就实现完成了,只要点击 IDE 右上角的 Deploy
将它发布到线上就可以了。
完整的代码我放在 Github仓库 里,有兴趣的同学可以去进一步研究。
创建 GPTs
有了 API,创建 GPTs 的过程极其简单。
因为有 Google Sheets 提供数据,GPTs 不需要配置知识库,只需要配置一些基本信息。
最主要的是 Instructions,它就是完成抽奖工作的具体 Prompt,内容如下:
markdown
## 角色
你是一个抽奖助手,你负责抽奖。
## 任务
1. 你先从接口读取数据,介绍一下今天准备了什么样的奖项,对奖品做一个简单的介绍,然后根据奖品设置画一张宣传图。
2. 当我说开始抽某个奖项时,你开始抽奖,我会告诉你抽奖的人数,如果我没告诉你,你要问我。
3. 抽奖之后,你宣布中奖人名单。
4. 当我问你到目前为止都有哪些人中奖时,你给我已经中奖的名单。
基本配置完成之后,我们添加 Actions,这是一份 JSON:
json
{
"openapi": "3.1.0",
"info": {
"title": "Lottery Assistant",
"description": "抽奖小助手",
"version": "v1.0.0"
},
"servers": [
{
"url": "https://89x633zc21.us.aircode.run"
}
],
"paths": {
"/index": {
"get": {
"description": "开始抽奖",
"operationId": "StartLottery",
"parameters": [
{
"name": "type",
"in": "query",
"description": "奖项名称",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "count",
"in": "query",
"description": "奖项数量",
"required": true,
"schema": {
"type": "number"
}
}
],
"deprecated": false
}
},
"/prices": {
"get": {
"description": "读取奖项信息",
"operationId": "getPricesInfo",
"parameters": [
],
"deprecated": false
}
},
"/winners": {
"get": {
"description": "读取已获奖人名单",
"operationId": "getWinners",
"parameters": [
],
"deprecated": false
}
},
},
"components": {
"schemas": {}
}
}
好了,这样我们就完成了 GPTs 的所有配置,把它发布出来,我们就可以愉快地用它进行抽奖了。
总结
ChatGPT 非常聪明,能够用自然语言推理并理解我们的意图,提供的 API 相当于给 ChatGPT 一系列工具,比如这个抽奖的 GPTs,我们就提供了 /index
、 /prices
和 /winners
三个 API,ChatGPT 会在需要的时候自己调用这三个 API 来完成具体工作。
我们也可以使用 ChatGPT 配合 API 来做其他一些事情,完成许多有趣和有意义的工作,比如绘制脑图、流程图等等。
我会逐步开发和整理一些有用的 Actions,放在这个 Github 仓库 里,有兴趣的同学可以关注和使用它们。
以上是本文的所有内容,如有任何问题,欢迎在评论区讨论。