跳到正文

HTTP 接口文档 ​

外呼派发接口背后的领取、容量、originate 与终态闭环见 外呼任务生命周期。

callout-server 默认监听 0.0.0.0:9920。所有接口(除特别说明)都走会话鉴权,所有响应都用统一信封。

通用约定 ​

响应信封 ​

jsonc
// 成功
{ "code": "0000", "data": { /* ... */ }, "msg": "获取成功" }
// 失败
{ "code": "404", "data": null, "msg": "中文错误信息" }

三个顶层字段始终存在,成功码固定为字符串 "0000",失败码通常与 HTTP status 一致。SSE 事件、二进制下载、音频流和 OPTIONS 预检不套用 JSON 信封;业务数据内部 原本名为 ok 的字段保持不变。 HTTP 状态码:200 成功;400 参数错误;401 未鉴权/凭证无效;403 权限不足; 404 资源不存在;409 冲突(重复编码/忙碌中/锁竞争/非空删除);410 资源已过期; 500 服务端错误;502/503 下游(FreeSWITCH/Redis)不可用。

鉴权 ​

启用鉴权(auth.enabled=true;代码缺省为 false,仓库 config.toml 已置为 true)时:

  1. POST /api/auth/login 用用户名/密码换取 token。
  2. 后续请求带 Authorization: Bearer <token>。
  3. GET /api/auth/me 返回当前用户与其 permissions 列表(前端据此做菜单/按钮门控)。

本地联调可设 CALLOUT_AUTH_ENABLED=false 关闭鉴权。启用时自动创建引导管理员 (默认 admin / 见部署配置,可用 CALLOUT_AUTH_BOOTSTRAP_ADMIN_USERNAME/PASSWORD 覆盖)。

内部服务间通信分两类鉴权:

  • 仅内部 token:/api/call-results、/api/call-progress、/api/transfers/route、/api/transfers/end、/api/scripts/runtime 只校验请求头 X-Callout-Internal-Token 与 server.serviceApi.authToken 相等(配置为空时放行)。
  • 会话或内部 token:AI 一族接口(/api/ai/nlu/analyze、/api/ai/chat/stream、/api/ai/chat/completions、 GET /api/ai/conversation/capabilities、/api/ai/generate、/api/ai/simulate-user)走 requireAuth():可携带管理端 Bearer 会话, 或请求头 X-Callout-Internal-Token(须与已配置的 server.serviceApi.authToken 相等,配置为空时不放行)。

角色与权限矩阵 ​

权限adminsupervisoragent
admin(用户/系统)✓
audit:read(审计)✓
line:write(线路)✓
campaign:write(任务)✓✓
campaign:dispatch(派发/会议监控/主管介入)✓✓
policy:write(策略)✓✓
compliance:write(DNC 合规)✓✓
compliance:update(编辑免打扰号码)✓
import:write(导入)✓✓
contact:write(联系人/结果)✓✓✓
agent:write(坐席花名册管理)✓✓
agent:operate(坐席自助:状态/呼出/回拨/转人工/踢会议成员)✓✓✓
ai:strategy(NLU / Chat 策略配置)✓✓
ai:usage(AI 用量与调用明细)✓✓
ai:generate(AI 生成流程脚本 / 策略)✓✓
ai:simulate(流程演示模拟用户)✓✓

agent:write(管理坐席)与 agent:operate(坐席本人操作)刻意分离:坐席能操作软电话工作流,但不能管理花名册。

