Content Types
0. 规范语言
本文中的规范关键字(MUST / SHOULD / MAY 等)按 conformance/normative-language.md 解释;仅大写形式具规范约束力。
体例说明:本文件字段表的「必填 / 必需」列采用 RFC 关键字(MUST / SHOULD / MAY)表达,区别于其余 models 对象表使用的
yes / no / conditional体例;二者语义对应关系为 MUST↔yes、MAY↔no、SHOULD/条件性↔conditional。
1. 目标
Arkret 的 message 标准对象、Strand Description、Strand synthesis / discussion 和可讨论的 Morph 需要承载远比纯文本丰富的内容,包括图片、视频、文件、代码块、地理位置等。本规范定义了结构化的内容类型系统 (Content Type System),使得:
- 所有客户端能够以一致的方式渲染各种消息类型
- 不支持某种内容类型的客户端能通过
fallback_text优雅降级 - E2EE 场景下加密信封 (
encrypted_content) 只包裹 Message 顶层的content——即整个 Content Block 对象,连同它内部的body、attachments等业务字段;strand_id/created_by等路由与归因 metadata 保持明文。attachments是 Content Block 的字段,物化 Message 顶层没有该属性(message.schema.json顶层unevaluatedProperties: false)
2. 设计原则
2.1 Content 是结构化的,不是裸字符串
Message 的 content 字段、ak.message.create / ak.message.revise Event Envelope 的 payload.content 或 payload.encrypted_content 字段、Strand Description 的顶层 content / encrypted_content、Strand synthesis track 的 tracks.synthesis.content / tracks.synthesis.encrypted_content,以及 Morph 的 content / encrypted_content 字段 MUST 使用本规范定义的结构化 JSON 格式或其 canonical encrypted envelope,而非依赖客户端猜测渲染方式。
2.2 单一 Content Block 架构
每条消息的 content 字段是一个 Content Block 对象,包含:
kind:内容类型标识body:人类可读的纯文本摘要 / fallback- 类型相关的专有字段
Message envelope 与 Content Block 的层级关系大致如下(Message 顶层完整 schema 见 strand-and-message.md §9.2,本文件后续章节只讨论 content 内部结构):
{ "id": "ak:message:...", "realm_id": "ak:realm:...", "strand_id": "ak:strand:...", "track_name": "discussion", "state": "active", "created_by": {"kind":"account","account_id":{"principal_id":"ak:did_core:webvh:z2gNJAM6eKtNKMnbxHuqHCnaw","station_id":"ak:did_core:webvh:z6mkfixturestationexample"}}, "created_at": "2026-04-26T00:00:00Z",
"content": { "kind": "ak.content.text", "body": "纯文本 fallback", "format": "markdown", "formatted_body": "..." }}因此本文档示例里的 kind / body / format 等字段都是 Content Block 内部字段,位于 Message content 之下;不要与 Message 顶层字段混在一层理解。
ak.message.create / ak.message.revise 的未加密 Event payload MUST 将这个 Content Block 对象放在 payload.content 字段中;E2EE payload MUST 将同一对象加密后放在 payload.encrypted_content。strand_id、blob_refs 等字段是 envelope / reducer metadata(Message 主键是顶层 id,不是 message_id;回复关系由 replies_to Relation 表达,物化 Message 对象无 reply_to_id 标量字段——ak.message.create payload 可携带 reply_to_id 作为创建便利,reducer 据此记录该消息的回复指向并投影为 replies_to 关系,不要求单独的 canonical ak.relation 事件),不能把消息正文直接写成 payload 顶层 body。
这里的 payload 指 Event Envelope 的 kind-specific 业务载荷容器;content 指该 payload 内部写入 Message / Strand / Morph 正文字段的 Content Block,不是 payload 的同义词。
Signal Extension 的 ak.message.stream plaintext 是提交前 transient preview,MUST NOT
验证或物化为 Content Block,也不得进入 Message revision chain、Realm history、搜索索引或
Blob 引用计数。只有接受后的 ak.message.create / ak.message.revise payload 中的
content(或 encrypted_content plaintext)才是本文件定义的 Content Block;preview 与
final 不同不构成 Content schema 错误。
2.3 复合消息使用 composite 类型
当一条消息需要同时包含文本和图片(例如带说明文字的截图),使用 composite 类型将多个 Content Block 组合。
3. Content Block 通用结构
{ "kind": "ak.content.<kind_name>", "body": "纯文本 fallback,用于通知、搜索索引和不支持该类型的客户端"}类型专有字段以同级 key 形式追加到该对象上(例如 format / formatted_body 见 §4.1 文本消息示例)。
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
kind | string | MUST | 内容类型标识符 |
body | string | MUST | 纯文本 fallback |
mentions | array | MAY | 结构化 direct mention 节点数组($defs.mention_node);唯一的 mention wire 承载位置,语义见 strand-and-message.md §9.4.1 |
audience_mentions | array | MAY | 结构化 audience mention 节点数组($defs.audience_mention_node),语义见 strand-and-message.md §9.4.3 |
4. 标准内容类型
4.1 文本消息 ak.content.text
最基础的消息类型。
{ "kind": "ak.content.text", "body": "@bob 请确认这个 item 的 legal 风险。", "format": "markdown", "formatted_body": "<mention did=\"ak:did_core:webvh:zHuXvTbhiRsj2KEPE64TLhzG4\">@bob</mention> 请确认这个 item 的 legal 风险。", "mentions": [ { "kind": "mention", "subject_account_id": { "principal_id": "ak:did_core:webvh:zHuXvTbhiRsj2KEPE64TLhzG4", "station_id": "ak:did_core:web:acme.example" }, "mention_text_original": "@bob" } ]}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
format | string | SHOULD | 格式化类型:plain, markdown, prosemirror_json |
formatted_body | string/object | MAY | 结构化的富文本内容(当 format 不为 plain 时使用) |
inline 正文上限(normative):ak.content.text.body MUST NOT 超过 256 KiB UTF-8 bytes(262,144)。超过该分界的纯文本正文 MUST 使用 §4.1.1 的 ak.content.long_text。JSON Schema 的 maxLength 只表达不宽于该值的 code-point 快速上限;权威检查 MUST 由 UTF-8 byte validator 执行。
4.1.1 长文本消息 ak.content.long_text
超过 inline 分界的纯文本正文使用本 Content Block:一个小型 inline fallback 加一个 Blob-backed 完整正文。它是 v1 core Content Block,不使用 requirements feature,服务端 MUST NOT 广告“支持 ak.content.text 但不支持 ak.content.long_text”——长正文是同一个 Message 基础模型的边界形态,把它设为可选会使合法 Message 在不同 core 实现间不可读。
plaintext 形态
{ "kind": "ak.content.long_text", "body": "前 4 KiB 内的可独立展示前缀……", "body_kind": "prefix", "blob_ref": "ak:blob:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "size_bytes": 700000, "line_count": 12000, "media_type": "text/markdown"}| 字段 | 类型 | 必需 | 规则 |
|---|---|---|---|
kind | const | MUST | ak.content.long_text |
body | string | MUST | fallback,≤ 4 KiB UTF-8 bytes(4,096) |
body_kind | enum | MUST | 闭集 prefix | summary |
line_count | uint64 | MAY | 按 §4.1.2 计算 |
blob_ref | hash blob ref | MUST | 完整 UTF-8 正文字节的内容地址,形如 ak:blob:(sha256|blake3):<64 hex> |
size_bytes | uint64 | MUST | §4.1.2 规范化后完整 UTF-8 正文字节数 |
media_type | enum | MUST | 闭集 text/plain | text/markdown;完整正文的唯一媒体类型及渲染判别字段 |
shape MUST 是 additionalProperties=false 的闭合对象,MUST NOT 携带 format。完整正文只允许 text/plain 或 text/markdown;结构化富文本应使用独立 Content Block/schema。
blob_ref 本身就是完整 plaintext 字节的 digest commitment,因此本 kind 不再增加重复的 content_digest。接收端 MUST 把 blob_ref=ak:blob:<suite>:<hex> 拆成 <suite>:<hex>,要求它与 Blob metadata 的 content_digest 相等,并对下载的规范化正文重算;三者任一不等即 digest_mismatch。UUID 形态 Blob ref 不具备该性质,故在本 Content Block 中 MUST NOT 使用;media_type MUST NOT 携带 ; charset=utf-8 等参数(charset 由本 kind 固定为 UTF-8)。
E2EE 形态
E2EE Message 的 long-text descriptor 位于已认证的 encrypted_content plaintext 中,完整正文 Blob 使用现有 encrypted_attachment descriptor:
{ "kind": "ak.content.long_text", "body": "已认证 fallback", "body_kind": "summary", "line_count": 12000, "attachment": { "blob_ref": "ak:blob:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "encrypted": true, "scheme": "ak.blob.stream_aead.v1", "key_ref": { "algorithm": "MLS", "group_state_ref": "ak:event:AckEwH4jJdfBphZALp-M3ga3R1KDhI2KpvVb8MZiOMbW" }, "content_key_salt": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8", "size_bytes": 700000, "media_type": "text/plain", "nonce_prefix": "AAAAAAAAAAAAAAAAAAAAAAAAAA", "segment_bytes": 262144, "encryption_algorithm": "mls_exporter_aead_xchacha20poly1305_stream" }}规则:
- 完整正文的明文字节数由
attachment.size_bytes承载(../crypto-media/media-and-blob.md§3.1:encrypted_attachment.size_bytes是明文字节数,段数由N=max(1,ceil(size_bytes/segment_bytes))本地派生)。本 kind MUST NOT 再定义plaintext_size_bytes之类的第二个明文尺寸字段; attachment.schemeMUST 是ak.blob.stream_aead.v1,attachment.algMUST 是对应的_stream算法。E2EE long text MUST NOT 使用 whole-file AEAD——强制 streaming 是为了让超过分界的正文能边下边验且内存有界,不作为同一语义的第二种可选形态;attachment.blob_refMUST 是 content-addressed,其中的<suite>:<hex>是按segment_index顺序拼接、每段包含 AEAD tag 的完整 stored ciphertext bytes 的唯一 wire commitment;attachment 不携 siblingciphertext_digest,独立 Blob metadata 的content_digest必须与 ref 内嵌值相等;attachment.media_typeMUST 是text/plain或text/markdown,MUST NOT 携带参数,charset 固定为 UTF-8;它是完整正文的唯一媒体类型及渲染判别字段,并继续按通用附件规则进入逐段 AAD。根级 MUST NOT 携带format或media_type,MUST NOT 从 Blob 服务 metadata 或 HTTP Content-Type 推断正文渲染类型;- 接收端 MUST 本地派生
N=max(1,ceil(attachment.size_bytes/attachment.segment_bytes)),并验证实际 segment 数与N一致;attachment MUST NOT 携带segment_count或epoch,epoch 从key_ref.group_state_ref指向的已验证 group state 派生(../crypto-media/media-and-blob.md§3.3.1);接收端 MUST NOT 从 stored ciphertext 长度反推明文长度; blob_ref内嵌整体 digest、逐段 AEAD、末段与顺序全部验证通过前,接收端 MUST NOT 把正文标成完整;- fallback
body已在 Message encrypted payload 内认证,Blob 服务 MUST NOT 改写。
4.1.2 文本规范化与计数(normative)
Blob 解码后的正文 MUST:
- 是有效 UTF-8,不带 BOM;
- 保留原始 Unicode scalar sequence,MUST NOT 做 NFC/NFKC 改写;
- 行结束统一使用 LF(U+000A);producer MUST 在计算 digest、size 与 line count 之前把 CRLF/CR 规范化为 LF;
- 除 LF、TAB 外 MUST NOT 含 C0 控制字符与 DEL;
- Markdown 按不可信输入消毒,MUST NOT 执行 raw HTML/script。短文本由
format=markdown选择;long-text 完整正文由明文根级media_type=text/markdown或 E2EEattachment.media_type=text/markdown选择。
size_bytes(plaintext 形态)与 attachment.size_bytes(E2EE 形态)都是上述规范化后完整 UTF-8 字节长度。fallback body 自身 MUST 满足相同的 UTF-8、LF、BOM、控制字符与 markdown 消毒规则;producer MUST 先规范化完整正文,再从该结果生成 prefix 或 summary,MUST NOT 对两者采用不同的换行 / Unicode 处理。
line_count 若存在,定义为:
empty text => 0non-empty text => count(U+000A) + (last scalar is U+000A ? 0 : 1)接收端 MUST 校验声明计数。计数不一致时正文无效,但 Message 的小 fallback 仍可显示。
4.1.3 Fallback 规则(normative)
body 是 timeline、通知可访问性、Blob 尚未到达和未知实现的安全 fallback。
body_kind=prefix:bodyMUST 是完整正文从 byte 0 开始、在 Unicode scalar 边界截断的前缀,MUST NOT 超过 4,096 UTF-8 bytes;接收端下载 Blob 后 MUST 验证前缀相等。前缀可在 code point 边界截断,不要求落在单词或段落边界。body_kind=summary:body是作者提供的摘要,不要求是正文前缀,MUST NOT 超过 4,096 UTF-8 bytes;UI MUST 标注为摘要,MUST NOT 拼接到完整正文前面。协议不尝试机器验证摘要忠实性。
4 KiB 是 UTF-8 字节限制,不是 JSON Schema maxLength 的 code point 数。schema MAY 给不宽于 4,096 的 maxLength 作为快速上限,但权威检查 MUST 由 UTF-8 byte validator 完成。
4.1.4 选择边界(normative)
选择基于最终规范化源正文,不是 gzip、ciphertext、JSON escaped 或上传 chunk 大小:
0..262144 bytes => ak.content.text262145 bytes and above => ak.content.long_text边界无重叠。ak.content.long_text MUST NOT 用来把 10 字节正文强行 Blob 化;普通文件另用 ak.content.file。
此外,最终 Event 仍 MUST 满足 ../conformance/scalability-constraints.md §2.1.1 的完整 Event Envelope 上限。这是一条更早的 transport 必要条件,不改变上述 content-kind 的强制分界:
ak.content.text只在正文 ≤256 KiB 且完整 Event 合法时可用;ak.content.long_text在正文 >256 KiB 时强制;在较小正文上仅当完整 Event 否则无法满足 1 MiB 硬上限时允许,且该例外 MUST 由完整 Event size validator 证明,MUST NOT 由实现任意选择。
因此 size_bytes / attachment.size_bytes 的 schema 下界不能写成 262,145——那会使上述例外不可表达。schema 只校验类型与非负性,“>256 KiB 或 Event-overflow 例外”由 normative validator 判定。
4.1.5 下游行为(normative)
- Timeline:先显示
body,下载 / 验证成功后替换为完整正文。 - Search:只能索引已解密且完整验证的 Blob 正文;fallback 可单独标记为 partial。
- Mentions:提交通知所需的 canonical mentions MUST 仍在所属 Content Block 的
mentions[]中(§3.2 的唯一 wire 承载位置),MUST NOT 要求服务端扫描 Blob。 - Reply/quote:引用 Message ID,不复制完整长正文。
- Push:MUST NOT 把 Blob 正文发送给 push provider;沿用 blind/visible profile 边界。
- Redaction / expiry:Message 不可见后 MUST 同步使 fallback、搜索索引、缓存和 Blob 访问失效;Blob GC 沿用现有引用追踪。
- Range:E2EE 按 AEAD segment 边界请求并验证。
- Offline:实现 MAY 只缓存 fallback;缓存完整正文 MUST 受 Realm / Message 生命周期清理。
未知 ak.content.long_text 的客户端按 §7.2 的 unknown-kind fallback:展示已认证 body,MUST NOT 把未知字段解释成 executable content,也 MUST NOT 假装正文完整。
客户端在提交 Message 前 MUST 完成 Blob 上传并取得稳定 hash ref;引用不存在、digest 不符、无权访问或 E2EE descriptor 不完整时按现有 Blob / Content 校验错误拒绝。
4.1.6 明确否决
- 给
ak.content.text加可选body_ref,形成两种语义; - 提高 Event 1 MiB 上限来容纳正文;
- 多个 Message chunk 拼成一条逻辑 Message;
- 复用
ak.content.file表达消息正文; - 用
ak.content.composite切段,破坏搜索 / quote / redaction 身份; - 允许 UUID
blob_ref并另猜内容是否被承诺; text/plain; charset=utf-8形态的 media type;- long text 支持
prosemirror_json; - E2EE long text 任意选择 whole-file 或 streaming AEAD;
- 只用 JSON Schema
maxLength声称执行了 UTF-8 byte limit。
4.2 图片消息 ak.content.image
{ "kind": "ak.content.image", "body": "screenshot.png", "blob_ref": "ak:blob:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "mime_type": "image/png", "width": 1920, "height": 1080, "size_bytes": 204800, "thumbnail": { "blob_ref": "ak:blob:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "mime_type": "image/webp", "width": 320, "height": 180, "size_bytes": 12400 }, "alt_text": "Release dashboard showing 3 critical issues"}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
blob_ref | string | MUST | Blob 内容地址 |
mime_type | string | MUST | MIME 类型 |
width | integer | SHOULD | 像素宽度 |
height | integer | SHOULD | 像素高度 |
size_bytes | integer | SHOULD | 字节数 |
thumbnail | object | SHOULD | 缩略图信息 |
alt_text | string | SHOULD | 无障碍访问文本描述 |
4.3 视频消息 ak.content.video
{ "kind": "ak.content.video", "body": "demo-recording.mp4", "blob_ref": "ak:blob:sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "mime_type": "video/mp4", "width": 1280, "height": 720, "duration_ms": 45000, "size_bytes": 10485760, "thumbnail": { "blob_ref": "ak:blob:sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", "mime_type": "image/jpeg", "width": 320, "height": 180 }}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
duration_ms | integer | SHOULD | 视频时长(毫秒) |
4.4 音频消息 ak.content.audio
{ "kind": "ak.content.audio", "body": "voice-memo.ogg", "blob_ref": "ak:blob:sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "mime_type": "audio/ogg", "duration_ms": 12000, "size_bytes": 96000, "waveform": [10, 25, 48, 62, 55, 30, 15, 8]}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
waveform | integer[] | MAY | 波形预览数据(0-100 的整数数组,用于 UI 渲染) |
4.5 文件消息 ak.content.file
{ "kind": "ak.content.file", "body": "Q2-financial-report.pdf", "blob_ref": "ak:blob:sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "mime_type": "application/pdf", "size_bytes": 2097152, "filename": "Q2-financial-report.pdf"}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
filename | string | MUST | 原始文件名 |
4.6 位置消息 ak.content.location
{ "kind": "ak.content.location", "body": "Meeting point: 37.7749° N, 122.4194° W", "geo_uri": "geo:37.7749,-122.4194", "label": "San Francisco Office", "description": "Main entrance, 2nd floor lobby"}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
geo_uri | string | MUST | RFC 5870 Geo URI |
label | string | SHOULD | 地点名称 |
description | string | MAY | 地点补充描述 |
4.7 代码块消息 ak.content.code
用于分享代码片段:
{ "kind": "ak.content.code", "body": "fn main() { println!(\"hello\"); }", "language": "rust", "code": "fn main() {\n println!(\"hello\");\n}"}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
language | string | SHOULD | 编程语言标识(用于语法高亮) |
code | string | MUST | 代码文本内容 |
4.8 通知消息 ak.content.notice
由系统或 Bot/Agent 生成的通知性消息,客户端 SHOULD 通过可感知的 presentation invariant 与用户撰写消息区分;具体样式、控件和文案属于实现自由:
{ "kind": "ak.content.notice", "body": "Agent completed task: Review legal docs", "format": "markdown", "formatted_body": "Agent completed task: **Review legal docs** [done]"}4.9 投票消息 ak.content.poll
根据去中心化协作需求,投票也是一种标准内容块:
{ "kind": "ak.content.poll", "body": "What should we order for the party?", "poll": { "kind": "disclosed", "max_selections": 1, "question": { "kind": "ak.content.text", "body": "What should we order for the party?" }, "answers": [ { "id": "pizza", "text": { "kind": "ak.content.text", "body": "Pizza 🍕" } }, { "id": "poutine", "text": { "kind": "ak.content.text", "body": "Poutine 🍟" } } ] }}poll block 字段:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
kind | const ak.content.poll | MUST | content block 判别。 |
body | string | MUST | fallback 文本,用于不支持 poll 渲染的客户端。 |
poll.kind | string | MUST | 计票披露模式;封闭枚举,v1 仅 disclosed 一个取值(schema 为 const),以 ak.content.poll content-block schema 为权威源,客户端 MUST NOT 自行扩展。 |
poll.max_selections | integer(≥ 1) | MUST | 单次响应最多可选 answer 数。 |
poll.question | content block | MAY | 题干富文本;省略时以 body 为题。 |
poll.answers[] | array | MUST | 候选项数组,每项 { id: string, text: content block };id 在同一 poll 内 MUST 唯一。 |
ak.content.poll.response block 字段:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
kind | const ak.content.poll.response | MUST | content block 判别。 |
body | string | MUST | fallback 文本。 |
poll_response.poll_ref | id:message | MUST | 指向承载该 poll 的 Message。 |
poll_response.selections | array<string> | MUST | 所选 answer id 列表;数量 MUST ≤ 对应 poll 的 max_selections。 |
v1 准入边界(normative):正式 poll 与 response 只允许出现在明文 ak.message.create.payload.content 中。producer MUST NOT 将上述两种 block 放入 encrypted_content;持钥客户端即使解密后读到相同的 JSON,也 MUST NOT 将它当作正式 poll、response 或计票输入。治理 Station 对不透明密文只按普通加密 Message 准入,不能声称已验证其 poll 类型、选项或分区;encrypted_content 与 poll_response_heads[] 同时出现 MUST 以 schema_violation 拒绝并零写入。明文 poll 可用于未启用 E2EE 且允许明文 content 的 scope;不得为开放加密投票泄露题目、答案或选择到明文 sidecar。
响应投票时,客户端发送 ak.content.poll.response Content Block,最小形态为 { "kind": "ak.content.poll.response", "body": <fallback>, "poll_response": { "poll_ref": id:message, "selections": array<string> } }。poll_ref MUST 解析到同一 Realm、同一有效 Circle scope 内已接受且包含目标 ak.content.poll block 的 Message;缺失、跨 scope 或不指向 poll 时接收方 MUST 拒绝 response Event。selections MUST 非空、无重复,且每个成员都属于该 poll 的闭合 answer-id 集合;去重后的成员数 MUST 不大于 max_selections。未知 answer 或超选 MUST 整体拒绝,不得过滤、截断或部分计票。
权威计票由已接受的标准 Message Event 流承担。response Event 保留在 canonical Event log 中作为审计事实,并折叠进目标 poll 的 PollState;它不物化为时间线中的独立 MessageState。poll Morph 或 Relation 只能投影此状态,不得成为第二真相源。canonical block schema 见 ../conformance/schema-registry.md 与 artifacts/schemas/content-block-poll.schema.json;对应一致性向量为 ak.vector.message.poll_reducer.v1。
4.9.1 改票声明与唯一计票(normative)
分区键为 (realm_id, effective Circle scope, poll_ref, JCS(Event.actor_id));Realm scope 与任一 Circle scope 不同。actor_id 保留完整 AccountId:Agent 自身作为 actor 时独立计票;代理写入仍归 actor_id,不得按 controller、裸 principal、设备、签名 key 或 executed_by 合并或拆分票。
一个分区恰好落在一条 authority stream 内(normative):分区键已经固定了 Realm 与 effective Circle scope,而 Realm stream 与每个 Circle stream 是各自独立的 authority stream(../sync/authority-commit-log.md §3)。因此同一分区的全部 response Event 都由同一个治理 Station 在同一条 stream 上接纳,各自取得该 stream 严格 +1 的 RealmCommit.stream_position。它们之间是全序:并发 head 在这个分区里不可能出现。
每个分区的输入集合 S 仅含通过上述验证、在当前有效性基线下 accepted 且非 quarantine 的 response Event,按 canonical Event digest 去重。当前票唯一由 position 决定:S 为空时该 actor 不计票,否则当前票是 S 中 stream_position 最大的那一条,完整采用它的 selections,不得合并多条 response 的选项。Event 不携带 domain_refs、producer_revision 或 hlc(../sync/authority-commit-log.md §2 的封闭禁止清单);created_at、HLC、墙钟时间、canonical Event digest、本地接收顺序与数据库 ID 都不参与当前票的选择,也不得把 position 较小者重新扶为当前票。
改票声明的唯一载体(normative):producer 改票时 MAY 在该明文 ak.message.create payload 顶层的 typed poll_response_heads[] 中逐条列出它替换的 response——每项是一对 {poll_event_ref, response_event_ref},前者指向承载该 poll 的 Event,后者指向被替换的 response Event(event-payload.schema.json#/$defs/poll_response_head,见 event-and-patch.md §2)。该字段只能与明文 poll response block 共存;治理 Station 以可见 block 验证本条确为 response,再校验每项的两个 ref 都落在本分区——poll_event_ref 指向同一 Realm、同一 effective Circle scope 内已接受且含目标 ak.content.poll block 的 Event,response_event_ref 指向同一分区(同 poll、同 actor、同 scope)内已接受的 response Event;指向其它消息、其它 poll、其它 actor、其它 scope 或无法解析时 MUST 整组拒绝该 response Event 并零写入。
poll_response_heads[] 不是 winner 判据,也不得被实现当作第二真相源:当前票只由 position 决定,声明只是 producer 对“本条替换了哪几条”的可验证陈述,供审计面与 UI 呈现改票轨迹。省略该字段或声明不完整的 response 不因此丧失或取得当前票。response_event_ref 指向本条 Event 自身在结构上不可能——payload 已被 event_id 覆盖——因此这里没有因果环,也不需要环检测。
消费方持有的永远是该 stream 的一个前缀:position 连续性校验(../sync/authority-commit-log.md §4)保证它不会把中间缺口当成“没有更晚的票”。缺口未补齐时 MUST 把该分区报告为 provisional,MUST NOT 宣布已收敛;补齐后当前票只沿 position 单向前移,原先可用的 response 不因此消失。本规则不新增通用 Event 接受状态或 wire reason code。
4.9.2 状态、合并与重建(normative)
内部状态 MUST 保留每条 response 的身份、selections、stream_position 与它声明的 poll_response_heads[],以及足以重放该分区的 canonical Event 资料或索引。当前票与 tally 只是派生缓存;“每 actor 计一票”不是“内部仅存一条 response”:position 较小的历史 response 是审计事实,MUST NOT 被删除。
同一有效性基线下,副本合并是 response 集合的并集,再在每个分区取 stream_position 最大者。该 join 天然满足交换律、结合律与幂等律,因为它就是同一条 stream 上一个全序的 max。poll_response_heads[] 的每一项 MUST 由该 Event 自身的已签 payload 验出,不能信任对端单独声称的覆盖关系。全量重放、迟到 position、重复输入及任意分片合并 MUST 得出相同的当前票、selections 与 tally。
追溯 quarantine、fork resolution 等改变 accepted 输入集合时,按既有有效性规则撤除失效输入,再重建 projection;跨有效性基线不能假设输入永远只增不减,当前票 MAY 因此回退到 position 更小的 response。snapshot/压缩 MUST 至少保存每个分区当前票的完整 response 与其 position,并 MUST 与上述集合语义等价,不得只保存展示 winner 或 tally 计数。
4.10 复合消息 ak.content.composite
当一条消息包含多种内容(如文字说明 + 图片 + 文件附件)时使用:
{ "kind": "ak.content.composite", "body": "Here's the updated design with the spec PDF attached.", "parts": [ { "kind": "ak.content.text", "body": "Here's the updated design with the spec PDF attached.", "format": "markdown" }, { "kind": "ak.content.image", "body": "design-v3.png", "blob_ref": "ak:blob:sha256:1111111111111111111111111111111111111111111111111111111111111111", "mime_type": "image/png", "width": 1920, "height": 1080 }, { "kind": "ak.content.file", "body": "spec-v3.pdf", "blob_ref": "ak:blob:sha256:2222222222222222222222222222222222222222222222222222222222222222", "mime_type": "application/pdf", "size_bytes": 1048576, "filename": "spec-v3.pdf" } ]}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
parts | ContentBlock[] | MUST | 按展示顺序排列的 Content Block 数组。parts 是 ak.content.composite 的 canonical wire 字段名;旧拼写 blocks MUST 被 schema 以 schema_violation 拒绝(见 spec/v1/artifacts/registry/forbidden-wire-fields.json)。 |
6. 引用与回复 (Reply)
6.1 回复关联
回复通过 Relation 表达(message --replies_to--> message),但为了渲染方便,消息的 content 中 MAY 内嵌引用上下文:
{ "kind": "ak.content.text", "body": "> Alice: 这个方案可行吗?\n\n我觉得需要再评估一下风险。", "format": "markdown", "reply_context": { "message_ref": "ak:message:AREJHXyA5b4XzVmfdrdizHFynW5zdwUwC68C8O1D5B8y", "sender_actor_id": { "kind": "account", "account_id": { "principal_id": "ak:did_core:webvh:z2gNJAM6eKtNKMnbxHuqHCnaw", "station_id": "ak:did_core:web:station.example" } }, "excerpt": "这个方案可行吗?" }}6.2 Fallback 规则
reply_context是渲染提示 (Rendering Hint),不是真相源。真正的回复关系由replies_toRelation 决定。- 若客户端在本地缓存/搜索索引中已有原消息,SHOULD 优先使用本地数据渲染引用块,忽略
reply_context.excerpt。 - 若客户端无法获取原消息(例如跨 Realm 引用或权限限制),则使用
reply_context.excerpt做降级展示。
7. 自定义与扩展类型
7.1 命名空间约定
- 标准类型使用
ak.content.*前缀 - 第三方扩展使用反向域名前缀,例如
com.acme.content.poll
7.2 未知类型的处理
客户端遇到不认识的 kind 时:
- MUST NOT 丢弃该消息
- SHOULD 使用
body字段做纯文本降级展示 - MAY 显示”不支持的消息类型”提示
8. 与 E2EE 的交互
在端到端加密场景下:
content字段的完整 JSON 对象被加密为encrypted_content- 每个
encrypted_contentMUST 符合artifacts/schemas/encrypted-envelope.schema.json;ak.message.create/ak.message.revise、Strand Description、tracks.synthesis分别使用其所在对象或 track 内明文content的 canonical envelope;没有明文内容对偶的 payload surface MAY 继续使用通用encrypted_payload body字段在密文信封中不保留明文副本(防止元数据泄露)- 用于推送通知的脱敏摘要由发送者的客户端单独生成并附在明文元数据中(参见
push-notifications.md)
9. v1 扩展规则
- Emoji / Sticker MUST 作为
ak.content.image或ak.content.fileblock 表达,并引用 content-addressed blob;v1 没有注册ak.content.sticker,ak.content.*前缀只能由 Arkret 注册,因此实现不得自行发明该 kind。客户端不得从未授权 URL 热加载私有表情资源。 - 投票的 v1 权威来源是已接受的标准 Message Event 流及 §4.9 reducer;可选
pollMorph/Relation 只能作确定性投影。其它表单类扩展 MAY 登记独立 Morph profile 并用 Relation 记录关系,但不得覆盖已登记 Event reducer 的状态。 - URL 预览 MUST 作为可丢弃的 rendering hint 或受控 preview blob 表达。服务端抓取私有链接前必须有用户或 Realm policy 授权,预览服务若接触正文或页面内容,MUST 列入
plaintext_visible_services。 - E2EE 场景下缩略图 SHOULD 由客户端生成并加密上传;服务端生成缩略图前必须被声明为 plaintext-visible service,并遵守
media-and-blob.md的 MIME、缓存和授权规则。