feat: deliver versioned SIP config to Cells

This commit is contained in:
2026-09-14 14:38:15 +08:00
parent 3a47914a6c
commit c8716608a2
26 changed files with 4665 additions and 21 deletions
@@ -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,确认保底资源、借用/归还边界及可满足的租户总承诺 | 用户、业务/运维 |
+225
View File
@@ -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'}
+564
View File
@@ -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,非当前完整验收结论)
+45
View File
@@ -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 配置