上表是角色内置权限(services/auth/auth.ts 的 rolePermissions,随角色固定)。 compliance:update 是独立的编辑按钮权限,普通角色需要在系统管理页显式授权,不默认授予主管或坐席。 合规页面访问仍需 compliance:write,因此普通用户使用页面编辑功能需要同时具备这两个权限。 超级管理员及本地免登录模式可编辑免打扰号码。 /api/users/*、/api/roles/*、/api/menus/* 一族另用一套数据库按钮权限(system:user:* / system:role:* / system:menu:*,由 db/seed.ts 播种进 callout.role_buttons),判定顺序是: 超级角色 R_SUPER、role=admin、或 buttons 里含 admin 通配 → 直接放行;否则要求 buttons 精确包含该权限码。因此这组权限不能靠改角色获得,要在系统管理页给角色勾按钮。

路由风格、分页与搜索 ​

  • 纯静态扁平路径:严禁在 URL 中使用动态路径参数(如 /:id),无参或简单标量查询使用 GET,复杂筛选分页与所有变更动作统一使用 POST + JSON 扁平请求体。
  • 分页与搜索:分页列表接口统一使用 POST + JSON 请求体(如 { "page": 1, "pageSize": 50, "q": "..." }),分页响应统一为 { rows, page, pageSize, total }。
  • 批量/单条删除:统一走 POST /api/<resource>/delete,请求体接收 { "ids": [...] }(单删传单元素数组 [id]),响应返回 { "ids": [...], "deleted": number }。

鉴权与权限 ​

POST /api/auth/login ​

请求:{ "username": "admin", "password": "******" } 响应 data:

jsonc
{
  "token": "<bearer-token>",
  "expiresAt": "2026-06-16T09:00:00.000Z",
  "user": { "id": 1, "username": "admin", "displayName": "管理员", "role": "admin" },
  "permissions": ["admin", "campaign:write", ...]
}

POST /api/auth/refresh ​

显式刷新当前登录会话有效期(顺延 auth.sessionTtlSeconds)。请求头携带当前 Authorization: Bearer <token>,响应 data 返回最新到期时间:

jsonc
{
  "expiresAt": "2026-06-17T09:00:00.000Z"
}

NOTE

系统同时内置会话自动滑动续期:普通请求到达时,若会话剩余生命周期不足总 sessionTtlSeconds 的 50%,服务端会在后台异步自动顺延,前端一般无需高频显式轮询刷新。

POST /api/auth/logout ​

注销当前会话(请求头带 Bearer Token)。data 返回已注销用户摘要。

GET /api/auth/me ​

返回当前用户与其 permissions 列表;未登录(或凭据无效)时直接返回 401。

GET /api/user/events ​

当前登录用户的全局实时事件流(text/event-stream,需登录)。由系统路由模块提供,连接建立时先发一帧 connected;同一账号在别处登录时,服务端向旧连接推 kickout ({ reason: "duplicate_login", message, timestamp })后主动断开,前端据此弹提示并跳登录页; 坐席状态发生流转时(预占、接听、释放、切状态)毫秒级下发 agent_status 事件驱动前端原地变色; 其余时间周期性下发 : ping\n\n 心跳注释维持连接。鉴权关闭时不做登录校验。

POST /api/auth/change-password ​

登录用户修改本人密码(请求体 { "oldPassword": "...", "newPassword": "..." })。


仪表盘、运行态与 AI 策略中心 ​

GET /api/ai/nlu/capabilities ​

由 callout-server 原生提供 NLU 能力发现接口。响应 data.actions 包含 decision / extract,每项提供已启用的 profiles[{ key, labelZh, descriptionZh }](平台已彻底废除默认概念,不再隐式回退,调用执行接口必须显式传参)。

注意:NLU 执行接口 POST /api/ai/nlu/analyze 同样由 callout-server 原生提供(默认端口 9920),供 callflow-esl 直连调用。

GET /api/ai/chat/capabilities ​

同一模式的 Chat 能力发现接口。 流程脚本 chat 节点的 chat_profile 只能取自本接口返回的 profile key, 见 流程脚本 Chat 节点开发。

AI 策略、模型与运行时生成(/api/ai/* 与 /api/scripts/*) ​

方法路径权限说明
GET/api/ai/llm-config会话/内部Token获取当前 LLM 全局推理配置、连接状态与场景模型映射
POST/api/ai/llm-config/updateai:strategy更新 LLM 全局推理配置、API Key 与场景模型超时参数
POST/api/ai/llm-config/test会话/内部Token测试指定 OpenAI 连接参数连通性并实时探测拉取可用模型列表
GET/api/ai/models会话/内部Token查询当前已连接/已启用的推理模型列表
POST/api/ai/usage/listai:usage分页查询 AI 调用用量明细与耗时记录(请求体 page、pageSize、service、action、profile、outcome)
POST/api/ai/usage/summaryai:usage查询 AI 调用总量、成功率分布与趋势看板(请求体 { "days"?: 14 })
POST/api/ai/nlu/analyze会话/内部Token统一 NLU 决策判定(action=decision)或槽位抽取(action=extract),返回 debug 诊断信息
POST/api/ai/nlu/evalai:strategy运营控制台单步调试 NLU 提示词与意图识别效果(返回识别意图与槽位结果)
GET/api/ai/nlu/capabilities会话/内部Token查询 NLU 支持的意图识别动作、模型以及可提取槽位元数据
POST/api/ai/chat/stream会话/内部Token大模型多轮流式 SSE 对话接口,流式下发 delta 与 complete
GET/api/ai/chat/capabilities会话/内部Token查询大模型对话能力与支持的模型列表
POST/api/scripts/node-prompt-updatecampaign:write原子局部更新指定流程节点的 Prompt 属性与微调超参(请求体含 scriptId、nodeId)
POST/api/ai/chat/completions会话/内部Token执行独立问答与对话策略(支持 start 与 query 模式,供知识库业务直连)
GET/api/ai/conversation/capabilities会话/内部Token查询已注册的独立对话策略列表与元数据
POST/api/ai/generateai:generate/内部Token按自然语言业务需求一键生成外呼流程图脚本(请求体 { "description", "last_error"? })
POST/api/ai/generate-profileai:generate/内部Token按流程节点属性与一句话指令生成对应 Prompt 与策略配置(请求体 { "action", "node", "variableCatalog"?, "instruction"? })
POST/api/ai/simulate-userai:simulate/内部Token流程演示:collect/router 判定与模拟客户回答(chat 节点改走 /api/ai/chat/stream,与运行时同源)

AI 全局配置与用量数据持久化存储于 PostgreSQL callout schema(表 ai_llm_configs 与 ai_usage_records)。流程节点策略与 Prompt 直接内联持久化于 callout.scripts 表的 flowJson 结构中(通过 POST /api/scripts/node-prompt-update 原子维护,无需全量重写画布)。三栏式话术策略运营工作台(含大纲树、微拓扑、一句话 Prompt 生成与常驻即时拨测仿真)原生内置于 callout-webpage 的「AI 策略中心」导航中。

独立对话策略(/api/ai/chat/completions) ​

独立对话策略是一套确定性、轻量级的单/多轮问答机制,与大模型 Chat Profile 相互独立。它采用确定性 JSON 请求/响应,不调用模型推理,直接供 callflow-esl 侧的知识库录音对话业务(如 kb-audio-chat-business)或联调测试使用。

GET /api/ai/conversation/capabilities ​

查询当前已注册的对话策略能力元数据:

jsonc
{
  "code": "0000",
  "data": {
    "strategies": [
      {
        "name": "kb-mock",
        "description": "以确定性文本回显模拟知识库问答,用于本地联调。"
      }
    ]
  },
  "msg": "请求成功"
}

POST /api/ai/chat/completions ​

执行对话策略。请求头携带 X-Callout-Internal-Token。支持两种动作模式:

  1. 会话开启 (action: "start"):
    • 请求:{ "strategy": "kb-mock", "action": "start", "sessionId": "sess-123" }
    • 响应 data:
      jsonc
      {
        "openingPrompt": "您好,请问有什么可以帮您?",
        "errorPrompt": "抱歉,系统暂时没有找到相关内容,请您换个说法再试试。"
      }
  2. 多轮问答 (action: "query"):
    • 请求:{ "strategy": "kb-mock", "action": "query", "sessionId": "sess-123", "text": "咨询套餐" }
    • 响应 data:
      jsonc
      {
        "answerText": "知识库模拟回答:咨询套餐",
        "hangup": false
      }

内部流程生成与模拟(/api/ai/generate* 与 /api/ai/simulate-user) ​

同时支持管理端会话(ai:generate / ai:simulate)或内部服务 Token(X-Callout-Internal-Token)调用:

  • POST /api/ai/generate:请求 { "description": "业务需求描述", "last_error"?: "..." },返回由大模型推理生成的完整流程节点图(nodes、edges)及建议新策略。
  • POST /api/ai/generate-profile:请求 { "action": "decision" | "extract" | "chat", "node": {...}, "variableCatalog"?: [...], "instruction"?: "..." },返回针对当前节点生成的 system_prompt 与微调超参。
  • POST /api/ai/simulate-user:请求 { "mode": "generate" 或 "classify"(必填), "node_type"?, "speaker"?: "user"/"bot", "bot_prompt"?, "user_text"?, "dialogue_history"?, "branches"?, "fields"?, "variables"? };mode=generate(需 bot_prompt)返回模拟客户回复(user_reply),mode=classify(需 user_text)按 node_type 返回意图判定或字段抽取结果。

仪表盘与运行态接口 ​

方法路径权限说明
GET/health公开服务与数据库连通性
GET/api/dashboard/overview会话数量、比率、近期活动/呼叫尝试
POST/api/dashboard/trends会话按天聚合的呼叫/结果趋势(请求体 { "days"?: 14 },范围 1–90)
POST/api/dashboard/call-stats会话呼叫统计分布聚合(请求体 { "campaignId"?: 1, "lineId"?: 1, "days"?: 7, "startDate"?: "...", "endDate"?: "..." },默认最近 7 天)
GET/api/media/config会话前端拼接录音和情绪复核音频播放地址所需的 baseUrl
POST/api/media/list会话分页查询指定类型的音频媒体元数据(请求体 { "page", "pageSize", "type"?: "recording" / "prompt" / "tts" })
POST/api/media/deletecampaign:write删除指定音频媒体文件(请求体 { "ids": [...] })
GET/api/runtime/overview会话运行中并发、卡住联系人、各任务进度、各线路并发、近期运行事件
GET/api/dashboard/runtime/overview会话/api/runtime/overview 的等价别名
GET/api/runtime/events会话SSE 运行事件流(text/event-stream)
POST/api/runtime/events-acknowledge会话确认某条运行事件/告警(请求体 { "eventId": 1 })
POST/api/audit/listadmin敏感操作审计日志(请求体 { "page", "pageSize", "q", "action", "resourceType", "operatorId" })
GET/api/system/configadmin脱敏系统配置(网络/鉴权/日志/媒体/运行时版本)
GET/api/system/statsadmin运行时长、内存/CPU、SSE 在线连接数与数据库连通性
POST/api/system/reloadadmin手动热重载运行时组件(请求体 { "target"?: "config" / "cache" / "rate-limiter" / "all", "reason"? },默认 all)

GET /api/runtime/overview 的 data(RuntimeOverviewResponse)含:activeContacts、stuckContacts、 runningBatches、scheduler.{submittedLastMinute,failedLastMinute,completedBatchesLastMinute}、 campaignProgress[]、lineConcurrency[]、cellConcurrency[](各 FS cell 的 { cellId, maxConcurrency, active },maxConcurrency 0 = 不限制)、stuckBatches[]、recentEvents[]。


业务编码(callflow 业务) ​

GET /api/businesses/list(或 POST /api/businesses/list) ​

列出 callflow 库中的 call_businesses,供创建任务时选择 businessCode(另提供 POST /api/businesses/detail、POST /api/businesses/create、POST /api/businesses/update、POST /api/businesses/delete 管理接口)。 响应统一包装在标准 ApiResponse({ code: "0000", data: { businesses: Business[] }, msg: "请求成功" })中,businesses 是按场景分类排序后的数组:

jsonc
{
  "code": "0000",
  "data": {
    "businesses": [
      {
        "businessCode": "callout-flow-business",
        "businessName": "通用外呼流程解释器",
        "businessCategory": "outbound",
        "isEnabled": 1,
        "remark": null
      }
    ]
  },
  "msg": "请求成功"
}

businessCategory 是代码注册表同步的四值主场景分类(outbound / inbound / assist / demo),仅供展示/分组;创建和更新任务仍提交 businessCode,分类不进入 campaign 数据、派单判断或 FreeSWITCH 变量。


流程脚本 scripts ​

流程脚本是系统二的话术资产:前端可视化编排节点图(start / send / collect / router / var_router / chat / end / transfer),运行时由 callflow-esl 的 callout-flow-business 按 runtime 接口输出的 JSON 逐节点执行。NLU 与对话节点契约分别见 flow-script-nlu-development.md 和 flow-script-chat-development.md。

方法路径权限说明
GET/api/scripts/list会话列出流程脚本,供外呼活动选择与管理页展示
POST/api/scripts/detail会话获取单个脚本详情(请求体 { "scriptId": 4 })
POST/api/scripts/savecampaign:write新建或更新流程脚本(更新时请求体传 scriptId;含名称、说明与 flowJson 节点图)
POST/api/scripts/clonecampaign:write克隆指定流程脚本(请求体 { "scriptId": 4, "name"?: "..." })
POST/api/scripts/draft-publishcampaign:write发布前图结构校验并刷新脚本更新时间(请求体 { "scriptId": 4, "status": "draft" 或 "published" };脚本无发布态字段,运行时不做发布过滤)
POST/api/scripts/node-prompt-updatecampaign:write原子局部更新指定流程节点的 Prompt 与超参(请求体含 scriptId、nodeId)
POST/api/scripts/deletecampaign:write软删除脚本,保留历史引用(请求体 { "ids": [1] })
POST/api/scripts/runtime内部 token输出 callout-flow-business 执行用的运行时 JSON(请求体 { "scriptId": 4 })

POST /api/scripts/runtime 不走会话鉴权:与 /api/call-results 一样比对 X-Callout-Internal-Token 与 server.serviceApi.authToken(配置为空时放行),供 callflow-esl 在通话内拉取。


外呼线路 lines ​

线路决定主叫号码与并发;任务必须绑定至少一条启用线路。

方法路径权限
GET/api/lines/list会话
POST/api/lines/createline:write(仅 admin;numbers[] 差量维护号码池)
POST/api/lines/updateline:write(请求体含 lineId;numbers[] 差量维护号码池)
POST/api/lines/status会话(线路状态快照,请求体 { "lineId"?: 1 },仅查询不启停)
POST/api/lines/deleteline:write(请求体 { "ids": [1] },仅软删未被引用的线路)
POST/api/lines/test-dialline:write(线路拨测,请求体 { "lineId": 1, "targetNumber": "...", "callerId"?: "..." })

POST /api/lines/create 请求体(CreateLineInput):

jsonc
{
  "name": "默认外呼线路",
  "code": "default-trunk",
  "dialStringTemplate": "sofia/gateway/my-trunk/{{destinationNumber}}",
  "callerIdNumber": "01088886666", // 可选,single_shared 模式默认主叫
  "callerIdName": "示例企业", // 可选
  "maxConcurrency": 10, // 该线路下所有任务共享
  "allocationMode": "single_shared", // single_shared | number_pool | bound_numbers
  "callerIdHeaderMode": "caller_id_only", // caller_id_only | p_preferred_identity | from_and_p_preferred_identity
  "numbers": [
    // number_pool / bound_numbers 模式的号码池
    {
      "number": "01088886601",
      "weight": 2,
      "maxConcurrency": 1,
      "isEnabled": 1
    }
  ],
  "isEnabled": 1,
  "description": "..."
}

号码分配模式:single_shared(一号多并发)/ number_pool(多号轮换,按 weight)/ bound_numbers(一号一并发)。响应 data.line: CalloutLineRow(含 numbers[])。


外呼任务 campaigns ​

方法路径权限说明
POST/api/campaigns/list会话列表(请求体 { "page", "pageSize", "q", "status" })
POST/api/campaigns/createcampaign:write创建任务
POST/api/campaigns/detail会话详情(请求体 { "campaignId": 1 }):任务 + 联系人总数 + 近期呼叫尝试 + 批次
POST/api/campaigns/updatecampaign:write更新(请求体含 campaignId 及状态/并发/重试/工作日/绑定线路/业务配置等)
POST/api/campaigns/status-updatecampaign:write切换任务状态(请求体 { "campaignId": 1, "action": "start" / "pause" / "stop" / "archive" })
POST/api/campaigns/deletecampaign:write软删任务(请求体 { "ids": [1] },直接软删,无引用校验)
POST/api/campaigns/preflight会话启动前预检(请求体 { "campaignId": 1 }):状态、计划窗口、待呼名单、线路、策略

POST /api/campaigns/create 请求体(CreateCampaignInput):

jsonc
{
  "name": "外呼平台演示任务",
  "businessCode": "callout-flow-business", // 必须能在 call_businesses 查到启用记录
  "cellId": null, // 可选;绑定 config.cells 声明的 FS cell,空 = 派发时自动分配
  "lineIds": [1], // 至少一条启用线路
  "maxConcurrency": 5,
  "maxRetries": 2,
  "retryIntervalSeconds": 600,
  "callIntervalMs": 1000,
  "workdayStart": "09:00", // 可选 HH:mm
  "workdayEnd": "20:00",
  "scheduledStartAt": null,
  "scheduledEndAt": null,
  "businessConfig": {
    "variables": { "script_id": "4" } // 仅字符串值会注入通道变量
  },
  "description": "..."
}

POST /api/campaigns/update(或 POST /api/campaigns/status-update)把 status 改为 running 即启动(被调度器扫描);改 paused 暂停自动派发。 启动校验仅发生在 POST /api/campaigns/status-update 的 action=start:若既无待呼/可重试联系人,也无活跃通话,返回 HTTP 409 且保留原状态;已归档任务同样不可启动。 允许提前启动并以 running 等待 scheduledStartAt;若设置了 scheduledEndAt,启动接口会重新取服务端时间, 当前时间大于或等于结束时间时返回 HTTP 409,状态保持不变。预检通过后到期或直接调用启动接口均受此限制。 POST /api/campaigns/update 直接落库、不做该校验。等待未来重试或仍有活跃通话时允许保持/切换为 running。

POST /api/campaigns/preflight 返回 data: { allowed, checks };每项检查包含 { code, ok, message }。 scheduled_window 只检查计划结束时间是否已到;未设置结束时间时该项通过。 未来排期通过时提示“允许启动,等待计划开始时间后派发”;allowed=true 表示当前任务允许启动, 立即派发仍须满足服务端计划窗口 [scheduledStartAt, scheduledEndAt)、策略、工作时段、间隔和容量约束。 各检查编码与请求、响应结构保持不变。详细等待与完成语义见外呼任务生命周期。

cellId 传字符串改绑 cell(须在 config.cells 中声明),传 null 或空串解绑。 businessConfig 使用整组替换语义,不接受旧的顶层 variables / intentKeywords。空的 variables 或 intentKeywords 会被省略。知识库实时盯屏任务可在业务专属配置中提交 { "businessCode": "kb-audio-chat-business", "businessConfig": { "intentKeywords": ["可以", "有兴趣"], "monitoringMode": "elastic", "monitoringPriority": 50 } }。 monitoringMode 支持 strict(无人时停止新派发)、elastic(允许 AI 无人值守)与 unattended;这两个监控属性只对 kb-audio-chat-business 生效,切换到其它业务时服务端会清理。

POST /api/campaigns/detail 的 data(CampaignDetail):{ campaign, contactTotal, attempts[], batches[] }。


联系人 contacts ​

方法路径权限说明
POST/api/contacts/list会话列表(请求体 { "campaignId", "page", "pageSize", "q", "status" })
POST/api/contacts/createcontact:write粘贴号码同步导入,单次最多 1,000 行(请求体含 campaignId)
POST/api/contacts/updatecontact:write更新单个联系人(请求体含 campaignId、contactId)
POST/api/contacts/pausecontact:write批量暂停(请求体 { "campaignId", "contactIds" },避免运行中继续派发)
POST/api/contacts/retrycontact:write重置指定联系人为 pending(请求体 { "campaignId", "contactIds" })
POST/api/contacts/deletecontact:write批量/单条软删(请求体 { "campaignId": 1, "ids": [...] },campaignId 必填)

POST /api/contacts/create 请求体(CreateContactsInput):

jsonc
{
  "campaignId": 1,
  "contacts": [
    {
      "phoneNumber": "13800138000",
      "customerName": "王先生",
      "remark": "...",
      "metadata": { "orderId": "A123", "level": "vip" }
    } // 仅字符串值会作为通道变量注入
  ]
}

响应与文件导入一致,包含 totalRows / createdRows / skippedRows / duplicateRows / failedRows / errors。 errors 每项只有 { rowNumber, phoneNumber, reason };计数恒满足 totalRows = createdRows + duplicateRows + failedRows、skippedRows = duplicateRows + failedRows。 手工新增与粘贴入口不限制号码格式,只要求号码非空,并由服务端唯一索引最终去重。 CSV/TSV 文件导入仍按提交的地区、运营商和固话选项执行号码校验。


派发、批次与重试 ​

方法路径权限说明
POST/api/campaigns/dispatchcampaign:dispatch手动派发,最多 100 个待呼联系人(请求体 { "campaignId": 1, "limit": 20 })
POST/api/campaigns/retrycampaign:write把匹配联系人重置为 pending(请求体 { "campaignId": 1, "status"?: "failed", "maxAttempts"?: 2 })
POST/api/campaigns/batches-list会话派发批次历史(请求体 { "campaignId": 1, "page": 1, "pageSize": 20 })
POST/api/campaigns/batches-cancelcampaign:write取消仍在 running 的批次(请求体 { "campaignId": 1, "batchId": 42 })
POST/api/attempts/list会话呼叫明细列表(请求体含 page、pageSize、campaignId、status、callStatus、emotionStatus)
POST/api/attempts/detail会话按 ID 返回完整、未软删除的呼叫明细(请求体 { "attemptId": 555 });不存在时返回 404

POST /api/campaigns/dispatch 请求体 { "campaignId": 1, "limit": 20 }(limit 校验范围 1–100,越界返回 400)。 响应(DispatchCampaignResponse)data.result(DispatchResult):

jsonc
{
  "campaignId": 1, "batchId": 42,
  "requestedLimit": 20, "effectiveLimit": 20, "requested": 8,
  "submitted": 8, "failed": 0, "remainingPending": 12,
  "campaignStatus": "running",
  "attempts": [
    { "contactId": 100, "phoneNumber": "138...", "attemptId": 555,
      "status": "accepted", "requestId": "<uuid>", "replyText": "+OK ...",
      "callStatus": "answered",                 // 启用回铃检测时带
      "ringbackDetection": { "status": "answered", "detectionSource": "answer", ... } }
  ]
}
  • 派发可能被外呼策略拦截 → 409,error 以「当前外呼策略禁止派发」开头。
  • 锁竞争 → 409「任务正在调度中」;Redis 锁不可用 → 503。
  • 无可派发联系人 / 无可用槽位 / 呼叫间隔未到时不落批次,batchId 返回 null,requested/submitted/failed 均为 0。

POST /api/campaigns/retry 请求体(RetryContactsInput):

jsonc
{ "campaignId": 1, "status": "failed" } // 可选 "maxAttempts": 2(当前不影响重置行为)

把匹配联系人改回 pending。status 可取 failed/submitted/completed/paused。 按联系人 ID 定向重置请用 POST /api/contacts/retry(请求体 { "campaignId": 1, "contactIds": [1, 2] })。


小批量同步文件导入 ​

文件解析和号码校验先完成,再进入事务锁定 campaign 并按 1,000 行分批写入。除 running 外的任务状态均可导入;运行中返回 409,且不写入联系人。数据库异常会整体回滚。

方法路径权限说明
POST/api/imports/uploadimport:write上传最大 5 MiB、最多 1,000 行的 CSV/TSV 并同步返回结果(FormData 字段 campaignId 指定任务)

空文件、表头或列映射错误、未闭合 CSV 引号、超过 1,000 行返回 400;超过 5 MiB 返回 413。所有拒绝都发生在联系人事务写入前。


外呼策略 policies ​

全局/任务级禁呼、时段、号码规则、频控、重试与并发限制;执行语义更严格优先(全局与任务级都必须通过)。

方法路径权限说明
POST/api/policies/list会话查询策略列表(JSON 请求体,支持 campaignId、scope、type、q 与分页)
POST/api/policies/createpolicy:write新建外呼策略
POST/api/policies/updatepolicy:write更新外呼策略(请求体含 policyId)
POST/api/policies/deletepolicy:write删除外呼策略(请求体 { "ids": [1] })
POST/api/policies/evaluate会话外呼策略试算

POST /api/policies/create 请求体(CreateCalloutPolicyInput):

jsonc
{
  "name": "工作时段",
  "scope": "global", // global | campaign(campaign 时需 campaignId)
  "type": "daily_window",
  "action": "allow_window", // block | allow_window | limit
  "priority": 10,
  "config": { "start": "09:00", "end": "20:00" },
  "isEnabled": 1
}

策略类型 type 与 config:

typeconfig 示例作用
blocked_dates{ "dates": ["2026-05-01"] }命中日期阻断
holiday_blocklist{ "dates":["2026-02-17"], "recurringDates":["01-01"], "workdayOverrides":["2026-02-14"], "region":"CN" }节假日阻断 + 补班放行
daily_window{ "start":"09:00", "end":"20:00" }仅允许每日时段
weekly_window{ "days":[1,2,3,4,5], "start":"09:00", "end":"18:00" }仅允许星期时段
phone_blacklist{ "numbers":["138...","139..."] }按号码阻断
phone_prefix_blocklist{ "prefixes":["170","171"] }按前缀阻断
contact_frequency_limit{ "days":7, "maxAttempts":2 }N 天最大触达次数
rate_limit{ "callIntervalMs":2000 }收紧呼叫间隔
retry_rule{ "maxRetries":1, "retryIntervalSeconds":1800 }收紧重试
concurrency_limit{ "maxConcurrency":3 }收紧并发

POST /api/policies/evaluate 试算(PolicyEvaluationRequest → PolicyEvaluationResult):

jsonc
// 请求
{ "campaignId": 1, "phoneNumber": "13800138000", "at": "2026-06-15T03:00:00.000Z" }
// 响应 data
{ "allowed": false, "reason": "命中策略:工作时段(当前不在允许时段)",
  "matchedPolicies": [ /* CalloutPolicyRow */ ],
  "effectiveLimits": { "maxConcurrency": 3, "maxRetries": 1, "retryIntervalSeconds": 1800, "callIntervalMs": 2000 } }

