mcpField
V
vapi-bootstrap-framework

vapi-bootstrap-framework

github.com/vapiaiAgent 工作流67295 次安装今天更新
代码仓库

关于 vapi-bootstrap-framework

这是一项实验性技能,可根据 `ROUGH_DRAFT.md` 搭建 Vapi 语音 Agent 项目,生成场景提示词和助手初始化入口。它仅用于明确要求的落地页 Agent 工作流,并非 Vapi 助手的标准开发方式。

主要功能

  1. 01搭建 Bun 和 TypeScript 项目文件
  2. 02从草稿提取场景与提示词
  3. 03按场景和语言构建助手配置
  4. 04生成幂等的助手初始化脚本
  5. 05默认使用英语和西班牙语

适用场景

  1. 01启动实验性的 Vapi 落地页 Agent 项目
  2. 02为多个通话场景生成助手
  3. 03根据项目简述初始化多语言版本

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vapiai/skills --skill vapi-bootstrap-framework

安装后在 Claude Code 中输入/vapi-bootstrap-framework确认已加载。

详细介绍

Vapi 引导框架

实验性参考工作流: 此技能复现了 Vapi 落地页 Agent 所用的架构。它不是构建 Vapi 助手的标准工作流,仅应在明确提出要求时使用。

根据单个 ROUGH_DRAFT.md 规格搭建完整的 Vapi 语音 Agent 项目。可在空文件夹中使用,也可与现有的 Bun + TypeScript 项目配合使用。此技能完成后,只需运行一次 bun run bootstrap,即可让整套 Agent 在 dashboard.vapi.ai 上线。

此技能会生成什么

项目脚手架(仅在文件不存在时创建,不会覆盖):

  • package.json — Bun + @vapi-ai/server-sdk + bootstrap 脚本
  • tsconfig.json — 严格的无输出 TypeScript 校验
  • .env.example — VAPI_API_KEY 占位符,以及每个 (scenario × language) 元组对应的一个配置项
  • .gitignore — node_modules、.env*.local、操作系统杂项文件

框架主干(始终创建 — 属于架构基础;后续所有更改都会扩展其中的插槽,不会修改主干本身):

  • src/assistants/languages.ts — 各语言的语音 + 转录器配置
  • src/assistants/loadPrompt.ts — loadPrompt(scenarioId, languageId) 合成器
  • src/assistants/scenarios/index.ts — 场景注册表
  • src/assistants/buildAssistant.ts — 为任意 (scenarioId, languageId) 元组合成完整的 Vapi 助手主体
  • src/assistants/prompts/shared/preambles/es.md — 西班牙语语言前置提示词(之后可扩展更多语言)

内容(始终创建 — 为草稿中识别出的每个场景各生成一套):

  • src/assistants/scenarios/<scenarioId>.ts — ID、名称,以及按语言设置的首条消息
  • src/assistants/prompts/<scenarioId>/body.md — 粗略的首版系统提示词
  • src/assistants/prompts/<scenarioId>/off-topic-es.md — 西班牙语引导语

入口点(始终创建):

  • src/bootstrap.ts — 遍历场景 × 语言的幂等双重循环

默认设置:语言为 en + es。模型为 openai gpt-4.1,温度为 0.5。语音使用 ElevenLabs eleven_turbo_v2(EN)/ eleven_multilingual_v2(ES)。转录器使用 Deepgram nova-3(EN)/ Soniox stt-rt-v4(ES)。仅当用户明确提出要求时才覆盖这些设置。

