PAYATHON 2026

如何配置 TAPI 监控并控制 Avaya IP Office 呼叫?

排障赵师傅

结论

当前代码收不到事件,是因为客户端没有建立完整的事件订阅链路:

  1. MyTapiEventHandler 只有类定义,没有创建实例,也没有通过 COM connection point 调用 Advise,因此 TAPI 没有事件接收对象。
  2. 代码没有调用 ITTAPI::put_EventFilter 订阅 TE_CALLNOTIFICATION、TE_CALLSTATE 等事件。
  3. 程序通过 CoInitialize 创建了 STA COM apartment,但等待循环只有 Sleep,没有处理 Windows 消息。封送到该线程的 COM 事件可能无法分派。
  4. LINECALLSTATE_OFFERING 属于 TAPI 2.x。使用 TAPI 3.x 时,应监听 TE_CALLSTATE,再调用 ITCallStateEvent::get_State 判断状态是否为 CS_OFFERING。

能够枚举所有 ITAddress,只能说明 TAPI Service Provider 已经加载,无法证明当前许可证、账号和线路允许第三方监控或控制。监控多个分机通常需要 Avaya IP Office 相应版本提供的第三方 CTI/TAPI 授权,例如 TAPI Link Pro 或 CTI Link Pro。许可证名称和权限项可能随 IP Office 版本变化,需要根据当前版本核对 Avaya 文档。

事件订阅流程

TAPI 3.x 的基本调用顺序如下:

  1. 初始化 COM 和 TAPI。
  2. 设置事件过滤器。
  3. 从 ITTAPI 获取 IConnectionPointContainer。
  4. 找到 IID_ITTAPIEventNotification 对应的 connection point。
  5. 创建事件处理器并调用 Advise。
  6. 对目标 ITAddress 调用 RegisterCallNotifications。
  7. 保持消息循环运行。
  8. 退出时调用 UnregisterNotifications 和 Unadvise。

RegisterCallNotifications 用于注册地址和呼叫权限范围,不会自动连接 MyTapiEventHandler。

监听 TE_CALLSTATE

要识别电话进入振铃或等待接听状态,应处理 TE_CALLSTATE:

HRESULT STDMETHODCALLTYPE Event(
    TAPI_EVENT tapiEvent,
    IDispatch* pEvent
) override {
    if (pEvent == nullptr) {
        return E_POINTER;
    }

    if (tapiEvent == TE_CALLNOTIFICATION) {
        ITCallNotificationEvent* notification = nullptr;

        HRESULT hr = pEvent->QueryInterface(
            IID_ITCallNotificationEvent,
            reinterpret_cast<void**>(&notification)
        );

        if (SUCCEEDED(hr)) {
            ITCallInfo* call = nullptr;

            if (SUCCEEDED(notification->get_Call(&call)) && call != nullptr) {
                BSTR callerId = nullptr;

                if (SUCCEEDED(call->get_CallInfoString(
                        CIS_CALLERIDNUMBER,
                        &callerId)) &&
                    callerId != nullptr) {
                    std::wcout << L"New call, caller: "
                               << callerId << std::endl;
                    SysFreeString(callerId);
                }

                call->Release();
            }

            notification->Release();
        }
    }

    if (tapiEvent == TE_CALLSTATE) {
        ITCallStateEvent* stateEvent = nullptr;

        HRESULT hr = pEvent->QueryInterface(
            IID_ITCallStateEvent,
            reinterpret_cast<void**>(&stateEvent)
        );

        if (SUCCEEDED(hr)) {
            CALL_STATE state = CS_IDLE;

            if (SUCCEEDED(stateEvent->get_State(&state))) {
                std::wcout << L"Call state: " << state << std::endl;

                if (state == CS_OFFERING) {
                    std::wcout << L"Incoming call is offering."
                               << std::endl;

                    // 在这里执行路由规则,但不要阻塞回调线程。
                }
            }

            stateEvent->Release();
        }
    }

    return S_OK;
}

TE_CALLNOTIFICATION 表示应用程序获得了一个呼叫对象,与 LINECALLSTATE_OFFERING 含义不同。呼叫状态的变化需要从 TE_CALLSTATE 事件中读取。

注册事件接收器

ITTAPI::Initialize 成功后,设置事件过滤器并注册 connection point:

#include <windows.h>
#include <tapi3.h>
#include <ocidl.h>
#include <iostream>

