把活动通知复制进日历,看似只是写一句话,实际却容易把开始时间、结束时间和地点混在一起。本文用 Gemini API 辅助起草活动说明,再由本地 Python 根据人工确认的字段制作日历文件。最终得到的是等待审核与手动导入的 ICS 草稿,不是自动发送的会议邀请。

练习适合会使用终端、希望把模型能力接入小工具的读者。本文提供原创虚构活动和可复查的代码;模型请求示例没有实际调用付费接口,不把示例文字冒充模型生成结果。本地日历拼装与折行逻辑可以独立检查,账户开通、模型可用性及导入显示需要在自己的环境验收。

一、先分清模型、脚本和日历的职责

模型只负责把已知议程写成简短说明。时间、地点与标题由人确认,不能让模型根据“下周一下午”自行推测。本地脚本负责转义文字、组织日历结构并写入新文件;日历软件负责实际导入与显示,最后仍需人工核对。

本篇使用 Google 官方当前入门文档中的 Python SDK 与 Interactions 调用方式。Gemini 是云端模型服务,安装 SDK 只是安装客户端,不会把模型部署到本地。使用 Gemini 网页聊天的账号体验,也不等于已经获得 API 配额或相同计费条件。

练习不连接邮箱,不添加参会人,不发送通知,也不替读者改已有日程。首次操作请使用单独的测试日历;如果导入软件要求选择目标日历,先确认它不是团队生产日历。

模型起草说明、人工确认字段、本地生成草稿的职责示意

二、开通 API 并准备隔离环境

先访问官方 AI Studio 的 API 密钥页面,按账号实际提示创建或选择项目,确认该项目的使用条件、可用地区、模型与配额。是否需要验证或开通结算,以当前项目页面为准。本文不承诺所有账号都能免费使用,也不把登录网页成功当作接口已经开通。

在新目录中建立 Python 虚拟环境。安装前检查当前 SDK 的 Python 版本要求,不要默认系统里任意旧版本都兼容。Windows PowerShell 的基本步骤如下;如果系统的 Python 启动命令不同,先确认解释器位置再调整。

mkdir calendar-draft
cd calendar-draft
python -m venv .venv
.venv\Scripts\python -m pip install -U google-genai

本教程直接使用虚拟环境里的解释器,不需要先修改系统执行策略。API 密钥放到当前进程的环境变量 GEMINI_API_KEY,模型标识放到 GEMINI_MODEL。前者使用自己的项目密钥,后者从账号实际可用模型中选择,并对照官方文档确认支持所用接口。

不要把密钥放进日历、提示词、公开仓库或截图。本文不提供真实密钥,也不要求打印环境变量。若密钥已泄露,应在控制台处理旧密钥,再更新本地配置;仅删掉截图不能撤销已有访问能力。

三、先制作一份不含歧义的活动卡

以下内容完全虚构:标题为“阅读会准备会”,北京时间二〇三〇年五月二十日十四点开始,十四点三十分结束,地点“线上测试室”。议程仅包括确认阅读材料、分配主持工作,不提供真实会议链接与参与者信息。

这张活动卡是唯一事实来源。开始与结束使用带时区的明确时间,不使用“明天下午两点”。如果现实任务还没有确定结束时间,应先向活动负责人确认,不能因为模型写得顺畅就补一个默认时长。

可以把下面代码保存为 write_description.py。它根据官方示例使用 genai.Client() 和 client.interactions.create(),模型名称由环境提供,输出仅写入说明草稿。运行前确认输入确实可以交给云服务处理。

import os
from pathlib import Path
from google import genai

target = Path("description-draft.txt")
if target.exists():
    raise FileExistsError("已有草稿,请先审核或另存")
model = os.environ["GEMINI_MODEL"]
client = genai.Client()
result = client.interactions.create(
    model=model,
    input=("请用中文写一段简短的活动说明,只用以下事实:"
           "阅读会准备会;议程为确认阅读材料、分配主持工作。"
           "不添加时间、地点、链接、承诺或参会名单。"
           "只输出说明正文,不输出代码或日历格式。"),
)
text = result.output_text
if not isinstance(text, str) or not text.strip():
    raise ValueError("没有可审核的说明正文")
with target.open("x", encoding="utf-8") as f:
    f.write(text.strip())
print("已保存说明草稿,尚未导入或发送")

通过虚拟环境解释器运行该文件。若接口未开通或请求失败,不要反复无上限重试;先看错误类别、项目状态与配额。程序退出并不意味着已经生成合格草稿,必须确认文件存在、内容非空,而且没有新增事实。

四、人工确认后另存通过审核的说明

打开说明草稿,逐句对照活动卡。若出现“将提供录播”“欢迎所有人参加”等原材料没有的承诺,应删去并记录,而不是认为这些只是润色。内容没有越界后,另存为 description-approved.txt,保留原草稿用于回查。

审核后的示例可以是“本次准备会将确认阅读材料,并分配主持工作。”这句话是本文人工编写的预期示例,不是一次模型实测结果。暂时无法调用 API 时,也可以先用这句文字练习下面的本地步骤,但应明确记录没有完成模型调用。

不要把模型返回的 ICS 代码直接当作成品。它可能看似格式完整,却把时区、结束时间、换行或转义写错。本练习让模型只处理说明,日历的重要字段始终来自人工确认记录,减少难以发现的自动改写。

