feat: implement Call Center queues (mod_callcenter)
- Investigated the real callcenter_config command surface via
'help callcenter_config' on the running FreeSWITCH before writing any
code: queues only have load/unload/reload (static XML + reload, no
'queue add' exists), while agents and tiers are fully dynamic via ESL
commands (agent add, tier add) -- no file involved. This shapes the next
phase (Agents/Tiers) differently from this one.
- queues table (tenant-scoped, RLS): strategy, moh/announce, wait times,
tier rules, discard/abandoned handling, skip-agents-with-external-calls,
recording_enabled
- packages/telephony: buildQueueXml()
- infrastructure/freeswitch: our own callcenter.conf.xml override (empties
the vanilla static agents/tiers -- those become fully dynamic in the next
phase) that includes callcenter_queues.conf.d/*.xml via X-PRE-PROCESS,
same pattern as the Sofia gateway directory
- apps/api/src/queues: CRUD (POST/GET/GET:id/DELETE) using the queues.view/
.manage permissions already in the seed
- b2bcall-fs-config (queue-sync.ts): one XML file per queue on a shared
volume, synced via Redis pub/sub (b2bcall:queues:sync) on create/delete
and once at boot -- same shape as trunk-sync.ts
- confirmed manually against the real FreeSWITCH, before coding the sync
logic: 'queue load <name>' fails ('Invalid Queue not found!') for a file
added after boot -- needs 'reloadxml' first to repopulate the in-memory
XML tree from disk; after that, 'queue reload <name>' alone handles both
create and update, no need to distinguish load vs reload
- verified end-to-end: created a queue (ROUND_ROBIN, maxWaitTime=120,
discardAbandonedAfter=90), 'callcenter_config queue list' showed the
correct values on the FreeSWITCH side; deleted it, list went back empty
docs/QUEUES.md
This commit is contained in:
@@ -4,3 +4,4 @@ export * from "./freeswitch-provider";
|
||||
export * from "./directory-xml";
|
||||
export * from "./gateway-xml";
|
||||
export * from "./dialplan-xml";
|
||||
export * from "./queue-xml";
|
||||
|
||||
78
packages/telephony/src/queue-xml.ts
Normal file
78
packages/telephony/src/queue-xml.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
function xmlEscape(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
const STRATEGY_VALUES: Record<string, string> = {
|
||||
LONGEST_IDLE_AGENT: "longest-idle-agent",
|
||||
ROUND_ROBIN: "round-robin",
|
||||
TOP_DOWN: "top-down",
|
||||
AGENT_WITH_LEAST_TALK_TIME: "agent-with-least-talk-time",
|
||||
AGENT_WITH_FEWEST_CALLS: "agent-with-fewest-calls",
|
||||
SEQUENTIALLY_BY_AGENT_ORDER: "sequentially-by-agent-order",
|
||||
RING_ALL: "ring-all",
|
||||
RING_PROGRESSIVELY: "ring-progressively",
|
||||
};
|
||||
|
||||
export interface QueueXmlParams {
|
||||
queueName: string; // ja no formato "<id>@<domain>"
|
||||
strategy: keyof typeof STRATEGY_VALUES;
|
||||
mohSound?: string;
|
||||
announceSound?: string;
|
||||
announceFrequency: number;
|
||||
maxWaitTime: number;
|
||||
maxWaitTimeWithNoAgent: number;
|
||||
agentNoAnswerStatus?: string;
|
||||
tierRulesApply: boolean;
|
||||
tierRuleWaitSecond: number;
|
||||
discardAbandonedAfter: number;
|
||||
abandonedResumeAllowed: boolean;
|
||||
skipAgentsWithExternalCalls: boolean;
|
||||
recordingEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* XML de fila do mod_callcenter (agente.md secao 50-51). Escrito em
|
||||
* autoload_configs/callcenter_queues.conf.d/<queue_id>.xml (volume Docker
|
||||
* compartilhado, incluído via X-PRE-PROCESS no callcenter.conf.xml —
|
||||
* mesmo padrão dos gateways Sofia). Carregado com `callcenter_config queue
|
||||
* reload <nome>` via ESL, não é resolvido dinamicamente por chamada como o
|
||||
* dialplan.
|
||||
*/
|
||||
export function buildQueueXml(params: QueueXmlParams): string {
|
||||
const lines: string[] = [];
|
||||
const param = (name: string, value: string | number | boolean | undefined) => {
|
||||
if (value === undefined || value === "") return;
|
||||
lines.push(` <param name="${name}" value="${xmlEscape(String(value))}"/>`);
|
||||
};
|
||||
|
||||
param("strategy", STRATEGY_VALUES[params.strategy]);
|
||||
param("moh-sound", params.mohSound);
|
||||
param("announce-sound", params.announceSound);
|
||||
param("announce-frequency", params.announceFrequency);
|
||||
param("time-base-score", "system");
|
||||
param("max-wait-time", params.maxWaitTime);
|
||||
param("max-wait-time-with-no-agent", params.maxWaitTimeWithNoAgent);
|
||||
param("agent-no-answer-status", params.agentNoAnswerStatus);
|
||||
param("tier-rules-apply", params.tierRulesApply);
|
||||
param("tier-rule-wait-second", params.tierRuleWaitSecond);
|
||||
param("discard-abandoned-after", params.discardAbandonedAfter);
|
||||
param("abandoned-resume-allowed", params.abandonedResumeAllowed);
|
||||
param("skip-agents-with-external-calls", params.skipAgentsWithExternalCalls);
|
||||
if (params.recordingEnabled) {
|
||||
param(
|
||||
"record-template",
|
||||
"$${recordings_dir}/${strftime(%Y-%m-%d-%H-%M-%S)}.${destination_number}.${caller_id_number}.${uuid}.wav",
|
||||
);
|
||||
}
|
||||
|
||||
return `<include>
|
||||
<queue name="${xmlEscape(params.queueName)}">
|
||||
${lines.join("\n")}
|
||||
</queue>
|
||||
</include>`;
|
||||
}
|
||||
Reference in New Issue
Block a user