DNC 免打扰名单 ​

派发前独立于策略表进行阻断;业务回写退订/拒呼时自动沉淀。

方法路径权限说明
POST/api/dnc/list会话分页查询(请求体 { "page", "pageSize", "q" })
POST/api/dnc/createcompliance:write新增免打扰号码
POST/api/dnc/updatecompliance:update按 ID 编辑未删除的免打扰号码
POST/api/dnc/batch-importcompliance:write批量导入免打扰号码
POST/api/dnc/deletecompliance:write删除免打扰号码(请求体 { "ids": [1] })

POST /api/dnc/create:{ "phoneNumber": "138...", "reason": "客户要求", "expiresAt": null }(expiresAt 为空=永久)。 已有同号码记录仍按新增接口的覆盖逻辑更新,可恢复已删除记录。

POST /api/dnc/update 请求示例:

json
{
  "dncId": 1,
  "phoneNumber": "13800000000",
  "source": "manual",
  "reason": null,
  "expiresAt": "2026-12-31T15:59:59.000Z"
}
  • dncId 为正整数;phoneNumber、source 必填,去除首尾空白后不能为空,分别最多 32、64 个字符。
  • reason 最多 255 个字符,传 null 清空;expiresAt 传 null 取消过期时间(永久有效)。两字段未传时保留原值。
  • 页面按浏览器本地时区显示与编辑时间,保存时转换为带时区的 ISO 时间;未修改过期时间时保留原始时间戳。已过期记录可通过修改过期时间续期。
  • 成功响应为统一信封中的 { "dnc": { /* 更新后的免打扰记录,含 isExpired */ } }(HTTP 200)。创建人和创建时间保持不变,更新修改人、修改时间,并在同一事务中记录修改前后审计快照。
  • 记录不存在或已删除返回 404,不恢复删除记录;号码与其他未删除记录重复返回 409(含数据库唯一约束并发冲突);非法参数返回 400,未登录返回 401,缺少编辑权限返回 403。

