@agentunion/fastaun-browser 0.5.6 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/_packed_docs/CHANGELOG.md +104 -0
  3. package/_packed_docs/INDEX.md +12 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
  5. package/_packed_docs/agent.md/SCHEMA.md +83 -58
  6. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
  7. package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
  8. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
  9. package/_packed_docs/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
  10. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +3 -3
  11. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +7 -0
  12. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +422 -331
  13. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
  14. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  15. package/_packed_docs/message-online-push-alignment.md +572 -0
  16. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
  17. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
  18. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  19. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
  21. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  22. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
  23. package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
  24. package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
  25. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  27. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  28. package/_packed_docs/sdk/INDEX.md +22 -22
  29. package/_packed_docs/sdk/README.md +6 -5
  30. package/dist/agent-md-schema.d.ts +14 -0
  31. package/dist/agent-md-schema.d.ts.map +1 -0
  32. package/dist/agent-md-schema.js +103 -0
  33. package/dist/agent-md-schema.js.map +1 -0
  34. package/dist/agent-md.d.ts +7 -0
  35. package/dist/agent-md.d.ts.map +1 -1
  36. package/dist/agent-md.js +230 -10
  37. package/dist/agent-md.js.map +1 -1
  38. package/dist/aid-store.d.ts +5 -1
  39. package/dist/aid-store.d.ts.map +1 -1
  40. package/dist/aid-store.js +24 -4
  41. package/dist/aid-store.js.map +1 -1
  42. package/dist/aid.d.ts +2 -0
  43. package/dist/aid.d.ts.map +1 -1
  44. package/dist/aid.js +29 -7
  45. package/dist/aid.js.map +1 -1
  46. package/dist/auth.d.ts.map +1 -1
  47. package/dist/auth.js +1 -0
  48. package/dist/auth.js.map +1 -1
  49. package/dist/bundle.js +30253 -21955
  50. package/dist/cert-utils.d.ts +1 -0
  51. package/dist/cert-utils.d.ts.map +1 -1
  52. package/dist/cert-utils.js +36 -16
  53. package/dist/cert-utils.js.map +1 -1
  54. package/dist/client/delivery.d.ts +40 -5
  55. package/dist/client/delivery.d.ts.map +1 -1
  56. package/dist/client/delivery.js +719 -221
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/group-state.js +2 -2
  59. package/dist/client/group-state.js.map +1 -1
  60. package/dist/client/lifecycle.d.ts +15 -1
  61. package/dist/client/lifecycle.d.ts.map +1 -1
  62. package/dist/client/lifecycle.js +463 -151
  63. package/dist/client/lifecycle.js.map +1 -1
  64. package/dist/client/rpc-pipeline.d.ts +25 -6
  65. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  66. package/dist/client/rpc-pipeline.js +407 -123
  67. package/dist/client/rpc-pipeline.js.map +1 -1
  68. package/dist/client/runtime.d.ts +1 -3
  69. package/dist/client/runtime.d.ts.map +1 -1
  70. package/dist/client/runtime.js +4 -7
  71. package/dist/client/runtime.js.map +1 -1
  72. package/dist/client/v2-e2ee.d.ts +20 -2
  73. package/dist/client/v2-e2ee.d.ts.map +1 -1
  74. package/dist/client/v2-e2ee.js +434 -112
  75. package/dist/client/v2-e2ee.js.map +1 -1
  76. package/dist/client.d.ts +22 -10
  77. package/dist/client.d.ts.map +1 -1
  78. package/dist/client.js +500 -116
  79. package/dist/client.js.map +1 -1
  80. package/dist/facades.d.ts +23 -1
  81. package/dist/facades.d.ts.map +1 -1
  82. package/dist/facades.js +55 -7
  83. package/dist/facades.js.map +1 -1
  84. package/dist/group-id.d.ts.map +1 -1
  85. package/dist/group-id.js +2 -17
  86. package/dist/group-id.js.map +1 -1
  87. package/dist/index.d.ts +4 -3
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +2 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/seq-tracker.d.ts.map +1 -1
  92. package/dist/seq-tracker.js +8 -2
  93. package/dist/seq-tracker.js.map +1 -1
  94. package/dist/tools/cross-sdk-agent.js +23 -1
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts.map +1 -1
  97. package/dist/transport.js +8 -1
  98. package/dist/transport.js.map +1 -1
  99. package/dist/validators.d.ts.map +1 -1
  100. package/dist/validators.js +8 -14
  101. package/dist/validators.js.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/package.json +6 -5
