@fonoster/amd 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -110
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -4
- package/package.json +7 -15
- package/dist/agi/AgiChannel.d.ts +0 -77
- package/dist/agi/AgiChannel.js +0 -171
- package/dist/agi/AgiServer.d.ts +0 -34
- package/dist/agi/AgiServer.js +0 -109
- package/dist/agi/server.d.ts +0 -16
- package/dist/agi/server.js +0 -154
- package/dist/agi/sessionRegistry.d.ts +0 -26
- package/dist/agi/sessionRegistry.js +0 -34
- package/dist/amd/buildAmdVariables.d.ts +0 -13
- package/dist/amd/buildAmdVariables.js +0 -98
- package/dist/audiosocket/server.d.ts +0 -9
- package/dist/audiosocket/server.js +0 -65
- package/dist/envs.d.ts +0 -8
- package/dist/envs.js +0 -56
- package/dist/probe/collectPcm.d.ts +0 -37
- package/dist/probe/collectPcm.js +0 -47
- package/dist/probe/runProbe.d.ts +0 -37
- package/dist/probe/runProbe.js +0 -110
- package/dist/probe/upsample.d.ts +0 -3
- package/dist/probe/upsample.js +0 -53
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -27
package/README.md
CHANGED
|
@@ -1,91 +1,19 @@
|
|
|
1
1
|
amd
|
|
2
2
|
=================
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
variables that Asterisk's own native `AMD()` application sets — so nothing
|
|
8
|
-
downstream (`apiserver`'s `createCreateVoiceClient.ts` / `mapAsteriskAmd.ts`)
|
|
9
|
-
needs to change to consume it.
|
|
4
|
+
Answering-machine detection for Fonoster. An ONNX whisper-tiny classifier reads the first seconds
|
|
5
|
+
of a call's audio and returns a status (HUMAN, MACHINE, VOICEMAIL, IVR or UNKNOWN), a confidence,
|
|
6
|
+
the detector name and the latency.
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
AppBridge loads it for the `amd` listener: it attaches a listen-only participant to the room,
|
|
9
|
+
collects the caller's audio (16 kHz L16) and calls `classifyPcm`. The model ships with the package
|
|
10
|
+
in `amd-model/`.
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
```ts
|
|
13
|
+
import { classifyPcm } from "@fonoster/amd";
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
The argument is the mode feature flag — see below.
|
|
20
|
-
2. Inside that AGI session, this service mints a UUID and runs
|
|
21
|
-
`EXEC AudioSocket <uuid>,<host>:<port>`, which blocks until the AudioSocket
|
|
22
|
-
side (a second listener in this same process) closes the connection.
|
|
23
|
-
3. The AudioSocket side buffers a few seconds of PCM and classifies it with
|
|
24
|
-
the bundled ONNX model — always the classifier's own five-way status
|
|
25
|
-
(HUMAN/MACHINE/VOICEMAIL/IVR/UNKNOWN) plus confidence, detector, and
|
|
26
|
-
latency; nothing is thrown away yet — then hangs up, which unblocks the
|
|
27
|
-
AGI session.
|
|
28
|
-
4. The AGI session maps that raw result to channel variables according to
|
|
29
|
-
the requested mode (see below), issues one `SET VARIABLE` per variable
|
|
30
|
-
over the same AGI connection, then closes the connection — which is what
|
|
31
|
-
tells Asterisk's `AGI()` app to resume the dialplan into
|
|
32
|
-
`Stasis(mediacontroller)`, exactly as it does today.
|
|
33
|
-
|
|
34
|
-
Every step is fail-open: a missing/corrupt model, a hung inference, a
|
|
35
|
-
timeout, or empty audio all resolve to an "unknown" result with a diagnostic
|
|
36
|
-
cause (`ML-TIMEOUT`, `ML-ERROR`, `ML-NO-AUDIO`) rather than stalling or
|
|
37
|
-
dropping the call.
|
|
38
|
-
|
|
39
|
-
## Modes: compact vs. full
|
|
40
|
-
|
|
41
|
-
The mode is a feature flag passed as the AGI call's first dialplan argument
|
|
42
|
-
(`agi_arg_1` on the wire) — not an env var, since it's a per-call decision
|
|
43
|
-
made in the dialplan, mirroring how `AMD_ENGINE` is already threaded through
|
|
44
|
-
as a channel variable.
|
|
45
|
-
|
|
46
|
-
- **`compact`** (the default — anything other than the literal `full`,
|
|
47
|
-
including no argument at all, falls back to this): collapses the
|
|
48
|
-
classifier's five-way status onto the same four-value `AMDSTATUS`
|
|
49
|
-
vocabulary Asterisk's native `AMD()` uses (`HUMAN`/`MACHINE`/`NOTSURE`),
|
|
50
|
-
with `VOICEMAIL`/`IVR` folded into `MACHINE` and a descriptive `AMDCAUSE`
|
|
51
|
-
(e.g. `MACHINE-VOICEMAIL`). Confidence below `AMD_MIN_CONFIDENCE`
|
|
52
|
-
is downgraded to `NOTSURE`/`ML-LOW-CONFIDENCE`. Only ever sets
|
|
53
|
-
`AMDSTATUS`/`AMDCAUSE` — a consumer built against native AMD() sees no
|
|
54
|
-
difference switching engines.
|
|
55
|
-
- **`full`**: sets `AMDSTATUS` to the classifier's own status
|
|
56
|
-
(`HUMAN`/`MACHINE`/`VOICEMAIL`/`IVR`/`UNKNOWN`, untouched by any
|
|
57
|
-
confidence threshold — the consumer applies its own policy), plus
|
|
58
|
-
`AMDCONFIDENCE` (0–1), `AMDDETECTOR` (model name), and `AMDLATENCYMS`.
|
|
59
|
-
|
|
60
|
-
Set it from the dialplan by giving `AMD_MODE` a value before the `AGI()`
|
|
61
|
-
call — apiserver already does this for API-originated calls via
|
|
62
|
-
`amd.mode` in `fonoster.yaml` (see below).
|
|
63
|
-
|
|
64
|
-
## Configuration
|
|
65
|
-
|
|
66
|
-
The sidecar reads these from its container environment only; it never loads a
|
|
67
|
-
`.env` file, in development either. Set them in the container's environment
|
|
68
|
-
(Compose `environment:`, a Kubernetes `env:`), or export them before
|
|
69
|
-
`npm run start:dev`.
|
|
70
|
-
|
|
71
|
-
| env | default | meaning |
|
|
72
|
-
|---|---|---|
|
|
73
|
-
| `AMD_AGI_PORT` | `4573` | FastAGI listener port |
|
|
74
|
-
| `AMD_AUDIOSOCKET_PORT` | `9092` | AudioSocket listener port |
|
|
75
|
-
| `AMD_AUDIOSOCKET_BIND_ADDR` | `0.0.0.0` | AudioSocket listener bind address |
|
|
76
|
-
| `AMD_AUDIOSOCKET_ADVERTISE_HOST` | `amd` | host Asterisk is told to connect to for the AudioSocket leg — must be reachable *from* Asterisk |
|
|
77
|
-
| `AMD_PROBE_MS` | `3000` | leading audio to gather before classifying |
|
|
78
|
-
| `AMD_TIMEOUT_MS` | `4000` | hard deadline for the whole AGI session |
|
|
79
|
-
| `AMD_MIN_CONFIDENCE` | `0.8` | compact mode only: verdicts below this are reported as `NOTSURE` |
|
|
80
|
-
| `AMD_MODEL_PATH` | *(bundled)* | override the model directory |
|
|
81
|
-
|
|
82
|
-
## Enabling it end-to-end
|
|
83
|
-
|
|
84
|
-
Set `amd.enabled: true` and `amd.engine: ml` in the API server's
|
|
85
|
-
`config/fonoster.yaml`. With the engine unset or set to anything but `ml`,
|
|
86
|
-
behavior is unchanged from Asterisk's native `AMD()`. Additionally set
|
|
87
|
-
`amd.mode: full` to opt into the richer variable set — omit it (or leave it
|
|
88
|
-
`compact`) to stay native-AMD-compatible.
|
|
15
|
+
const verdict = await classifyPcm(pcm);
|
|
16
|
+
```
|
|
89
17
|
|
|
90
18
|
## Regression corpus
|
|
91
19
|
|
|
@@ -106,31 +34,3 @@ few exist on purpose, to track known confusions (e.g. formal/synthesized IVR
|
|
|
106
34
|
scripts reading as MACHINE) rather than hide them.
|
|
107
35
|
Licensing and attribution for every third-party clip is in
|
|
108
36
|
`test/fixtures/regression/ATTRIBUTION.md`.
|
|
109
|
-
|
|
110
|
-
## Automated verification
|
|
111
|
-
|
|
112
|
-
`test/agiAudiosocket.integration.test.ts` starts the real AGI and AudioSocket
|
|
113
|
-
listeners in-process, plays both sides of Asterisk's role over raw TCP (the
|
|
114
|
-
FastAGI header handshake with `agi_arg_1` set, `EXEC AudioSocket`, streaming
|
|
115
|
-
real fixture PCM frame-by-frame, then reading back whatever `SET VARIABLE`
|
|
116
|
-
commands the server issues), and asserts the values that arrive over that
|
|
117
|
-
wire match calling the classifier directly on the same audio — for both
|
|
118
|
-
compact (default) and full mode, plus a case confirming an unrecognized mode
|
|
119
|
-
argument falls back to compact. This is the primary proof that the
|
|
120
|
-
AGI+AudioSocket mechanism and the mode flag work end-to-end; run it with:
|
|
121
|
-
|
|
122
|
-
```sh
|
|
123
|
-
npm run integration
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Status after the rebuild
|
|
127
|
-
|
|
128
|
-
Asterisk is retired (cleanbreak CB-B6), so the AGI and AudioSocket front end has no caller and its
|
|
129
|
-
Asterisk smoke test is gone. The classifier stays: it moves behind a voice-worker listener in
|
|
130
|
-
`add-media-node` task 4.3.
|
|
131
|
-
|
|
132
|
-
## Development
|
|
133
|
-
|
|
134
|
-
```sh
|
|
135
|
-
npm run start:dev -w @fonoster/amd
|
|
136
|
-
```
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,4 @@
|
|
|
16
16
|
* See the License for the specific language governing permissions and
|
|
17
17
|
* limitations under the License.
|
|
18
18
|
*/
|
|
19
|
-
export * from "./agi/server";
|
|
20
|
-
export * from "./audiosocket/server";
|
|
21
19
|
export * from "./amd/AmdModel";
|
|
22
|
-
export * from "./amd/buildAmdVariables";
|
|
23
|
-
export * from "./probe/runProbe";
|
package/dist/index.js
CHANGED
|
@@ -32,8 +32,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
32
32
|
* See the License for the specific language governing permissions and
|
|
33
33
|
* limitations under the License.
|
|
34
34
|
*/
|
|
35
|
-
__exportStar(require("./agi/server"), exports);
|
|
36
|
-
__exportStar(require("./audiosocket/server"), exports);
|
|
37
35
|
__exportStar(require("./amd/AmdModel"), exports);
|
|
38
|
-
__exportStar(require("./amd/buildAmdVariables"), exports);
|
|
39
|
-
__exportStar(require("./probe/runProbe"), exports);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonoster/amd",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.31.0",
|
|
4
|
+
"description": "Answering-machine-detection model (ONNX) used by AppBridge",
|
|
5
5
|
"author": "Pedro Sanders <psanders@fonoster.com>",
|
|
6
6
|
"homepage": "https://github.com/fonoster/fonoster#readme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -14,20 +14,12 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"prebuild": "rimraf ./dist tsconfig.tsbuildinfo",
|
|
16
16
|
"build": "tsc -b tsconfig.json",
|
|
17
|
-
"
|
|
18
|
-
"generate:readme": "node ../../.scripts/gen-readme.cjs",
|
|
19
|
-
"start:dev": "nodemon --exec tsx --watch src src/server.ts"
|
|
17
|
+
"generate:readme": "fonoster-readme"
|
|
20
18
|
},
|
|
21
19
|
"dependencies": {
|
|
22
|
-
"@fonoster/common": "^0.
|
|
23
|
-
"@fonoster/
|
|
24
|
-
"
|
|
25
|
-
"@fonoster/types": "^0.30.0",
|
|
26
|
-
"onnxruntime-node": "1.21.1",
|
|
27
|
-
"uuid": "^11.0.3"
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@types/uuid": "^10.0.0"
|
|
20
|
+
"@fonoster/common": "^0.31.0",
|
|
21
|
+
"@fonoster/types": "^0.31.0",
|
|
22
|
+
"onnxruntime-node": "1.30.0"
|
|
31
23
|
},
|
|
32
24
|
"files": [
|
|
33
25
|
"dist",
|
|
@@ -43,5 +35,5 @@
|
|
|
43
35
|
"bugs": {
|
|
44
36
|
"url": "https://github.com/fonoster/fonoster/issues"
|
|
45
37
|
},
|
|
46
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "f0a5102dddeea0f70f659234108805bc10690f2e"
|
|
47
39
|
}
|
package/dist/agi/AgiChannel.d.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
3
|
-
* http://github.com/fonoster/fonoster
|
|
4
|
-
*
|
|
5
|
-
* This file is part of Fonoster
|
|
6
|
-
*
|
|
7
|
-
* Licensed under the MIT License (the "License");
|
|
8
|
-
* you may not use this file except in compliance with
|
|
9
|
-
* the License. You may obtain a copy of the License at
|
|
10
|
-
*
|
|
11
|
-
* https://opensource.org/licenses/MIT
|
|
12
|
-
*
|
|
13
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
-
* See the License for the specific language governing permissions and
|
|
17
|
-
* limitations under the License.
|
|
18
|
-
*/
|
|
19
|
-
import { EventEmitter } from "events";
|
|
20
|
-
import * as net from "net";
|
|
21
|
-
/**
|
|
22
|
-
* Thrown when the channel hangs up (or the socket closes) while a command is
|
|
23
|
-
* in flight, or when a command is attempted on an already-hung-up channel.
|
|
24
|
-
*/
|
|
25
|
-
declare class AgiHangupError extends Error {
|
|
26
|
-
constructor(message?: string);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* One FastAGI session. Minimal, hand-rolled client for the handful of
|
|
30
|
-
* commands this module needs (EXEC, SET VARIABLE) — see AgiServer.ts for why
|
|
31
|
-
* this exists instead of a dependency: the only maintained npm package for
|
|
32
|
-
* this protocol (`asteriskagi`) shipped a broken `require()` in its latest
|
|
33
|
-
* release at the time this was written.
|
|
34
|
-
*
|
|
35
|
-
* The AGI protocol is a simple line-based request/response: write one
|
|
36
|
-
* command line, read back one `200 result=...` line. Asterisk may also send
|
|
37
|
-
* an unsolicited `HANGUP` line at any time if the channel hangs up mid-command.
|
|
38
|
-
*/
|
|
39
|
-
declare class AgiChannel extends EventEmitter {
|
|
40
|
-
readonly channel: string;
|
|
41
|
-
readonly uniqueid: string;
|
|
42
|
-
readonly variables: Record<string, string>;
|
|
43
|
-
private socket;
|
|
44
|
-
private buffer;
|
|
45
|
-
private ended;
|
|
46
|
-
private pending?;
|
|
47
|
-
constructor(props: {
|
|
48
|
-
socket: net.Socket;
|
|
49
|
-
variables: Record<string, string>;
|
|
50
|
-
});
|
|
51
|
-
get hungup(): boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Dialplan arguments passed after the AGI URI, e.g.
|
|
54
|
-
* `AGI(agi://amd:4573,${AMD_MODE})` arrives as `agi_arg_1`, read
|
|
55
|
-
* here as `args[0]`. Standard AGI behavior, same for FastAGI as for a
|
|
56
|
-
* local script.
|
|
57
|
-
*/
|
|
58
|
-
get args(): string[];
|
|
59
|
-
private onData;
|
|
60
|
-
private resolvePending;
|
|
61
|
-
private end;
|
|
62
|
-
/** Sends one raw FastAGI command line and waits for its response. */
|
|
63
|
-
send(command: string): Promise<void>;
|
|
64
|
-
/** Runs a dialplan application via `EXEC`, e.g. `exec("AudioSocket", args)`. */
|
|
65
|
-
exec(application: string, args?: string): Promise<void>;
|
|
66
|
-
setVariable(name: string, value: string): Promise<void>;
|
|
67
|
-
/**
|
|
68
|
-
* Ends the AGI session cleanly once this side is done issuing commands.
|
|
69
|
-
* This — not a special "done" command — is how Asterisk's AGI() app knows
|
|
70
|
-
* to return control to the dialplan: it detects the AGI script closing the
|
|
71
|
-
* connection. Without this, a real channel would sit in AGI() forever.
|
|
72
|
-
* Distinct from `end()`'s hangup semantics: this is *us* finishing, not
|
|
73
|
-
* the far end going away, so it doesn't emit "hangup".
|
|
74
|
-
*/
|
|
75
|
-
close(): void;
|
|
76
|
-
}
|
|
77
|
-
export { AgiChannel, AgiHangupError };
|
package/dist/agi/AgiChannel.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AgiHangupError = exports.AgiChannel = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
6
|
-
* http://github.com/fonoster/fonoster
|
|
7
|
-
*
|
|
8
|
-
* This file is part of Fonoster
|
|
9
|
-
*
|
|
10
|
-
* Licensed under the MIT License (the "License");
|
|
11
|
-
* you may not use this file except in compliance with
|
|
12
|
-
* the License. You may obtain a copy of the License at
|
|
13
|
-
*
|
|
14
|
-
* https://opensource.org/licenses/MIT
|
|
15
|
-
*
|
|
16
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
17
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
-
* See the License for the specific language governing permissions and
|
|
20
|
-
* limitations under the License.
|
|
21
|
-
*/
|
|
22
|
-
const events_1 = require("events");
|
|
23
|
-
/**
|
|
24
|
-
* Thrown when the channel hangs up (or the socket closes) while a command is
|
|
25
|
-
* in flight, or when a command is attempted on an already-hung-up channel.
|
|
26
|
-
*/
|
|
27
|
-
class AgiHangupError extends Error {
|
|
28
|
-
constructor(message = "AGI channel hung up") {
|
|
29
|
-
super(message);
|
|
30
|
-
this.name = "AgiHangupError";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
exports.AgiHangupError = AgiHangupError;
|
|
34
|
-
/**
|
|
35
|
-
* One FastAGI session. Minimal, hand-rolled client for the handful of
|
|
36
|
-
* commands this module needs (EXEC, SET VARIABLE) — see AgiServer.ts for why
|
|
37
|
-
* this exists instead of a dependency: the only maintained npm package for
|
|
38
|
-
* this protocol (`asteriskagi`) shipped a broken `require()` in its latest
|
|
39
|
-
* release at the time this was written.
|
|
40
|
-
*
|
|
41
|
-
* The AGI protocol is a simple line-based request/response: write one
|
|
42
|
-
* command line, read back one `200 result=...` line. Asterisk may also send
|
|
43
|
-
* an unsolicited `HANGUP` line at any time if the channel hangs up mid-command.
|
|
44
|
-
*/
|
|
45
|
-
class AgiChannel extends events_1.EventEmitter {
|
|
46
|
-
constructor(props) {
|
|
47
|
-
var _a, _b;
|
|
48
|
-
super();
|
|
49
|
-
this.buffer = "";
|
|
50
|
-
this.ended = false;
|
|
51
|
-
this.onData = (data) => {
|
|
52
|
-
if (this.ended)
|
|
53
|
-
return;
|
|
54
|
-
this.buffer += data.toString();
|
|
55
|
-
let newline = this.buffer.indexOf("\n");
|
|
56
|
-
while (newline >= 0) {
|
|
57
|
-
const line = this.buffer.slice(0, newline).replace(/\r$/, "").trim();
|
|
58
|
-
this.buffer = this.buffer.slice(newline + 1);
|
|
59
|
-
if (line === "HANGUP") {
|
|
60
|
-
this.end();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (line)
|
|
64
|
-
this.resolvePending(line);
|
|
65
|
-
newline = this.buffer.indexOf("\n");
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
this.socket = props.socket;
|
|
69
|
-
this.variables = props.variables;
|
|
70
|
-
this.channel = (_a = props.variables.channel) !== null && _a !== void 0 ? _a : "";
|
|
71
|
-
this.uniqueid = (_b = props.variables.uniqueid) !== null && _b !== void 0 ? _b : "";
|
|
72
|
-
this.socket.on("data", this.onData);
|
|
73
|
-
this.socket.once("end", () => this.end());
|
|
74
|
-
this.socket.once("close", () => this.end());
|
|
75
|
-
this.socket.once("error", (err) => this.end(err));
|
|
76
|
-
}
|
|
77
|
-
get hungup() {
|
|
78
|
-
return this.ended;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Dialplan arguments passed after the AGI URI, e.g.
|
|
82
|
-
* `AGI(agi://amd:4573,${AMD_MODE})` arrives as `agi_arg_1`, read
|
|
83
|
-
* here as `args[0]`. Standard AGI behavior, same for FastAGI as for a
|
|
84
|
-
* local script.
|
|
85
|
-
*/
|
|
86
|
-
get args() {
|
|
87
|
-
const result = [];
|
|
88
|
-
for (let i = 1; this.variables[`arg_${i}`] !== undefined; i++) {
|
|
89
|
-
result.push(this.variables[`arg_${i}`]);
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
93
|
-
resolvePending(line) {
|
|
94
|
-
const pending = this.pending;
|
|
95
|
-
if (!pending)
|
|
96
|
-
return;
|
|
97
|
-
this.pending = undefined;
|
|
98
|
-
// "200 result=1 ..." on success; "5xx ..." or a negative result on
|
|
99
|
-
// failure — either way this only needs to settle the promise, not parse
|
|
100
|
-
// the (unused) payload.
|
|
101
|
-
const match = line.match(/^\d+\s+result=(-?\d+)/);
|
|
102
|
-
const result = match ? Number(match[1]) : NaN;
|
|
103
|
-
if (line.startsWith("2") && !(Number.isFinite(result) && result < 0)) {
|
|
104
|
-
pending.resolve();
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
pending.reject(new Error(`AGI command failed: ${line}`));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
end(cause) {
|
|
111
|
-
if (this.ended)
|
|
112
|
-
return;
|
|
113
|
-
this.ended = true;
|
|
114
|
-
this.socket.off("data", this.onData);
|
|
115
|
-
const pending = this.pending;
|
|
116
|
-
this.pending = undefined;
|
|
117
|
-
pending === null || pending === void 0 ? void 0 : pending.reject(new AgiHangupError());
|
|
118
|
-
if (!this.socket.destroyed)
|
|
119
|
-
this.socket.end();
|
|
120
|
-
this.emit("hangup", cause);
|
|
121
|
-
}
|
|
122
|
-
/** Sends one raw FastAGI command line and waits for its response. */
|
|
123
|
-
send(command) {
|
|
124
|
-
if (this.ended)
|
|
125
|
-
return Promise.reject(new AgiHangupError());
|
|
126
|
-
if (this.pending) {
|
|
127
|
-
return Promise.reject(new Error("An AGI command is already in progress"));
|
|
128
|
-
}
|
|
129
|
-
return new Promise((resolve, reject) => {
|
|
130
|
-
this.pending = { resolve, reject };
|
|
131
|
-
if (!this.socket.writable || this.socket.destroyed) {
|
|
132
|
-
this.end();
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
this.socket.write(command + "\n", "utf8", (err) => {
|
|
136
|
-
if (err)
|
|
137
|
-
this.end(err);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
/** Runs a dialplan application via `EXEC`, e.g. `exec("AudioSocket", args)`. */
|
|
142
|
-
exec(application, args) {
|
|
143
|
-
return this.send(`EXEC ${application} ${args !== null && args !== void 0 ? args : ""}`);
|
|
144
|
-
}
|
|
145
|
-
setVariable(name, value) {
|
|
146
|
-
return this.send(`SET VARIABLE ${name} "${value}"`);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Ends the AGI session cleanly once this side is done issuing commands.
|
|
150
|
-
* This — not a special "done" command — is how Asterisk's AGI() app knows
|
|
151
|
-
* to return control to the dialplan: it detects the AGI script closing the
|
|
152
|
-
* connection. Without this, a real channel would sit in AGI() forever.
|
|
153
|
-
* Distinct from `end()`'s hangup semantics: this is *us* finishing, not
|
|
154
|
-
* the far end going away, so it doesn't emit "hangup".
|
|
155
|
-
*/
|
|
156
|
-
close() {
|
|
157
|
-
if (this.ended)
|
|
158
|
-
return;
|
|
159
|
-
this.ended = true;
|
|
160
|
-
this.socket.off("data", this.onData);
|
|
161
|
-
// A command can still be in flight (e.g. the caller gave up on it via an
|
|
162
|
-
// external race/timeout rather than waiting for a response) — settle it
|
|
163
|
-
// so that awaiter doesn't hang forever, same as end()'s hangup path.
|
|
164
|
-
const pending = this.pending;
|
|
165
|
-
this.pending = undefined;
|
|
166
|
-
pending === null || pending === void 0 ? void 0 : pending.reject(new AgiHangupError("AGI session closed locally"));
|
|
167
|
-
if (!this.socket.destroyed)
|
|
168
|
-
this.socket.end();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
exports.AgiChannel = AgiChannel;
|
package/dist/agi/AgiServer.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
3
|
-
* http://github.com/fonoster/fonoster
|
|
4
|
-
*
|
|
5
|
-
* This file is part of Fonoster
|
|
6
|
-
*
|
|
7
|
-
* Licensed under the MIT License (the "License");
|
|
8
|
-
* you may not use this file except in compliance with
|
|
9
|
-
* the License. You may obtain a copy of the License at
|
|
10
|
-
*
|
|
11
|
-
* https://opensource.org/licenses/MIT
|
|
12
|
-
*
|
|
13
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
-
* See the License for the specific language governing permissions and
|
|
17
|
-
* limitations under the License.
|
|
18
|
-
*/
|
|
19
|
-
import { EventEmitter } from "events";
|
|
20
|
-
/**
|
|
21
|
-
* Minimal FastAGI (`agi://host:port`) server: Asterisk connects, sends a
|
|
22
|
-
* block of `agi_*` header lines terminated by a blank line, then this emits
|
|
23
|
-
* "call" with an AgiChannel the caller can issue EXEC/SET VARIABLE commands
|
|
24
|
-
* on. See AgiChannel.ts for why this is hand-rolled rather than a dependency.
|
|
25
|
-
*/
|
|
26
|
-
declare class AgiServer extends EventEmitter {
|
|
27
|
-
private server;
|
|
28
|
-
constructor(props: {
|
|
29
|
-
port: number;
|
|
30
|
-
});
|
|
31
|
-
private handleConnection;
|
|
32
|
-
close(): void;
|
|
33
|
-
}
|
|
34
|
-
export { AgiServer };
|
package/dist/agi/AgiServer.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.AgiServer = void 0;
|
|
37
|
-
/**
|
|
38
|
-
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
39
|
-
* http://github.com/fonoster/fonoster
|
|
40
|
-
*
|
|
41
|
-
* This file is part of Fonoster
|
|
42
|
-
*
|
|
43
|
-
* Licensed under the MIT License (the "License");
|
|
44
|
-
* you may not use this file except in compliance with
|
|
45
|
-
* the License. You may obtain a copy of the License at
|
|
46
|
-
*
|
|
47
|
-
* https://opensource.org/licenses/MIT
|
|
48
|
-
*
|
|
49
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
50
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
51
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
52
|
-
* See the License for the specific language governing permissions and
|
|
53
|
-
* limitations under the License.
|
|
54
|
-
*/
|
|
55
|
-
const events_1 = require("events");
|
|
56
|
-
const net = __importStar(require("net"));
|
|
57
|
-
const AgiChannel_1 = require("./AgiChannel");
|
|
58
|
-
/**
|
|
59
|
-
* Minimal FastAGI (`agi://host:port`) server: Asterisk connects, sends a
|
|
60
|
-
* block of `agi_*` header lines terminated by a blank line, then this emits
|
|
61
|
-
* "call" with an AgiChannel the caller can issue EXEC/SET VARIABLE commands
|
|
62
|
-
* on. See AgiChannel.ts for why this is hand-rolled rather than a dependency.
|
|
63
|
-
*/
|
|
64
|
-
class AgiServer extends events_1.EventEmitter {
|
|
65
|
-
constructor(props) {
|
|
66
|
-
super();
|
|
67
|
-
this.server = net.createServer(this.handleConnection.bind(this));
|
|
68
|
-
this.server.on("error", (err) => this.emit("error", err));
|
|
69
|
-
this.server.listen(props.port, () => this.emit("ready", props.port));
|
|
70
|
-
}
|
|
71
|
-
handleConnection(socket) {
|
|
72
|
-
let buffer = "";
|
|
73
|
-
const onData = (data) => {
|
|
74
|
-
buffer += data.toString();
|
|
75
|
-
const delimiter = buffer.match(/\r?\n\r?\n/);
|
|
76
|
-
if (!delimiter || delimiter.index === undefined)
|
|
77
|
-
return;
|
|
78
|
-
socket.off("data", onData);
|
|
79
|
-
socket.off("error", onError);
|
|
80
|
-
const header = buffer.slice(0, delimiter.index);
|
|
81
|
-
const remainder = buffer.slice(delimiter.index + delimiter[0].length);
|
|
82
|
-
const variables = {};
|
|
83
|
-
header.split(/\r?\n/).forEach((line) => {
|
|
84
|
-
if (!line.startsWith("agi_"))
|
|
85
|
-
return;
|
|
86
|
-
const separator = line.indexOf(":");
|
|
87
|
-
if (separator < 0)
|
|
88
|
-
return;
|
|
89
|
-
variables[line.slice(4, separator).trim()] = line
|
|
90
|
-
.slice(separator + 1)
|
|
91
|
-
.trim();
|
|
92
|
-
});
|
|
93
|
-
const channel = new AgiChannel_1.AgiChannel({ socket, variables });
|
|
94
|
-
this.emit("call", channel);
|
|
95
|
-
if (remainder)
|
|
96
|
-
socket.unshift(Buffer.from(remainder));
|
|
97
|
-
};
|
|
98
|
-
const onError = (err) => {
|
|
99
|
-
if (this.listenerCount("error"))
|
|
100
|
-
this.emit("error", err);
|
|
101
|
-
};
|
|
102
|
-
socket.once("error", onError);
|
|
103
|
-
socket.on("data", onData);
|
|
104
|
-
}
|
|
105
|
-
close() {
|
|
106
|
-
this.server.close();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
exports.AgiServer = AgiServer;
|
package/dist/agi/server.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { AgiServer } from "./AgiServer";
|
|
2
|
-
/**
|
|
3
|
-
* Starts the FastAGI listener. Asterisk's `AGI(agi://amd:4573,${AMD_MODE})`
|
|
4
|
-
* blocks the channel here. Per session: read the requested mode from the
|
|
5
|
-
* dialplan's first AGI argument (`compact`, the default, or `full`), mint a
|
|
6
|
-
* UUID, EXEC AudioSocket to pull the leading audio (blocks until the
|
|
7
|
-
* AudioSocket side hangs up), await the correlated raw result, map it to
|
|
8
|
-
* however many channel variables that mode produces, SET each, then return
|
|
9
|
-
* so the dialplan resumes at Stasis(mediacontroller).
|
|
10
|
-
*
|
|
11
|
-
* Always fail-open: deadlines bound both the wait for a verdict and the wait
|
|
12
|
-
* for AudioSocket() to return, and the AGI connection is always closed so the
|
|
13
|
-
* dialplan resumes even when no variables could be set.
|
|
14
|
-
*/
|
|
15
|
-
declare function startAgiServer(): AgiServer;
|
|
16
|
-
export { startAgiServer };
|