agenticerp_voice 0.1.0
agenticerp_voice: ^0.1.0 copied to clipboard
Voice SDK for the AgenticERP call centre: place phone calls to customers from your Flutter app through your company hotline, with the call audio in the app.
agenticerp_voice #
SDK Flutter để gọi điện cho khách hàng qua tổng đài AgenticERP ngay trong ứng dụng của bạn: nhân viên bấm "Gọi" → khách thấy số hotline của doanh nghiệp → âm thanh đi ngay trong app → cuộc gọi được ghi âm và lưu vào lịch sử tổng đài như mọi cuộc gọi khác.
| Thành phần | Việc |
|---|---|
AerpClient |
Kết nối tổng đài bằng token: connect(token) · updateToken · sự kiện kết nối |
AerpCall |
Một cuộc gọi: makeCall(AerpCallParams) · hangup · mute · hold · getRecording |
AerpAudioManager |
Loa ngoài / chọn thiết bị âm thanh |
AerpResult |
Kết quả mọi thao tác: status, code, message |
English version below.
1. Chuẩn bị (phía tổng đài) #
Quản trị viên tổng đài vào Tổng đài → Cấu hình → SDK gọi điện → Tạo API key và nhận:
API Key SID(dạngSK…)API Key Secret— chỉ hiện một lần, lưu vào cấu hình máy chủ của bạn- Địa chỉ máy chủ cho SDK (ví dụ
https://erp.example.com)
Mỗi nhân viên gọi qua SDK phải là một người dùng tổng đài có quyền gọi ra và ít nhất một đầu số gọi
ra. userId trong token được khớp với người dùng theo cách quản trị chọn khi tạo key: email
(mặc định), số máy lẻ, hoặc ID người dùng.
2. Máy chủ của bạn ký token #
Token là JWT HS256, ký bằng API Key Secret, do máy chủ của bạn phát cho app.
Không bao giờ đưa Secret vào app di động.
| Claim | Bắt buộc | Ý nghĩa |
|---|---|---|
iss |
✔ | API Key SID |
jti |
✔ | Mã duy nhất của token |
exp |
✔ | Thời điểm hết hạn (giây Unix). Không được dài hơn "Hạn token tối đa" của key (mặc định 24 giờ) |
userId |
✔ | Email / số máy lẻ / ID người dùng của nhân viên (theo cách khớp của key) |
Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
function voiceToken(userEmail) {
return jwt.sign(
{ jti: crypto.randomUUID(), userId: userEmail },
process.env.API_KEY_SECRET,
{ algorithm: 'HS256', issuer: process.env.API_KEY_SID, expiresIn: '1h' },
);
}
PHP (firebase/php-jwt)
use Firebase\JWT\JWT;
$token = JWT::encode([
'iss' => getenv('API_KEY_SID'),
'jti' => bin2hex(random_bytes(16)),
'exp' => time() + 3600,
'userId' => $userEmail,
], getenv('API_KEY_SECRET'), 'HS256');
C# (System.IdentityModel.Tokens.Jwt)
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(apiKeySecret));
var token = new JwtSecurityToken(
issuer: apiKeySid,
claims: [new Claim("jti", Guid.NewGuid().ToString()), new Claim("userId", userEmail)],
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
Python (PyJWT)
import time, uuid, jwt
token = jwt.encode(
{"iss": API_KEY_SID, "jti": str(uuid.uuid4()), "exp": int(time.time()) + 3600, "userId": user_email},
API_KEY_SECRET, algorithm="HS256")
Gợi ý: cấp token hạn 1 giờ, chỉ cho nhân viên đã đăng nhập vào hệ thống của bạn.
3. Cài đặt #
flutter pub add agenticerp_voice
Yêu cầu: Flutter ≥ 3.27 (Dart ≥ 3.6). Android minSdk 21+, iOS 13+.
Android — android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
iOS — ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Cần micro để nói chuyện với khách hàng qua tổng đài.</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
UIBackgroundModes/audio giữ cuộc gọi khi nhân viên chuyển sang app khác. Trong ios/Podfile đặt
platform :ios, '13.0'.
Quyền micro: SDK không tự hỏi quyền — app xin quyền trước khi gọi (ví dụ dùng permission_handler,
xem example/).
4. Sử dụng #
Kết nối #
import 'package:agenticerp_voice/agenticerp_voice.dart';
final client = AerpClient(baseUrl: 'https://erp.example.com');
client.events.listen((event) async {
switch (event) {
case AerpClientConnected(:final user):
print('Đã kết nối: ${user.displayName}, máy lẻ ${user.extension}');
case AerpTokenWillExpire(): // ~60 giây trước khi hết hạn
case AerpRequestAccessToken(): // phiên đã hết hạn / bị thu hồi
await client.updateToken(await myServer.fetchVoiceToken());
case AerpClientDisconnected(:final reason):
print('Mất kết nối: $reason');
case AerpClientError(:final code, :final message):
print('Lỗi $code: $message');
}
});
final result = await client.connect(await myServer.fetchVoiceToken());
if (!result.status) print('connect thất bại: ${result.code}');
// Các đầu số nhân viên được dùng để gọi ra (khách sẽ thấy số này):
for (final n in client.fromNumbers) print('${n.number} ${n.isDefault ? "(mặc định)" : ""}');
Phiên sống đúng bằng hạn token. Nếu phiên hết hạn giữa cuộc gọi, âm thanh vẫn tiếp tục; chỉ cần
updateToken(...) là SDK nối lại việc theo dõi cuộc gọi (và gửi lại lệnh gác máy nếu nhân viên đã gác
trong lúc chờ).
Gọi điện #
final call = AerpCall(client);
call.events.listen((event) {
switch (event) {
case AerpSignalingStateChanged(:final state, :final reason):
// calling → ringing → answered → ended (hoặc busy)
print('Trạng thái: ${state.name} ${reason ?? ""}');
case AerpMediaStateChanged(:final state):
print('Âm thanh: ${state.name}');
case AerpCallInfoUpdated(:final info):
print('Thời lượng: ${call.talkDuration}');
case AerpCallError(:final code, :final message):
print('Lỗi cuộc gọi $code: $message');
}
});
final result = await call.makeCall(const AerpCallParams(
to: '0912345678', // số khách hàng
from: '02812345678', // tuỳ chọn — bỏ trống = đầu số mặc định của nhân viên
customData: {'booking_id': 'BK-1024'}, // mã của bạn, đọc lại ở call.info.customData
));
if (!result.status) print('Không gọi được: ${result.code} — ${result.message}');
await call.mute(true); // tắt tiếng
await call.setSpeakerphoneOn(true); // loa ngoài
await call.hold(true); // giữ máy (khách nghe nhạc chờ), chỉ khi đang nói chuyện
await call.hangup(); // gác máy
await call.destroy(); // giải phóng khi đóng màn hình
Mỗi AerpCall dùng cho một cuộc gọi; mỗi AerpClient có tối đa một cuộc gọi đang diễn ra.
Trong lúc đổ chuông SDK tự phát nhạc chờ (tắt bằng AerpCallOptions(playRingback: false)).
Ghi âm #
final rec = await call.getRecording(); // sau khi cuộc gọi kết thúc
if (rec.available) openUrl(rec.playbackUrl!); // link ký sẵn, hiệu lực ~10 phút
Tệp ghi âm cần vài giây để tải lên sau khi gác máy; nếu available == false hãy thử lại sau.
5. Trạng thái và mã lỗi #
Trạng thái cuộc gọi (AerpSignalingState): calling (đang quay số) → ringing (máy khách đổ
chuông) → answered (khách nghe máy) → ended / busy.
Lý do kết thúc (reason / call.info.endReason): local_hangup (nhân viên gác máy),
remote_hangup / normal (khách gác máy), no_answer, busy, unreachable (thuê bao không liên lạc
được — nhà mạng phát thông báo), cancelled, media_lost, hoặc mã lỗi của tổng đài.
Mã lỗi thường gặp (AerpResult.code):
| Mã | Ý nghĩa | Cách xử lý |
|---|---|---|
sdk_token_expired |
Token hết hạn | Lấy token mới, gọi updateToken |
sdk_token_signature_invalid |
Sai Secret | Kiểm tra API_KEY_SECRET trên máy chủ của bạn |
sdk_token_ttl_too_long |
Hạn token dài hơn mức cho phép của key | Giảm exp |
sdk_key_invalid / sdk_key_revoked |
SID sai hoặc key đã tắt | Liên hệ quản trị tổng đài |
sdk_user_not_found |
userId không khớp nhân viên nào |
Kiểm tra email / máy lẻ của nhân viên |
forbidden |
Nhân viên chưa có quyền gọi ra | Quản trị cấp quyền Tổng đài |
invalid_phone |
Số điện thoại không hợp lệ | Kiểm tra số |
dnc_blocked |
Số nằm trong danh sách không gọi | — |
call_permission_denied |
Máy lẻ không được gọi loại số này (quốc tế, 1900…) | Quản trị nâng quyền gọi |
from_number_not_allowed |
Đầu số from không thuộc quyền của nhân viên |
Dùng số trong client.fromNumbers |
no_from_number |
Nhân viên chưa được cấp đầu số gọi ra | Quản trị cấp đầu số |
call_failed |
Tổng đài không đặt được cuộc gọi | Xem message, thử lại |
call_in_progress |
Đang có cuộc gọi khác | Gác cuộc gọi trước |
not_connected |
Chưa connect |
Gọi connect(token) |
network_error / timeout |
Mất mạng | Thử lại |
6. App ví dụ #
example/ là app hoàn chỉnh: nhập địa chỉ tổng đài + token, chọn đầu số, gọi, tắt tiếng, loa ngoài,
giữ máy, gác máy. Example dùng gói agenticerp_voice trên pub.dev, nên có thể chép riêng thư mục này
làm khung cho app của bạn. Chạy: cd example && flutter pub get && flutter run.
English #
agenticerp_voice lets your Flutter app place phone calls to customers through the AgenticERP
call centre: the customer sees the company hotline, the call audio runs inside your app, and the
call is recorded and logged in the call centre like any other call. Main types: AerpClient
(connection + token), AerpCall (one call), AerpAudioManager (loudspeaker / devices) and
AerpResult (result of every operation).
1. Get an API key. A call-centre admin opens Call Center → Configuration → Calling SDK →
Create API key and gets an API Key SID, an API Key Secret (shown once — store it on your
server) and the server address. Each staff member calling through the SDK must be a call-centre
user with call-out permission and at least one outbound hotline; the token's userId is matched by
email (default), extension number or user id, as chosen on the key.
2. Your server signs tokens. HS256 JWT signed with the Secret, claims iss (SID), jti, exp
(not longer than the key's maximum lifetime, 24 h by default) and userId. Samples for Node.js, PHP,
C# and Python are in section 2 above. Never ship the Secret in the app.
3. Install. flutter pub add agenticerp_voice, then add the Android permissions and the iOS
Info.plist keys from section 3. Flutter ≥ 3.27, Android minSdk 21, iOS 13. Ask for the microphone
permission in your app before calling.
4. Use. AerpClient(baseUrl:) → connect(token) → listen to events and call
updateToken(newToken) on AerpTokenWillExpire / AerpRequestAccessToken. Then
AerpCall(client).makeCall(AerpCallParams(to: ..., from: ..., customData: ...)), follow
AerpSignalingStateChanged (calling → ringing → answered → ended/busy), and use mute,
setSpeakerphoneOn, hold, hangup, getRecording, destroy. One call per AerpCall, one active
call per client. A local ringback tone plays while ringing (disable with
AerpCallOptions(playRingback: false)).
5. Errors. Every operation returns AerpResult(status, code, message); the codes are listed in
the table in section 5 and in AerpErrorCode.
6. Example. example/ is a complete demo app that depends on the pub.dev release, so you can
copy the folder as a starting point (cd example && flutter pub get && flutter run).