WheatField
WheatField

如何在 iMessage 上做一个 ChatGPT 聊天机器人

June 3, 20241589 words, 8 min read
Authors

随着 OpenAI 的 API 支持模型越多越丰富,价格也越来越便宜。很多人应该都结合这些 API 做了一些有趣的项目,比如聊天机器人、摘要提取等。聊天机器人可以跟 IM 平台对接,比如 Telegram、Discord、Slack 或者微信等。

但今天我们做点不一样的,我们在 iMessage 平台上做一个聊天机器人。

两个问题

要在 iMessage 上做一个聊天机器人,我们需要解决两个问题:

  1. 如何接入 iMessage 平台,也就是如何发送和接收 iMessage
  2. 如何实现聊天机器人,即如何处理消息

第二个问题比较简单,像 GPT4、Gemini 这样的模型已经可以很好地处理对话。强力的模型加上合适的 prompt,实现一个通用聊天机器人几乎没有难度。

难点在第一个,iMessage 是一个封闭的生态,没有公开的 API 或 SDK。那我们如何接入 iMessage 平台呢?

简单的来说,我们用 AppleScript 来发送消息,通过读取 iMessage 数据库来获取消息。

困难的地方在于,要做到这两点,首先需要有一台 Mac,因为

  1. 需要用到 AppleScript;
  2. 需要为 bot 配置一个 iMessage 账号,然后使用这个账号是用来接收和发送消息。

有了这两个条件,我们就可以开搞了。

问题一:接入 iMessage 平台

发送消息

iMessage 没有公开的 API,我们无法直接通过代码来发送消息。但有一个间接的方法,那就是通过 AppleScript 来实现。

AppleScript 是苹果自带的一个脚本语言,旨在让用户能够通过脚本来控制 Mac 上的应用程序。我们可以通过 AppleScript 来操控 iMessage 给特定用户发送消息。

例如我们想给用户 [email protected] 发送消息 Hello, Alice!,我们可以通过以下脚本来实现。

osascript -e 'tell application "Messages" to send "Hello, Alice!" to buddy "[email protected]"'

这里的 osascript 是 macOS 中的一个命令行工具,用于运行 AppleScript 和其他 OSA(Open Scripting Architecture)脚本。有了这个办法,我们可以通过 Python 来封装这个命令,把它当作一个接口来使用。

def send_message(user, message):
    os.system(f"osascript -e 'tell application "Messages" to send "{message}" to buddy "{user}"'")

体验如何?

实际使用上,这个接口执行速度还是非常快的,基本上发出去的消息几乎感觉不到延迟,跟直接在 iMessage 上聊天感觉差不多。

当然也不是完全没有问题,并行发送多条消息时,即使是通过(真)多线程调用接口,实际上仍然是一个一个发送的,每条消息之前有一定的延迟。那如果这个机器人要处理大量消息,可能就会有明显的延迟了。

接收消息

按道理来说,我们也可以通过 AppleScript 来实现接收消息,但实际上并不是很好用。笔者测试下来发现总是报错,后来几经波折,总算找到了一个更好的方法。

这个方法是通过直接读取 iMessage 的数据库文件 ~/Library/Messages/chat.db 来获取消息。chat.db 数据库中包含了多个表,分别用于存储消息、联系人、聊天会话等信息。比如:

  1. attachment: 存储消息中的附件(如图片、视频、文件等)的信息。
  2. chat: 存储聊天会话的信息。
  3. chat_handle_join: 一个连接表,用于将聊天会话与联系人(handle)关联起来。
  4. chat_message_join: 一个连接表,用于将聊天会话与消息(message)关联起来。
  5. deleted_messages: 存储已删除的消息。
  6. handle: 存储联系人信息,如电话号码或电子邮件地址。
  7. message: 存储所有的消息内容。

对实现一个聊天机器人来说,比较重要的两个表是 messagehandle

Message 表存储了所有的消息,包括发送者、接收者、消息内容等一共 81 个字段(部分截取字段如下表)。

# sqlite3 ~/Library/Messages/chat.db "PRAGMA table_info(message);
0|ROWID|INTEGER|0||1
1|guid|TEXT|1||0
2|text|TEXT|0||0
3|replace|INTEGER|0|0|0
4|service_center|TEXT|0||0
5|handle_id|INTEGER|0|0|0
6|subject|TEXT|0||0
7|country|TEXT|0||0
...
78|part_count|INTEGER|0||0
79|is_stewie|INTEGER|0|0|0
80|is_kt_verified|INTEGER|0|0|0

Handle 表存储了联系人信息,包括联系人的 id、电话号码、邮箱等信息。如果要获取某个特定联系人的消息,比如 [email protected],可以通过联合表 messagehandle 来查询。

SELECT
    datetime(message.date / 1000000000 + strftime('%s','2001-01-01 00:00:00'), 'unixepoch', 'localtime') as date,
    message.is_from_me,
    message.text
FROM message
JOIN handle ON message.handle_id = handle.ROWID
WHERE handle.id = [email protected]
ORDER BY message.date;

其中 handle.id 就是用户的 iMessage 账号,可以是邮箱、电话号码等。从这里拿到消息后,我们可以通过一些逻辑来处理消息,比如把消息传给模型来生成回复。

问题二:接入模型

这一块就比较容易处理了,Gemini、OpenAI 的 API 也都比较好用,只要把消息传给模型,然后把模型生成的回复发送出去就可以了。

from openai import OpenAI
client = OpenAI(api_key="your-api-key")

def generate_reply(message:str)->str:
    response =  client.create(
        model="gpt-4.0-turbo",
        messages=[
          {"role": "system", "content": "You are a helpful assistant."},
          {"role": "user", "content": message},
        ]
    )
    return response.choices[0].message.content

更多功能

如果只是做一个简单的聊天机器人,上面的功能就够了。

但如果想支持更多功能,比如多个模型之间进行切换,system prompt 设置等,就需要从代码实现层面来支持。毕竟 iMessage 不像 Telegram 那样有丰富的 API,我们需要通过代码来实现这些功能。

参考 Telegram bot 的实现,我们可以指定以 / 开头的命令来执行特定的功能。比如:

Available commands:
1 /models, list available models
2 /setmodel <model_id>, set model
3 /help, show this help
4 <text>, generate response for text
5 /start, start the bot
6 /stop, stop the bot
7 /role, show all roles
8 /setrole <role_id>, set role
9 /info, show current model and role

命令识别通过规则匹配,然后执行相应的功能。

小结

本文介绍了如何在 iMessage 平台上实现一个聊天机器人。实现方式是通过 AppleScript 来发送信息,通过读取 iMessage 数据库来接收消息。聊天部分则通过调用 OpenAI 的 API 来实现。

在 iMessage 上做一个聊天机器人的好处是,无需要安装额外的应用,随时随地都可以在 iPhone, iPad 上进行聊天。当然也有一些局限性,比如需要有 Mac 设备,需要配置 iMessage 账号等。