工作流

  1. 检查项目根目录中是否有 ROUGH_DRAFT.md。
    • 如果存在 → 继续执行第 2 步。
    • 如果不存在 → 告知用户,此技能需要一份粗略草稿才能继续。提供下方“ROUGH_DRAFT.md 模板”中的模板;可以替用户粘贴模板,也可以等用户自行提供。在没有草稿之前,不要继续执行此步骤之后的操作。
  2. 检测场景 — 每个 ## N. <scenario name> 标题代表一个场景。根据名称生成 snake_case 格式的 scenarioId(例如,“Lead Qualification & Screening” → qualification;“Appointment Scheduling” → appointment)。不确定时,选择最短且含义明确的名词。尽量缩短 ID——它们将用作环境变量名。
  3. 提取开场白 — 在每个场景下,查找 **On the page**、**Opening**、**Greeting**,或该部分中的第一段引号内容。这就是 firstMessage.en,须逐字保留。
  4. 提炼流程 — **What happens**(或含义相当的说明文字)将作为初步草稿版 body.md。以角色设定为主,不是契约——暂不添加失败规则、精确措辞或脚本化的退出话术。保持简短。
  5. 搭建项目根目录 — 对于 package.json、tsconfig.json、.env.example、.gitignore:如果文件不存在,则根据下方模板创建。如果 .env.example 已存在,则追加缺失的 VAPI_ASSISTANT_<SCENARIO>_<LANG> 项;绝不要重排或删除现有行。如果 package.json 已存在,则保持原样,但要确认依赖项中包含 @vapi-ai/server-sdk,且脚本中包含 bootstrap 和 typecheck——如果缺少任何一项,告知用户,不要编辑文件。
  6. 生成框架骨架 — 从下方模板逐字复制五个骨架文件。它们在不同项目间保持不变;只有场景注册表中的导入内容会变化。
  7. 生成每个场景的文件 — 针对每个检测到的场景,创建一个 scenarios/<id>.ts、一个 prompts/<id>/body.md 和一个 prompts/<id>/off-topic-es.md。
  8. 接入注册表 — scenarios/index.ts 导入每个场景并导出 SCENARIOS 常量、ScenarioId、SCENARIO_IDS、scenarioFor。
  9. 使用下方的双循环模板编写 src/bootstrap.ts。
  10. 将开场白翻译成西班牙语 — 使用自然的拉丁美洲西班牙语,不翻译品牌名称。
  11. 告知用户如何运行(见下方“验证”)。

在此技能中,将每个场景的 clientTools 数组保持为空——功能性信息采集工具将在后续步骤中添加。此处也不要将 body.md 改写成契约——那是单独的步骤。

文件模板

模板中使用以下占位符,需按项目替换:

  • <PROJECT_NAME> — 根据草稿标题生成的 slug(# Rough draft — <X> → 将 X 转换为 snake_case/kebab-case)。默认值:vapi-voice-agents。
  • <SCENARIO_ID> — snake_case 场景 ID(例如 qualification)
  • <SCENARIO_NAME> — 易读的名称(例如 Lead Qualification)
  • <FIRST_MESSAGE_EN> — 草稿中的开场白,逐字保留
  • <FIRST_MESSAGE_ES> — 自然的西班牙语译文
  • <BODY_DRAFT> — 提炼后的初步草稿提示词
  • <SCENARIO_IMPORTS> — 每个场景一条 import { <id> } from "./<id>.ts";,按字母顺序排列
  • <SCENARIO_KEYS> — SCENARIOS = { ... } 中以逗号分隔的场景 ID
  • <ENV_ASSISTANT_SLOTS> — 每个(场景 × 语言)一条注释行:# VAPI_ASSISTANT_<SCENARIO>_<LANG>=
package.json(仅在文件不存在时)
{
  "name": "<PROJECT_NAME>",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "bootstrap": "bun run src/bootstrap.ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@vapi-ai/server-sdk": "^1.2.0"
  },
  "devDependencies": {
    "@types/bun": "^1.3.13",
    "typescript": "^5.9.3"
  },
  "packageManager": "[email protected]"
}
tsconfig.json(仅在文件不存在时)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "strict": true,
    "types": ["bun"]
  },
  "include": ["src/**/*.ts"]
}
.env.example(不存在时创建;已存在时扩展)
# Get this from https://dashboard.vapi.ai/keys
VAPI_API_KEY=

# One slot per (scenario × language). First `bun run bootstrap` prints the
# ids; paste them here, then re-run for idempotent updates.
<ENV_ASSISTANT_SLOTS>
.gitignore(仅在文件不存在时)
# dependencies
node_modules