权限按钮在 src/db/seed.ts 的「合规与数据」菜单中声明,由现有种子同步机制写入;无需修改数据库结构。


结果回写(内部) ​

POST /api/call-results ​

内部接口(token 鉴权,非会话)。由 callflow-esl 业务在通话结束时回写。 完整字段、定位回退顺序、时长推导、录音地址归一化、transcript 落库语义以及级联副作用(DNC/情绪分析/状态联动/重试与任务闭环)见 result-schemas.md;业务侧怎么组装见 outbound-business-example.md。最小示例:

http
POST /api/call-results
Content-Type: application/json
X-Callout-Internal-Token: <server.serviceApi.authToken>

{ "requestId": "<callout_request_id>", "callStatus": "completed",
  "payload": { "result": { "recognizedText": "客户有意向" } } }

实时盯屏与人工介入 live-calls ​

坐席「一人盯多路、有意向即插入」的实时监控链路。数据源是 callflow-esl 的 kb-audio-chat-business 在通话过程中逐轮上报的进度事件写入 Redis TTL 共享快照, 并通过 Pub/Sub 向每个实例的本机 SSE 客户端广播。Redis 是必选依赖,服务启动时连接失败会 直接退出,不提供内存降级。快照不写业务库(挂机后的整段回写 /api/call-results 仍是权威数据)。链路与配置 详见 transfer-to-human.md §盯屏介入 与业务文档 callflow-esl/docs/existing-businesses.md。

