feat: deliver versioned SIP config to Cells
This commit is contained in:
@@ -85,7 +85,7 @@
|
||||
|
||||
## 4. HTTP OpenAPI 规划
|
||||
|
||||
两个服务分别使用自己的 Base URL,不共享服务实现;下表路径均为相对各自服务根路径。
|
||||
业务两个服务分别使用自己的 Base URL,不共享服务实现;SIP 管理后台另有独立管理 Base URL,不纳入下表普通业务路径。下表路径均为相对对应服务根路径。
|
||||
|
||||
| 提供方 | 方法与路径 | 最小权限 | 结果 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -635,7 +635,7 @@ HTTP 错误码与 MQ reason_code 共用词汇但不是一一映射;MQ 没有 H
|
||||
|
||||
| 核验项 | 已接受规则/待核验参数 | 责任方 |
|
||||
| --- | --- | --- |
|
||||
| 接口归属及域名 | 两份 OpenAPI;SaaS 是否已有可复用资产服务、测试 Base URL | 用户、SaaS |
|
||||
| 接口归属及域名 | 业务两份 OpenAPI;SIP 管理后台使用独立管理 Base URL,SaaS 仅访问其只读 Trunk 目录/状态 | 用户、SaaS、运维 |
|
||||
| 服务身份 | 现有服务令牌体系优先;issuer/audience/scope、租户映射、轮换和 mTLS | 双方、运维 |
|
||||
| 任务绑定与版本 | 初始版本 1、CAS 控制、停止不可恢复、可信任务归属来源 | 用户、SaaS |
|
||||
| 租户绑定与传输(已确认) | 一对一/不原地变更/保留期不复用,224字节预算及超限停发;Mock注册可先开发,真实租户资料和传输能力接入时核验 | SaaS、运维 |
|
||||
@@ -645,6 +645,7 @@ HTTP 错误码与 MQ reason_code 共用词汇但不是一一映射;MQ 没有 H
|
||||
| 执行幂等 | 新增 execution_id,与 command_id 分离;业务重新外呼许可及去重保留 | 用户、SaaS |
|
||||
| 停止语义 | drain/hangup 显式选择;挂断权限与多 Cell 生效判据 | 用户、SaaS |
|
||||
| 线路/AI 配置 | route/caller/agent 引用及同步来源;LLM/TTS 新规范、失败兜底 | 用户、供应方 |
|
||||
| Asterisk/SIP 管理后台与 SaaS 只读目录(Mock 已实现,生产待核验) | 不纳入普通业务七条路径;由独立管理后台统一写入并发布多机器 Trunk/codec_profile、主叫/被叫规则、出口池、并发/CPS,提供版本、CAS、审计、校验、发布、停用和回滚;SaaS 仅读取脱敏 Trunk 配置/能力/版本/健康状态;管理写接口、SaaS 只读接口和普通调度接口隔离认证凭据、issuer/audience/权限域,普通调度 Token 必须拒绝管理写接口 | 用户、SaaS、运维 |
|
||||
| MQ 环境 | 租户独立命令队列已确认;采用 `agent-call` 命名空间、direct 命令 exchange、`agent-call.tenant.{tenant_key}.call.execute` 路由及 `tenant_key` 原样透传;冻结精确绑定、生命周期、队列数上限、quorum/HA、ACL、重试/DLQ 与死信可靠性 | 运维、双方 |
|
||||
| 租户公平与背压 | 公平调度架构已确认;冻结轮转批量/周期、活跃队列发现、权重、prefetch、接收窗口、并发/CPS、发布速率/积压上限、拒绝发布策略、多实例协调及等待指标 | 用户、双方、运维 |
|
||||
| 保底与借用 | 默认不承诺固定开始时限;如需 SLA,确认保底资源、借用/归还边界及可满足的租户总承诺 | 用户、业务/运维 |
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# yaml-language-server: $schema=https://json-schema.org/draft/2020-12/schema
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: agent-call Cell Agent API
|
||||
version: 1.0.0
|
||||
description: >-
|
||||
Restricted mTLS API used by the SIP management backend to deliver a
|
||||
versioned Trunk snapshot to one voice Cell. The Cell validates the SHA-256
|
||||
snapshot, applies it with an atomic file replacement, reloads Asterisk,
|
||||
restores the previous file on reload failure, and returns applied only
|
||||
after the reload succeeds. A disabled snapshot removes the Cell-local
|
||||
Trunk fragment and reloads Asterisk.
|
||||
servers:
|
||||
- url: https://cell.internal:9443
|
||||
description: Cell management network only
|
||||
tags:
|
||||
- name: health
|
||||
- name: trunk-apply
|
||||
paths:
|
||||
/healthz/live:
|
||||
get:
|
||||
tags: [health]
|
||||
operationId: live
|
||||
responses:
|
||||
'200':
|
||||
description: Cell Agent is alive
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/Health'}
|
||||
/v1/sip/trunks/{trunk_id}/apply:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
post:
|
||||
tags: [trunk-apply]
|
||||
operationId: applyTrunk
|
||||
security: [{CellManagementMtls: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/RequestId'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/Publication'}
|
||||
responses:
|
||||
'200':
|
||||
description: Asterisk has loaded the exact snapshot
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/Acknowledgement'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409':
|
||||
description: Revision is stale or has a gap
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
'502':
|
||||
description: Asterisk rejected the apply or reload
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
/v1/sip/trunks/{trunk_id}/state:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
get:
|
||||
tags: [trunk-apply]
|
||||
operationId: getTrunkState
|
||||
security: [{CellManagementMtls: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Durable Cell apply state
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/State'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
components:
|
||||
securitySchemes:
|
||||
CellManagementMtls:
|
||||
type: mutualTLS
|
||||
description: Management backend client certificate signed by the Cell CA.
|
||||
parameters:
|
||||
TrunkId:
|
||||
name: trunk_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'}
|
||||
RequestId:
|
||||
name: X-Request-ID
|
||||
in: header
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
schemas:
|
||||
Health:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, mode, cell_id]
|
||||
properties:
|
||||
status: {type: string, const: ok}
|
||||
mode: {type: string, const: real}
|
||||
cell_id: {type: string}
|
||||
CodecProfile:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [allowed, preferred]
|
||||
properties:
|
||||
allowed:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items: {type: string, enum: [PCMA, PCMU]}
|
||||
preferred: {type: string, enum: [PCMA, PCMU]}
|
||||
SipConfig:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [host, port, transport, auth_mode, register, credential_ref]
|
||||
properties:
|
||||
host: {type: string, minLength: 1, maxLength: 253}
|
||||
port: {type: integer, minimum: 1, maximum: 65535}
|
||||
transport: {type: string, enum: [udp, tcp, tls]}
|
||||
auth_mode: {type: string, enum: [ip, digest]}
|
||||
register: {type: boolean}
|
||||
credential_ref:
|
||||
type: [string, 'null']
|
||||
description: Secret-store reference only; plaintext is forbidden.
|
||||
TrunkConfig:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- display_name
|
||||
- enabled
|
||||
- sip
|
||||
- codec_profile
|
||||
- caller_ids
|
||||
- dial_prefix
|
||||
- egress_pool_id
|
||||
- max_concurrency
|
||||
- max_cps
|
||||
properties:
|
||||
display_name: {type: string, minLength: 1, maxLength: 256}
|
||||
enabled: {type: boolean}
|
||||
sip: {$ref: '#/components/schemas/SipConfig'}
|
||||
codec_profile: {$ref: '#/components/schemas/CodecProfile'}
|
||||
caller_ids:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items: {type: string, minLength: 1, maxLength: 128}
|
||||
dial_prefix: {type: string, maxLength: 32}
|
||||
egress_pool_id: {type: string}
|
||||
max_concurrency: {type: integer, minimum: 1}
|
||||
max_cps: {type: integer, minimum: 1}
|
||||
Publication:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [mode, cell_id, trunk_id, revision, config, config_sha256]
|
||||
properties:
|
||||
mode: {type: string, const: real}
|
||||
cell_id: {type: string}
|
||||
trunk_id: {type: string}
|
||||
revision: {type: integer, minimum: 1}
|
||||
config: {$ref: '#/components/schemas/TrunkConfig'}
|
||||
config_sha256:
|
||||
type: string
|
||||
pattern: '^[0-9a-f]{64}$'
|
||||
Acknowledgement:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
[mode, cell_id, trunk_id, revision, config_sha256, status, idempotent]
|
||||
properties:
|
||||
mode: {type: string, const: real}
|
||||
cell_id: {type: string}
|
||||
trunk_id: {type: string}
|
||||
revision: {type: integer, minimum: 1}
|
||||
config_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
status: {type: string, const: applied}
|
||||
idempotent: {type: boolean}
|
||||
State:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
[
|
||||
mode,
|
||||
cell_id,
|
||||
trunk_id,
|
||||
desired_revision,
|
||||
applied_revision,
|
||||
status,
|
||||
updated_at,
|
||||
]
|
||||
properties:
|
||||
mode: {type: string, const: real}
|
||||
cell_id: {type: string}
|
||||
trunk_id: {type: string}
|
||||
desired_revision: {type: integer, minimum: 1}
|
||||
applied_revision: {type: integer, minimum: 0}
|
||||
status: {type: string, enum: [applying, applied, failed]}
|
||||
last_error: {type: [string, 'null']}
|
||||
updated_at: {type: string, format: date-time}
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required: [error]
|
||||
properties:
|
||||
error:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Invalid publication or hash
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
Forbidden:
|
||||
description: Certificate or Cell identity is not authorized
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
NotFound:
|
||||
description: State does not exist
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
@@ -0,0 +1,564 @@
|
||||
# yaml-language-server: $schema=https://json-schema.org/draft/2020-12/schema
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: agent-call Asterisk/SIP Management API
|
||||
version: 1.0.0
|
||||
description: >-
|
||||
Independent Asterisk/SIP management backend. Admin write operations are
|
||||
separate from the SaaS read-only Trunk directory and from ordinary
|
||||
scheduling APIs. In mock mode publication records are intents only. In
|
||||
real mode a publication is successful only after every selected Cell Agent
|
||||
returns a matching mTLS acknowledgement.
|
||||
servers:
|
||||
- url: https://sip-admin.internal
|
||||
description: Restricted operator management network
|
||||
- url: https://sip-read.internal
|
||||
description: SaaS read-only service network
|
||||
tags:
|
||||
- name: health
|
||||
- name: admin-trunks
|
||||
- name: admin-cells
|
||||
- name: saas-readonly
|
||||
paths:
|
||||
/healthz/live:
|
||||
get:
|
||||
tags: [health]
|
||||
operationId: live
|
||||
responses:
|
||||
'200':
|
||||
description: Service is alive
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Health'
|
||||
/admin/v1/trunks:
|
||||
get:
|
||||
tags: [admin-trunks]
|
||||
operationId: listAdminTrunks
|
||||
security: [{SipAdminBearer: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: All Trunks, including unpublished revisions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AdminTrunkList'
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/admin/v1/trunks/{trunk_id}:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
get:
|
||||
tags: [admin-trunks]
|
||||
operationId: getAdminTrunk
|
||||
security: [{SipAdminBearer: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Trunk configuration and revisions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AdminTrunk'
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
put:
|
||||
tags: [admin-trunks]
|
||||
operationId: createTrunkRevision
|
||||
security: [{SipAdminBearer: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/IfMatch'}
|
||||
- {$ref: '#/components/parameters/RequestId'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TrunkConfig'
|
||||
responses:
|
||||
'200':
|
||||
description: New draft revision for an existing Trunk
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AdminTrunk'}
|
||||
'201':
|
||||
description: New Trunk with its first draft revision
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AdminTrunk'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/admin/v1/trunks/{trunk_id}/publish:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
post:
|
||||
tags: [admin-trunks]
|
||||
operationId: publishTrunk
|
||||
security: [{SipAdminBearer: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/IfMatch'}
|
||||
- {$ref: '#/components/parameters/RequestId'}
|
||||
responses:
|
||||
'200':
|
||||
description: >-
|
||||
Published revision after all selected Cell acknowledgements
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AdminTrunk'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/admin/v1/trunks/{trunk_id}/disable:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
post:
|
||||
tags: [admin-trunks]
|
||||
operationId: disableTrunk
|
||||
security: [{SipAdminBearer: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/IfMatch'}
|
||||
- {$ref: '#/components/parameters/RequestId'}
|
||||
responses:
|
||||
'200':
|
||||
description: Trunk disabled after selected Cell acknowledgements
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AdminTrunk'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/admin/v1/trunks/{trunk_id}/rollback:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
post:
|
||||
tags: [admin-trunks]
|
||||
operationId: rollbackTrunk
|
||||
security: [{SipAdminBearer: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/IfMatch'}
|
||||
- {$ref: '#/components/parameters/RequestId'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [target_revision]
|
||||
properties:
|
||||
target_revision: {type: integer, minimum: 1}
|
||||
responses:
|
||||
'200':
|
||||
description: >-
|
||||
New revision copied from the target after Cell acknowledgements
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AdminTrunk'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/admin/v1/trunks/{trunk_id}/publications:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
get:
|
||||
tags: [admin-trunks]
|
||||
operationId: listTrunkPublications
|
||||
security: [{SipAdminBearer: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Per-Cell publication intents
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [mode, publications]
|
||||
properties:
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
publications:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/Publication'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/admin/v1/trunks/{trunk_id}/audit:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
get:
|
||||
tags: [admin-trunks]
|
||||
operationId: listTrunkAudit
|
||||
security: [{SipAdminBearer: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Immutable management audit entries
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [mode, audit]
|
||||
properties:
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
audit:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/AuditEntry'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/admin/v1/cells:
|
||||
get:
|
||||
tags: [admin-cells]
|
||||
operationId: listCells
|
||||
security: [{SipAdminBearer: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Registered multi-machine voice Cells
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [mode, cells]
|
||||
properties:
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
cells:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/Cell'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/admin/v1/cells/{cell_id}:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/CellId'}
|
||||
put:
|
||||
tags: [admin-cells]
|
||||
operationId: registerCell
|
||||
security: [{SipAdminBearer: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/IfMatch'}
|
||||
- {$ref: '#/components/parameters/RequestId'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CellConfig'}
|
||||
responses:
|
||||
'200':
|
||||
description: Updated Cell revision
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/Cell'}
|
||||
'201':
|
||||
description: Registered Cell
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/Cell'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/readonly/v1/sip/trunks:
|
||||
get:
|
||||
tags: [saas-readonly]
|
||||
operationId: listAuthorizedTrunks
|
||||
security: [{SaasTrunkReadBearer: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Published Trunks authorized for this SaaS principal
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReadonlyTrunkList'
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/readonly/v1/sip/trunks/{trunk_id}:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TrunkId'}
|
||||
get:
|
||||
tags: [saas-readonly]
|
||||
operationId: getAuthorizedTrunk
|
||||
security: [{SaasTrunkReadBearer: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Published, sanitized Trunk metadata
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ReadonlyTrunk'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
components:
|
||||
securitySchemes:
|
||||
SipAdminBearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque
|
||||
description: >-
|
||||
Dedicated operator/backend credential for SIP management writes. It is
|
||||
not accepted by the SaaS read-only API or ordinary scheduling API.
|
||||
SaasTrunkReadBearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque
|
||||
description: >-
|
||||
Dedicated SaaS read-only credential. It cannot publish, modify, disable,
|
||||
rollback, or access Cell management.
|
||||
parameters:
|
||||
TrunkId:
|
||||
name: trunk_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'}
|
||||
CellId:
|
||||
name: cell_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'}
|
||||
IfMatch:
|
||||
name: If-Match
|
||||
in: header
|
||||
required: true
|
||||
description: Exact latest revision required for CAS; quotes are accepted.
|
||||
schema: {type: integer, minimum: 0}
|
||||
RequestId:
|
||||
name: X-Request-ID
|
||||
in: header
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Invalid configuration or request
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
Unauthorized:
|
||||
description: Missing or wrong authentication domain
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
Forbidden:
|
||||
description: Credential lacks the required scope
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
Conflict:
|
||||
description: CAS conflict or no compatible Cell
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
NotFound:
|
||||
description: Resource is not visible or does not exist
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ErrorResponse'}
|
||||
schemas:
|
||||
Mode:
|
||||
type: string
|
||||
enum: [mock, real]
|
||||
Health:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, mode]
|
||||
properties:
|
||||
status: {type: string, const: ok}
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
CodecProfile:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [allowed, preferred]
|
||||
properties:
|
||||
allowed:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items: {type: string, enum: [PCMA, PCMU]}
|
||||
preferred: {type: string, enum: [PCMA, PCMU]}
|
||||
SipConfig:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [host, port, transport, auth_mode, register]
|
||||
properties:
|
||||
host: {type: string, minLength: 1, maxLength: 253}
|
||||
port: {type: integer, minimum: 1, maximum: 65535}
|
||||
transport: {type: string, enum: [udp, tcp, tls]}
|
||||
auth_mode: {type: string, enum: [ip, digest]}
|
||||
register: {type: boolean}
|
||||
credential_ref:
|
||||
type: string
|
||||
writeOnly: true
|
||||
description: >-
|
||||
Secret-store reference only; plaintext credentials are forbidden.
|
||||
TrunkConfig:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- display_name
|
||||
- enabled
|
||||
- sip
|
||||
- codec_profile
|
||||
- caller_ids
|
||||
- dial_prefix
|
||||
- egress_pool_id
|
||||
- max_concurrency
|
||||
- max_cps
|
||||
properties:
|
||||
display_name: {type: string, minLength: 1, maxLength: 256}
|
||||
enabled: {type: boolean}
|
||||
sip: {$ref: '#/components/schemas/SipConfig'}
|
||||
codec_profile: {$ref: '#/components/schemas/CodecProfile'}
|
||||
caller_ids:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items: {type: string, minLength: 1, maxLength: 128}
|
||||
dial_prefix: {type: string, maxLength: 32}
|
||||
egress_pool_id:
|
||||
type: string
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
max_concurrency: {type: integer, minimum: 1}
|
||||
max_cps: {type: integer, minimum: 1}
|
||||
CellConfig:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [egress_pool_id, codec_capabilities, status, max_concurrency]
|
||||
properties:
|
||||
egress_pool_id:
|
||||
type: string
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
codec_capabilities:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items: {type: string, enum: [PCMA, PCMU]}
|
||||
status: {type: string, enum: [healthy, draining, disabled]}
|
||||
max_concurrency: {type: integer, minimum: 1}
|
||||
management_url:
|
||||
type: string
|
||||
format: uri
|
||||
pattern: '^https://'
|
||||
description: >-
|
||||
mTLS Cell Agent endpoint. Required when mode=real; credentials and
|
||||
query strings are not allowed.
|
||||
RevisionInfo:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [revision, state, created_at, created_by]
|
||||
properties:
|
||||
revision: {type: integer, minimum: 1}
|
||||
state: {type: string, enum: [draft, publishing, published, superseded]}
|
||||
created_at: {type: string, format: date-time}
|
||||
created_by: {type: string}
|
||||
AdminTrunk:
|
||||
type: object
|
||||
required:
|
||||
- mode
|
||||
- trunk_id
|
||||
- latest_revision
|
||||
- active_revision
|
||||
- status
|
||||
- compatible_cell_ids
|
||||
- latest
|
||||
- active
|
||||
- versions
|
||||
properties:
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
trunk_id: {type: string}
|
||||
latest_revision: {type: integer, minimum: 1}
|
||||
active_revision: {type: integer, minimum: 0}
|
||||
status: {type: string, enum: [draft, published, disabled]}
|
||||
updated_at: {type: string, format: date-time}
|
||||
compatible_cell_ids: {type: array, items: {type: string}}
|
||||
latest: {$ref: '#/components/schemas/TrunkView'}
|
||||
active: {$ref: '#/components/schemas/TrunkView'}
|
||||
versions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RevisionInfo'
|
||||
TrunkView:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/TrunkConfig'}
|
||||
- type: object
|
||||
properties:
|
||||
trunk_id: {type: string}
|
||||
credential_configured: {type: boolean}
|
||||
asterisk_allow:
|
||||
type: array
|
||||
items: {type: string, enum: [alaw, ulaw]}
|
||||
ReadonlyTrunk:
|
||||
type: object
|
||||
required:
|
||||
- mode
|
||||
- trunk_id
|
||||
- revision
|
||||
- status
|
||||
- config
|
||||
properties:
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
trunk_id: {type: string}
|
||||
revision: {type: integer, minimum: 1}
|
||||
status: {type: string, const: published}
|
||||
updated_at: {type: string, format: date-time}
|
||||
config: {$ref: '#/components/schemas/TrunkView'}
|
||||
AdminTrunkList:
|
||||
type: object
|
||||
required: [mode, trunks]
|
||||
properties:
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
trunks: {type: array, items: {$ref: '#/components/schemas/AdminTrunk'}}
|
||||
ReadonlyTrunkList:
|
||||
type: object
|
||||
required: [mode, trunks]
|
||||
properties:
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
trunks:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReadonlyTrunk'
|
||||
Cell:
|
||||
type: object
|
||||
required: [mode, cell_id, revision, config, updated_at, updated_by]
|
||||
properties:
|
||||
mode: {$ref: '#/components/schemas/Mode'}
|
||||
cell_id: {type: string}
|
||||
revision: {type: integer, minimum: 1}
|
||||
config: {$ref: '#/components/schemas/CellConfig'}
|
||||
updated_at: {type: string, format: date-time}
|
||||
updated_by: {type: string}
|
||||
Publication:
|
||||
type: object
|
||||
required: [trunk_id, revision, cell_id, status, updated_at]
|
||||
properties:
|
||||
trunk_id: {type: string}
|
||||
revision: {type: integer, minimum: 1}
|
||||
cell_id: {type: string}
|
||||
status: {type: string, enum: [pending, applied, failed]}
|
||||
error_code: {type: [string, 'null']}
|
||||
updated_at: {type: string, format: date-time}
|
||||
AuditEntry:
|
||||
type: object
|
||||
required:
|
||||
- audit_id
|
||||
- resource_type
|
||||
- resource_id
|
||||
- action
|
||||
- revision
|
||||
- actor
|
||||
- details_json
|
||||
- created_at
|
||||
properties:
|
||||
audit_id: {type: string}
|
||||
resource_type: {type: string, const: trunk}
|
||||
resource_id: {type: string}
|
||||
action:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
upsert,
|
||||
publish,
|
||||
publish_failed,
|
||||
disable,
|
||||
disable_failed,
|
||||
rollback,
|
||||
rollback_failed,
|
||||
]
|
||||
revision: {type: integer, minimum: 0}
|
||||
actor: {type: string}
|
||||
request_id: {type: [string, 'null']}
|
||||
details_json: {type: string}
|
||||
created_at: {type: string, format: date-time}
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required: [error]
|
||||
properties:
|
||||
error:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
@@ -300,7 +300,9 @@ bash -n deploy/asterisk.sh
|
||||
|
||||
下一实施入口是整改计划F0→F1~F4,对D01~D11补齐模式保护、租户隔离、可靠性、协议媒体与验收证据;真实SIP排障及D12外部协调并行,不以线路不通阻塞本地整改。真实替换仍须取得SaaS、SIP/Asterisk、ASR/LLM/TTS、OSS、出口、容量和运维证据;无需重新讨论已接受方案。具体供应商差异单独登记,不让Mock假成功掩盖缺口。
|
||||
|
||||
**新增待办(SaaS 对接前,归入 D04/D12):SIP 编解码配置化。** 不得把 `ulaw`/`alaw` 写死在全局生成器或逐呼配置中;在供应商/Trunk 或受控 `codec_profile` 中维护允许编码及优先顺序,对外使用 `PCMA`/`PCMU` 等规范名称,Asterisk 渲染为 `alaw`/`ulaw`。SaaS 呼叫消息只可选择已授权的 `trunk_id`/`codec_profile_id`,不能任意注入 codec;调度时校验 Trunk 与 Cell/出口能力交集,无交集即拒绝发起,不静默回退、不因编码失败自动重拨,并记录实际 SDP 协商编码。实现前同步补充配置校验、OpenAPI/MQ 字段(如确需暴露)、迁移/发布流程及回归验收。
|
||||
**已实现 Mock 垂直切片(SaaS 对接前,归入 D04/D12):SIP 编解码配置化。** 编码不再写死在逐呼配置中;管理后台按供应商/Trunk 的受控 `codec_profile` 维护允许编码及优先顺序,对外使用 `PCMA`/`PCMU`,Asterisk 渲染为 `alaw`/`ulaw`。SaaS 呼叫消息只可引用已授权的 `trunk_id`/`codec_profile_id`,不能任意注入 codec;发布时校验 Trunk 与 Cell/出口能力交集,无交集拒绝,不静默回退、不因编码失败自动重拨,并保留实际 SDP 编码字段。当前实现位于 `agent_call/sip_management.py`,契约为 `docs/contracts/sip-management.openapi.yaml`,回归覆盖见 `tests/test_sip_management.py`;真实 Asterisk 配置适配、Cell 应用回执和生产 codec 能力登记仍待完成。
|
||||
|
||||
**已实现 Mock 垂直切片(多机器 SIP 管理,归入 D01/D04/D10/D12):独立 Asterisk/SIP 管理后台。** 独立管理后台作为 SIP Trunk、`codec_profile`、主叫/被叫规则、出口池、并发/CPS 及多机器 Cell 发布的唯一写入面,已提供持久化配置、版本/CAS、审计、校验、发布、停用、回滚、Cell 能力登记和每 Cell 发布意图;发布失败保留旧版本,新配置只影响新呼叫。SaaS 不拥有线路配置写权限,仅通过独立只读接口读取已发布 Trunk 的脱敏配置、能力、版本和健康状态;普通调度 Token、SaaS 只读凭据均不能访问管理写接口。管理写接口、SaaS 只读接口和普通调度接口使用隔离的认证凭据/issuer/audience/权限域;不返回 SIP/ARI 明文凭据,只接受受控 `secret_ref`。实现文件为 `agent_call/sip_management.py`、`agent_call/sip_management_main.py`,契约为 `docs/contracts/sip-management.openapi.yaml`,Compose 使用 `sip-management` profile;仍待真实 Asterisk/Cell 应用适配、TLS/网络隔离、生产身份提供方接入和多机故障验收。
|
||||
|
||||
### 10.1 历史执行记录(2026-09-12~13,非当前完整验收结论)
|
||||
|
||||
|
||||
@@ -91,6 +91,51 @@ python3 deploy/aliyun_host.py --config .local/aliyun.json --apply
|
||||
- `.local/aliyun-host.json`和锁文件是恢复依据,不能在出错后直接删除来强行重试。多个控制机必须共用明确的操作责任,不能各自用独立状态并行创建。
|
||||
- 该脚本只准备ECS/EIP,不安装Docker、不上传SSH私钥、不配置DNS/HTTPS、不迁移活动通话。测试完成后先询问用户是否清理;未获明确清理指令时保留唯一 `project=agent-call` 可用/运行实例,不自动删除或创建第二台。用户明确要求清理后,只停止并删除本次创建的实例;竞价回收后的监控/自动恢复尚未实现。
|
||||
|
||||
### 3.4 独立 Asterisk/SIP 管理后台(Mock/real)
|
||||
|
||||
SIP Trunk、codec、主叫/被叫规则、出口池和多机器 Cell 配置只能由独立管理后台写入;SaaS 只访问只读 Trunk 目录/状态。管理后台使用独立 SQLite 持久化、版本/CAS、审计、发布、停用和回滚接口,契约见 `docs/contracts/sip-management.openapi.yaml`。
|
||||
|
||||
```bash
|
||||
# 只在受控环境注入两套不同的 opaque token;不要使用普通 HTTP_TOKENS。
|
||||
export SIP_ADMIN_TOKENS='{"replace-admin-token":{"subject":"ops","issuer":"ops","audience":"agent-call.sip-admin","scopes":["*"],"trunk_ids":"*"}}'
|
||||
export SIP_READ_TOKENS='{"replace-saas-read-token":{"subject":"saas","issuer":"saas","audience":"agent-call.sip-read","scopes":["sip.trunk.read"],"trunk_ids":["trunk-primary"]}}'
|
||||
# Mock 仅登记持久化发布意图;real 还要求下列 mTLS 文件已注入。
|
||||
export SIP_MANAGEMENT_MODE=real
|
||||
export SIP_CELL_TLS_DIR=/etc/agent-call/sip-management-tls
|
||||
export SIP_CELL_TLS_CA_FILE=/run/sip-tls/ca.pem
|
||||
export SIP_CELL_TLS_CERT_FILE=/run/sip-tls/client.pem
|
||||
export SIP_CELL_TLS_KEY_FILE=/run/sip-tls/client-key.pem
|
||||
docker compose --profile sip-management up -d --build sip-management
|
||||
curl --fail http://127.0.0.1:18090/healthz/live
|
||||
```
|
||||
|
||||
- 管理写接口为 `/admin/v1/...`;SaaS 只读接口为 `/readonly/v1/sip/trunks...`,不提供 SaaS 写入线路的路径。
|
||||
- 所有变更写接口要求 `If-Match` 和 `X-Request-ID`;发布只接受存在健康 Cell 且 Trunk 编码能力有交集的配置。没有交集拒绝发布,不静默回退或自动重拨。
|
||||
- `mock` 模式的 `pending` publication 只表示待 Cell 应用的持久意图;`real` 模式只有所有选定 Cell Agent 返回匹配的版本/SHA-256 `applied` 回执后才激活版本。任一 Cell 失败时保留旧 active revision,并保留逐 Cell 失败证据。
|
||||
|
||||
### 3.5 Cell Agent(真实 Asterisk Cell)
|
||||
|
||||
Cell Agent 与管理后台部署在管理网,通过 mTLS HTTPS 接收完整 Trunk 快照;契约见 `docs/contracts/cell-agent.openapi.yaml`。Cell 本地必须预先让 Asterisk 主配置 include `/etc/asterisk/pjsip.d/*.conf`,之后每次发布只原子替换对应片段并执行受控 `asterisk -rx 'pjsip reload'`,不能逐呼改写或重载共享配置。
|
||||
|
||||
```bash
|
||||
# 在真实 Asterisk Cell 上运行;证书文件由受控 secret/PKI 注入,不能提交仓库。
|
||||
export CELL_ID=cell-beijing-01
|
||||
export CELL_AGENT_HOST=10.0.0.21
|
||||
export CELL_AGENT_PORT=9443
|
||||
export CELL_AGENT_DB=/var/lib/agent-call/cell-agent.sqlite3
|
||||
export CELL_ASTERISK_CONFIG_DIR=/etc/asterisk/pjsip.d
|
||||
export CELL_ASTERISK_CLI=/usr/sbin/asterisk
|
||||
export CELL_TLS_CA_FILE=/etc/agent-call/cell-tls/ca.pem
|
||||
export CELL_TLS_CERT_FILE=/etc/agent-call/cell-tls/server.pem
|
||||
export CELL_TLS_KEY_FILE=/etc/agent-call/cell-tls/server-key.pem
|
||||
python3 -m agent_call.cell_agent_main
|
||||
```
|
||||
|
||||
- Cell Agent 默认只监听回环;生产必须显式绑定管理网地址并在安全组仅允许管理后台 mTLS 流量,不能暴露公网。
|
||||
- `management_url` 必须是 HTTPS 且不包含凭据、查询串或片段;real 模式登记 Cell 时缺失该字段会拒绝。
|
||||
- 当前 Cell Agent 对 `auth_mode=digest` 失败关闭,直到 Cell 本地 secret resolver 经过单独审核;不会把 `secret_ref` 渲染成明文,也不会静默改用 IP 鉴权。
|
||||
- 以上代码路径和 Mock/故障测试已完成;真实证书、实际多机 Cell/Asterisk reload、网络隔离和供应商线路回执仍必须在目标环境逐项验收,不能由 Mock 通过替代。
|
||||
|
||||
## 4. ASR Web启动与浏览器验证
|
||||
|
||||
### 4.1 配置
|
||||
|
||||
Reference in New Issue
Block a user