Documentation
¶
Overview ¶
Package historyinfo implements RFC 7044 History-Info and RFC 5806 Diversion header processing for the SIP call-transfer / retargeting path.
Why we need this:
- When a call arrives at us on trunk-number T and the business layer (AI tool / DTMF / inbound REFER) decides to retarget to agent A, we run a B2BUA bridge (see docs/sip_gap_analysis.md § "转接架构说明"). That means the agent's UAS sees an INVITE whose Request-URI is the agent, From is OUR trunk's CLI, To is the agent — there is no surface signal that this call was originally targeted at T or who retargeted it.
- Downstream PBXs / phones use History-Info (modern, RFC 7044) or Diversion (legacy, RFC 5806) to render "original called number was T" and to drive routing rules ("if Diversion is present, do not deflect again"). Without these headers, transferred calls look indistinguishable from primary calls and the agent can't tell the customer "您拨打的 trunk T 已为您转人工".
- We emit BOTH headers because real-world PBX populations are mixed: Avaya/Cisco modern firmware honor History-Info, but Yealink/Polycom phones and older Asterisk read Diversion.
This package does parsing/formatting only. The wiring into INVITE emission lives in pkg/sip/outbound; the wiring into the transfer chain lives in pkg/sip/conversation.
Index ¶
Constants ¶
const ( DiversionUnknown = "unknown" DiversionUserBusy = "user-busy" DiversionNoAnswer = "no-answer" DiversionUnconditional = "unconditional" DiversionTimeOfDay = "time-of-day" DiversionDoNotDisturb = "do-not-disturb" DiversionDeflection = "deflection" DiversionFollowMe = "follow-me" DiversionOutOfService = "out-of-service" DiversionAway = "away" )
DiversionReason values per RFC 5806 §4.1.1.
Variables ¶
This section is empty.
Functions ¶
func ExtractURIFromToHeader ¶
ExtractURIFromToHeader strips the display-name and angle brackets from a To header value, returning just the URI ("sip:foo@bar"). On malformed input returns "" so callers can short-circuit before emitting a malformed History-Info entry.
Lives here because the only consumers are this package and the conversation transfer code that calls AppendTransferEntry.
func FormatChain ¶
FormatChain renders an entire History-Info chain as a comma- separated header value. Pass to msg.SetHeader("History-Info", ...). Empty input → "".
func FormatDiversionChain ¶
FormatDiversionChain renders the Diversion header value (multiple rows comma-separated, RFC 5806 §4.1).
func MaxCounter ¶
MaxCounter returns the largest counter value in the chain; used when extending so the new entry can be counter=Max+1.
func NextIndex ¶
NextIndex returns "N+1" where N is the largest top-level integer index in chain. Used when we extend the chain on retarget — RFC 7044 §4.1 says the new entry's index must be at the same depth as the prior entry plus one. For our B2BUA we always extend flat.
Examples (chain index sequence → returned next):
(empty) → "1" "1" → "2" "1", "2" → "3" "1", "1.1" → "2" (siblings of root) "1", "2", "2.1" → "3"
Types ¶
type Diversion ¶
type Diversion struct {
URI string
Reason string // free-form per RFC 5806; commonly one of DiversionXxx
Counter int // hop counter; missing → 0; we increment when extending
Privacy string // "full" / "name" / "uri" / "off" / ...
Limit int // hop limit (counter <= limit before drop); 0 = unset
Screen string // "yes" / "no" — was the diversion source verified
ExtraParams []string
}
Diversion is one row of an RFC 5806 Diversion header chain. Unlike History-Info, Diversion does NOT carry indices — chain order is by header appearance order, oldest first.
func AppendDiversionEntry ¶
AppendDiversionEntry returns a new Diversion chain with one additional entry pointing at originalToURI (RFC 5806 semantics: "the call was diverted FROM originalToURI"). The new entry's counter is incremented by 1 over the chain's current max.
We default reason to "unconditional" since our retarget is platform-initiated (AI / pool / REFER), not a phone busy/no-answer. Callers can override (e.g. DiversionDeflection for REFER-driven).
func ParseDiversionChain ¶
ParseDiversionChain parses Diversion header value(s). Lenient — same philosophy as ParseChain.
type Entry ¶
type Entry struct {
// URI is the bare target URI (e.g. "sip:+8613800138000@trust.example").
// Whether/not angle-bracketed, we normalise on parse to no brackets.
URI string
// Index is the dotted-decimal index per RFC 7044 §4.1. We always
// emit flat integers like "1", "2", "3" (no forking).
Index string
// ReasonHeader optionally carries a SIP Reason header value
// embedded in the Request-URI's hi-target-param `Reason=...`. RFC
// 7044 §4.2: the param is percent-encoded; we decode on parse and
// re-encode on format. Common values for our case:
// "SIP;cause=302;text=\"Moved Temporarily\"" — generic retarget
// "SIP;cause=480;text=\"AI Transfer\"" — transfer to agent
// "SIP;cause=487;text=\"Cancelled\"" — caller hung up
// Empty = no reason param (legal, just less informative downstream).
ReasonHeader string
// PrivacyHeader: history-info-targeted-toparam Privacy value
// (RFC 7044 §4.3). Rare; we currently don't emit but parse for
// round-tripping.
PrivacyHeader string
// Extra opaque params we encountered on parse but don't model.
// Carried through on format so we don't strip carrier annotations.
Extra []string
}
Entry is one entry in the History-Info chain. RFC 7044 §4: each Request-URI the request has traversed becomes one entry, indexed in dotted decimal so forking proxies can model branches. We do not fork (we're a B2BUA), so our indices are always flat integers.
func AppendTransferEntry ¶
func AppendTransferEntry(inboundChain []Entry, originalToURI, newTargetURI, reasonHeader string) []Entry
AppendTransferEntry returns a new History-Info chain with one additional entry representing the retarget. originalToURI is the To URI from the inbound INVITE; newTargetURI is what we are about to send the outbound INVITE to. reasonHeader is a SIP Reason header value (or "" to omit).
On first call with an empty inbound chain we synthesise a root entry for originalToURI as well, so the resulting chain represents the full path (original → new) instead of just (new).
func ParseChain ¶
ParseChain parses a (possibly folded multi-instance) History-Info header value into entries. We are LENIENT on input — the goal is to forward whatever upstream produced, not validate carrier behaviour. Unparseable rows are skipped.
func (Entry) Format ¶
Format renders one entry in wire format:
<sip:+8613800138000@trust.example?Reason=SIP%3Bcause%3D302>;index=1
Per RFC 7044 §4: the Reason / Privacy params live inside the URI hi-target as URI-headers (after `?`), separated by `&`, percent encoded; the `;index=` and other hi-target-params live OUTSIDE the angle brackets on the entry.