方法路径权限说明
POST/api/call-progress内部 tokencallflow-esl 逐轮上报通话进度(started/segment/intervention/ended),更新共享快照并广播
POST/api/live-calls/listagent:operate当前用户实际获配及介入前保留只读通话的快照与全局容量摘要(请求体 { "campaignId"?: 1 })
POST/api/live-calls/detailagent:operate获取单路实时通话快照详情(请求体 { "callKey": "..." })
GET/api/live-calls/events?campaignIdagent:operate多路复用 SSE:推送实际获配及保留只读通话的字幕/关键词/介入/归属变化事件
POST/api/live-monitor/heartbeatagent:operate登记并续租盯屏 Presence,按坐席实时状态计算接管容量并触发重分配
POST/api/live-monitor/presence-leaveagent:operate主动离开盯屏并立即释放分配;异常断线由 TTL 回收
POST/api/live-calls/interveneagent:operate坐席认领人工介入(请求体 { "callKey": "..." }),信号转发给 callflow-esl(原子抢占,重复返回 409)
POST/api/live-calls/intervention/cancelagent:operate仅介入发起人取消仍为 requested 的请求(请求体 { "callKey": "..." });已消费进入转接返回 409
POST/api/live-calls/releaseagent:operate当前监控责任人主动让出(请求体 { "callKey": "..." });本次 Presence 生命周期内不再分回本人
POST/api/live-calls/hangupcampaign:dispatch主管/管理员以 NORMAL_CLEARING 结束整通客户业务(请求体 { "callKey": "..." }),写审计与运行事件
POST/api/live-monitor/bargeagent:operate主管强插指定实时通话(请求体 { "callKey", "supervisorExtension", "mode": "barge" })
POST/api/live-monitor/whisperagent:operate主管耳语指导指定实时通话(请求体 { "callKey", "supervisorExtension", "mode": "whisper" })
POST/api/live-monitor/interceptagent:operate主管拦截接管指定实时通话(请求体 { "callKey", "supervisorExtension", "mode": "intercept" })
  • callKey 形如 attempt:<attemptId>(或 request:<requestId>)。
  • /api/live-monitor/barge、whisper、intercept 请求体统一为 { "callKey", "supervisorExtension", "mode" }(callId 可替代 callKey);supervisorExtension 与 mode 为预留参数,三个路由当前都执行同一介入认领逻辑(等同 /api/live-calls/intervene)。
  • segment 事件的 speaker 取值:bot(AI 播报)/ customer(客户 ASR)/ agent(人工介入后坐席的会议转写)/ supervisor(主管发言);介入成功后坐席与客户的发言仍持续上报。
  • 意向关键词在 campaign 的 businessConfig.intentKeywords 中配置;收到客户 ASR 片段命中 任一关键词时,SSE 推 intent-alert 事件,盯屏页高亮 + 提示音。
  • 每路通话只分配一个主监控人;分配按 businessConfig.monitoringPriority、意向命中、等待时间、 坐席 cell 与页面任务过滤执行。同一登录用户的多个标签页合计仍受服务端容量限制。
  • 介入期间只保留介入通话的实际责任,其余原获配通话以 monitorViewMode=retained 继续只读展示; monitorAssignment 可为 null(等待接管)或指向另一坐席。介入结束后保留视图自动清除。
  • strict 任务会把活跃盯屏容量加入任务/线路/cell 的派发上限计算;无人时联系人保持待呼。
  • 介入生效有节点边界延迟(业务播完当前录音/收音后才转接),典型 3~10 秒。
  • Redis 已启用但不可用时,快照、SSE 建连和介入返回 503,不会退化为各实例分裂的本地状态。