# env (local-only secrets)
.env*.local

# os junk
.DS_Store

# bun
*.tsbuildinfo
src/assistants/languages.ts
/**
 * Per-language voice + transcriber stack. Adding a 3rd language is one
 * entry in each record below.
 */
import type { LanguageId } from "./loadPrompt.ts";

export type { LanguageId };

interface VoiceConfig {
  provider: "11labs";
  model: string;
  voiceId: string;
}

interface TranscriberConfig {
  provider: "deepgram" | "soniox";
  model: string;
  language: string;
}

const VOICE_BY_LANGUAGE: Record<LanguageId, VoiceConfig> = {
  en: {
    provider: "11labs",
    model: "eleven_turbo_v2",
    voiceId: "ZoiZ8fuDWInAcwPXaVeq",
  },
  es: {
    provider: "11labs",
    model: "eleven_multilingual_v2",
    voiceId: "JYyJjNPfmNJdaby8LdZs",
  },
};

const TRANSCRIBER_BY_LANGUAGE: Record<LanguageId, TranscriberConfig> = {
  en: { provider: "deepgram", model: "nova-3", language: "en" },
  es: { provider: "soniox", model: "stt-rt-v4", language: "es" },
};

export const voiceFor = (languageId: LanguageId): VoiceConfig =>
  VOICE_BY_LANGUAGE[languageId];

export const transcriberFor = (languageId: LanguageId): TranscriberConfig =>
  TRANSCRIBER_BY_LANGUAGE[languageId];
src/assistants/loadPrompt.ts
/**
 * EN returns body.md unchanged. ES prepends a Spanish preamble with the
 * scenario's off-topic redirects spliced into {{OFF_TOPIC_LINES}}.
 * One body.md per scenario drives every language variant.
 */
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

export type LanguageId = "en" | "es";

const PROMPT_DIR = resolve(import.meta.dir, "prompts");

const read = (relativePath: string): string =>
  readFileSync(resolve(PROMPT_DIR, relativePath), "utf8");

export const loadPrompt = (
  scenarioId: string,
  languageId: LanguageId,
): string => {
  const body = read(`${scenarioId}/body.md`);
  if (languageId === "en") return body;

  const offTopic = read(`${scenarioId}/off-topic-${languageId}.md`).trim();
  const preamble = read(`shared/preambles/${languageId}.md`).replace(
    "{{OFF_TOPIC_LINES}}",
    offTopic,
  );
  return `${preamble}\n\n${body}`;
};
src/assistants/scenarios/index.ts
/**
 * Scenario registry. Adding a new scenario is one entry here plus one new
 * file under `./<scenario-id>.ts` and matching prompts under `../prompts/<id>/`.
 */
// <SCENARIO_IMPORTS>

export const SCENARIOS = {
  // <SCENARIO_KEYS>
} as const;

export type ScenarioId = keyof typeof SCENARIOS;

export const SCENARIO_IDS = Object.keys(SCENARIOS) as ScenarioId[];

export const scenarioFor = (id: ScenarioId) => SCENARIOS[id];
src/assistants/buildAssistant.ts
/**
 * Compose a full Vapi assistant body for a (scenario, language) tuple.
 * Voice + transcriber come from languages.ts; prompt from loadPrompt;
 * name + firstMessage + clientTools from the scenario.
 */
import { transcriberFor, voiceFor } from "./languages.ts";
import { loadPrompt, type LanguageId } from "./loadPrompt.ts";
import { scenarioFor, type ScenarioId } from "./scenarios/index.ts";

export type { LanguageId, ScenarioId };

export const buildAssistant = (
  scenarioId: ScenarioId,
  languageId: LanguageId,
) => {
  const scenario = scenarioFor(scenarioId);
  const systemPrompt = loadPrompt(scenarioId, languageId);

  return {
    name: `${languageId.toUpperCase()} - ${scenario.name}`,
    firstMessage: scenario.firstMessage[languageId],
    voice: voiceFor(languageId),
    transcriber: transcriberFor(languageId),
    model: {
      provider: "openai" as const,
      model: "gpt-4.1",
      temperature: 0.5,
      messages: [{ role: "system" as const, content: systemPrompt }],
      tools: scenario.clientTools,
    },
  };
};
src/assistants/scenarios/<SCENARIO_ID>.ts
/**
 * <SCENARIO_NAME> scenario. Plain data: id, name, language-keyed first
 * message. Later steps add `clientTools` (capture tools fire mid-call).
 */