ITTAPI* pTapi = nullptr;
IConnectionPoint* pConnectionPoint = nullptr;
MyTapiEventHandler* pEventHandler = nullptr;

DWORD adviseCookie = 0;
long registrationId = 0;

// 创建并初始化 pTapi 后设置事件过滤器。
HRESULT hr = pTapi->put_EventFilter(
    TE_CALLNOTIFICATION |
    TE_CALLSTATE |
    TE_CALLINFOCHANGE
);

if (FAILED(hr)) {
    std::wcerr << L"put_EventFilter failed: 0x"
               << std::hex << hr << std::endl;
    // 执行清理并退出。
}

// 获取 TAPI 的 connection point。
IConnectionPointContainer* pContainer = nullptr;

hr = pTapi->QueryInterface(
    IID_IConnectionPointContainer,
    reinterpret_cast<void**>(&pContainer)
);

if (SUCCEEDED(hr)) {
    hr = pContainer->FindConnectionPoint(
        IID_ITTAPIEventNotification,
        &pConnectionPoint
    );
    pContainer->Release();
}

if (FAILED(hr)) {
    std::wcerr << L"FindConnectionPoint failed: 0x"
               << std::hex << hr << std::endl;
    // 执行清理并退出。
}

// 实例必须存活到 Unadvise 完成。
pEventHandler = new MyTapiEventHandler();

hr = pConnectionPoint->Advise(
    static_cast<IUnknown*>(pEventHandler),
    &adviseCookie
);

if (FAILED(hr)) {
    std::wcerr << L"Advise failed: 0x"
               << std::hex << hr << std::endl;
    // 执行清理并退出。
}

// pSpecificAddress 是已经找到的目标 ITAddress。
hr = pTapi->RegisterCallNotifications(
    pSpecificAddress,
    VARIANT_TRUE,             // monitor
    VARIANT_FALSE,            // owner,先仅监控
    TAPIMEDIATYPE_AUDIO,
    0,
    &registrationId
);

if (FAILED(hr)) {
    std::wcerr << L"RegisterCallNotifications failed: 0x"
               << std::hex << hr << std::endl;
}

排查事件接收问题时,可以先使用:

fMonitor = VARIANT_TRUE;
fOwner   = VARIANT_FALSE;

这组参数只申请 monitor privilege。将 fOwner 设为 VARIANT_TRUE 会同时申请 owner privilege。如果提供程序、许可证或当前线路不支持该权限,排查时还会多出一层权限问题。先确认监控正常,再单独验证 owner privilege 和呼叫控制能力。

STA 线程需要处理消息

原代码的循环只调用 Sleep(100),无法可靠分派 COM 事件。可改为带消息处理的循环:

bool running = true;
MSG msg{};