坐席与软电话 agents ​

方法路径权限说明
GET/api/agents/list会话坐席 + 坐席组列表(亦支持 POST /api/agents/list 与 GET|POST /api/agent-groups/list)
POST/api/agents/detail会话获取单个坐席详情(请求体 { "agentId": 1 })
GET/api/agents/extensionsagent:writeFreeSWITCH 可用分机(含 freeswitchReachable)
POST/api/agents/extensionsagent:write同 GET(GET/POST 双注册)
GET/api/agents/phone-statusesagent:operate全部坐席的非敏感注册摘要:agentId / phoneExtension / registrationStatus;四态为 registered / unregistered / unconfigured / unknown
POST/api/agents/phone-statusesagent:operate同 GET(GET/POST 双注册)
POST/api/agent-groups/createagent:write创建坐席组
POST/api/agent-groups/updateagent:write更新坐席组(请求体含 id 或 groupId)
POST/api/agent-groups/deleteagent:write删除坐席组(请求体 { "ids": [1] })
POST/api/agents/createagent:write创建坐席
POST/api/agents/updateagent:write更新坐席(请求体含 id 或 agentId)
POST/api/agents/deleteagent:write删除坐席(请求体 { "ids": [1] },忙碌中不可删)
GET/api/agents/me/statusagent:operate当前用户绑定且启用的坐席状态及电话注册态摘要;未绑定时 agent: null
POST/api/agents/me/statusagent:operate切换本人绑定坐席状态(请求体 { "status": "idle" }),仅接受 idle / offline;手动示闲需电话 registered(可用),忙碌期间禁止手动切换
POST/api/agents/statusagent:operate切换指定坐席状态(请求体 { "agentId": 1, "status": "idle" });仅接受 idle / offline,旧 online 兼容映射为 idle;普通坐席仅限本人,主管/管理员可修改全部,但均不能切换忙碌坐席
POST/api/agents/callbacksagent:operate人工回拨(请求体含 agentId):originate 坐席分机后 bridge 客户
POST/api/agents/manual-callsagent:operate人工呼出任意客户号码(请求体含 agentId)
POST/api/agents/transfersagent:operate工作台对客户号码发起坐席↔客户桥接呼叫(请求体含 agentId,≠AI会议转人工,见 transfer-to-human.md §7)
POST/api/softphone-calls/listagent:operate软电话呼出/回拨/转人工记录分页列表(请求体含 page、pageSize、agentId、campaignId)