// Rename `scenarioId` to the generated <SCENARIO_ID> identifier.
export const scenarioId = {
  id: "<SCENARIO_ID>" as const,
  name: "<SCENARIO_NAME>",
  firstMessage: {
    en: "<FIRST_MESSAGE_EN>",
    es: "<FIRST_MESSAGE_ES>",
  },
  clientTools: [] as const,
};

// Rename this type to the generated <PascalCase scenario id>Scenario name.
export type ScenarioIdScenario = typeof scenarioId;

如果 <FIRST_MESSAGE_EN> 较长,为便于阅读,可将其拆分为多个拼接的字符串片段(每行一个完整分句,并使用 " + " 连接)。

src/assistants/prompts/<SCENARIO_ID>/body.md

粗略的初稿——由角色设定驱动,不是契约。建议结构:

# <SCENARIO_NAME> voice agent

You are the <SCENARIO_NAME> voice agent for Vapi. <One sentence on context — who you're talking to and why.>

<One paragraph distilling **What happens** from the rough draft: the questions to ask, the data to collect, the routing logic, the wrap-up.>

Be warm and curious. Ask one question at a time. If they go off-topic, redirect briefly and return to the next missing field. Keep replies short — you are speaking, not typing.

不要加入 ## Absolute rules、失败规则或预设的退出路径。这些是后续步骤。

src/assistants/prompts/<SCENARIO_ID>/off-topic-es.md

两行简短的西班牙语引导语,针对该场景:

- "Buena pregunta — el equipo te puede ayudar con eso. ¿Podemos seguir con <next field>?"
- "Tomo nota, lo vemos después. Mientras tanto, cuéntame <one short ask tied to the scenario>."
src/assistants/prompts/shared/preambles/es.md
# IDIOMA / LANGUAGE OVERRIDE

The contract that follows this preamble is written in English. **This call is in Spanish.**

Override the language rule of the contract:

- ALL agent speech MUST be in natural, conversational Latin American Spanish. Translate the exact wordings, examples, and acks from the contract — don't switch back to English mid-sentence.
- Read brand and product names in their original form. Don't translate them.
- When you call any capture tool, **always pass free-text fields as a short English summary**, regardless of the call language. Cross-language analytics depend on it.

## Off-topic redirects (use verbatim)

If the visitor goes off-topic, pick one of these and then return to the next missing field:

{{OFF_TOPIC_LINES}}

---
src/bootstrap.ts
/**
 * Idempotent upsert across (scenario × language). One entry per tuple, keyed
 * by VAPI_ASSISTANT_<SCENARIO>_<LANG>. First run creates + prints ids;
 * subsequent runs update in place.
 *
 * Run with `bun run bootstrap`. Bun auto-loads .env.local.
 */
import { VapiClient } from "@vapi-ai/server-sdk";

import {
  buildAssistant,
  type LanguageId,
  type ScenarioId,
} from "./assistants/buildAssistant.ts";
import { SCENARIO_IDS } from "./assistants/scenarios/index.ts";

const LANGUAGES: LanguageId[] = ["en", "es"];

const envVarFor = (scenarioId: ScenarioId, languageId: LanguageId): string =>
  `VAPI_ASSISTANT_${scenarioId.toUpperCase()}_${languageId.toUpperCase()}`;

const requireEnv = (name: string): string => {
  const value = process.env[name];
  if (!value) {
    console.error(`✗ Missing env var: ${name}. See .env.example.`);
    process.exit(1);
  }
  return value;
};