while (running) {
    DWORD result = MsgWaitForMultipleObjects(
        0,
        nullptr,
        FALSE,
        100,
        QS_ALLINPUT
    );

    if (result == WAIT_OBJECT_0) {
        while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
            if (msg.message == WM_QUIT) {
                running = false;
                break;
            }

            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    if (GetAsyncKeyState('Q') & 0x0001) {
        running = false;
    }
}

不要在事件回调中执行耗时的数据库查询、网络请求或复杂路由计算。可以复制后续任务需要的呼叫信息,并把任务投递到工作线程。需要跨线程使用 COM 接口指针时,必须按照 COM 规则进行封送,不能直接把裸指针交给另一个线程。

清理注册和资源

退出时按注册的逆序释放资源:

if (registrationId != 0) {
    pTapi->UnregisterNotifications(registrationId);
    registrationId = 0;
}

if (pConnectionPoint != nullptr && adviseCookie != 0) {
    pConnectionPoint->Unadvise(adviseCookie);
    adviseCookie = 0;
}

if (pEventHandler != nullptr) {
    pEventHandler->Release();
    pEventHandler = nullptr;
}

if (pConnectionPoint != nullptr) {
    pConnectionPoint->Release();
    pConnectionPoint = nullptr;
}

pTapi->Shutdown();
pTapi->Release();
CoUninitialize();

事件处理器需要一直存活到 Unadvise 完成。在 Advise 后过早调用 Release,或在 Unadvise 前销毁对象,会导致后续回调丢失,严重时还可能访问无效内存。

检查目标地址

当前代码使用子串匹配:

if (addressName.find(targetExtension) != std::wstring::npos)

这种写法可能选错地址。例如查找 5002 时,名称中包含 50020 的地址也会匹配。应先打印完整地址名,再根据 Avaya TSP 返回的实际格式进行精确比较:

if (addressName == targetExtension) {
    found = true;
    break;
}

如果地址名中还包含用户名、设备类型或其他前缀,可以建立明确的“分机号到 ITAddress”映射,避免依赖任意子串匹配。

还要检查地址能力,确认该地址支持音频呼叫、监控以及所需的呼叫控制操作。能够枚举地址,并不代表它支持 Transfer、BlindTransfer 或 owner privilege。

核对 Avaya IP Office 配置

Avaya 侧需要确认以下条件:

  • TAPI/TSPI 客户端版本、Windows 位数和 C++ 程序位数相互兼容。32 位 TSP 通常只能供 32 位进程使用,64 位环境需要安装对应组件。
  • TSP 配置的 IP Office 地址、账号和认证信息正确。
  • 当前账号有权查看需要监控的用户或分机。
  • 系统拥有第三方呼叫监控和控制所需的许可证。只有基础或单用户 TAPI 能力时,可能可以枚举地址,但无法监控任意分机。
  • 目标分机确实由 IP Office 控制,测试呼叫也经过该地址,没有提前被其他队列、寻线组、SIP 终端或转发规则改道。
  • IP Office、TSP 和客户端组件属于受支持的组合。

不同 IP Office 版本使用的许可证名称、管理界面和权限项不完全相同,不能仅根据“能看到全部分机”判断 CTI 权限已经配置完成。

可以先用 Avaya 随 TSP 提供的测试工具,或一个最小化 TAPI 测试程序监控同一分机。如果测试工具也收不到事件,应先检查许可证、账号权限和 TSP 配置。

按业务规则转移呼叫

收到 CS_OFFERING 后,可以从 ITCallStateEvent 获取 ITCallInfo,再查询 ITBasicCallControl:

ITCallInfo* callInfo = nullptr;

if (SUCCEEDED(stateEvent->get_Call(&callInfo)) &&
    callInfo != nullptr) {
    ITBasicCallControl* callControl = nullptr;

    HRESULT hr = callInfo->QueryInterface(
        IID_ITBasicCallControl,
        reinterpret_cast<void**>(&callControl)
    );

    if (SUCCEEDED(hr)) {
        hr = callControl->BlindTransfer(
            _bstr_t(L"5003")
        );

        if (FAILED(hr)) {
            std::wcerr << L"BlindTransfer failed: 0x"
                       << std::hex << hr << std::endl;
        }

        callControl->Release();
    }

    callInfo->Release();
}

这段代码能否成功,取决于 Avaya TSP 暴露的能力、当前呼叫状态、owner privilege、号码格式和 IP Office 权限。部分环境不允许在 CS_OFFERING 阶段直接转移,可能要求先应答,也可能需要通过 PBX 的转接、寻线组或短代码等机制完成路由。调用前应先查询地址和呼叫能力,再确定合适的调用时机。

呼叫事件可能重复触发,状态也可能快速变化。路由逻辑应根据呼叫标识维护状态,防止同一呼叫在多次 TE_CALLSTATE 回调中被重复转移。

TAPI 3.x 不适用时的选择

可以根据用途考虑以下方案:

  • 如果 Avaya TSP 对 TAPI 2.x 的支持更完整,可以直接使用 TAPI 2.x。此时使用 LINE_CALLSTATE 和 LINECALLSTATE_OFFERING,不要把 TAPI 2.x 常量与 TAPI 3.x COM 接口混用。
  • 如果只需要获取呼叫活动或生成统计数据,可以评估 Avaya DevLink。它主要提供事件和状态数据,不能直接假定它具备完整的呼叫控制能力。
  • 如果路由规则比较固定,可以优先在 IP Office 内配置寻线组、短代码、呼叫路由或其他 PBX 规则,以免依赖外部应用持续在线。
  • 如果外部业务系统必须实时决策,应使用当前 IP Office 版本正式支持的 CTI 接口,并确认该接口允许第三方监控和呼叫控制。

排查时可以先完成一个最小闭环:只注册一个明确的 ITAddress,只监听 TE_CALLNOTIFICATION | TE_CALLSTATE,使用 monitor privilege,并在 STA 线程中运行消息循环。收到 CS_OFFERING 后,再加入 owner privilege 和转移逻辑。这样可以分别确认客户端订阅、Avaya 权限和具体呼叫控制能力是否正常。

备注:内容仅供参考。