手动提交 busy 返回 400 参数错误,忙碌由系统控制。两个状态变更接口在坐席 busy 时均返回 409,提示“呼叫占用中,结束后自动示闲”;示闲还需电话注册态为 registered,未注册、未配置或不可判定时返回 409,非占用时允许离线。手动更新通过数据库条件排除 busy,与路由预占竞争时若条件不再满足,返回 409,不覆盖系统忙碌状态。状态成功变更后向所有在线客户端广播 agent_status,本人、主管操作以及系统预占和释放均通过该事件同步工作台与顶部菜单。

坐席公开结构只包含唯一终端标识 phoneExtension,不再返回 sipUsername。升级兼容期内, 创建/更新接口仍会把旧请求中单独出现的 sipUsername 映射到 phoneExtension;两个非空值 不一致时返回 400,禁止静默选择。

POST /api/agents/manual-calls 请求体:

jsonc
{
  "agentId": 1,
  "phoneNumber": "13800138000",
  "campaignId": 1,
  "contactId": 100,
  "note": "..."
}
// 也可只传 agentId + contactId,号码从联系人取

回拨/呼出会经 submitAgentBridgeCall 先 originate 坐席分机、接通后 &bridge 客户号码, 并落 agent_call_records 记录(direction=callback/manual_call)。


浏览器软电话会话 softphone-sessions ​

坐席浏览器 SIP 软电话链路:整体流程、坐席 SIP 字段、配置与 FreeSWITCH 侧要求见 softphone-sip.md。callout-server 在这组接口上做权限校验与 agent_call_records 落库,实时会话本体代理到 callflow-esl 的 browser-softphone-session-hub。

方法路径权限说明
GET/api/agents/me/sip-configagent:operate当前登录坐席的浏览器 SIP 注册配置(坐席与用户按 agents.userId 一对一绑定)
POST/api/agents/me/sip-configagent:operate同 GET(GET/POST 双注册)
GET/api/softphone/ice-config会话浏览器 SIP 注册前取 ICE / TURN 短期凭据;turn.enabled=false 时 iceServers 为空数组
POST/api/softphone/ice-config会话同 GET(兼容 POST 调用)
POST/api/softphone/ice-servers会话同 GET /api/softphone/ice-config(别名)
GET/api/agents/sip-configsagent:operate当前用户可见的已启用 SIP 配置列表(admin / supervisor 可见全部)
POST/api/softphone-sessions/createagent:operate创建会话:先落 agent_call_records,再请求 callflow-esl 创建实时会话;返回 sessionId、serviceNumber 与浏览器拨号必须原样携带的 sipExtraHeaders
GET/api/softphone-sessions/events?sessionId=...agent:operateSSE 实时事件流(session / message / tts / error);EventSource 场景支持 ?access_token= 查询参数鉴权
POST/api/softphone-sessions/messagesagent:operate坐席打字文本(请求体含 sessionId),经 TTS 注入客户腿播放
POST/api/softphone-sessions/controlsagent:operate控制命令(请求体含 sessionId):{"type":"hold"} / resume / {"type":"dtmf","digits":"..."} / {"type":"transfer","targetNumber":"..."}
POST/api/softphone-sessions/notesagent:operate更新会话备注(请求体含 sessionId、notes,另有可选 intent)
POST/api/softphone-sessions/endagent:operate结束会话(请求体含 sessionId,同时结束会议与录音收尾)
POST/api/softphone-calls/detailagent:operate坐席通话记录详情(请求体 { "id": 1 } 或 { "softphoneCallId": 1 }):消息时间线、TTS 事件、录音信息与分角色转写
POST/api/softphone-calls/deletecompliance:write软删除坐席通话记录(请求体:{"ids":[1,2]},响应:{"ids":[1,2],"deleted":2})