const main = async () => {
  const vapi = new VapiClient({ token: requireEnv("VAPI_API_KEY") });
  const created: Array<{ envVar: string; id: string }> = [];

  for (const scenarioId of SCENARIO_IDS) {
    for (const languageId of LANGUAGES) {
      const envVar = envVarFor(scenarioId, languageId);
      const existingId = process.env[envVar];
      const body = buildAssistant(
        scenarioId,
        languageId,
      ) as unknown as Parameters<typeof vapi.assistants.create>[0];

      const label = `${scenarioId}/${languageId}`;
      let updated = false;

      if (existingId) {
        try {
          await vapi.assistants.update({
            id: existingId,
            ...body,
          } as unknown as Parameters<typeof vapi.assistants.update>[0]);
          console.log(`✓ Updated ${label} → ${existingId}`);
          updated = true;
        } catch (err) {
          const statusCode = (err as { statusCode?: number })?.statusCode;
          if (statusCode === 404) {
            console.log(
              `  ${envVar}=${existingId} not found in this org; creating a new assistant.`,
            );
          } else {
            throw err;
          }
        }
      }

      if (!updated) {
        const assistant = await vapi.assistants.create(body);
        console.log(`✓ Created ${label} → ${assistant.id}`);
        created.push({ envVar, id: assistant.id });
      }
    }
  }

  if (created.length > 0) {
    console.log("\nAdd these to .env.local:");
    for (const { envVar, id } of created) {
      console.log(`  ${envVar}=${id}`);
    }
    console.log(
      "\nThen re-run `bun run bootstrap` to confirm idempotent updates.",
    );
  } else {
    console.log(
      `\nAll ${SCENARIO_IDS.length * LANGUAGES.length} assistants updated in place.`,
    );
  }
};

main().catch((err) => {
  console.error("✗ Bootstrap failed:", err);
  process.exit(1);
});

ROUGH_DRAFT.md 模板

如果用户还没有粗略初稿,请提供这个框架(他们填写尖括号中的内容,技能会据此搭建项目):

# Rough draft — <project name>

## 1. <First scenario name>

**On the page**: "<one verbatim opening line the agent will say>"

**What happens**: <One paragraph: the questions the agent asks, the data it collects, what it does with edge cases, how it wraps up.>

## 2. <Second scenario name>

**Opening**: "<one verbatim opening line>"

**What happens**: <One paragraph.>

一个场景可以,三个场景也可以。技能会根据粗略初稿中的内容调整引导流程。

调整默认值

如果用户要求使用不同的默认值,只修改对应的部分,不要改动周围的结构:

  • 使用不同的语言:在 VOICE_BY_LANGUAGE 和 TRANSCRIBER_BY_LANGUAGE 中添加条目,在 loadPrompt.ts 中扩展 LanguageId 联合类型,添加 prompts/shared/preambles/<lang>.md,为每个场景添加 firstMessage.<lang>,将该语言添加到 bootstrap.ts 中的 LANGUAGES,并在 .env.example 中添加对应的 VAPI_ASSISTANT_<SCENARIO>_<LANG> 配置项。
  • 使用不同的模型 / 温度:只修改 buildAssistant.ts。
  • 使用不同的语音服务提供方:只修改 languages.ts 中的 VoiceConfig 和 VOICE_BY_LANGUAGE。

验证

生成文件后,告诉用户运行:

bun install
bun run typecheck              # validates locally; creates nothing in Vapi
cp .env.example .env.local     # add VAPI_API_KEY from dashboard.vapi.ai/org/api-keys
bun run bootstrap              # creates one assistant per (scenario × language)
# paste the printed VAPI_ASSISTANT_<SCENARIO>_<LANG>=<id> lines into .env.local
bun run bootstrap              # second run prints "Updated ..." for every tuple

对于 N 个场景 × 2 种语言,用户应能在 dashboard.vapi.ai 中看到 N × 2 个助手。每个助手都有一个绿色的“与助手通话”按钮——Agent 会带着用户走完整个流程,但目前仍较为粗略(尚无失败规则,也没有采集工具)。这是预期情况;后续的提示词会进一步完善它们。

评论

支持 Markdown