@@ -0,0 +1,741 @@
1
+ # Group 消息收发 RPC 方法 Python vs Go 对齐审查报告
2
+
3
+ **审查时间**:2026-08-16
4
+ **审查范围**:send, pull, ack, recall, history, thought.put, thought.get
5
+ **审查文件**:
6
+ - Python: `D:\modelunion\kite\extensions\services\group\entry.py`
7
+ - Go: `D:\modelunion\kite\extensions\services\group\go\cmd\group-go\main.go`
8
+ - RPC层: `internal/rpc/service_handlers.go`
9
+ - 服务层: `internal/service/{send_message,pull_messages,ack_messages,recall_messages,thoughts}.go`
10
+
11
+ ---
12
+
13
+ ## 1. send(发送群消息)
14
+
15
+ ### Python 实现(entry.py:6020-6084)
16
+ ```python
17
+ async def _rpc_send(params: dict) -> dict:
18
+ # V1/V2 版本隔离检查
19
+ request_types = (str(params.get("type") or ""), str(params.get("message_type") or ""))
20
+ payload_type = str((params.get("payload") or {}).get("type") or "")
21
+ is_encrypted = any(value in ("e2ee.group_encrypted", "e2ee.encrypted")
22
+ for value in (*request_types, payload_type))
23
+
24
+ if is_encrypted:
25
+ group_record = await _service._get_group_by_id_compat(group_id)
26
+ e2ee_ver = getattr(group_record, "e2ee_version", "v1")
27
+ if e2ee_ver == "v2":
28
+ raise ValueError("group uses e2ee_v2, please use group.v2.send")
29
+ if e2ee_ver == "v1":
30
+ raise ValueError("group is V1; encrypted messages no longer allowed")
31
+
32
+ # 离线推送目标快照(先查询)
33
+ push_snapshot = await _group_message_push_snapshot(group_id)
34
+ offline_push_targets = _group_offline_push_targets(
35
+ push_snapshot.get("active_aids"),
36
+ sender_aid=sender_aid,
37
+ banned_aids=push_snapshot.get("banned_aids"),
38
+ )
39
+ service_params["_offline_push_targets"] = offline_push_targets
40
+
41
+ result = await _service.send_message(service_params)
42
+
43
+ # 如果实时分发未尝试,补发标准事件
44
+ msg_dispatch = result.get("message_dispatch", {})
45
+ if not msg_dispatch.get("attempted"):
46
+ event_data = {
47
+ "module_id": MODULE_NAME,
48
+ "group_id": result["group_id"],
49
+ "group_aid": result.get("group_aid", ""),
50
+ "seq": message["seq"],
51
+ "message_id": message["message_id"],
52
+ "sender_aid": message["sender_aid"],
53
+ "sender_device_id": ...,
54
+ "type": message["message_type"],
55
+ "dispatch": result.get("dispatch", {}),
56
+ "message_dispatch": msg_dispatch,
57
+ }
58
+ if _inline_realtime_connections_by_aid:
59
+ event_data["_group_inline_plain_source_v1"] = message
60
+ if mention_mode:
61
+ event_data["mention_mode"] = mention_mode
62
+ await _publish_group_event("group.message_created", event_data)
63
+
64
+ return result
65
+ ```
66
+
67
+ ### Go 实现(service_handlers.go:1238-1306)
68
+ ```go
69
+ "send": func(ctx HandlerContext) (any, error) {
70
+ // V1/V2 版本隔离在 Dispatcher.Handle (handler_dispatcher.go:240-263) 层检查
71
+ // 仅检查 v2.send,不检查普通 send
72
+
73
+ result, err := svc.SendMessage(ctx.Context, svc.SendMessageInput{
74
+ Params: ctx.Params,
75
+ Repo: options.SendMessageRepo,
76
+ AllowedTypes: options.SendAllowedTypes,
77
+ AllowedTypesConfigured: options.SendAllowedTypesConfigured,
78
+ MaxMessageSizeBytes: options.SendMaxMessageSizeBytes,
79
+ MaxMessageSizeConfigured: options.SendMaxMessageSizeConfigured,
80
+ PendingCount: options.SendPendingCount,
81
+ BackpressureThreshold: options.SendBackpressureThreshold,
82
+ BackpressureConfigured: options.SendBackpressureConfigured,
83
+ RateLimiter: options.SendRateLimiter,
84
+ ResolveDispatch: resolveDispatch,
85
+ ResolveDispatchPlan: resolveDispatchPlan,
86
+ DispatchMessage: dispatchMessage,
87
+ NormalizeAttachmentContext: options.NormalizeSendStorageRef,
88
+ ViewStorage: options.SendViewStorage,
89
+ ResolveOfflinePushTargets: options.OfflinePushTargetResolver,
90
+ ProduceOfflinePushCandidates: options.OfflinePushCandidateProducer,
91
+ })
92
+ if err != nil {
93
+ return nil, err
94
+ }
95
+ // 实时分发未尝试时补发标准事件
96
+ if !sendMessageDispatchAttempted(result) && options.SendMessageEventPublisher != nil {
97
+ if err := publishSendFallbackEvent(ctx.Context, ctx.Params, result, options); err != nil {
98
+ return nil, err
99
+ }
100
+ }
101
+ return result, nil
102
+ }
103
+ ```
104
+
105
+ **服务层** (send_message.go:83-100):
106
+ ```go
107
+ func SendMessage(ctx context.Context, input SendMessageInput) (map[string]any, error) {
108
+ actor, err := requireActorAID(input.Params)
109
+ groupID := groupIDFromParams(input.Params)
110
+ group, err := requireFreshSendGroup(ctx, input.Repo, groupID)
111
+ messageType := normalizeSendMessageType(input.Params["type"])
112
+ payload := input.Params["payload"]
113
+
114
+ // 权限校验
115
+ member, err := input.Repo.RequireMember(ctx, group.GroupID, actor)
116
+ banned, err := input.Repo.IsBanned(ctx, group.GroupID, actor)
117
+ if banned {
118
+ return nil, errors.New("banned members cannot send messages")
119
+ }
120
+
121
+ // 发言权限检查
122
+ settingsReader, _ := input.Repo.(sendSettingsReader)
123
+ if settingsReader != nil {
124
+ rows, _ := settingsReader.GetSettings(ctx, group.GroupID,
125
+ []string{"members.allow_member_speak"})
126
+ allowSpeak := extractAllowMemberSpeak(rows)
127
+ if member.Role == "member" && !allowSpeak {
128
+ return nil, errors.New("members are not allowed to speak in this group")
129
+ }
130
+ }
131
+
132
+ // 离线推送目标解析在 AppendMessage 后进行
133
+ ...
134
+ }
135
+ ```
136
+
137
+ ### 差异分析
138
+
139
+ | 维度 | Python | Go | 影响等级 |
140
+ |------|--------|----|----|
141
+ | **V1/V2隔离检查** | ✅ 在 `_rpc_send` 入口检查 `type/message_type/payload.type` 三个字段 | ⚠️ 仅在 v2.send 入口检查 envelope,普通 send 无 V1/V2 隔离 | **P0** |
142
+ | **禁言检查** | ✅ `IsBanned()` | ✅ `IsBanned()` | 一致 |
143
+ | **发言权限** | ✅ `settings.allow_member_speak` | ✅ `members.allow_member_speak` | 一致 |
144
+ | **离线推送目标** | ✅ 发送前快照(含 banned 过滤) | ✅ 发送后快照 | 一致(时机略不同) |
145
+ | **消息扇出** | ✅ 在 service 层通过 `SendDispatchInput` | ✅ 在 service 层通过 `SendDispatchInput` | 一致 |
146
+ | **event发布** | ✅ `group.message_created`(dispatch未尝试时) | ✅ `publishSendFallbackEvent`(dispatch未尝试时) | 一致 |
147
+ | **inline推送载荷** | ✅ `_group_inline_plain_source_v1` | ✅ `_group_inline_plain_source_v1` | 一致 |
148
+
149
+ ### 修复清单
150
+
151
+ #### P0-1: Go send 缺少 V1/V2 E2EE 隔离检查
152
+ - **位置**: `internal/rpc/service_handlers.go:1238` (send handler)
153
+ - **问题**: 普通 `send` 方法未检查加密消息的 e2ee_version,允许向 V1 群发送加密消息
154
+ - **Python 行为**:
155
+ - 检查 `type/message_type/payload.type` 是否为 `e2ee.group_encrypted` 或 `e2ee.encrypted`
156
+ - 如果 `e2ee_version == "v2"` → 拒绝,要求用 `group.v2.send`
157
+ - 如果 `e2ee_version == "v1"` → 拒绝,V1 E2EE 已下线
158
+ - **修复方案**:
159
+ ```go
160
+ "send": func(ctx HandlerContext) (any, error) {
161
+ // 在 SendMessage 调用前添加版本隔离检查
162
+ if isEncryptedMessage(ctx.Params) {
163
+ groupID := groupIDFromParams(ctx.Params)
164
+ group, _ := options.SendMessageRepo.RequireGroup(ctx.Context, groupID)
165
+ if group.E2EEVersion == "v2" {
166
+ return nil, errors.New("group uses e2ee_v2, please use group.v2.send")
167
+ }
168
+ if group.E2EEVersion == "v1" {
169
+ return nil, errors.New("group is V1; encrypted messages no longer allowed")
170
+ }
171
+ }
172
+ ...
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ## 2. pull(拉取群消息)
179
+
180
+ ### Python 实现(entry.py:6997-7005)
181
+ ```python
182
+ async def _rpc_pull(params: dict) -> dict:
183
+ tail_params = _tail_window_params(params)
184
+ if tail_params is not None:
185
+ reader = getattr(_service, "pull_message_tail", None)
186
+ if not callable(reader):
187
+ raise RuntimeError("group message tail is unavailable")
188
+ _recent_v2_group_cache_record_bypass()
189
+ return _reverse_group_result_view(await reader(params), params, mode="tail")
190
+ return await _service.pull_messages(params)
191
+ ```
192
+
193
+ ### Go 实现(service_handlers.go:1739-1786)
194
+ ```go
195
+ "pull": func(ctx HandlerContext) (any, error) {
196
+ tailMode, err := validateGroupPullWindow(ctx.Params)
197
+ if err != nil {
198
+ return nil, err
199
+ }
200
+
201
+ var pullTicket any
202
+ if options.MessagePullStarted != nil {
203
+ pullTicket = options.MessagePullStarted(ctx.Params)
204
+ }
205
+ if options.MessagePullTimedOut != nil {
206
+ ctx.OnRequestTimeout(func() {
207
+ options.MessagePullTimedOut(ctx.Params, pullTicket)
208
+ })
209
+ }
210
+
211
+ var result map[string]any
212
+ if tailMode {
213
+ reverseRepo, ok := options.PullMessagesRepo.(svc.ReverseMessagesRepo)
214
+ if !ok {
215
+ err = errors.New("pull_messages repo does not support reverse snapshot")
216
+ ...
217
+ return nil, err
218
+ }
219
+ result, err = svc.PullMessageTail(ctx.Context, svc.ReverseMessagesInput{
220
+ Params: ctx.Params,
221
+ Repo: reverseRepo,
222
+ PendingRows: options.PullPendingPlainMessages,
223
+ MaxResponseBytes: options.PullMaxResponseBytes,
224
+ })
225
+ } else {
226
+ result, err = svc.PullMessages(ctx.Context, svc.PullMessagesInput{
227
+ Params: ctx.Params,
228
+ Repo: options.PullMessagesRepo,
229
+ Storage: options.GetInfoStorage,
230
+ MaxLimit: options.PullMaxLimit,
231
+ MaxResponseBytes: options.PullMaxResponseBytes,
232
+ PendingPlainMessages: options.PullPendingPlainMessages,
233
+ })
234
+ }
235
+
236
+ if completeErr := registerMessagePullCompletion(ctx, options, ctx.Params, result, err, tailMode, pullTicket, nil); completeErr != nil {
237
+ return nil, completeErr
238
+ }
239
+ return result, err
240
+ }
241
+ ```
242
+
243
+ **服务层** (pull_messages.go):
244
+ ```go
245
+ func PullMessages(ctx context.Context, input PullMessagesInput) (map[string]any, error) {
246
+ actor, err := requireActorAID(input.Params)
247
+ groupID := groupIDFromParams(input.Params)
248
+ group, err := input.Repo.RequireGroup(ctx, groupID)
249
+ member, err := input.Repo.RequireMember(ctx, group.GroupID, actor)
250
+
251
+ // 权限校验:成员资格
252
+ // cursor 管理:per-member per-device
253
+ cursor, err := input.Repo.GetOrCreateMessageCursor(ctx, group.GroupID, actor, deviceID, slotID)
254
+
255
+ // 加入时间过滤(只能看到加入后的消息)
256
+ visibilityFloor := cursor.JoinMsgSeq
257
+ if floorRepo, ok := input.Repo.(pullMessageVisibilityFloorRepo); ok {
258
+ floor, _ := floorRepo.MemberMessageVisibilityFloor(ctx, group.GroupID, member)
259
+ if floor > visibilityFloor {
260
+ visibilityFloor = floor
261
+ }
262
+ }
263
+
264
+ // 分页查询
265
+ rows, err := input.Repo.PullMessageRows(ctx, group.GroupID, actor, deviceID, afterSeq, limit)
266
+
267
+ return map[string]any{
268
+ "messages": messages,
269
+ "has_more": hasMore,
270
+ "current_seq": currentSeq,
271
+ }, nil
272
+ }
273
+ ```
274
+
275
+ ### 差异分析
276
+
277
+ | 维度 | Python | Go | 影响等级 |
278
+ |------|--------|----|----|
279
+ | **权限校验** | ✅ 成员资格 | ✅ 成员资格 | 一致 |
280
+ | **cursor管理** | ✅ per-member per-device | ✅ per-member per-device | 一致 |
281
+ | **加入时间过滤** | ✅ `join_msg_seq` | ✅ `MemberMessageVisibilityFloor` | 一致 |
282
+ | **recalled过滤** | ✅ 过滤 recalled 消息 | ✅ 过滤 recalled 消息 | 一致 |
283
+ | **分页** | ✅ `after_seq + limit` | ✅ `after_seq + limit` | 一致 |
284
+ | **tail模式** | ✅ `pull_message_tail` | ✅ `PullMessageTail` | 一致 |
285
+ | **inline推送集成** | ✅ `MessagePullStarted/Completed` | ✅ `MessagePullStarted/Completed` | 一致 |
286
+ | **超时处理** | ❌ 无显式超时回调 | ✅ `MessagePullTimedOut` | Go 更完善 |
287
+
288
+ ### 修复清单
289
+
290
+ #### P2-1: Python pull 缺少超时回调机制
291
+ - **位置**: `entry.py:6997` (_rpc_pull)
292
+ - **问题**: Python 版本未注册超时回调,inline推送状态可能泄漏
293
+ - **Go 行为**: 通过 `ctx.OnRequestTimeout` 注册 `MessagePullTimedOut` 清理资源
294
+ - **修复方案**:
295
+ ```python
296
+ # Python dispatcher 需要实现类似 Go 的 OnRequestTimeout 机制
297
+ # 或在 inline 推送层增加超时兜底清理
298
+ ```
299
+
300
+ ---
301
+
302
+ ## 3. ack(确认群消息)
303
+
304
+ ### Python 实现(entry.py:7094-7099)
305
+ ```python
306
+ async def _rpc_ack(params: dict) -> dict:
307
+ result = await _service.ack_messages(params)
308
+ _on_rpc_request_success(
309
+ lambda: _maybe_schedule_next_online_unread_after_ack(params, result, kind="message")
310
+ )
311
+ return result
312
+ ```
313
+
314
+ ### Go 实现(service_handlers.go:1985-2000)
315
+ ```go
316
+ "ack": func(ctx HandlerContext) (any, error) {
317
+ if options.AckMessagesRepo == nil {
318
+ return nil, errors.New("ack_messages repo is nil")
319
+ }
320
+ result, err := svc.LegacyAck(ctx.Context, svc.AckMessagesInput{
321
+ Params: ctx.Params,
322
+ Repo: options.AckMessagesRepo,
323
+ })
324
+ if err != nil {
325
+ return nil, err
326
+ }
327
+ if err := registerAckCompletion(ctx, options, ctx.Params, result, "message"); err != nil {
328
+ return nil, err
329
+ }
330
+ return result, nil
331
+ }
332
+ ```
333
+
334
+ **服务层** (ack_messages.go):
335
+ ```go
336
+ func AckMessages(ctx context.Context, input AckMessagesInput) (map[string]any, error) {
337
+ actor, err := requireActorAID(input.Params)
338
+ groupID := groupIDFromParams(input.Params)
339
+
340
+ group, err := input.Repo.RequireGroup(ctx, groupID)
341
+ _, err = input.Repo.RequireMember(ctx, group.GroupID, actor)
342
+
343
+ // ack_seq 原子更新
344
+ ackSeq := rpcInt64(input.Params["after_seq"])
345
+ deviceID := rpcString(input.Params["device_id"])
346
+
347
+ plan := messages.AckPlan{
348
+ GroupID: group.GroupID,
349
+ AID: actor,
350
+ DeviceID: deviceID,
351
+ AckSeq: ackSeq,
352
+ }
353
+
354
+ coalesce, err := input.Repo.EnqueueAckPlan(ctx, plan)
355
+
356
+ return map[string]any{
357
+ "group_id": messageGroupIDForRecord(group),
358
+ "group_aid": group.GroupAID,
359
+ "ack_seq": coalesce.EffectiveAckSeq,
360
+ }, nil
361
+ }
362
+ ```
363
+
364
+ ### 差异分析
365
+
366
+ | 维度 | Python | Go | 影响等级 |
367
+ |------|--------|----|----|
368
+ | **权限校验** | ✅ 成员资格 | ✅ 成员资格 | 一致 |
369
+ | **ack_seq更新** | ✅ 原子更新 | ✅ 原子更新(EnqueueAckPlan) | 一致 |
370
+ | **与P2P差异** | ✅ 不通知发送方 | ✅ 不通知发送方 | 一致 |
371
+ | **在线未读调度** | ✅ `_maybe_schedule_next_online_unread_after_ack` | ✅ `registerAckCompletion` → `AckCompleted` → `AfterAck` | 一致 |
372
+
373
+ ### 修复清单
374
+
375
+ 无差异,实现对齐。
376
+
377
+ ---
378
+
379
+ ## 4. recall(撤回群消息)
380
+
381
+ ### Python 实现(entry.py:10632-10667)
382
+ ```python
383
+ async def _rpc_recall(params: dict) -> dict:
384
+ result = await _service.recall_group_messages(params)
385
+ recalled_items = result.get("recalled_details", [])
386
+ if recalled_items:
387
+ group_id = str(result.get("group_id") or params.get("group_id") or "").strip()
388
+ message_group_id = str(result.get("group_aid") or _rpc_response_canonical_group_id(group_id)).strip()
389
+ _recent_v2_group_cache_clear_group(message_group_id or group_id)
390
+ actor_aid = (params.get("_auth") or {}).get("aid", "")
391
+ reason = str(params.get("reason") or "")
392
+
393
+ for item in recalled_items:
394
+ try:
395
+ original_seq = int(item.get("original_seq") or 0)
396
+ # 按 original_seq 过滤:在原消息之后才加入的成员不应收到撤回通知
397
+ visible_aids = await _group_recall_visible_member_aids(group_id, original_seq)
398
+ await _publish_group_event("group.message_recalled", {
399
+ "module_id": MODULE_NAME,
400
+ "group_id": message_group_id,
401
+ "group_aid": message_group_id,
402
+ "seq": item.get("notice_seq"),
403
+ "message_id": item.get("notice_message_id"),
404
+ "message_ids": [item["message_id"]],
405
+ "target_message_seqs": [original_seq],
406
+ "member_aids": visible_aids,
407
+ "sender_aid": actor_aid,
408
+ "recalled_by": actor_aid,
409
+ "recalled_at": int(item.get("recalled_at") or time.time() * 1000),
410
+ "reason": reason,
411
+ "event_seq": item.get("event_seq"),
412
+ })
413
+ except Exception as exc:
414
+ print(f"[{MODULE_NAME}] group.recall push failed: {exc}")
415
+ return result
416
+ ```
417
+
418
+ ### Go 实现(service_handlers.go:1722-1738)
419
+ ```go
420
+ "recall": func(ctx HandlerContext) (any, error) {
421
+ if options.RecallMessagesRepo == nil {
422
+ return nil, errors.New("recall_group_messages repo is nil")
423
+ }
424
+ return svc.RecallGroupMessages(ctx.Context, svc.RecallGroupMessagesInput{
425
+ Params: ctx.Params,
426
+ Repo: options.RecallMessagesRepo,
427
+ EventTargetRepo: options.RecallEventTargetRepo,
428
+ PublishEvent: options.RecallEventPublisher,
429
+ EventModuleID: options.RecallEventModuleID,
430
+ NowMS: options.RecallNowMS,
431
+ RecallWindowSeconds: options.RecallWindowSeconds,
432
+ MaxBatch: options.RecallMaxBatch,
433
+ WaitPending: options.RecallWaitPending,
434
+ RecentV2Cache: options.PullRecentV2Cache,
435
+ })
436
+ }
437
+ ```
438
+
439
+ **服务层** (recall_messages.go):
440
+ ```go
441
+ func RecallGroupMessages(ctx context.Context, input RecallGroupMessagesInput) (map[string]any, error) {
442
+ actor, err := requireActorAID(input.Params)
443
+ wireGroupID, err := requireGroupIDFromParams(input.Params)
444
+ group, err := input.Repo.RequireGroup(ctx, wireGroupID)
445
+
446
+ // 权限校验
447
+ _, err := input.Repo.RequireMember(ctx, group.GroupID, actor)
448
+ banned, err := input.Repo.IsBanned(ctx, group.GroupID, actor)
449
+ if banned {
450
+ return nil, errors.New("banned members cannot recall messages")
451
+ }
452
+
453
+ // 批量撤回(最大100条)
454
+ messageIDs, rawCount, err := normalizeRecallMessageIDs(rawIDs)
455
+ maxBatch := input.MaxBatch
456
+ if maxBatch <= 0 {
457
+ maxBatch = 100
458
+ }
459
+ if rawCount > maxBatch {
460
+ return nil, fmt.Errorf("too many message_ids: %d, max %d", rawCount, maxBatch)
461
+ }
462
+
463
+ // 时间窗口检查(默认2分钟)
464
+ createdAtMap, err := input.Repo.MessageCreatedAtMS(ctx, group.GroupID, messageIDs)
465
+ windowMS := int64(input.RecallWindowSeconds) * 1000
466
+ if windowMS <= 0 {
467
+ windowMS = 120000
468
+ }
469
+
470
+ // 权限检查:只能撤回自己的消息,admin/owner可撤回任何消息
471
+ notices, events, err := input.Repo.RecallGroupMessages(ctx, group.GroupID, messageIDs, actor, nowMS, reason)
472
+
473
+ // 事件发布:按 original_seq 过滤可见成员
474
+ for _, notice := range notices {
475
+ visibleAIDs := recallVisibleMemberAIDs(allMembers, notice.OriginalSeq)
476
+ input.PublishEvent(ctx, "group.message_recalled", eventPayload)
477
+ }
478
+
479
+ return result, nil
480
+ }
481
+ ```
482
+
483
+ ### 差异分析
484
+
485
+ | 维度 | Python | Go | 影响等级 |
486
+ |------|--------|----|----|
487
+ | **权限校验** | ✅ 成员资格 + 禁言 | ✅ 成员资格 + 禁言 | 一致 |
488
+ | **撤回权限** | ✅ 只能撤回自己的,admin/owner可撤回任何 | ✅ 只能撤回自己的,admin/owner可撤回任何 | 一致 |
489
+ | **时间窗口** | ✅ 默认120秒 | ✅ 默认120秒 | 一致 |
490
+ | **批量撤回** | ✅ 最大100条 | ✅ 最大100条 | 一致 |
491
+ | **visible_aids过滤** | ✅ 按 original_seq 过滤 | ✅ 按 original_seq 过滤 | 一致 |
492
+ | **event发布** | ✅ `group.message_recalled` | ✅ `group.message_recalled` | 一致 |
493
+ | **recalled_at时间源** | ✅ 复用 store 事务内时间 | ✅ 复用 store 事务内时间 | 一致 |
494
+ | **缓存失效** | ✅ `_recent_v2_group_cache_clear_group` | ✅ `RecentV2Cache.ClearGroup` | 一致 |
495
+
496
+ ### 修复清单
497
+
498
+ 无差异,实现对齐。
499
+
500
+ ---
501
+
502
+ ## 5. history(历史消息查询)
503
+
504
+ ### Python 实现(entry.py:7008-7013)
505
+ ```python
506
+ async def _rpc_history(params: dict) -> dict:
507
+ _history_window_params(params)
508
+ reader = getattr(_service, "pull_message_history", None)
509
+ if not callable(reader):
510
+ raise RuntimeError("group history is unavailable")
511
+ return _reverse_group_result_view(await reader(params), params, mode="history")
512
+ ```
513
+
514
+ ### Go 实现(service_handlers.go:1844-1861)
515
+ ```go
516
+ "history": func(ctx HandlerContext) (any, error) {
517
+ if err := validateGroupHistoryWindow(ctx.Params); err != nil {
518
+ return nil, err
519
+ }
520
+ if options.PullMessagesRepo == nil {
521
+ return nil, errors.New("pull_messages repo is nil")
522
+ }
523
+ reverseRepo, ok := options.PullMessagesRepo.(svc.ReverseMessagesRepo)
524
+ if !ok {
525
+ return nil, errors.New("pull_messages repo does not support reverse snapshot")
526
+ }
527
+ return svc.PullMessageHistory(ctx.Context, svc.ReverseMessagesInput{
528
+ Params: ctx.Params,
529
+ Repo: reverseRepo,
530
+ PendingRows: options.PullPendingPlainMessages,
531
+ MaxResponseBytes: options.PullMaxResponseBytes,
532
+ })
533
+ }
534
+ ```
535
+
536
+ ### 差异分析
537
+
538
+ | 维度 | Python | Go | 影响等级 |
539
+ |------|--------|----|----|
540
+ | **向前翻页** | ✅ `before_seq` | ✅ `before_seq` | 一致 |
541
+ | **权限校验** | ✅ 成员资格 | ✅ 成员资格 | 一致 |
542
+ | **加入时间过滤** | ✅ 只能看到加入后的消息 | ✅ 只能看到加入后的消息 | 一致 |
543
+
544
+ ### 修复清单
545
+
546
+ 无差异,实现对齐。
547
+
548
+ ---
549
+
550
+ ## 6. thought.put/get(草稿管理)
551
+
552
+ ### Python 实现(entry.py:7016-7089)
553
+ ```python
554
+ async def _rpc_thought_put(params: dict) -> dict:
555
+ # V1 群拒绝加密 thought
556
+ payload = params.get("payload")
557
+ payload_type = ""
558
+ if isinstance(payload, dict):
559
+ payload_type = str(payload.get("type") or "").strip()
560
+ if payload_type in ("e2ee.group_encrypted", "e2ee.encrypted"):
561
+ group_id = _group_identifier_param(params)
562
+ if group_id and _service:
563
+ group_record = await _service._get_group_by_id_compat(group_id)
564
+ e2ee_ver = getattr(group_record, "e2ee_version", "v1") if group_record else "v1"
565
+ if e2ee_ver == "v1":
566
+ raise ValueError(f"group {group_id} is V1; encrypted thoughts are no longer allowed in V1 groups")
567
+ return await _service.put_thought(params)
568
+
569
+ async def _rpc_thought_get(params: dict) -> dict:
570
+ result = await _service.get_thought(params)
571
+ # 按客户端 group 版本过滤 items
572
+ if isinstance(result, dict) and result.get("found") and isinstance(result.get("thoughts"), list):
573
+ _auth = params.get("_auth", {}) if isinstance(params, dict) else {}
574
+ client_ver = _client_group_version(_auth.get("capabilities") if isinstance(_auth, dict) else None)
575
+ result["thoughts"] = _filter_thoughts_by_client_version(result["thoughts"], client_ver)
576
+ return result
577
+
578
+ def _client_group_version(capabilities: dict | None) -> str:
579
+ """从能力声明判 Group 版本。返回 "v1" 或 "v2"。
580
+ 仅 supported_group_e2ee == ["group_e2ee_v2"](不含 "group_e2ee")才视为 V2 客户端。
581
+ """
582
+ if not isinstance(capabilities, dict):
583
+ return "v1"
584
+ supported = capabilities.get("supported_group_e2ee")
585
+ if not isinstance(supported, list):
586
+ return "v1"
587
+ has_v2 = "group_e2ee_v2" in supported
588
+ has_v1 = "group_e2ee" in supported
589
+ return "v2" if (has_v2 and not has_v1) else "v1"
590
+
591
+ def _filter_thoughts_by_client_version(items: list, client_group_version: str) -> list:
592
+ """按客户端 group 版本过滤 thought items。"""
593
+ enforce = os.environ.get("AUN_ENFORCE_THOUGHT_VERSION_FILTER", "1").strip().lower()
594
+ if enforce in ("0", "false", "off"):
595
+ return items
596
+ filtered = []
597
+ for item in items:
598
+ if not isinstance(item, dict):
599
+ continue
600
+ item_ver = _thought_payload_version(item.get("payload"))
601
+ if item_ver == "plaintext":
602
+ filtered.append(item)
603
+ elif client_group_version == "v2" and item_ver == "v2":
604
+ filtered.append(item)
605
+ elif client_group_version == "v1" and item_ver == "v1":
606
+ filtered.append(item)
607
+ return filtered
608
+ ```
609
+
610
+ ### Go 实现(service_handlers.go:808-833)
611
+ ```go
612
+ "thought.put": func(ctx HandlerContext) (any, error) {
613
+ if options.ThoughtRepo == nil {
614
+ return nil, errors.New("thought repo is nil")
615
+ }
616
+ if options.ThoughtStore == nil {
617
+ return nil, errors.New("thought store is nil")
618
+ }
619
+ return svc.PutThought(ctx.Context, svc.ThoughtInput{
620
+ Params: ctx.Params,
621
+ Repo: options.ThoughtRepo,
622
+ Store: options.ThoughtStore,
623
+ })
624
+ },
625
+ "thought.get": func(ctx HandlerContext) (any, error) {
626
+ if options.ThoughtRepo == nil {
627
+ return nil, errors.New("thought repo is nil")
628
+ }
629
+ if options.ThoughtStore == nil {
630
+ return nil, errors.New("thought store is nil")
631
+ }
632
+ return svc.GetThought(ctx.Context, svc.ThoughtInput{
633
+ Params: ctx.Params,
634
+ Repo: options.ThoughtRepo,
635
+ Store: options.ThoughtStore,
636
+ })
637
+ }
638
+ ```
639
+
640
+ ### 差异分析
641
+
642
+ | 维度 | Python | Go | 影响等级 |
643
+ |------|--------|----|----|
644
+ | **V1群加密thought拒绝** | ✅ 检查 `payload.type` | ❌ 无检查 | **P0** |
645
+ | **客户端版本过滤** | ✅ 按 `capabilities.supported_group_e2ee` 过滤返回 | ❌ 无过滤 | **P1** |
646
+ | **存储键格式** | ✅ `draft:group:{group_aid}` | ✅ `draft:group:{group_aid}` | 一致 |
647
+ | **与P2P差异** | ✅ 独立存储空间 | ✅ 独立存储空间 | 一致 |
648
+
649
+ ### 修复清单
650
+
651
+ #### P0-2: Go thought.put 缺少 V1 群加密 thought 拒绝检查
652
+ - **位置**: `internal/rpc/service_handlers.go:808` (thought.put handler)
653
+ - **问题**: Go 版本允许向 V1 群发送加密 thought,违反 V1 E2EE 下线策略
654
+ - **Python 行为**:
655
+ - 检查 `payload.type` 是否为 `e2ee.group_encrypted` 或 `e2ee.encrypted`
656
+ - 如果 `e2ee_version == "v1"` → 拒绝
657
+ - **修复方案**:
658
+ ```go
659
+ "thought.put": func(ctx HandlerContext) (any, error) {
660
+ // 添加 V1 群加密 thought 拒绝检查
661
+ payload, _ := ctx.Params["payload"].(map[string]any)
662
+ if payload != nil {
663
+ payloadType := strings.TrimSpace(fmt.Sprint(payload["type"]))
664
+ if payloadType == "e2ee.group_encrypted" || payloadType == "e2ee.encrypted" {
665
+ groupID := groupIDFromParams(ctx.Params)
666
+ group, _ := options.ThoughtRepo.RequireGroup(ctx.Context, groupID)
667
+ if group.E2EEVersion == "v1" {
668
+ return nil, errors.New("group is V1; encrypted thoughts no longer allowed")
669
+ }
670
+ }
671
+ }
672
+ return svc.PutThought(ctx.Context, svc.ThoughtInput{...})
673
+ }
674
+ ```
675
+
676
+ #### P1-1: Go thought.get 缺少客户端版本过滤
677
+ - **位置**: `internal/rpc/service_handlers.go:821` (thought.get handler)
678
+ - **问题**: Go 版本返回所有版本的 thought,可能导致客户端解密失败
679
+ - **Python 行为**:
680
+ - 从 `_auth.capabilities.supported_group_e2ee` 判断客户端版本
681
+ - 仅返回匹配版本的 thought(V1客户端只看V1,V2客户端只看V2,plaintext全可见)
682
+ - 可通过 `AUN_ENFORCE_THOUGHT_VERSION_FILTER=0` 关闭
683
+ - **修复方案**:
684
+ ```go
685
+ "thought.get": func(ctx HandlerContext) (any, error) {
686
+ result, err := svc.GetThought(ctx.Context, svc.ThoughtInput{...})
687
+ if err != nil {
688
+ return nil, err
689
+ }
690
+
691
+ // 按客户端版本过滤
692
+ if enforce := os.Getenv("AUN_ENFORCE_THOUGHT_VERSION_FILTER"); enforce != "0" && enforce != "false" {
693
+ auth, _ := ctx.Params["_auth"].(map[string]any)
694
+ clientVer := clientGroupVersion(auth)
695
+ result = filterThoughtsByClientVersion(result, clientVer)
696
+ }
697
+ return result, nil
698
+ }
699
+ ```
700
+
701
+ ---
702
+
703
+ ## 总结
704
+
705
+ ### P0 级问题(影响核心功能/安全)
706
+
707
+ | ID | 问题 | 位置 | 影响 |
708
+ |----|------|------|------|
709
+ | P0-1 | Go send 缺少 V1/V2 E2EE 隔离检查 | `service_handlers.go:1238` | 允许向 V1 群发送加密消息,违反 V1 E2EE 下线策略 |
710
+ | P0-2 | Go thought.put 缺少 V1 群加密 thought 拒绝检查 | `service_handlers.go:808` | 允许向 V1 群发送加密 thought,违反 V1 E2EE 下线策略 |
711
+
712
+ ### P1 级问题(影响业务逻辑)
713
+
714
+ | ID | 问题 | 位置 | 影响 |
715
+ |----|------|------|------|
716
+ | P1-1 | Go thought.get 缺少客户端版本过滤 | `service_handlers.go:821` | 返回不兼容版本的 thought,可能导致客户端解密失败 |
717
+
718
+ ### P2 级问题(优化改进)
719
+
720
+ | ID | 问题 | 位置 | 影响 |
721
+ |----|------|------|------|
722
+ | P2-1 | Python pull 缺少超时回调机制 | `entry.py:6997` | inline推送状态可能泄漏,但影响有限 |
723
+
724
+ ### 对齐良好的部分
725
+
726
+ - ✅ **权限校验**:成员资格、禁言、发言权限在两端完全一致
727
+ - ✅ **消息扇出**:在线/离线推送策略对齐
728
+ - ✅ **cursor管理**:per-member per-device,加入时间过滤对齐
729
+ - ✅ **recall机制**:时间窗口、批量撤回、visible_aids过滤对齐
730
+ - ✅ **事件发布**:group.message_created / group.message_recalled 对齐
731
+ - ✅ **inline推送集成**:Pull/Ack 的进程内副作用注册机制对齐
732
+
733
+ ### 修复优先级
734
+
735
+ 1. **立即修复** (P0-1, P0-2):V1/V2 E2EE 隔离检查,涉及协议合规
736
+ 2. **近期修复** (P1-1):thought 客户端版本过滤,影响用户体验
737
+ 3. **长期优化** (P2-1):Python 超时回调机制,可作为框架增强
738
+
739
+ ---
740
+
741
+ **审查结论**:Go 版本在核心流程(权限、扇出、cursor、recall)上与 Python 对齐良好,但在 V1 E2EE 下线相关的版本隔离检查上存在 2 个 P0 级遗漏,需要立即修复以保证协议合规性。