上班族最真实的需求,往往不是"写多炫的代码",而是"把重复的办公琐事自动化"。这一课,我们把 Skill 的枪口对准三类高频场景:协作办公、文档产出、全栈开发。
一、飞书 Skill:办公流协同
飞书是很多团队的协作中枢。飞书 Skill / CLI 能帮你:
- 发消息、建群:让 Codex 把执行结果自动推送到群里;
- 操作文档、表格:读写云文档、多维表格;
- 编排审批/任务:把 AI 能力接入团队既有流程。
典型场景:Codex 跑完一份周报,自动通过飞书发给对应群;或从飞书表格里读取数据、处理后写回。AI 从"站在旁边"变成"融进流程"。
示例:发送消息到群
要把消息发到飞书群,需要先拿到应用的访问凭证 tenant_access_token,再调用发送消息接口。下面用 Python requests 演示:
import os
import requests
# 1. 配置凭证:建议从环境变量读取,不要硬编码在代码里
APP_ID = os.getenv("FEISHU_APP_ID", "cli_xxxxxxxx")
APP_SECRET = os.getenv("FEISHU_APP_SECRET", "xxxxxxxx")
# 2. 获取 tenant_access_token(自建应用使用 internal 接口)
token_resp = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": APP_ID, "app_secret": APP_SECRET},
timeout=10,
)
token_resp.raise_for_status()
tenant_access_token = token_resp.json()["tenant_access_token"]
# 3. 发送文本消息到群聊
CHAT_ID = "oc_xxxxxxxx" # 群聊的 chat_id,可在飞书群设置或事件中获取
headers = {
"Authorization": f"Bearer {tenant_access_token}",
"Content-Type": "application/json; charset=utf-8",
}
payload = {
"receive_id": CHAT_ID,
"msg_type": "text",
"content": '{"text": "Codex 本周周报已生成,请查收。"}',
}
send_resp = requests.post(
"https://open.feishu.cn/open-apis/im/v1/messages",
params={"receive_id_type": "chat_id"},
headers=headers,
json=payload,
timeout=10,
)
send_resp.raise_for_status()
print("发送结果:", send_resp.json())
如何配置凭证:在飞书开放平台创建自建应用,进入「凭证与基础信息」页获取 App ID 和 App Secret;将机器人加入目标群聊,并在「权限管理」中开通 im:message、im:message:send_as_bot 等消息发送权限。建议把 FEISHU_APP_ID、FEISHU_APP_SECRET 写入环境变量或 .env 文件,避免把密钥提交到仓库。
常见错误处理与排查
实际联调时,常见的三类问题可以这样定位:
- token 获取失败:先检查 APP_ID、APP_SECRET 是否来自同一个自建应用,再确认网络能否访问 open.feishu.cn;响应体里 code != 0 时,直接打印 msg 通常能定位问题。
- 权限不足:机器人需要有 im:message、im:message:send_as_bot 等权限,并且已经被拉进目标群聊;否则接口会返回“无权限”相关提示。
- chat_id 无效:确认发送时使用 receive_id_type=chat_id,并且机器人确实在该群中;群主或管理员可在群设置中获取 chat_id。
下面把前面的发送逻辑封装成一个带异常捕获的版本,便于把问题打印出来快速排查:
import json
import os
import requests
APP_ID = os.getenv("FEISHU_APP_ID", "cli_xxxxxxxx")
APP_SECRET = os.getenv("FEISHU_APP_SECRET", "xxxxxxxx")
def get_tenant_access_token():
"""获取 tenant_access_token,失败时抛出带 msg 的异常。"""
resp = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": APP_ID, "app_secret": APP_SECRET},
timeout=10,
)
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"获取 token 失败:code={data.get('code')}, msg={data.get('msg')}")
return data["tenant_access_token"]
def send_text_to_chat(chat_id, text):
"""发送文本消息,并针对 token、权限、chat_id 三类常见错误给出排查提示。"""
try:
token = get_tenant_access_token()
except Exception as exc:
print("[token 获取失败] 请检查 APP_ID/APP_SECRET 是否正确、网络是否可达:", exc)
return False
url = "https://open.feishu.cn/open-apis/im/v1/messages"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=utf-8",
}
payload = {
"receive_id": chat_id,
"msg_type": "text",
"content": json.dumps({"text": text}, ensure_ascii=False),
}
try:
resp = requests.post(
url,
params={"receive_id_type": "chat_id"},
headers=headers,
json=payload,
timeout=10,
)
data = resp.json()
except requests.RequestException as exc:
print("[网络请求异常] 请检查本地网络、代理或超时设置:", exc)
return False
if data.get("code") != 0:
msg = str(data.get("msg", ""))
if "permission" in msg.lower() or "权限" in msg:
print("[权限不足] 请检查应用是否开通 im:message 等权限,以及机器人是否已加入该群。")
elif "chat" in msg.lower() or "not found" in msg.lower():
print("[chat_id 无效] 请确认 receive_id_type=chat_id,且机器人已加入目标群聊。")
else:
print(f"[发送失败] code={data.get('code')}, msg={msg}")
return False
print("发送成功:", data)
return True
这样,发送消息时遇到 token、权限或群聊参数问题,都能在日志里快速定位到原因,而不是只看到一句“请求失败”。
实战示例:读写飞书多维表格
多维表格(Bitable)常用于把任务、需求、跟进记录等结构化数据存成在线表格。飞书 API 同样通过 tenant_access_token 访问。下面用 Python requests 演示「读取记录 + 写入一条记录」的最小闭环。
关键参数怎么拿?
- app_token:打开多维表格后,浏览器地址栏里 /base/ 后面的字符串;
- table_id:地址栏里 ?table= 后面的字符串,或调用「列出数据表」接口获取;
- 字段名:可以直接看表格表头,也可以调用字段接口 GET /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields 拿到完整的字段名与类型。
import os
import requests
APP_ID = os.getenv("FEISHU_APP_ID", "cli_xxxxxxxx")
APP_SECRET = os.getenv("FEISHU_APP_SECRET", "xxxxxxxx")
APP_TOKEN = os.getenv("FEISHU_BITABLE_APP_TOKEN", "bascnxxxxxxxx")
TABLE_ID = os.getenv("FEISHU_BITABLE_TABLE_ID", "tblxxxxxxxx")
def get_tenant_access_token():
"""获取 tenant_access_token。"""
resp = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": APP_ID, "app_secret": APP_SECRET},
timeout=10,
)
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"获取 token 失败:code={data.get('code')}, msg={data.get('msg')}")
return data["tenant_access_token"]
def api_headers(token):
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=utf-8",
}
def list_fields(token):
"""读取表的字段定义,确认字段名和类型。"""
url = (
"https://open.feishu.cn/open-apis/bitable/v1/apps"
f"/{APP_TOKEN}/tables/{TABLE_ID}/fields"
)
resp = requests.get(url, headers=api_headers(token), timeout=10)
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"读取字段失败:code={data.get('code')}, msg={data.get('msg')}")
return data["data"]["items"]
def list_records(token):
"""读取多维表格中的记录。"""
url = (
"https://open.feishu.cn/open-apis/bitable/v1/apps"
f"/{APP_TOKEN}/tables/{TABLE_ID}/records/search"
)
resp = requests.post(url, headers=api_headers(token), json={"page_size": 50}, timeout=10)
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"读取记录失败:code={data.get('code')}, msg={data.get('msg')}")
return data["data"]["items"]
def create_record(token, fields):
"""新增一条记录,fields 的 key 要与表格字段名完全一致。"""
url = (
"https://open.feishu.cn/open-apis/bitable/v1/apps"
f"/{APP_TOKEN}/tables/{TABLE_ID}/records"
)
resp = requests.post(url, headers=api_headers(token), json={"fields": fields}, timeout=10)
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"写入记录失败:code={data.get('code')}, msg={data.get('msg')}")
return data["data"]["record"]
token = get_tenant_access_token()
# 1) 先看表里有哪些字段,避免字段名写错
for f in list_fields(token):
print("字段名:", f["field_name"], "| 类型:", f["type"])
# 2) 读取已有记录,fields 中就是「字段名 → 字段值」
records = list_records(token)
for r in records:
print("记录 id:", r["record_id"], "| 内容:", r["fields"])
# 3) 写入一条新记录;单行文本字段直接传字符串,多选/人员等字段需要传列表
new_record = create_record(token, {"任务名称": "整理飞书多维表格脚本", "状态": "进行中"})
print("新增记录 id:", new_record["record_id"])
常见错误排查
- 读不到字段或记录,但 code == 0:先确认 APP_TOKEN、TABLE_ID 是否正确;从地址栏复制的 app_token 不要带 /base/ 前缀。
- 字段写入报错或值写入后为空:字段名必须与表格里完全一致;多选、人员、附件等字段要按对应格式传值,可以用 list_fields 返回的 type 做核对。
- 权限不足:确认应用已开通多维表格权限(如 bitable:app 读写权限),并且应用被添加为多维表格的协作者,否则会返回“无权限”或读取不到数据。
批量写入多条记录
当需要一次性导入多条数据(例如把一批任务批量录入多维表格)时,逐条调用「创建记录」接口会非常慢。飞书提供了批量创建接口,一次请求即可写入最多 500 条记录。下面用 Python requests 演示:
import os
import requests
APP_ID = os.getenv("FEISHU_APP_ID", "cli_xxxxxxxx")
APP_SECRET = os.getenv("FEISHU_APP_SECRET", "xxxxxxxx")
APP_TOKEN = os.getenv("FEISHU_BITABLE_APP_TOKEN", "bascnxxxxxxxx")
TABLE_ID = os.getenv("FEISHU_BITABLE_TABLE_ID", "tblxxxxxxxx")
def get_tenant_access_token():
"""获取 tenant_access_token。"""
resp = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": APP_ID, "app_secret": APP_SECRET},
timeout=10,
)
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"获取 token 失败:code={data.get('code')}, msg={data.get('msg')}")
return data["tenant_access_token"]
def batch_create_records(token, records):
"""批量创建记录。records 是「字段名 → 字段值」的字典列表。"""
url = (
"https://open.feishu.cn/open-apis/bitable/v1/apps"
f"/{APP_TOKEN}/tables/{TABLE_ID}/records/batch_create"
)
resp = requests.post(
url,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=utf-8",
},
json={"records": [{"fields": r} for r in records]},
timeout=15,
)
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"批量写入失败:code={data.get('code')}, msg={data.get('msg')}")
return data["data"]["records"]
token = get_tenant_access_token()
# 构造 fields 列表:每个元素是一个「字段名 → 字段值」的字典
# 单行文本直接传字符串;多选、人员等字段需要传列表
new_records = [
{"任务名称": "批量导入脚本编写", "状态": "进行中"},
{"任务名称": "接口联调与验证", "状态": "待开始"},
{"任务名称": "文档整理与归档", "状态": "已完成"},
]
created = batch_create_records(token, new_records)
for record in created:
print("新增记录 id:", record["record_id"], "| 内容:", record["fields"])
如何构造 fields 列表
- 每个元素是一个字典,key 必须与表格字段名完全一致,value 是字段值;
- 单行文本、数字、日期等字段直接传对应类型的值(如字符串、数字);
- 多选、人员、附件等字段需要按飞书要求的格式传列表,例如多选字段传 ["进行中", "高优先级"],人员字段传 [{"id": "ou_xxx"}];
- 建议先用前面 list_fields 返回的字段名与 type 做一次核对,避免字段名拼写不一致导致整批失败。
处理部分失败
批量接口是「整体成功或整体失败」的语义:只要有一条记录字段校验不通过,整批都不会写入,响应体里 code != 0,msg 会给出具体失败原因。因此建议:
- 先小批量试写:正式导入前先用 1~2 条记录验证字段名和值格式,确认无误后再全量导入;
- 按条定位问题:如果批量失败,把 msg 打印出来,通常能直接看到是哪个字段、什么类型不匹配;也可以把 records 拆成单条,用前面「创建记录」接口逐条调用,定位到具体出错的那条;
- 做好幂等与去重:批量接口不会自动去重,重复调用会写入重复记录。导入前可以先读取已有记录做比对,或给表格加一个唯一标识字段(如「任务编号」)用于去重。
二、Office 自动化 Skill:产出专业交付物
Word、PPT 是职场最普遍的交付形态,也是最重复的劳动。Office 自动化 Skill 能:
- 生成 Word:把结构化内容排版成规范文档;
- 生成 PPT:把要点快速转成演示文稿;
- 读取与汇总:从既有 Office 文件里提取数据、信息。
这样,"写报告、做 PPT"这类耗时活,就能由 Codex 兜底生成初稿,你只需做最后的润色与把关。
实战示例:用 python-docx 生成规范 Word 文档
想把一份结构化数据(例如周报里的任务列表)自动转成排版规范的 Word 文档,python-docx 是最常用的 Python 方案。它既支持标题、段落,也能把列表数据渲染成表格。
先安装依赖:
pip install python-docx
下面这段代码演示了「标题 → 项目符号段落 → 表格」的完整流程:
from docx import Document
from docx.shared import Pt, RGBColor
# 1. 创建文档
doc = Document()
# 2. 标题:level 控制标题级别
doc.add_heading("项目周报", level=1)
doc.add_heading("一、本周完成事项", level=2)
# 3. 段落:把结构化列表渲染为项目符号段落
tasks = [
"完成飞书机器人消息推送联调",
"输出 Q3 数据报表并归档",
"修复订单模块两个线上问题",
]
for task in tasks:
doc.add_paragraph(task, style="List Bullet")
# 4. 自定义段落样式:加粗、字号与颜色
p = doc.add_paragraph()
run = p.add_run("以上事项均已通过人工复核。")
run.bold = True
run.font.size = Pt(10.5)
run.font.color.rgb = RGBColor(0x40, 0x40, 0x40)
# 5. 表格:把结构化数据渲染为规范表格
doc.add_heading("二、任务明细", level=2)
rows = [
("编号", "任务", "负责人", "状态"),
("1", "飞书机器人接入", "小张", "已完成"),
("2", "数据报表输出", "小李", "进行中"),
("3", "订单模块修复", "小王", "已完成"),
]
table = doc.add_table(rows=0, cols=4)
table.style = "Light Grid Accent 1" # 内置表格样式
for row_idx, row_data in enumerate(rows):
cells = table.add_row().cells
for col_idx, value in enumerate(row_data):
cells[col_idx].text = value
# 表头加粗
if row_idx == 0:
for paragraph in cells[col_idx].paragraphs:
for run in paragraph.runs:
run.bold = True
# 6. 保存文档
doc.save("周报.docx")
标题、段落、表格样式怎么设?
- 标题:add_heading(text, level) 的 level 从 0 到 4,分别对应文档标题和各级小标题,字体与编号由 Word 内置样式自动处理;
- 段落:add_paragraph(text, style="List Bullet") 直接生成项目符号列表;需要更细的控制时,可以用 add_run() 追加文本,并对单个 run 设置 bold、font.size、font.color.rgb;
- 表格:add_table(rows=0, cols=4) 创建表格,table.style 引用 Word 内置样式(如 Light Grid Accent 1),再逐行写入数据。表头加粗可以通过遍历第一行单元格里的 runs 来实现。
这样,原本需要手工排版的任务明细,就能由一段脚本稳定地输出成规范 Word 文档,你只需要在保存后做最终审阅。
读取既有 Word 文档:提取段落与表格
除了生成文档,python-docx 也支持读取已有的 .docx 文件,把里面的段落和表格内容提取出来,便于做数据汇总、内容审核或二次加工。下面演示如何遍历文档元素:
from docx import Document
# 1. 打开既有文档
doc = Document("周报.docx")
# 2. 遍历所有段落,打印段落文本
print("===== 段落内容 =====")
for i, para in enumerate(doc.paragraphs):
text = para.text.strip()
if text: # 跳过空段落
print(f"[段落 {i}] {text}")
# 3. 遍历所有表格,逐行逐单元格打印
print("\\n===== 表格内容 =====")
for t_idx, table in enumerate(doc.tables):
print(f"— 表格 {t_idx} —")
for r_idx, row in enumerate(table.rows):
cells = [cell.text.strip() for cell in row.cells]
print(f"行 {r_idx}: {cells}")
如何遍历文档元素
- doc.paragraphs:返回文档中所有顶层段落的列表,每个元素是 Paragraph 对象,用 .text 取纯文本。注意它只包含正文段落,不包含表格里的文字。
- doc.tables:返回文档中所有表格的列表,每个 Table 对象有 .rows(行)和 .columns(列);遍历 row.cells 即可拿到每个单元格,用 .text 取内容。
- 按文档顺序遍历:doc.paragraphs 和 doc.tables 是分开的,无法直接反映「段落与表格的先后顺序」。如果需要按原始排版顺序读取,可以遍历 doc.element.body 的 XML 子节点,判断每个节点是段落还是表格:
from docx import Document
from docx.table import Table
from docx.text.paragraph import Paragraph
doc = Document("周报.docx")
for child in doc.element.body.iterchildren():
if child.tag.endswith("}p"): # 段落节点
para = Paragraph(child, doc)
if para.text.strip():
print("段落:", para.text)
elif child.tag.endswith("}tbl"): # 表格节点
table = Table(child, doc)
for row in table.rows:
print("表格行:", [cell.text.strip() for cell in row.cells])
这样,无论是「只提取正文段落」「只提取表格数据」,还是「按文档原始顺序逐元素读取」,都能用 python-docx 稳定实现,方便把既有 Word 内容接入后续的自动化流程。
三、全栈 Skill:辅助开发
对开发者来说,还有一个实用的"小白全栈 Skill"方向:
- 快速生成前后端骨架;
- 提供脚手架与模板;
- 辅助数据库设计、接口定义。
它的价值在于降低"从 0 到能跑"的门槛,尤其适合原型验证和内部工具开发。
实战示例:用 Codex 生成待办事项应用
让 Codex 生成一个可运行的前后端骨架,往往只需一句自然语言需求,例如:
帮我生成一个简单的待办事项应用:后端用 FastAPI + SQLite 提供增删改查接口,前端用 React + Vite 展示列表并支持添加、完成、删除;请给出目录结构和运行命令。
技术栈选择
- 后端:FastAPI + SQLite,轻量、无需独立数据库服务;
- 前端:React + Vite,启动快、适合原型验证;
- 联调:利用 Vite 代理,把 /api 请求转发到后端。
目录结构
todo-app/
├── backend/
│ ├── main.py
│ └── requirements.txt
└── frontend/
├── index.html
├── src/
│ └── App.jsx
├── vite.config.js
└── package.json
后端关键代码:backend/main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import sqlite3
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
def get_conn():
conn = sqlite3.connect("todo.db")
conn.row_factory = sqlite3.Row
return conn
def init_db():
with get_conn() as conn:
conn.execute(
"CREATE TABLE IF NOT EXISTS todos ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"title TEXT NOT NULL, "
"done INTEGER DEFAULT 0)"
)
@app.get("/api/todos")
def list_todos():
with get_conn() as conn:
rows = conn.execute("SELECT id, title, done FROM todos").fetchall()
return [{"id": r["id"], "title": r["title"], "done": bool(r["done"])} for r in rows]
@app.post("/api/todos")
def add_todo(payload: dict):
title = payload.get("title", "").strip()
if not title:
raise HTTPException(status_code=400, detail="title 不能为空")
with get_conn() as conn:
cur = conn.execute("INSERT INTO todos (title) VALUES (?)", (title,))
conn.commit()
todo_id = cur.lastrowid
return {"id": todo_id, "title": title, "done": False}
@app.put("/api/todos/{todo_id}")
def toggle_todo(todo_id: int):
with get_conn() as conn:
conn.execute("UPDATE todos SET done = 1 – done WHERE id = ?", (todo_id,))
conn.commit()
return {"ok": True}
@app.delete("/api/todos/{todo_id}")
def delete_todo(todo_id: int):
with get_conn() as conn:
conn.execute("DELETE FROM todos WHERE id = ?", (todo_id,))
conn.commit()
return {"ok": True}
init_db()
前端关键代码:frontend/src/App.jsx
import { useEffect, useState } from "react";
export default function App() {
const [todos, setTodos] = useState([]);
const [title, setTitle] = useState("");
async function load() {
const res = await fetch("/api/todos");
setTodos(await res.json());
}
async function add() {
await fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
setTitle("");
load();
}
async function remove(id) {
await fetch(`/api/todos/${id}`, { method: "DELETE" });
load();
}
async function toggle(id) {
await fetch(`/api/todos/${id}`, { method: "PUT" });
load();
}
useEffect(() => {
load();
}, []);
return (
<main>
<h1>待办事项</h1>
<div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button onClick={add}>添加</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.title}
</span>
<button onClick={() => toggle(todo.id)}>完成</button>
<button onClick={() => remove(todo.id)}>删除</button>
</li>
))}
</ul>
</main>
);
}
前端代理配置:frontend/vite.config.js
export default {
server: {
proxy: {
"/api": "http://localhost:8000",
},
},
};
前端依赖安装
开始前端前,确认本机已安装 Node.js。本示例建议使用 Node.js 18 及以上版本(Vite 5 需要 Node.js 18+,旧版 Node 可能触发语法或构建错误)。先用下面的命令检查版本:
node -v
npm -v
在 frontend/package.json 中声明如下依赖,其中 react、react-dom 放在 dependencies,vite、@vitejs/plugin-react 放在 devDependencies:
{
"name": "todo-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.4.2"
}
}
然后进入前端目录安装:
cd frontend
npm install
npm install 常见网络问题与解决
- 下载缓慢或超时:可以切换国内 npm 镜像源,例如执行 npm config set registry https://registry.npmmirror.com 后再重新 npm install;也可以临时使用 npm install –registry=https://registry.npmmirror.com。
- 二进制文件下载失败:部分依赖(如 esbuild)需要按平台下载二进制文件,可能受网络策略影响。可先删除 node_modules 和 package-lock.json,切换镜像或配置代理后重新安装。
- 公司网络限制 npm 仓库:可设置 https_proxy 环境变量,或改用公司私有源(如 Verdaccio / Nexus)。
- 版本冲突或启动异常:优先核对 Node.js 版本是否满足 Vite 要求,并确认依赖版本与 package.json 一致。
如何运行
cd backend
pip install fastapi uvicorn
uvicorn main:app –reload –port 8000
cd frontend
npm install
npm run dev
常见问题排查:后端启动与前端联调
跑通这个示例时,问题大多集中在「后端起不来」和「前端跨域 / 代理不生效」两类,可以按下面的顺序排查。
1. FastAPI 后端启动失败
- 端口被占用:如果启动时看到 [Errno 48] Address already in use(macOS / Linux)或 [WinError 10048](Windows),说明 8000 端口已被其他进程占用。可以先找到占用进程,再换一个端口启动。
# macOS / Linux 查看端口占用
lsof -i :8000
# Windows 查看端口占用
netstat -ano | findstr :8000
# 换到 8001 端口启动,并同步修改前端代理
uvicorn main:app –reload –port 8001
// frontend/vite.config.js:同步把代理目标改成 8001
export default {
server: {
proxy: {
"/api": "http://localhost:8001",
},
},
};
- 模块导入失败:如果报错 ModuleNotFoundError: No module named 'fastapi',说明依赖没有安装到当前 Python 环境,执行:
cd backend
pip install fastapi uvicorn
建议先创建并激活虚拟环境,再安装依赖,避免污染系统 Python 环境:
python -m venv venv
# macOS / Linux
source venv/bin/activate
# Windows
venv\\Scripts\\activate
pip install fastapi uvicorn
- 提示找不到 main:app:确认命令是在 backend 目录下执行,且文件名确实叫 main.py。如果从项目根目录启动,可以改用路径形式:
uvicorn backend.main:app –reload –port 8000
- todo.db 相关报错:示例用 sqlite3.connect("todo.db"),SQLite 会在当前工作目录创建数据库。建议固定在 backend 目录启动 uvicorn,否则换目录启动后可能出现「表不存在」或找不到数据库文件的情况。
2. 前端跨域或 Vite 代理不生效
浏览器 Console 如果出现类似 Access to fetch … has been blocked by CORS policy 的提示,本质是「代理没有生效,请求被浏览器当成跨域请求挡住了」。可以按下面排查。
- 请求地址必须写相对路径:前端 fetch("/api/todos") 应写相对路径,不要写成 http://localhost:8000/api/todos;写成绝对地址会绕过 Vite 代理,直接跨域到后端。
- 检查 vite.config.js 的位置和内容:文件必须放在 frontend 根目录,server.proxy 的 key 是 /api,target 是后端实际端口。
- 修改配置后重启 Vite:vite.config.js 修改后不会自动热更新,需要停掉 npm run dev 再重新启动。
- 确认后端已在对应端口启动:代理只是负责转发,如果后端没启动或端口不一致,前端会报 500、ECONNREFUSED 等错误。可以先用命令验证后端:
curl http://localhost:8000/api/todos
- 查看 Vite 终端日志:代理转发失败时,Vite 终端通常会打印目标连接错误,先看日志再改。
如果不想用 Vite 代理、坚持让前端直接请求后端地址,则需要在前端请求里写完整地址,同时保证后端 CORS 配置覆盖该来源:
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
修改后重启后端让配置生效。
对比方案:使用 Next.js 全栈框架实现待办事项应用
除了「FastAPI + React 分离架构」,还可以让 Codex 生成一个基于 Next.js 全栈框架 的版本。两者都能实现同样的待办事项功能,但架构思路和适用场景有明显差异。
与 FastAPI + React 分离架构的差异
- 部署形态:分离架构需要分别部署后端(FastAPI)和前端(Vite),并处理跨域或代理;Next.js 把 API 路由和页面打包在同一个应用里,部署更简单,适合 Serverless 平台。
- 开发语言:分离架构后端用 Python,前端用 JavaScript/TypeScript;Next.js 全栈统一使用 JavaScript/TypeScript,前后端可以共享类型定义,减少联调成本。
- 数据层:分离架构用 SQLite 存数据;Next.js 示例通常配合 Prisma + SQLite 或直接使用文件/内存存储,也可以无缝切换到 PostgreSQL。
- 适用场景:如果团队已有 Python 后端或需要复杂的数据处理,选 FastAPI 分离架构更合适;如果追求「一个仓库、一套语言、快速上线」,或要部署到 Vercel 等平台,Next.js 全栈更省心。
核心代码片段:API 路由 + 前端组件
下面是一个最小可运行的 Next.js(App Router)待办事项示例。先创建项目:
npx create-next-app@latest todo-next –ts –app –use-npm
API 路由:app/api/todos/route.ts
import { NextResponse } from "next/server";
// 用内存数组模拟数据库,重启后数据会清空
let todos: { id: number; title: string; done: boolean }[] = [];
let nextId = 1;
export async function GET() {
return NextResponse.json(todos);
}
export async function POST(request: Request) {
const body = await request.json();
const title = (body.title ?? "").trim();
if (!title) {
return NextResponse.json({ error: "title 不能为空" }, { status: 400 });
}
const todo = { id: nextId++, title, done: false };
todos.push(todo);
return NextResponse.json(todo, { status: 201 });
}
前端组件:app/page.tsx
"use client";
import { useEffect, useState } from "react";
type Todo = { id: number; title: string; done: boolean };
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([]);
const [title, setTitle] = useState("");
async function load() {
const res = await fetch("/api/todos");
setTodos(await res.json());
}
async function add() {
await fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
setTitle("");
load();
}
useEffect(() => {
load();
}, []);
return (
<main>
<h1>待办事项(Next.js)</h1>
<div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button onClick={add}>添加</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</main>
);
}
运行方式:
cd todo-next
npm run dev
浏览器访问 http://localhost:3000 即可。由于 API 路由和页面同源,天然不存在跨域问题,也无需配置代理。
四、几点使用心得
三类 Skill 怎么选
如果你刚接触办公自动化,可以先根据自己的核心需求,对照下面的表格快速选择切入点:
| 飞书 Skill | 团队协作、消息推送、审批/任务编排 | 发消息、建群,读写云文档与多维表格,把 AI 接入既有办公流 | 飞书开放平台 API、飞书 CLI、Python requests | 中等,需要配置应用凭证和消息权限 |
| Office 自动化 Skill | 报告、Word / PPT 等交付物产出 | 生成规范 Word / PPT,读取并汇总 Office 文件内容 | python-docx、python-pptx | 较低,脚本逻辑直观,适合入门 |
| 全栈 Skill | 原型验证、内部工具与前后端骨架开发 | 生成前后端骨架、脚手架与模板,辅助数据库设计和接口定义 | FastAPI、SQLite、React、Vite | 中等偏高,需要一定开发基础 |
简单总结一句:要协作推送选飞书,要文档交付选 Office,要快速做出小应用选全栈 Skill。
小结
- 飞书 Skill:消息、文档、流程协同,让 AI 融进办公流。
- Office Skill:自动化生成 Word/PPT 并读写文件。
- 全栈 Skill:快速生成原型与骨架,降低开发门槛。
下一课,我们看浏览器自动化与业务场景 Skill 的概览。
本文是《OpenAI Codex 从零基础到精通》第 14 课。
总结与行动清单
核心要点
- 飞书 Skill:可自动化消息推送与多维表格读写,让 AI 融进团队办公流;
- Office 自动化 Skill:用 python-docx 等工具生成规范 Word/PPT,并读取既有文件做汇总;
- 全栈 Skill:用 Codex 快速生成前后端骨架,降低「从 0 到能跑」的门槛。
下一步动手实践