五、本地生成一个最小日历草稿

将下面代码保存为 make_calendar.py。示例使用固定 UTC+8 的虚构北京时间,并转换成以 Z 结尾的 UTC 时间。它仅处理本次单个事件,不承担重复日程、复杂时区规则或会议邀请协议等额外功能。

from pathlib import Path
from datetime import datetime, timezone, timedelta
from uuid import uuid4

def escape_text(s):
    s = s.replace("\r\n", "\n").replace("\r", "\n")
    return (s.replace("\\", "\\\\").replace("\n", "\\n")
             .replace(";", "\\;").replace(",", "\\,"))

def fold(line):
    parts, current = [], ""
    for char in line:
        if len((current + char).encode("utf-8")) > 75:
            parts.append(current)
            current = " "
        current += char
    parts.append(current)
    return "\r\n".join(parts)

def build_calendar(description):
    local = timezone(timedelta(hours=8))
    start = datetime(2030, 5, 20, 14, 0, tzinfo=local)
    end = datetime(2030, 5, 20, 14, 30, tzinfo=local)
    if end <= start:
        raise ValueError("结束必须晚于开始")
    stamp = lambda t: t.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    rows = ["BEGIN:VCALENDAR", "VERSION:2.0",
            "PRODID:-//Zhixingzhiwai//Calendar Draft//ZH",
            "BEGIN:VEVENT", "UID:" + str(uuid4()) + "@example.invalid",
            "DTSTAMP:" + stamp(datetime.now(timezone.utc)),
            "DTSTART:" + stamp(start), "DTEND:" + stamp(end),
            "SUMMARY:" + escape_text("阅读会准备会"),
            "LOCATION:" + escape_text("线上测试室"),
            "DESCRIPTION:" + escape_text(description),
            "END:VEVENT", "END:VCALENDAR"]
    return "\r\n".join(fold(row) for row in rows) + "\r\n"

if __name__ == "__main__":
    description = Path("description-approved.txt").read_text("utf-8").strip()
    if not description:
        raise ValueError("审核后的说明不能为空")
    with Path("event-draft.ics").open("xb") as f:
        f.write(build_calendar(description).encode("utf-8"))
    print("仅生成本地草稿,请手动验收")

日历使用回车换行,文本中的逗号、分号、反斜杠与换行需按格式转义。长行按 UTF-8 字节长度折行,不能把中文字符简单当成一个字节,也不能在一个字符的字节中间切开。折行后的续行开头保留一个空格,供读取端还原。

脚本使用新建模式写文件;已有同名草稿时会停止,不覆盖它。若要修改活动,应先明确是替换已有事件还是生成另一份测试草稿。本例每次生成新的 UID,重复导入可能形成多个事件,不能把再次运行误解为更新原日程。

时间字段、文字转义与手动导入三道检查的示意

六、导入前后分别验收

导入前先用文本编辑器检查标题、地点、说明和时间字段。示例开始时间应为 20300520T060000Z,结束为 20300520T063000Z。这是固定北京时间转 UTC 后的结果,不是让用户把当地时钟改成六点。

随后把文件导入测试日历,按该软件官方支持的入口操作,不把双击文件的行为当作所有系统一致。确认软件显示时区、日期、十四点到十四点三十分的半小时时长、中文地点与完整说明。若软件当前显示另一时区,显示钟点可能不同,应同时核对时区与实际时刻。

测试中再用含逗号、分号、反斜杠和多行中文的说明检查文本还原;确认长说明不会截断或出现乱码。只看文件扩展名正确不够,程序写出文件也不等于目标日历已验收。本文的离线检查不替代实际客户端导入测试。

七、排错与后续落地边界

如果提示找不到模块,先确认安装和运行都使用同一虚拟环境;认证或权限失败,检查项目与密钥归属;模型不存在或接口不支持,回到账号可用模型及当前官方文档核对,不靠猜测更换一串名称。空输出时保留错误记录,不自动写入“活动待定”掩盖失败。

导入后差八小时,优先检查是否错误地把本地时间直接加上 Z;出现乱码,检查编码及折行;出现重复事件,检查是否反复导入了不同 UID 的草稿。删除测试事件时只处理本次明确生成的测试对象,不批量清理整个日历。

落地成团队工具前,还需要补充授权、活动版本、修改流程、重复检测与目标客户端测试。若未来增加发送邀请或参会人,必须重新设计权限与确认步骤;不能因为本地文件生成成功,就直接开通邮件发送。可靠的小工具从清楚的职责边界开始,而不是把更多操作都交给同一个提示词。

八、把这次练习留下可追溯的记录

最后保存活动卡、说明原稿、审核说明、生成脚本与导入检查结果,分别标注哪些经过实际检查、哪些仍待验证。不要在日志里保留密钥;错误排查只需要记录接口类型、错误类别、所选模型和运行环境,不需要公开完整账号信息。

如果准备处理多场活动,先约定稳定的活动编号和修改规则,再做批量导入。本例的随机标识适合隔离练习,并不具备根据业务编号更新日程的能力。面对变更,应先确认目标事件和日历软件的行为,不能简单重跑脚本期待所有旧内容自动同步。先完成一个可核验的小闭环,再扩展批量功能,能够避免把一个时间错误复制到许多人的日历里。

官方参考