转人工会议 conferences ​

详见 transfer-to-human.md。

方法路径权限说明
POST/api/transfers/route内部 tokenAI 业务转人工路由:预占空闲坐席 + originate 入会(callflow-esl 调用)
POST/api/transfers/end内部 token客户挂机幂等收尾:校验客户 UUID,结束会议全部坐席/主管腿并释放坐席
GET/api/conferences/listcampaign:dispatch进行中会话列表(附 FS 实时成员数)
POST/api/conferences/detailcampaign:dispatch会话详情 + 成员腿(请求体 { "conferenceId": "..." })
POST/api/conferences/participants/addagent: agent:operate / supervisor: campaign:dispatch加成员/就地切换模式(请求体含 conferenceId,盲转/协商转/主管监听/强插/耳语指导)
POST/api/conferences/participants/removeagent:operate踢出一名成员腿(请求体 { "conferenceId": "...", "softphoneCallId": 12 })
POST/api/conferences/supervisor-modeagent:operate主管在会模式切换(monitor / whisper / barge)
POST/api/conferences/muteagent:operate静音/取消静音会议成员(请求体 { "conferenceId", "memberId", "mute" })
POST/api/conferences/kickagent:operate按会议成员 ID 踢出成员(请求体 { "conferenceId", "memberId" })
POST/api/conferences/lockagent:operate锁定/解锁会议室(请求体 { "conferenceId", "lock" })

录音与转写 ​

方法路径权限说明
GET/api/media/config会话返回兼容默认值 freeswitchRecordingBaseUrl、按 cell 映射 freeswitchRecordingBaseUrlsByCell 与 emotionAudioBaseUrl
POST/api/attempts/detail会话返回单条完整呼叫明细(请求体 { "attemptId": 555 });记录不存在或已软删除时返回 404
POST/api/attempts/transcript-segments会话按外呼尝试查询分角色转写片段(请求体 { "attemptId": 555 })
POST/api/attempts/deletecompliance:write软删通话记录(请求体:{"ids":[1,2]},响应:{"ids":[1,2],"deleted":2}),并清除 contacts.lastAttemptId

外呼录音由业务回写到 call_attempts.recordingUrl,数据库和呼叫尝试 API 响应都只保留文件名短值;前端按 attempt.cellId 从 freeswitchRecordingBaseUrlsByCell 选择前缀,历史空 cell 使用兼容默认值,非空失效 cell 不生成错误 URL。情绪分析 worker 使用相同规则。情绪分析音频仍用 emotionAudioBaseUrl 和短值拼接。


用户管理与系统管理 RBAC(/api/routes/*、/api/users/*、/api/roles/*、/api/menus/*) ​

callout-webpage 系统管理页与动态菜单使用的一组接口,落库在 callout schema 的 users / roles / menus / user_roles / role_menus / role_buttons 上:

  • 分页参数统一为 page(默认 1)/ pageSize(默认 20)。
  • 写操作全部走 POST + JSON 扁平静态路径,删除统一走 POST /api/<resource>/delete,请求体接收 { ids: T[] }(单删传单元素数组 [id]),响应统一返回 { ids: T[], deleted: number }。
  • 权限是 system:* 按钮权限,不在角色内置权限表里,判定规则见 角色与权限矩阵。
  • 用户与角色的写操作都会记一条审计日志(POST /api/audit/list 可查)。
方法路径权限说明
GET/api/routes/user-routes会话当前用户可见的动态路由树(按角色菜单授权生成)
GET/api/routes/constant-routes会话常量路由,当前固定返回空数组
POST/api/users/listsystem:user:view用户分页列表(username / displayName / nickName / userPhone / userEmail / userGender / status 过滤)
POST/api/users/detailsystem:user:view用户详情(请求体 { id })
POST/api/users/createsystem:user:create新建用户(含 userRoles[]、可绑定 agentId)
POST/api/users/updatesystem:user:update按 body id 更新用户
POST/api/users/deletesystem:user:delete删除用户(请求体 { ids: number[] },返回 { ids, deleted })
POST/api/users/password-resetsystem:user:update重置指定用户密码(请求体 { id, password },返回 { id })
POST/api/roles/listsystem:role:view角色分页列表(roleName / roleCode / status 过滤)
GET/api/roles/allsystem:role:view全部角色(下拉用)
GET/api/roles/permissions会话系统内置全量权限码清单(供权限矩阵配置)
POST/api/roles/createsystem:role:create新建角色
POST/api/roles/updatesystem:role:update更新角色
POST/api/roles/deletesystem:role:delete删除角色(请求体 { ids: number[] },返回 { ids, deleted })
POST/api/roles/menu-authsystem:role:auth查询角色已授权菜单 ID
POST/api/roles/menu-auth-updatesystem:role:auth覆盖角色菜单授权
POST/api/roles/button-authsystem:role:auth查询角色已授权按钮码
POST/api/roles/button-auth-updatesystem:role:auth覆盖角色按钮授权
GET/api/menus/listsystem:menu:view菜单树
GET/api/menus/buttonssystem:menu:view全部可授权按钮码
POST/api/menus/createsystem:menu:create新建菜单
POST/api/menus/updatesystem:menu:update更新菜单
POST/api/menus/status-updatesystem:menu:update启停菜单
POST/api/menus/deletesystem:menu:delete删除菜单(请求体 { ids: number[] },返回 { ids, deleted })

首次启动会播种超级角色 R_SUPER 与全部 system:* 按钮码 (db/seed.ts);持有 R_SUPER、role=admin 或 admin 按钮码的账号 无需逐个勾选即可访问本组接口。

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。