通用约定¶
概述¶
本章定义 OASP 协议中的通用约定,包括数据格式、命名规则和序列化规范。所有实现必须遵循这些约定。
规范分层(Normative / Informative)¶
OASP 是一份线缆协议(wire protocol)规范:它约定通信双方在网络上交换什么,而不约定任何一端内部如何实现。为防止规范文本越界替消费方做实现决策,全协议内容分为两层。
规范层(Normative)¶
规范层是所有实现必须遵守的契约。仅包含线缆可观测的内容:
- 事件名与方向(
{namespace}:{action}:{target},AddIn→Server / Server→AddIn / 单向通知) - 请求/响应的字段、形状与类型
- 字段语义(每个字段表示什么)
- 错误码及其实现中立的触发条件(描述"出现了何种可观测条件",而非"因为用了哪种技术")
- 可观测的顺序、幂等性、持久化与可见性保证(如"
success: true后该变更对后续get可见")
非规范层(Informative)¶
非规范层是实现提示,帮助实现者落地,但不构成契约,实现可自由偏离:
- 用什么库或语言、由服务端还是客户端执行某事件、走在线 API 还是离线文件处理
- 性能特征(延迟、吞吐)
- 特定实现路径下的副作用与操作步骤(如某路径需先
save()、完成后需重载文档) - 字段命名与某宿主 API 的对齐关系(便于
cast的提示)
标注方式
非规范内容应明确标注,例如使用 !!! info "实现提示(非规范 / Informative)" admonition,或在行文中以"建议 / 一种可行实现 / 仅供参考"等措辞表达,避免与 MUST 级契约混淆。同一事件可被多种实现路径满足时,规范层只描述对所有路径一致的部分,路径差异下沉到实现提示或消费方仓库。
时间戳¶
格式¶
所有时间戳使用 Unix 毫秒时间戳(自 1970-01-01 00:00:00 UTC 以来的毫秒数)。
正确示例:
错误示例:
时区¶
时间戳总是表示 UTC 时间,不包含时区信息。客户端负责根据需要转换为本地时区。
精度¶
虽然使用毫秒精度,但实际精度取决于系统实现。通常精度在 1-10 毫秒范围内。
字段命名¶
传输层命名¶
Socket.IO 传输的 JSON 数据使用 camelCase 命名。
正确示例:
{
"requestId": "abc123",
"documentUri": "file:///path/to/doc.docx",
"isEmpty": true,
"paragraphCount": 10
}
错误示例:
命名规则总结¶
| 场景 | 命名风格 | 示例 |
|---|---|---|
| JSON 字段名 | camelCase | documentUri, requestId |
| 事件名 | kebab-with-colon | word:get:selection |
| 错误码 | SCREAMING_SNAKE_CASE | SELECTION_EMPTY |
| 枚举值 | PascalCase | InsertionPoint, Paragraph |
请求 ID¶
格式¶
请求 ID 使用 UUID v4 格式。
正确示例:
生成¶
- Server 端生成请求 ID
- 每个请求必须有唯一的 ID
- AddIn 在响应中必须返回相同的请求 ID
用途¶
- 请求-响应关联: 将响应与对应的请求匹配
- 去重: 识别重复请求
- 日志追踪: 跨系统追踪请求链路
- 超时处理: 标识超时的请求
文档 URI¶
格式¶
文档 URI 使用 file:// 协议。
格式:
示例:
编码¶
- 路径中的特殊字符使用 URL 编码
- 空格编码为
%20
示例:
大小写¶
- macOS/Linux: 区分大小写
- Windows: 不区分大小写
建议实现时统一转换为小写进行比较(在 Windows 上)。
字符编码¶
文本内容¶
所有文本内容使用 UTF-8 编码。
JSON 数据¶
JSON 数据使用 UTF-8 编码,不使用 BOM。
换行符¶
文本内容中的换行符:
- Windows: \r\n (CRLF)
- macOS/Linux: \n (LF)
建议:AddIn 应保留文档原有的换行符风格,不做自动转换。
颜色值¶
格式¶
颜色使用 十六进制格式,带 # 前缀。
支持的格式:
示例:
大小写¶
颜色值不区分大小写,但建议使用大写字母。
数值单位¶
字号¶
字号使用 磅 (point) 为单位。
位置和尺寸¶
PPT 中的位置和尺寸使用 磅 (point) 为单位。
换算关系: - 1 英寸 = 72 磅 - 1 厘米 ≈ 28.35 磅
像素¶
图片尺寸使用 像素 (pixel) 为单位。
可选字段¶
空值处理¶
可选字段为空时:
- 推荐:省略字段
- 可接受:设置为 null
- 不推荐:设置为空字符串 ""
推荐:
可接受:
不推荐:
默认值¶
当可选字段省略时,使用文档中指定的默认值。各事件定义中会说明默认值。
数组¶
空数组¶
空数组使用 [],不使用 null。
正确:
不推荐:
索引¶
数组索引从 0 开始。
布尔值¶
布尔值使用 JSON 原生的 true / false。
正确:
错误:
超时约定¶
默认超时¶
| 操作类型 | 默认超时 |
|---|---|
| 简单查询 | 10 秒 |
| 复杂查询 | 30 秒 |
| 修改操作 | 30 秒 |
| 批量操作 | 60 秒 |
超时处理¶
- Server 端应在超时后标记请求为失败
- 不进行自动重试
- 返回
TIMEOUT错误码 - AddIn 收到超时后的响应应忽略
协议版本与兼容性¶
OASP 体系有三方(AI Agent / Server / AddIn),但协议层只涉及 Server ↔ AddIn 两端——AI Agent 通过 MCP/API 接入 Server,不直接参与 Socket.IO 通信。为避免新旧两端在网络上「说不同的话」(协议错位),OASP 在连接握手阶段强制校验协议版本:不兼容的 AddIn 在 connect 阶段即被拒绝,不会进入业务通信。
本节定义版本号语义与兼容性判定;握手载体与拒绝流程见连接与握手 · 协议版本握手,不匹配错误码见错误处理 · PROTOCOL_VERSION_MISMATCH。
版本号语义(MAJOR.MINOR.PATCH)¶
协议版本号采用语义化版本,格式 MAJOR.MINOR.PATCH,单一来源为 pyproject.toml 的 version 字段(当前 0.3.0,由 bump-my-version 管理)。
| 位 | 触发条件(任一即 bump) | 兼容性 |
|---|---|---|
| MAJOR | 删除/重命名事件或字段;更改字段类型、语义或必需性;更改事件命名或路由语义;删除或更改已有错误码 | 不同 MAJOR 完全不兼容 |
| MINOR | 新增事件;新增可选字段;新增枚举值;新增错误码;新增命名空间 | 见下方判定规则 |
| PATCH | bug 修复、文档澄清、错误文案优化(不改变任何线缆可观测行为) | 同 MAJOR.MINOR 内 永远兼容 |
PATCH 必须 wire 字节兼容
PATCH 升级 MUST 保持 wire format 字节兼容——不得新增/删除/重命名任何字段(含可选字段)、不得改类型/值域/必需性、不得改事件名或错误码取值。任何改变序列化字节序列的改动 MUST 走 MINOR(v0.x 阶段亦视为破坏性变更)。
兼容性判定规则¶
握手时由 Server 判定连接的 AddIn 是否兼容。下文 client 指 AddIn 在握手中声明的 oaspVersion,server 指 Server 自身的 OASP 版本。
v0.x(MAJOR = 0,不稳定阶段)——任何 MINOR 都可能是破坏性变更,故 MAJOR.MINOR 必须严格相等(PATCH 可自由差异):
v1.0+(MAJOR ≥ 1,稳定阶段)——MAJOR 必须相等,且 Server MINOR 必须 ≥ Client MINOR(较新的 Server 向后兼容较旧的 AddIn):
is_compatible(client, server) =
client.major == server.major AND client.major >= 1 AND client.minor <= server.minor
判定公式与 A2C-SMCP 协议一致,升级次序相同:Server 先于 AddIn 升级(OASP 中 Server 通常先部署,且是请求发起方)。
v1.0+ 已知版本降级(OASP 两方模型独有)
Server 在握手时已记录 AddIn 的 oaspVersion。v1.0+ 阶段,较新的 Server 向较旧 AddIn 发送请求时 SHOULD 避免使用该 AddIn 的 MINOR 尚未引入的事件——握手放行只保证「能连」,避免错位还需 Server 据已知版本自我约束。这是两方单连接模型的便利;A2C 的无状态 HTTP 闸门不持有对端版本,做不到这一点。
参考实现(Python):
from dataclasses import dataclass
@dataclass(frozen=True)
class OaspVersion:
major: int
minor: int
patch: int
@classmethod
def parse(cls, s: str) -> "OaspVersion":
parts = s.split(".")
if len(parts) != 3:
raise ValueError(f"Invalid version: {s}")
return cls(int(parts[0]), int(parts[1]), int(parts[2]))
def __str__(self) -> str:
return f"{self.major}.{self.minor}.{self.patch}"
def is_compatible(client: OaspVersion, server: OaspVersion) -> bool:
if client.major != server.major:
return False
if client.major == 0:
return client.minor == server.minor # v0.x 严格匹配 MINOR
return client.minor <= server.minor # v1.0+ Server 向后兼容
版本声明载体(oaspVersion)¶
AddIn 连接时 MUST 在 auth 对象中声明 oaspVersion(与 clientId / documentUri 同处一个握手入口)。
- 版本是「能不能说同一种话」(协议层),认证/路由是「你是谁、操作哪个文档」(业务层);二者同在
auth,由 Server 的connecthandler 一次性校验 - OASP 是两方单连接模型(一个文档仅一个 AddIn 连接),不存在 A2C 三方房间的「传递性」需求(A2C 需保证同房间内 Agent 与 Computer 互相兼容,才把版本闸门下沉到 HTTP 中间件层)。OASP 只有一对端点,
connecthandler 校验 +ConnectionRefusedError携带结构化拒绝数据已足够,无需引入 HTTP 中间件 oaspVersionSHOULD 由 AddIn SDK 从内置版本常量自动拼入,避免业务代码手动传值导致漂移
非目标与边界¶
- 非目标(有意排除,保持机制最小):capabilities 特性发现、自动协商降级、单 Server 实例多协议版本共存、peer-to-peer 协商。多版本并存靠部署拓扑解决——一个 Server 实例只讲一个协议版本。
- 版本握手 ≠ 运行时能力:协议版本握手回答「两端能不能说同一种 OASP」(连接期、一次性、强制);运行时能力(如某 Office.js
PowerPointApirequirement set 1.2 / 1.8 是否可用)回答「这台宿主此刻能不能做某动作」(操作期、按需)。二者正交——握手通过不代表某具体能力可用;能力不满足在操作期由3016 API_NOT_SUPPORTED反应式处理,不在握手期校验。