实战:全能 AI 助手
实战:打造你的全能 AI 助手
终极 Boss 关 -- 用 Codex SDK 构建一个类似 OpenClaw 的 AI 助手框架:接入飞书/Discord 等通讯平台,支持 Claude Skills 扩展,内置定时任务调度。
我们要做什么
你可能听说过 OpenClaw -- 一个能接入各种通讯平台的开源 AI 助手。我们要做一个类似的东西,但用 Codex SDK 做 AI 引擎。
最终效果
- 在飞书群里 @机器人 问问题 -> AI 自动回复
- 发
/skill list-> 查看已安装的能力插件 - 发
/report my-project-> 触发代码审查 Skill - 每天早上 9 点自动发送项目健康报告
技术架构
四层解耦,每层独立可替换:
+---------------------------------------------+
| 通讯平台 Adapters |
| +----------+ +----------+ +----------+ |
| | 飞书 | | Discord | | CLI | |
| +----+-----+ +----+-----+ +----+-----+ |
| +--------------+--------------+ |
| | |
| v |
| 消息路由 Router |
| (命令解析 + 分发) |
| | |
| v |
| +------------------------------------+ |
| | AI 引擎 Engine | |
| | (Codex SDK + Thread) | |
| +-------------------+----------------+ |
| / | \ |
| +---------+ +--------+ +----------+ |
| | Skills | | Memory | |Scheduler | |
| | 技能扩展 | | 线程记忆| | 定时任务 | |
| +---------+ +--------+ +----------+ |
+---------------------------------------------+项目初始化
bashmkdir codex-assistant && cd codex-assistant
git init
npm init -y
npm install @openai/codex-sdk node-cron gray-matter glob
npm install -D typescript tsx @types/node目录结构:
codex-assistant/
├── src/
│ ├── types.ts # 统一类型定义
│ ├── adapters/
│ │ ├── base.ts # 适配器抽象基类
│ │ ├── cli.ts # CLI 调试适配器
│ │ └── feishu.ts # 飞书适配器
│ ├── engine.ts # AI 引擎(核心)
│ ├── skills.ts # Skills 管理器
│ ├── scheduler.ts # 定时任务调度器
│ ├── router.ts # 消息路由
│ └── index.ts # 启动入口
├── skills/ # Skills 存放目录
│ └── code-review.md # 示例 Skill
├── tsconfig.json
└── package.jsontsconfig.json:
json{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}第一步:统一消息类型(types.ts)
不管消息从哪个平台来(飞书、Discord、终端),统一成一个格式:
typescript// src/types.ts
/** 收到的消息 -- 统一格式 */
export interface Message {
id: string;
platform: "feishu" | "discord" | "slack" | "cli";
channelId: string;
userId: string;
userName: string;
content: string;
timestamp: Date;
}
/** 要发送的回复 */
export interface Reply {
content: string;
channelId: string;
replyToMessageId?: string;
}
/** 适配器接口 -- 每个通讯平台实现一个 */
export interface Adapter {
readonly name: string;
start(): Promise<void>;
stop(): Promise<void>;
onMessage(handler: (msg: Message) => Promise<void>): void;
sendReply(reply: Reply): Promise<void>;
}设计思路:Adapter 接口定义了"接收消息"和"发送回复"两个能力,跟具体平台无关。以后要接入新平台(Telegram、微信),只要实现这个接口就行。
第二步:平台适配器
适配器基类(adapters/base.ts)
typescript// src/adapters/base.ts
import { EventEmitter } from "events";
import type { Message, Reply, Adapter } from "../types.ts";
export abstract class BaseAdapter extends EventEmitter implements Adapter {
abstract readonly name: string;
abstract start(): Promise<void>;
abstract stop(): Promise<void>;
abstract sendReply(reply: Reply): Promise<void>;
onMessage(handler: (msg: Message) => Promise<void>): void {
this.on("message", handler);
}
protected emitMessage(msg: Message): void {
this.emit("message", msg);
}
}CLI 调试适配器(adapters/cli.ts)
开发时用的,在终端里直接和 AI 对话:
typescript// src/adapters/cli.ts
import { createInterface, type Interface } from "readline/promises";
import { stdin as input, stdout as output } from "process";
import { randomUUID } from "crypto";
import { BaseAdapter } from "./base.ts";
import type { Reply } from "../types.ts";
export class CliAdapter extends BaseAdapter {
readonly name = "cli";
private rl: Interface | null = null;
private running = false;
async start(): Promise<void> {
this.rl = createInterface({ input, output });
this.running = true;
console.log("AI 助手已启动(CLI 模式)");
console.log("输入消息开始对话,输入 /help 查看命令列表");
console.log("─".repeat(50));
this.loop();
}
private async loop(): Promise<void> {
while (this.running && this.rl) {
try {
const content = await this.rl.question("\n你: ");
if (!content.trim()) continue;
if (content.trim() === "/quit" || content.trim() === "/exit") {
console.log("再见!");
this.running = false;
break;
}
this.emitMessage({
id: randomUUID(),
platform: "cli",
channelId: "cli-default",
userId: "local-user",
userName: "你",
content: content.trim(),
timestamp: new Date(),
});
} catch {
// readline 被关闭
break;
}
}
}
async sendReply(reply: Reply): Promise<void> {
console.log(`\n助手: ${reply.content}`);
}
async stop(): Promise<void> {
this.running = false;
this.rl?.close();
}
}飞书适配器(adapters/feishu.ts)
接入飞书机器人,通过 Webhook 接收消息,通过 API 发送回复:
typescript// src/adapters/feishu.ts
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http";
import { randomUUID } from "crypto";
import { BaseAdapter } from "./base.ts";
import type { Reply } from "../types.ts";
interface FeishuConfig {
appId: string;
appSecret: string;
port?: number;
verificationToken?: string;
}
export class FeishuAdapter extends BaseAdapter {
readonly name = "feishu";
private server: Server;
private port: number;
private appId: string;
private appSecret: string;
private accessToken: string = "";
private tokenExpiry: number = 0;
constructor(config: FeishuConfig) {
super();
this.appId = config.appId;
this.appSecret = config.appSecret;
this.port = config.port || 3000;
this.server = createServer((req, res) => this.handleRequest(req, res));
}
async start(): Promise<void> {
await this.refreshToken();
this.server.listen(this.port);
console.log(`飞书适配器已启动,监听端口 ${this.port}`);
console.log(`请在飞书开放平台配置事件回调 URL: http://your-server:${this.port}/webhook`);
}
async stop(): Promise<void> {
this.server.close();
}
// 获取/刷新 Tenant Access Token
private async refreshToken(): Promise<void> {
const res = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
app_id: this.appId,
app_secret: this.appSecret,
}),
});
const data = await res.json() as { tenant_access_token: string; expire: number };
this.accessToken = data.tenant_access_token;
this.tokenExpiry = Date.now() + (data.expire - 60) * 1000;
}
private async ensureToken(): Promise<string> {
if (Date.now() >= this.tokenExpiry) {
await this.refreshToken();
}
return this.accessToken;
}
// 处理飞书 Webhook 回调
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
if (req.method !== "POST" || req.url !== "/webhook") {
res.writeHead(404);
res.end();
return;
}
const body = await this.readBody(req);
const data = JSON.parse(body);
// 飞书的 URL 验证请求
if (data.type === "url_verification") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ challenge: data.challenge }));
return;
}
// 处理消息事件
if (data.header?.event_type === "im.message.receive_v1") {
const event = data.event;
const msgContent = JSON.parse(event.message.content);
this.emitMessage({
id: event.message.message_id,
platform: "feishu",
channelId: event.message.chat_id,
userId: event.sender.sender_id.open_id,
userName: event.sender.sender_id.open_id,
content: msgContent.text?.replace(/@_all|@\w+/g, "").trim() || "",
timestamp: new Date(parseInt(event.message.create_time) * 1000),
});
}
res.writeHead(200);
res.end("ok");
}
// 发送回复消息
async sendReply(reply: Reply): Promise<void> {
const token = await this.ensureToken();
await fetch("https://open.feishu.cn/open-apis/im/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
receive_id: reply.channelId,
msg_type: "text",
content: JSON.stringify({ text: reply.content }),
}),
});
}
private readBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => resolve(body));
req.on("error", reject);
});
}
}飞书开放平台配置步骤请参考后文"接入飞书的完整步骤"。
第三步:Skills 系统 -- 兼容 Claude Skills 格式
这是最有趣的部分。我们实现一套 Skills 系统,兼容 Claude 的 SKILL.md 格式。
SKILL.md 文件格式
每个 Skill 就是一个 Markdown 文件,头部用 YAML frontmatter 定义元数据:
yaml---
name: code-review
description: 自动代码审查,分析代码质量并给出改进建议
trigger: /review
version: "1.0.0"
author: your-name
tags:
- code
- review
- quality
arguments:
- name: target
description: 审查目标(git ref 或文件路径)
required: false
default: "HEAD~1"
- name: format
description: 输出格式
required: false
default: "text"
---
## 指令
你是一个资深代码审查专家,拥有 10 年以上的软件开发经验。
请根据用户的要求审查代码,从以下维度分析:
1. Bug 和逻辑错误
2. 安全隐患
3. 性能问题
4. 代码风格
5. 可维护性
审查标准:
- 只报告真正有意义的问题
- 每个问题给出具体的修改建议
- 如果代码写得好,也要指出Skills 管理器实现
typescript// src/skills.ts
import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, unlinkSync } from "fs";
import { join, basename } from "path";
import { globSync } from "glob";
import matter from "gray-matter";
/** Skill 元数据(来自 YAML frontmatter) */
export interface SkillMeta {
name: string;
description: string;
trigger?: string;
version?: string;
author?: string;
tags?: string[];
arguments?: SkillArgument[];
}
export interface SkillArgument {
name: string;
description: string;
required?: boolean;
default?: string;
}
/** 一个完整的 Skill */
export interface Skill {
meta: SkillMeta;
instructions: string; // markdown body = AI 的指令
filePath: string;
}
export class SkillManager {
private skills: Map<string, Skill> = new Map();
private skillsDir: string;
constructor(skillsDir: string = "./skills") {
this.skillsDir = skillsDir;
if (!existsSync(skillsDir)) {
mkdirSync(skillsDir, { recursive: true });
}
this.loadAll();
}
/** 加载 skills 目录下所有 .md 文件 */
private loadAll(): void {
const files = globSync(join(this.skillsDir, "**/*.md"));
for (const file of files) {
try {
const skill = this.parseSkillFile(file);
if (skill) {
this.skills.set(skill.meta.name, skill);
}
} catch (err) {
console.warn(`跳过无效 Skill 文件: ${file}`, err);
}
}
console.log(`已加载 ${this.skills.size} 个 Skills`);
}
/** 解析单个 SKILL.md 文件 */
private parseSkillFile(filePath: string): Skill | null {
const raw = readFileSync(filePath, "utf-8");
const { data, content } = matter(raw);
const meta = data as SkillMeta;
if (!meta.name) {
console.warn(`Skill 文件缺少 name 字段: ${filePath}`);
return null;
}
return {
meta,
instructions: content.trim(),
filePath,
};
}
/** 安装 Skill -- 从本地路径复制到 skills 目录 */
install(sourcePath: string): Skill {
if (!existsSync(sourcePath)) {
throw new Error(`文件不存在: ${sourcePath}`);
}
const skill = this.parseSkillFile(sourcePath);
if (!skill) {
throw new Error(`无效的 Skill 文件: ${sourcePath}`);
}
const destPath = join(this.skillsDir, basename(sourcePath));
cpSync(sourcePath, destPath);
skill.filePath = destPath;
this.skills.set(skill.meta.name, skill);
console.log(`已安装 Skill: ${skill.meta.name}`);
return skill;
}
/** 搜索 Skill -- 按名称、描述、标签模糊匹配 */
search(query: string): Skill[] {
const q = query.toLowerCase();
return Array.from(this.skills.values()).filter((skill) => {
const { name, description, tags } = skill.meta;
return (
name.toLowerCase().includes(q) ||
description.toLowerCase().includes(q) ||
(tags || []).some((t) => t.toLowerCase().includes(q))
);
});
}
/** 获取指定 Skill */
get(name: string): Skill | undefined {
return this.skills.get(name);
}
/** 列出所有 Skill */
list(): Skill[] {
return Array.from(this.skills.values());
}
/** 卸载 Skill */
uninstall(name: string): boolean {
const skill = this.skills.get(name);
if (!skill) return false;
try {
unlinkSync(skill.filePath);
} catch {
// 文件可能已经不存在
}
this.skills.delete(name);
console.log(`已卸载 Skill: ${name}`);
return true;
}
/** 根据消息内容匹配 Skill trigger */
matchTrigger(message: string): { skill: Skill; args: Record<string, string> } | null {
for (const skill of this.skills.values()) {
if (!skill.meta.trigger) continue;
if (message.startsWith(skill.meta.trigger)) {
// 解析参数:trigger 后面的内容按空格分割
const argsStr = message.slice(skill.meta.trigger.length).trim();
const argParts = argsStr ? argsStr.split(/\s+/) : [];
const args: Record<string, string> = {};
(skill.meta.arguments || []).forEach((argDef, i) => {
args[argDef.name] = argParts[i] || argDef.default || "";
});
return { skill, args };
}
}
return null;
}
/** 构建包含 Skill 指令的 Prompt */
buildPrompt(skill: Skill, args: Record<string, string>, userMessage: string): string {
let prompt = skill.instructions;
// 替换参数占位符
for (const [key, value] of Object.entries(args)) {
prompt = prompt.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
}
// 附加用户原始消息和参数信息
prompt += `\n\n---\n用户原始消息: ${userMessage}`;
prompt += `\n参数: ${JSON.stringify(args)}`;
return prompt;
}
}第四步:AI 引擎(engine.ts)
封装 Codex SDK,为每个聊天频道维护独立的对话线程:
typescript// src/engine.ts
import { Codex, type Thread } from "@openai/codex-sdk";
import type { SkillManager } from "./skills.ts";
interface EngineConfig {
defaultModel?: string;
sandboxMode?: "read-only" | "workspace-write" | "danger-full-access";
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
systemPrompt?: string;
}
export class AIEngine {
private codex: Codex;
private threads: Map<string, Thread> = new Map();
private skillManager: SkillManager;
private config: EngineConfig;
constructor(skillManager: SkillManager, config: EngineConfig = {}) {
this.codex = new Codex();
this.skillManager = skillManager;
this.config = {
defaultModel: config.defaultModel,
sandboxMode: config.sandboxMode || "read-only",
reasoningEffort: config.reasoningEffort || "medium",
systemPrompt: config.systemPrompt || "你是一个友好的 AI 助手,擅长编程和技术问题。回复请用中文。",
};
}
/** 处理聊天消息 */
async chat(channelId: string, message: string): Promise<string> {
// 1. 检查是否匹配某个 Skill
const match = this.skillManager.matchTrigger(message);
let prompt: string;
if (match) {
prompt = this.skillManager.buildPrompt(match.skill, match.args, message);
console.log(` 匹配 Skill: ${match.skill.meta.name}`);
} else {
prompt = message;
}
// 2. 如果是首次对话,注入 system prompt
if (!this.threads.has(channelId)) {
prompt = `${this.config.systemPrompt}\n\n---\n\n${prompt}`;
}
// 3. 获取或创建该频道的对话线程
const thread = this.getOrCreateThread(channelId);
// 4. 调用 Codex SDK
try {
const turn = await thread.run(prompt);
return turn.finalResponse || "(AI 没有返回文本回复)";
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(` AI 引擎错误: ${msg}`);
return `抱歉,处理时出了点问题: ${msg}`;
}
}
/** 获取或创建线程 */
private getOrCreateThread(channelId: string): Thread {
let thread = this.threads.get(channelId);
if (!thread) {
thread = this.codex.startThread({
model: this.config.defaultModel,
sandboxMode: this.config.sandboxMode,
modelReasoningEffort: this.config.reasoningEffort,
skipGitRepoCheck: true,
});
this.threads.set(channelId, thread);
}
return thread;
}
/** 重置某个频道的对话 */
resetThread(channelId: string): void {
this.threads.delete(channelId);
console.log(` 已重置频道 ${channelId} 的对话`);
}
/** 重置所有对话 */
resetAll(): void {
this.threads.clear();
console.log(" 已重置所有对话");
}
}第五步:定时任务调度器(scheduler.ts)
用 node-cron 实现 Cron 表达式驱动的定时任务:
typescript// src/scheduler.ts
import cron from "node-cron";
import { randomUUID } from "crypto";
/** 定时任务配置 */
export interface ScheduledTask {
id: string;
name: string;
cronExpression: string; // Cron 表达式,如 "0 9 * * *"(每天9点)
prompt: string; // 要发给 AI 的指令
channelId: string; // 结果发到哪个频道
skillName?: string; // 可选:绑定某个 Skill
enabled: boolean;
}
type TaskExecutor = (task: ScheduledTask) => Promise<void>;
export class Scheduler {
private tasks: Map<string, { config: ScheduledTask; job: cron.ScheduledTask }> = new Map();
private executor: TaskExecutor;
constructor(executor: TaskExecutor) {
this.executor = executor;
}
/** 添加定时任务 */
add(taskConfig: Omit<ScheduledTask, "id" | "enabled">): ScheduledTask {
const task: ScheduledTask = {
...taskConfig,
id: randomUUID().slice(0, 8),
enabled: true,
};
// 验证 Cron 表达式
if (!cron.validate(task.cronExpression)) {
throw new Error(`无效的 Cron 表达式: ${task.cronExpression}`);
}
const job = cron.schedule(task.cronExpression, async () => {
console.log(`\n定时任务触发: ${task.name}`);
try {
await this.executor(task);
} catch (err) {
console.error(` 任务执行失败: ${err}`);
}
});
this.tasks.set(task.id, { config: task, job });
console.log(`定时任务已添加: ${task.name} (${task.cronExpression}) [ID: ${task.id}]`);
return task;
}
/** 删除定时任务 */
remove(id: string): boolean {
const entry = this.tasks.get(id);
if (!entry) return false;
entry.job.stop();
this.tasks.delete(id);
console.log(`定时任务已删除: ${entry.config.name}`);
return true;
}
/** 列出所有定时任务 */
list(): ScheduledTask[] {
return Array.from(this.tasks.values()).map((e) => e.config);
}
/** 停止所有任务 */
stopAll(): void {
for (const { job } of this.tasks.values()) {
job.stop();
}
console.log("所有定时任务已停止");
}
}Cron 表达式速查
| 表达式 | 含义 |
|---|---|
0 9 * * * |
每天 9:00 |
0 9 * * 1 |
每周一 9:00 |
0 */2 * * * |
每 2 小时 |
*/30 * * * * |
每 30 分钟 |
0 9 1 * * |
每月 1 号 9:00 |
第六步:消息路由(router.ts)
解析命令并分发到对应的处理器:
typescript// src/router.ts
import type { Message } from "./types.ts";
import type { AIEngine } from "./engine.ts";
import type { SkillManager } from "./skills.ts";
import type { Scheduler } from "./scheduler.ts";
export class Router {
constructor(
private engine: AIEngine,
private skillManager: SkillManager,
private scheduler: Scheduler,
) {}
/** 路由消息 -- 返回回复内容 */
async handle(msg: Message): Promise<string> {
const { content } = msg;
// 内置命令
if (content === "/help") return this.handleHelp();
if (content === "/reset") return this.handleReset(msg.channelId);
if (content.startsWith("/skill")) return this.handleSkillCommand(content);
if (content.startsWith("/schedule")) return this.handleScheduleCommand(content, msg.channelId);
// 普通消息 -> AI 引擎
return this.engine.chat(msg.channelId, content);
}
private handleHelp(): string {
return [
"AI 助手命令列表:",
"",
"直接输入文字 -- 跟 AI 对话",
"",
"Skills 管理:",
" /skill list -- 查看已安装 Skills",
" /skill search <关键词> -- 搜索 Skills",
" /skill install <路径> -- 安装 Skill",
" /skill info <名称> -- 查看 Skill 详情",
" /skill uninstall <名称> -- 卸载 Skill",
"",
"定时任务:",
' /schedule add <名称> <cron> <指令> -- 添加定时任务',
" /schedule list -- 查看所有定时任务",
" /schedule remove <ID> -- 删除定时任务",
"",
"其他:",
" /reset -- 重置当前对话",
" /help -- 显示本帮助",
].join("\n");
}
private handleReset(channelId: string): string {
this.engine.resetThread(channelId);
return "对话已重置,让我们重新开始吧!";
}
private handleSkillCommand(content: string): string {
const parts = content.split(/\s+/);
const subCmd = parts[1];
const arg = parts.slice(2).join(" ");
switch (subCmd) {
case "list": {
const skills = this.skillManager.list();
if (skills.length === 0) return "暂无已安装的 Skills。";
return [
"已安装的 Skills:",
"",
...skills.map((s) => {
const trigger = s.meta.trigger ? ` (触发: ${s.meta.trigger})` : "";
return ` - ${s.meta.name}${trigger} -- ${s.meta.description}`;
}),
].join("\n");
}
case "search": {
if (!arg) return "用法: /skill search <关键词>";
const results = this.skillManager.search(arg);
if (results.length === 0) return `未找到匹配 "${arg}" 的 Skill。`;
return [
`搜索结果 (${results.length} 个):`,
"",
...results.map((s) => ` - ${s.meta.name} -- ${s.meta.description}`),
].join("\n");
}
case "install": {
if (!arg) return "用法: /skill install <文件路径>";
try {
const skill = this.skillManager.install(arg);
return `已安装 Skill: ${skill.meta.name} -- ${skill.meta.description}`;
} catch (err) {
return `安装失败: ${err instanceof Error ? err.message : err}`;
}
}
case "info": {
if (!arg) return "用法: /skill info <名称>";
const skill = this.skillManager.get(arg);
if (!skill) return `未找到 Skill: ${arg}`;
const argsList = (skill.meta.arguments || [])
.map((a) => ` - ${a.name}${a.required ? " (必填)" : ""}: ${a.description}`)
.join("\n");
return [
`Skill: ${skill.meta.name}`,
` 描述: ${skill.meta.description}`,
` 版本: ${skill.meta.version || "未知"}`,
` 作者: ${skill.meta.author || "未知"}`,
` 触发: ${skill.meta.trigger || "无"}`,
` 标签: ${(skill.meta.tags || []).join(", ") || "无"}`,
argsList ? ` 参数:\n${argsList}` : " 参数: 无",
].join("\n");
}
case "uninstall": {
if (!arg) return "用法: /skill uninstall <名称>";
return this.skillManager.uninstall(arg)
? `已卸载 Skill: ${arg}`
: `未找到 Skill: ${arg}`;
}
default:
return "用法: /skill [list|search|install|info|uninstall]";
}
}
private handleScheduleCommand(content: string, channelId: string): string {
const parts = content.split(/\s+/);
const subCmd = parts[1];
switch (subCmd) {
case "add": {
// /schedule add <name> <cron> <prompt...>
const name = parts[2];
const cronExpr = parts[3];
const prompt = parts.slice(4).join(" ");
if (!name || !cronExpr || !prompt) {
return '用法: /schedule add <名称> <cron表达式> <AI指令>\n示例: /schedule add morning-report "0 9 * * *" 生成今日代码仓库健康报告';
}
try {
const task = this.scheduler.add({
name,
cronExpression: cronExpr,
prompt,
channelId,
});
return `定时任务已添加!\n 名称: ${task.name}\n 计划: ${task.cronExpression}\n ID: ${task.id}`;
} catch (err) {
return `添加失败: ${err instanceof Error ? err.message : err}`;
}
}
case "list": {
const tasks = this.scheduler.list();
if (tasks.length === 0) return "暂无定时任务。";
return [
"定时任务列表:",
"",
...tasks.map((t) =>
` [${t.id}] ${t.name} -- ${t.cronExpression} -> "${t.prompt.slice(0, 30)}${t.prompt.length > 30 ? "..." : ""}"`
),
].join("\n");
}
case "remove": {
const id = parts[2];
if (!id) return "用法: /schedule remove <ID>";
return this.scheduler.remove(id)
? `定时任务已删除: ${id}`
: `未找到任务: ${id}`;
}
default:
return "用法: /schedule [add|list|remove]";
}
}
}第七步:启动入口(index.ts)
把所有组件组装起来:
typescript// src/index.ts
import { SkillManager } from "./skills.ts";
import { AIEngine } from "./engine.ts";
import { Scheduler } from "./scheduler.ts";
import { Router } from "./router.ts";
import { CliAdapter } from "./adapters/cli.ts";
// 需要接入飞书时取消下面的注释:
// import { FeishuAdapter } from "./adapters/feishu.ts";
async function main() {
console.log("正在启动 AI 助手...\n");
// 1. 初始化 Skills 系统
const skillManager = new SkillManager("./skills");
// 2. 初始化 AI 引擎
const engine = new AIEngine(skillManager, {
sandboxMode: "read-only",
reasoningEffort: "medium",
systemPrompt: "你是一个友好的 AI 助手,擅长编程和技术问题。回复简洁明了,用中文。",
});
// 3. 选择适配器
// CLI 模式(开发调试用)
const adapter = new CliAdapter();
// 飞书模式(生产环境用)
// const adapter = new FeishuAdapter({
// appId: process.env.FEISHU_APP_ID!,
// appSecret: process.env.FEISHU_APP_SECRET!,
// port: 3000,
// });
// 4. 初始化定时调度器
const scheduler = new Scheduler(async (task) => {
console.log(`\n执行定时任务: ${task.name}`);
const result = await engine.chat(task.channelId, task.prompt);
await adapter.sendReply({
content: `[定时任务: ${task.name}]\n\n${result}`,
channelId: task.channelId,
});
});
// 5. 初始化消息路由
const router = new Router(engine, skillManager, scheduler);
// 6. 注册消息处理器
adapter.onMessage(async (msg) => {
console.log(`\n[${msg.platform}] ${msg.userName}: ${msg.content}`);
const reply = await router.handle(msg);
await adapter.sendReply({
content: reply,
channelId: msg.channelId,
replyToMessageId: msg.id,
});
});
// 7. 启动!
await adapter.start();
// 优雅退出
process.on("SIGINT", async () => {
console.log("\n\n正在停止...");
scheduler.stopAll();
await adapter.stop();
process.exit(0);
});
}
main().catch((err) => {
console.error("启动失败:", err);
process.exit(1);
});创建示例 Skill
写一个示例 Skill 文件,方便测试:
yaml# skills/code-review.md
---
name: code-review
description: 自动代码审查,分析代码质量并给出改进建议
trigger: /review
version: "1.0.0"
author: demo
tags:
- code
- review
arguments:
- name: target
description: 审查目标(git ref 或文件路径)
required: false
default: "HEAD~1"
---
## 指令
你是一个资深代码审查专家。请分析以下代码变更:
审查目标: {{target}}
请从以下维度分析:
1. Bug 和逻辑错误
2. 安全隐患
3. 性能问题
4. 代码风格
每个问题给出具体的修改建议。如果代码写得好,也请指出。试一试
启动 AI 助手:
bashnpx tsx src/index.ts然后试试这些操作:
你: 你好,你能做什么?
助手: 我是一个 AI 助手,我可以帮你...
你: /help
助手: AI 助手命令列表...
你: /skill list
助手: 已安装的 Skills:
- code-review (触发: /review) -- 自动代码审查...
你: /skill info code-review
助手: Skill: code-review
描述: 自动代码审查...
你: /review HEAD~3
助手: (AI 会根据 code-review Skill 的指令进行代码审查)
你: /schedule add daily-check "0 9 * * *" 检查当前项目有没有新的安全漏洞
助手: 定时任务已添加!
名称: daily-check
计划: 0 9 * * *
ID: a1b2c3d4
你: /schedule list
助手: 定时任务列表:
[a1b2c3d4] daily-check -- 0 9 * * * -> "检查当前项目有没有新的安全漏洞"
你: /reset
助手: 对话已重置,让我们重新开始吧!接入飞书的完整步骤
创建飞书应用
- 登录 飞书开放平台
- 创建企业自建应用
- 获取
App ID和App Secret
配置机器人能力
- 在应用的"添加应用能力"里,开启"机器人"
- 设置机器人名称和头像
配置事件订阅
- 在"事件订阅"页面,设置请求 URL:
http://your-server:3000/webhook - 订阅事件:
im.message.receive_v1(接收消息)
- 在"事件订阅"页面,设置请求 URL:
配置权限
- 开通权限:
im:message(获取和发送消息)
- 开通权限:
启动服务
bash
export FEISHU_APP_ID="cli_xxxxx" export FEISHU_APP_SECRET="xxxxx" npx tsx src/index.ts发布应用
- 在开放平台提交审核,审核通过后即可在飞书中使用
扩展方向
这个框架的每一层都可以继续扩展:
更多适配器
- Discord:用
discord.js库,监听messageCreate事件 - Slack:用 Bolt for JavaScript,配置 Event Subscriptions
- Telegram:用
telegraf库 - 微信企业号:类似飞书,Webhook + API
Skill 增强
- Skill 市场:从 GitHub 仓库一键安装 Skills(
/skill install https://github.com/...) - Skill 链:一个 Skill 触发另一个,形成工作流
- Skill 权限:不同用户能用不同的 Skills
更多能力
- 线程持久化:用
thread.id+resumeThread()实现跨重启的长期记忆 - Web 管理面板:Express/Fastify 搭一个管理页面,可视化管理 Skills 和定时任务
- Docker 部署:打包成 Docker 镜像,一键部署到云服务器
- 多租户:不同的群/频道使用不同的 AI 配置和 Skills
小结
这一章我们从零构建了一个完整的 AI 助手框架:
| 组件 | 干什么的 | 用到的技术 |
|---|---|---|
| Adapter | 接入通讯平台 | EventEmitter、HTTP Server |
| Router | 解析命令、分发消息 | 字符串解析 |
| Engine | AI 对话核心 | Codex SDK、Thread 管理 |
| Skills | 能力扩展 | YAML frontmatter、gray-matter |
| Scheduler | 定时任务 | node-cron |
整个框架 500 行左右的 TypeScript,但五脏俱全:可插拔的平台适配、兼容 Claude 的 Skills 格式、Cron 定时调度。
你可以基于这个框架,快速构建出属于自己的 AI 助手!