@senguoyun/dsh-arkme 0.1.79 → 0.1.80

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 (107) hide show
  1. package/docs/reaction-desktop-review.md +33 -0
  2. package/docs/reaction-pre-merge-review.md +78 -0
  3. package/lib/client.js +14212 -10463
  4. package/lib/client.js.map +1 -1
  5. package/lib/harness-model-client.js +8 -0
  6. package/lib/harness-native-selection-client.js +8 -0
  7. package/lib/harness-sidebar-client.js +165 -16
  8. package/lib/index.js +84 -1
  9. package/lib/persistent-extension.js +1 -1
  10. package/lib/{persistent-runtime-CB7mEj3E.js → persistent-runtime-CGclHTN1.js} +792 -165
  11. package/lib/sdk.js +8 -0
  12. package/lib/types/arkme-service.d.ts +1 -0
  13. package/lib/types/arkme-service.d.ts.map +1 -1
  14. package/lib/types/client/ArkmeDataManagementSettings.d.ts.map +1 -1
  15. package/lib/types/client/ArkmeDayTimeline.d.ts +2 -0
  16. package/lib/types/client/ArkmeDayTimeline.d.ts.map +1 -1
  17. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  18. package/lib/types/client/ArkmePersonalDayCalendar.d.ts.map +1 -1
  19. package/lib/types/client/ArkmeReactionActorCard.d.ts +7 -0
  20. package/lib/types/client/ArkmeReactionActorCard.d.ts.map +1 -0
  21. package/lib/types/client/ArkmeReactionHistorySettings.d.ts +7 -0
  22. package/lib/types/client/ArkmeReactionHistorySettings.d.ts.map +1 -0
  23. package/lib/types/client/ArkmeReactionNotification.d.ts +11 -0
  24. package/lib/types/client/ArkmeReactionNotification.d.ts.map +1 -0
  25. package/lib/types/client/ArkmeReactionPhrases.d.ts +8 -0
  26. package/lib/types/client/ArkmeReactionPhrases.d.ts.map +1 -0
  27. package/lib/types/client/ArkmeReactionPreview.d.ts +42 -0
  28. package/lib/types/client/ArkmeReactionPreview.d.ts.map +1 -0
  29. package/lib/types/client/ArkmeRichComposerInput.d.ts.map +1 -1
  30. package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -1
  31. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  32. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +2 -1
  33. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  34. package/lib/types/client/ReactionLabel.d.ts +8 -0
  35. package/lib/types/client/ReactionLabel.d.ts.map +1 -0
  36. package/lib/types/client/ReactionPhraseDialog.d.ts +14 -0
  37. package/lib/types/client/ReactionPhraseDialog.d.ts.map +1 -0
  38. package/lib/types/client/calendar-activity-model.d.ts +1 -1
  39. package/lib/types/client/calendar-activity-model.d.ts.map +1 -1
  40. package/lib/types/client/composer-draft-store.d.ts +6 -1
  41. package/lib/types/client/composer-draft-store.d.ts.map +1 -1
  42. package/lib/types/client/conversation-window-sync.d.ts.map +1 -1
  43. package/lib/types/client/harness-account-sessions.d.ts +6 -0
  44. package/lib/types/client/harness-account-sessions.d.ts.map +1 -1
  45. package/lib/types/client/harness-session-dropdown.d.ts.map +1 -1
  46. package/lib/types/client/harness-session-origin.d.ts +2 -1
  47. package/lib/types/client/harness-session-origin.d.ts.map +1 -1
  48. package/lib/types/client/harness-session-summary.d.ts +11 -2
  49. package/lib/types/client/harness-session-summary.d.ts.map +1 -1
  50. package/lib/types/client/outgoing-call-runtime.d.ts.map +1 -1
  51. package/lib/types/client/reaction-expression.d.ts +5 -0
  52. package/lib/types/client/reaction-expression.d.ts.map +1 -0
  53. package/lib/types/client/reaction-label-content.d.ts +20 -0
  54. package/lib/types/client/reaction-label-content.d.ts.map +1 -0
  55. package/lib/types/client/reaction-library.d.ts +26 -0
  56. package/lib/types/client/reaction-library.d.ts.map +1 -0
  57. package/lib/types/client/reaction-locate-layout.d.ts +5 -0
  58. package/lib/types/client/reaction-locate-layout.d.ts.map +1 -0
  59. package/lib/types/client/reaction-notifications.d.ts +49 -0
  60. package/lib/types/client/reaction-notifications.d.ts.map +1 -0
  61. package/lib/types/client/reaction-phrase-palette.d.ts +99 -0
  62. package/lib/types/client/reaction-phrase-palette.d.ts.map +1 -0
  63. package/lib/types/client/reaction-phrases.d.ts +7 -0
  64. package/lib/types/client/reaction-phrases.d.ts.map +1 -0
  65. package/lib/types/client/reaction-preview-store.d.ts +70 -0
  66. package/lib/types/client/reaction-preview-store.d.ts.map +1 -0
  67. package/lib/types/client/reaction-visible-target.d.ts +3 -0
  68. package/lib/types/client/reaction-visible-target.d.ts.map +1 -0
  69. package/lib/types/client/record-input-capture.d.ts +2 -0
  70. package/lib/types/client/record-input-capture.d.ts.map +1 -1
  71. package/lib/types/client/redesign/styles.d.ts.map +1 -1
  72. package/lib/types/client/ui-controller.d.ts +2 -1
  73. package/lib/types/client/ui-controller.d.ts.map +1 -1
  74. package/lib/types/client/use-phrase-layout-motion.d.ts +2 -0
  75. package/lib/types/client/use-phrase-layout-motion.d.ts.map +1 -0
  76. package/lib/types/client/use-reaction-history.d.ts +10 -0
  77. package/lib/types/client/use-reaction-history.d.ts.map +1 -0
  78. package/lib/types/client/use-reaction-phrase-drag.d.ts +19 -0
  79. package/lib/types/client/use-reaction-phrase-drag.d.ts.map +1 -0
  80. package/lib/types/host-api.d.ts.map +1 -1
  81. package/lib/types/reaction-contract.d.ts +217 -0
  82. package/lib/types/reaction-contract.d.ts.map +1 -0
  83. package/lib/types/sdk/index.d.ts +2 -0
  84. package/lib/types/sdk/index.d.ts.map +1 -1
  85. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  86. package/lib/types/services/chat-service.d.ts +6 -0
  87. package/lib/types/services/chat-service.d.ts.map +1 -1
  88. package/lib/types/services/profile-service.d.ts +1 -1
  89. package/lib/types/services/profile-service.d.ts.map +1 -1
  90. package/lib/types/services/reaction-service.d.ts +28 -0
  91. package/lib/types/services/reaction-service.d.ts.map +1 -0
  92. package/lib/types/services/recording-import-gateway.d.ts +3 -1
  93. package/lib/types/services/recording-import-gateway.d.ts.map +1 -1
  94. package/lib/types/services/recording-import-upload-retry.d.ts +6 -0
  95. package/lib/types/services/recording-import-upload-retry.d.ts.map +1 -0
  96. package/lib/types/services/world-service.d.ts +3 -0
  97. package/lib/types/services/world-service.d.ts.map +1 -1
  98. package/lib/types/tools/business/conversation/index.d.ts.map +1 -1
  99. package/lib/types/tools/business/conversation/reactions.d.ts +3 -0
  100. package/lib/types/tools/business/conversation/reactions.d.ts.map +1 -0
  101. package/lib/types/tools/business/index.d.ts.map +1 -1
  102. package/lib/types/tools/ports/conversations.d.ts +1 -0
  103. package/lib/types/tools/ports/conversations.d.ts.map +1 -1
  104. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  105. package/lib/types/types.d.ts +2 -1
  106. package/lib/types/types.d.ts.map +1 -1
  107. package/package.json +5 -5
@@ -1951,6 +1951,372 @@ var ServiceRuntime = class {
1951
1951
  }
1952
1952
  };
1953
1953
  //#endregion
1954
+ //#region src/services/reaction-service.ts
1955
+ function invalid$11() {
1956
+ throw new ArkmePluginError("reaction-response-invalid", "表态数据不完整,请重试", true);
1957
+ }
1958
+ function integer$4(value, min = 0) {
1959
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= min ? value : invalid$11();
1960
+ }
1961
+ function expression(raw) {
1962
+ const value = objectValue$2(raw);
1963
+ if (typeof value.text !== "string" || Array.from(value.text).length > 20) return invalid$11();
1964
+ const result = { text: value.text };
1965
+ for (const key of [
1966
+ "emoji",
1967
+ "hand",
1968
+ "color"
1969
+ ]) if (value[key] !== void 0) {
1970
+ if (typeof value[key] !== "string" || value[key].length > 64) return invalid$11();
1971
+ result[key] = value[key];
1972
+ }
1973
+ return result;
1974
+ }
1975
+ function array(raw, max) {
1976
+ return Array.isArray(raw) && raw.length <= max ? raw : invalid$11();
1977
+ }
1978
+ function key(raw) {
1979
+ return typeof raw === "string" && /^[a-f0-9]{64}$/.test(raw) ? raw : invalid$11();
1980
+ }
1981
+ function group(raw) {
1982
+ const value = objectValue$2(raw);
1983
+ return {
1984
+ key: key(value.key),
1985
+ expression: expression(value.expression),
1986
+ count: integer$4(value.count, 1),
1987
+ actorIds: array(value.actor_user_ids ?? [], 3).map((id) => integer$4(id, 1))
1988
+ };
1989
+ }
1990
+ function state(raw) {
1991
+ const value = objectValue$2(raw);
1992
+ return {
1993
+ revision: integer$4(value.revision),
1994
+ selections: array(value.selections, 56).map((raw) => {
1995
+ const item = objectValue$2(raw);
1996
+ return {
1997
+ key: key(item.key),
1998
+ expression: expression(item.expression),
1999
+ at: integer$4(item.at)
2000
+ };
2001
+ })
2002
+ };
2003
+ }
2004
+ function library(raw) {
2005
+ const value = objectValue$2(raw);
2006
+ return {
2007
+ revision: integer$4(value.revision),
2008
+ items: array(value.items, 58).map(expression)
2009
+ };
2010
+ }
2011
+ function boolean(raw) {
2012
+ return typeof raw === "boolean" ? raw : invalid$11();
2013
+ }
2014
+ function targetKey(target) {
2015
+ return JSON.stringify([
2016
+ target.chat_session_uid ?? "",
2017
+ target.rel_uid ?? "",
2018
+ target.record_uid ?? "",
2019
+ target.owned === true
2020
+ ]);
2021
+ }
2022
+ /** One Host owner for Browser, SDK and granted Tools; identities are resolved from signed message references. */
2023
+ var ReactionService = class {
2024
+ runtime;
2025
+ resolve;
2026
+ worldReference;
2027
+ chatSourceKey;
2028
+ historyContext;
2029
+ constructor(runtime, resolve, worldReference, chatSourceKey, historyContext) {
2030
+ this.runtime = runtime;
2031
+ this.resolve = resolve;
2032
+ this.worldReference = worldReference;
2033
+ this.chatSourceKey = chatSourceKey;
2034
+ this.historyContext = historyContext;
2035
+ }
2036
+ async request(input, signal) {
2037
+ const session = await this.runtime.requireSession();
2038
+ const accountKey = `${this.runtime.config.environment}:${session.userId}`;
2039
+ if (!input || input.accountKey !== accountKey) throw new ArkmePluginError("reaction-account-changed", "账号已变化,请重新打开表态", false);
2040
+ let path, body;
2041
+ const ids = /* @__PURE__ */ new Map();
2042
+ const resolve = async (target) => {
2043
+ if (!target || typeof target.id !== "string" || !target.id || target.id.length > 1024 || !(typeof target.worldRecordRef === "string" && !target.sourceRef && !target.messageActionRef || !target.worldRecordRef && typeof target.sourceRef === "string" && typeof target.messageActionRef === "string")) throw new ArkmePluginError("reaction-target-invalid", "表态目标无效", false);
2044
+ return await this.resolve(target);
2045
+ };
2046
+ switch (input.action) {
2047
+ case "notifications":
2048
+ path = "notifications/query";
2049
+ body = {
2050
+ after_id: input.after_id ?? "",
2051
+ limit: input.limit
2052
+ };
2053
+ break;
2054
+ case "notifications-read":
2055
+ if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > 50) return invalid$11();
2056
+ path = "notifications/read";
2057
+ body = { items: input.items.map((item) => ({
2058
+ id: key(item.id),
2059
+ revision: integer$4(item.revision, 1)
2060
+ })) };
2061
+ break;
2062
+ case "query": {
2063
+ if (!Array.isArray(input.targets) || input.targets.length < 1 || input.targets.length > 50) throw new ArkmePluginError("reaction-query-invalid", "表态查询数量无效", false);
2064
+ const targets = [], seenIDs = /* @__PURE__ */ new Set();
2065
+ for (const target of input.targets) {
2066
+ const wire = await resolve(target), key = targetKey(wire);
2067
+ if (seenIDs.has(target.id)) throw new ArkmePluginError("reaction-query-duplicate", "表态目标重复", false);
2068
+ seenIDs.add(target.id);
2069
+ const aliases = ids.get(key);
2070
+ if (aliases) aliases.push(target.id);
2071
+ else {
2072
+ ids.set(key, [target.id]);
2073
+ targets.push(wire);
2074
+ }
2075
+ }
2076
+ path = "query";
2077
+ body = { targets };
2078
+ break;
2079
+ }
2080
+ case "set":
2081
+ if (typeof input.active !== "boolean") throw new ArkmePluginError("reaction-state-invalid", "缺少表态状态", false);
2082
+ path = "set";
2083
+ body = {
2084
+ target: await resolve(input.target),
2085
+ expression: input.expression,
2086
+ active: input.active,
2087
+ expected_revision: input.expected_revision,
2088
+ request_id: input.request_id
2089
+ };
2090
+ break;
2091
+ case "actors":
2092
+ path = "actors/query";
2093
+ body = {
2094
+ target: await resolve(input.target),
2095
+ key: input.key,
2096
+ after_user_id: input.after_user_id,
2097
+ limit: input.limit
2098
+ };
2099
+ break;
2100
+ case "groups":
2101
+ path = "groups/query";
2102
+ body = {
2103
+ target: await resolve(input.target),
2104
+ after_key: input.after_key,
2105
+ limit: input.limit
2106
+ };
2107
+ break;
2108
+ case "history":
2109
+ path = "history/query";
2110
+ body = {
2111
+ start_at: input.start_at,
2112
+ end_at: input.end_at,
2113
+ limit: input.limit,
2114
+ ...input.before_at === void 0 ? {} : {
2115
+ before_at: input.before_at,
2116
+ before_id: input.before_id
2117
+ }
2118
+ };
2119
+ break;
2120
+ case "received":
2121
+ path = "received/query";
2122
+ body = {
2123
+ limit: input.limit,
2124
+ world_only: input.world_only,
2125
+ ...input.before_at === void 0 ? {} : {
2126
+ before_at: input.before_at,
2127
+ before_id: input.before_id
2128
+ }
2129
+ };
2130
+ break;
2131
+ case "history-policy-query":
2132
+ path = "history-policy/query";
2133
+ body = {};
2134
+ break;
2135
+ case "history-policy-set":
2136
+ path = "history-policy/set";
2137
+ body = {
2138
+ expected_revision: input.expected_revision,
2139
+ locked: input.locked
2140
+ };
2141
+ break;
2142
+ case "library-query":
2143
+ path = "library/query";
2144
+ body = {};
2145
+ break;
2146
+ case "library-set":
2147
+ path = "library/set";
2148
+ body = {
2149
+ items: input.items,
2150
+ expected_revision: input.expected_revision,
2151
+ request_id: input.request_id
2152
+ };
2153
+ break;
2154
+ default: throw new ArkmePluginError("reaction-operation-invalid", "不支持的表态操作", false);
2155
+ }
2156
+ const before = await this.runtime.requireSession();
2157
+ if (`${this.runtime.config.environment}:${before.userId}` !== accountKey) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
2158
+ const result = objectValue$2(await this.runtime.authenticatedPost(`/api/v1/reactions/${path}`, body, session, signal));
2159
+ const current = await this.runtime.requireSession();
2160
+ if (signal?.aborted || `${this.runtime.config.environment}:${current.userId}` !== accountKey) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
2161
+ if (input.action === "query") {
2162
+ if (!Array.isArray(result.items)) throw new ArkmePluginError("reaction-response-invalid", "表态数据不完整", true);
2163
+ const seen = /* @__PURE__ */ new Set();
2164
+ return { items: array(result.items, 50).flatMap((raw) => {
2165
+ const item = objectValue$2(raw), wireKey = targetKey(objectValue$2(item.target)), aliases = ids.get(wireKey);
2166
+ if (!aliases || seen.has(wireKey)) return invalid$11();
2167
+ seen.add(wireKey);
2168
+ const snapshot = {
2169
+ mine: state(item.mine),
2170
+ groups: array(item.groups, 100).map(group),
2171
+ has_more: boolean(item.has_more),
2172
+ actors_visible: boolean(item.actors_visible),
2173
+ private: boolean(item.private)
2174
+ };
2175
+ return aliases.map((id) => ({
2176
+ target_id: id,
2177
+ ...snapshot
2178
+ }));
2179
+ }) };
2180
+ }
2181
+ if (input.action === "notifications-read") return { ok: boolean(result.ok) };
2182
+ if (input.action === "notifications") {
2183
+ if (!this.chatSourceKey) return invalid$11();
2184
+ const items = await Promise.all(array(result.items, 50).map(async (raw) => {
2185
+ const item = objectValue$2(raw), target = objectValue$2(item.target);
2186
+ if (typeof target.chat_session_uid !== "string" || !target.chat_session_uid || typeof item.record_uid !== "string" || !item.record_uid) return invalid$11();
2187
+ return {
2188
+ id: key(item.id),
2189
+ revision: integer$4(item.revision, 1),
2190
+ actorUserId: integer$4(item.actor_user_id, 1),
2191
+ sourceKey: await this.chatSourceKey(session.userId, target.chat_session_uid),
2192
+ itemUid: item.record_uid,
2193
+ recordOwnerUserId: integer$4(item.record_owner_user_id, 1),
2194
+ sendAtMillis: integer$4(item.attach_at),
2195
+ text: typeof item.text === "string" ? item.text.slice(0, 320) : "",
2196
+ selections: array(item.selections, 56).map((raw) => {
2197
+ const selected = objectValue$2(raw);
2198
+ return {
2199
+ key: key(selected.key),
2200
+ expression: expression(selected.expression),
2201
+ at: integer$4(selected.at)
2202
+ };
2203
+ })
2204
+ };
2205
+ }));
2206
+ if (signal?.aborted || `${this.runtime.config.environment}:${(await this.runtime.requireSession()).userId}` !== accountKey) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
2207
+ return {
2208
+ items,
2209
+ after_id: result.after_id === "" ? "" : key(result.after_id),
2210
+ has_more: boolean(result.has_more)
2211
+ };
2212
+ }
2213
+ if (input.action === "library-query") return library(result);
2214
+ if (input.action === "history-policy-query") return {
2215
+ revision: integer$4(result.revision),
2216
+ locked: boolean(result.locked)
2217
+ };
2218
+ if (input.action === "history-policy-set") {
2219
+ if (![
2220
+ "updated",
2221
+ "unchanged",
2222
+ "revision_conflict"
2223
+ ].includes(String(result.outcome))) return invalid$11();
2224
+ const policy = objectValue$2(result.policy);
2225
+ return {
2226
+ outcome: result.outcome,
2227
+ policy: {
2228
+ revision: integer$4(policy.revision),
2229
+ locked: boolean(policy.locked)
2230
+ }
2231
+ };
2232
+ }
2233
+ if (input.action === "library-set") {
2234
+ if (![
2235
+ "updated",
2236
+ "idempotent",
2237
+ "revision_conflict"
2238
+ ].includes(String(result.outcome))) return invalid$11();
2239
+ return {
2240
+ outcome: result.outcome,
2241
+ library: library(result.library)
2242
+ };
2243
+ }
2244
+ if (input.action === "set") {
2245
+ if (![
2246
+ "updated",
2247
+ "unchanged",
2248
+ "idempotent",
2249
+ "revision_conflict"
2250
+ ].includes(String(result.outcome))) return invalid$11();
2251
+ return {
2252
+ outcome: result.outcome,
2253
+ state: state(result.state)
2254
+ };
2255
+ }
2256
+ if (input.action === "actors") return {
2257
+ user_ids: array(result.user_ids, 100).map((id) => integer$4(id, 1)),
2258
+ has_more: boolean(result.has_more)
2259
+ };
2260
+ if (input.action === "groups") return {
2261
+ items: array(result.items, 100).map(group),
2262
+ has_more: boolean(result.has_more)
2263
+ };
2264
+ if (input.action === "history" || input.action === "received") {
2265
+ const rows = array(result.items, 100).map(objectValue$2);
2266
+ const contextRows = rows.filter((item) => item.restricted === false);
2267
+ const contexts = input.action === "history" && this.historyContext ? await this.historyContext(contextRows.map((item) => {
2268
+ const original = objectValue$2(item.original_message);
2269
+ return {
2270
+ sessionUID: String(objectValue$2(item.target).chat_session_uid ?? ""),
2271
+ senderID: integer$4(item.sender_user_id ?? 0),
2272
+ sourceKind: String(item.source_kind ?? ""),
2273
+ ...typeof original.record_uid === "string" && original.record_uid ? { message: {
2274
+ recordUID: original.record_uid,
2275
+ ownerUserID: integer$4(original.owner_user_id, 1),
2276
+ sendAtMillis: integer$4(original.send_at, 1)
2277
+ } } : {}
2278
+ };
2279
+ }), signal) : [];
2280
+ if (signal?.aborted || `${this.runtime.config.environment}:${(await this.runtime.requireSession()).userId}` !== accountKey) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
2281
+ const contextByRow = new Map(contextRows.map((row, index) => [row, contexts[index]]));
2282
+ return {
2283
+ items: await Promise.all(rows.map(async (item) => {
2284
+ const target = objectValue$2(item.target);
2285
+ if (typeof target.chat_session_uid !== "string" || typeof target.rel_uid !== "string") return invalid$11();
2286
+ const targetID = createHash("sha256").update(JSON.stringify([session.userId, targetKey(target)])).digest("hex");
2287
+ const received = input.action === "received" ? {
2288
+ actor_user_id: integer$4(item.actor_user_id, 1),
2289
+ private: boolean(item.private),
2290
+ ...input.world_only && typeof target.record_uid === "string" && this.worldReference ? { target: {
2291
+ id: targetID,
2292
+ worldRecordRef: await this.worldReference(target.record_uid)
2293
+ } } : {}
2294
+ } : {};
2295
+ const restricted = boolean(item.restricted);
2296
+ return {
2297
+ ...received,
2298
+ ...!restricted ? contextByRow.get(item) : {},
2299
+ event_uid: key(item.event_uid),
2300
+ target_id: targetID,
2301
+ expression: expression(item.expression),
2302
+ active: boolean(item.active),
2303
+ at: integer$4(item.at),
2304
+ restricted,
2305
+ ...!restricted && typeof item.source_kind === "string" ? { source_kind: item.source_kind } : {},
2306
+ ...!restricted && typeof item.text === "string" ? { text: item.text.slice(0, 320) } : {}
2307
+ };
2308
+ })),
2309
+ has_more: boolean(result.has_more),
2310
+ ...result.has_more ? {
2311
+ before_at: integer$4(result.before_at),
2312
+ before_id: key(result.before_id)
2313
+ } : {}
2314
+ };
2315
+ }
2316
+ return invalid$11();
2317
+ }
2318
+ };
2319
+ //#endregion
1954
2320
  //#region src/managed-ai/image-preparer.ts
1955
2321
  const MAXIMUM_DECODED_IMAGE_PIXELS = 4e7;
1956
2322
  const MAXIMUM_SOURCE_IMAGE_BYTES = 64 << 20;
@@ -44298,7 +44664,6 @@ var ArkmeChatRealtimeRuntime = class {
44298
44664
  //#region src/services/chat-realtime-service.ts
44299
44665
  const MAX_PROJECTION_RETRIES = 5;
44300
44666
  const MAX_ATTENTION_SUMMARY_RETRY_DELAY_MS = 3e4;
44301
- const MAX_NATIVE_NOTIFICATION_DELIVERY_CONCURRENCY = 3;
44302
44667
  const MAX_NOTIFICATION_HINTS_PER_TIMELINE_READ = 50;
44303
44668
  const NOTIFICATION_EXPIRY_MILLIS = 5 * 6e4;
44304
44669
  const CHAT_NOTIFICATION_DIAGNOSTICS = process.env.ARKME_CHAT_NOTIFICATION_DIAGNOSTICS === "1";
@@ -44410,6 +44775,7 @@ var ChatRealtimeService = class {
44410
44775
  }
44411
44776
  dispose() {
44412
44777
  this.disposed = true;
44778
+ this.attentionOwnerGeneration += 1;
44413
44779
  this.attentionController.abort();
44414
44780
  if (this.projectionTimer !== void 0) clearTimeout(this.projectionTimer);
44415
44781
  if (this.connectionBaselineRetryTimer !== void 0) clearTimeout(this.connectionBaselineRetryTimer);
@@ -45018,7 +45384,7 @@ var ChatRealtimeService = class {
45018
45384
  accountUserId: notificationHint?.accountUserId ?? current?.accountUserId,
45019
45385
  accountOwnerGeneration: notificationHint?.accountOwnerGeneration ?? current?.accountOwnerGeneration
45020
45386
  });
45021
- this.scheduleProjectionFlush(200);
45387
+ this.scheduleProjectionFlush(0);
45022
45388
  }
45023
45389
  scheduleProjectionFlush(delayMillis) {
45024
45390
  if (this.projectionInFlight || this.disposed) return;
@@ -45037,7 +45403,7 @@ var ChatRealtimeService = class {
45037
45403
  const now = Date.now();
45038
45404
  let delay = Number.POSITIVE_INFINITY;
45039
45405
  for (const projection of this.pendingChatProjections.values()) {
45040
- if (projection.refreshSource !== false) delay = Math.min(delay, 200);
45406
+ if (projection.refreshSource !== false) delay = 0;
45041
45407
  for (const candidate of projection.notificationHints) {
45042
45408
  const dueAtMillis = candidate.nextAttemptAtMillis ?? now + notificationRetryDelay(candidate.attempts);
45043
45409
  delay = Math.min(delay, Math.max(0, dueAtMillis - now));
@@ -45175,26 +45541,29 @@ var ChatRealtimeService = class {
45175
45541
  if (pending.length === 0) return [];
45176
45542
  for (const [uid, projection] of pending) if (projection.refreshSource === false && this.source.cachedChatSourceByKey(`${String(session.userId)}:${uid}`) === void 0) projection.refreshSource = true;
45177
45543
  const sourcePending = pending.filter(([, projection]) => projection.refreshSource !== false);
45178
- const bundles = /* @__PURE__ */ new Map();
45179
- if (sourcePending.length > 0) {
45180
- const sessionUids = sourcePending.map(([uid]) => uid).sort();
45181
- const projectionBatchKey = sessionUids.join("|");
45182
- const displayData = await this.runtime.authenticatedChatPost("/api/v1/chats/display-snapshots", { chat_session_uids: sessionUids }, session, void 0, {
45183
- lane: sourcePending.some(([, projection]) => projection.notificationHints.length > 0) ? "interactive-read" : "background-read",
45184
- key: `projection:display:${projectionBatchKey}`
45185
- });
45186
- for (const raw of listValue$18(displayData.items)) {
45187
- const bundle = objectValue$2(raw);
45188
- const uid = stringValue$2(objectValue$2(bundle.session).chat_session_uid).trim();
45189
- if (uid !== "") bundles.set(uid, bundle);
45544
+ const displayReady = (async () => {
45545
+ const bundles = /* @__PURE__ */ new Map();
45546
+ if (sourcePending.length > 0) {
45547
+ const sessionUids = sourcePending.map(([uid]) => uid).sort();
45548
+ const projectionBatchKey = sessionUids.join("|");
45549
+ const displayData = await this.runtime.authenticatedChatPost("/api/v1/chats/display-snapshots", { chat_session_uids: sessionUids }, session, void 0, {
45550
+ lane: sourcePending.some(([, projection]) => projection.notificationHints.length > 0) ? "interactive-read" : "background-read",
45551
+ key: `projection:display:${projectionBatchKey}`
45552
+ });
45553
+ for (const raw of listValue$18(displayData.items)) {
45554
+ const bundle = objectValue$2(raw);
45555
+ const uid = stringValue$2(objectValue$2(bundle.session).chat_session_uid).trim();
45556
+ if (uid !== "") bundles.set(uid, bundle);
45557
+ }
45190
45558
  }
45191
- }
45192
- const tailItemsByUid = /* @__PURE__ */ new Map();
45193
- const failedUids = /* @__PURE__ */ new Set();
45194
- const timelinePending = pending.filter(([, projection]) => projection.refreshSource !== false || projection.notificationHints.some((candidate) => candidate.resolvedNotification === void 0));
45195
- for (let offset = 0; offset < timelinePending.length; offset += 3) {
45196
- const chunk = timelinePending.slice(offset, offset + 3);
45197
- (await Promise.allSettled(chunk.map(async ([uid, projection]) => {
45559
+ return bundles;
45560
+ })().catch(() => void 0);
45561
+ const failed = [];
45562
+ const notificationLanes = Array.from({ length: 3 }, () => Promise.resolve());
45563
+ let nextNotificationLane = 0;
45564
+ const project = async ([uid, projection]) => {
45565
+ let timelineProjection;
45566
+ if (projection.refreshSource !== false || projection.notificationHints.some((candidate) => candidate.resolvedNotification === void 0)) {
45198
45567
  const cached = this.source.cachedChatSourceByKey(`${String(session.userId)}:${uid}`);
45199
45568
  const unresolvedHints = projection.notificationHints.filter((candidate) => candidate.resolvedNotification === void 0);
45200
45569
  const firstHintSequence = unresolvedHints.reduce((minimum, item) => Math.min(minimum, item.hint.latestSequence), projection.latestSequence);
@@ -45209,32 +45578,24 @@ var ChatRealtimeService = class {
45209
45578
  lane: projection.notificationHints.length > 0 ? "interactive-read" : "background-read",
45210
45579
  key: `projection:tail:${uid}:${String(afterSequence)}:${String(notificationAttempt)}`
45211
45580
  });
45212
- const bundle = bundles.get(uid);
45581
+ const bundles = await displayReady;
45582
+ if (bundles === void 0 && projection.refreshSource !== false) throw new Error("Chat display projection failed");
45583
+ const bundle = bundles?.get(uid);
45213
45584
  const sessionKind = numberValue$21(objectValue$2(bundle).session_kind ?? objectValue$2(objectValue$2(bundle).session).session_kind);
45214
45585
  const sourceKind = sessionKind === 2 ? "group_chat" : sessionKind === 1 || sessionKind === 3 ? "private_chat" : cached?.kind === "group_chat" || cached?.kind === "private_chat" ? cached.kind : void 0;
45215
- return [uid, {
45586
+ timelineProjection = {
45216
45587
  items: await this.projectionReader.chatTimelineItems(data, session, uid, sourceKind, bundle),
45217
45588
  notificationIdentities: timelineNotificationIdentities(data)
45218
- }];
45219
- }))).forEach((result, index) => {
45220
- const uid = chunk[index]?.[0];
45221
- if (uid === void 0) return;
45222
- if (result.status === "fulfilled") tailItemsByUid.set(result.value[0], result.value[1]);
45223
- else failedUids.add(uid);
45224
- });
45225
- }
45226
- if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45227
- const updates = [];
45228
- const sidebarNotificationHints = [];
45229
- const notifications = [];
45230
- for (const [uid, projection] of pending) {
45231
- const bundle = bundles.get(uid);
45232
- if (projection.refreshSource !== false && bundle === void 0 || failedUids.has(uid)) {
45233
- failedUids.add(uid);
45234
- continue;
45589
+ };
45235
45590
  }
45591
+ if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45592
+ const bundles = await displayReady;
45593
+ const updates = [];
45594
+ const sidebarNotificationHints = [];
45595
+ const notifications = [];
45596
+ const bundle = bundles?.get(uid);
45597
+ if (projection.refreshSource !== false && bundle === void 0) return [[uid, projection]];
45236
45598
  const cacheKey = `${String(session.userId)}:${uid}`;
45237
- const timelineProjection = tailItemsByUid.get(uid);
45238
45599
  const timelineItems = timelineProjection?.items ?? [];
45239
45600
  try {
45240
45601
  const cachedSource = this.source.cachedChatSourceByKey(cacheKey);
@@ -45244,8 +45605,7 @@ var ChatRealtimeService = class {
45244
45605
  ...projection,
45245
45606
  refreshSource: true
45246
45607
  });
45247
- failedUids.add(uid);
45248
- continue;
45608
+ return [[uid, projection]];
45249
45609
  }
45250
45610
  if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45251
45611
  if (projection.refreshSource !== false) this.source.setChatSourceByKey(cacheKey, source);
@@ -45335,117 +45695,137 @@ var ChatRealtimeService = class {
45335
45695
  });
45336
45696
  }
45337
45697
  } catch {
45338
- failedUids.add(uid);
45698
+ return [[uid, projection]];
45339
45699
  }
45340
- }
45341
- if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45342
- if (updates.length > 0) {
45343
- this.source.invalidateSourceListCache(session.userId, "root");
45344
- this.runtime.invalidateCalendarDates(this.runtime.requestScope(session.userId), updates.flatMap((update) => [update.source.activeAtMillis, ...update.timelineItems.map((item) => item.sendAtMillis)]));
45345
- this.emitChatClientEvent({
45346
- type: "sessions-delta",
45347
- revision: this.nextChatClientRevision(),
45348
- updates
45349
- });
45350
- const emittedAtMillis = Date.now();
45351
- for (const { candidate } of sidebarNotificationHints) notificationDiagnostic("notification_sidebar_delta_emitted", {
45352
- eventUid: candidate.hint.eventUid,
45353
- connectionGeneration: candidate.connectionGeneration,
45354
- attempt: candidate.attempts,
45355
- emittedAtMillis,
45356
- ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, emittedAtMillis - candidate.receivedAtMillis) }
45357
- });
45358
- this.refreshAttentionSummary();
45359
- }
45360
- const deliveries = [];
45361
- for (let offset = 0; offset < notifications.length; offset += MAX_NATIVE_NOTIFICATION_DELIVERY_CONCURRENCY) {
45362
- const chunk = notifications.slice(offset, offset + MAX_NATIVE_NOTIFICATION_DELIVERY_CONCURRENCY);
45363
- deliveries.push(...await Promise.all(chunk.map(async (entry) => {
45364
- const { notification, candidate } = entry;
45365
- if (ownerGeneration !== this.attentionOwnerGeneration) return {
45366
- entry,
45367
- fallbackToBrowser: false,
45368
- outcome: "exception"
45369
- };
45370
- const dispatchStartedAtMillis = Date.now();
45371
- try {
45372
- const outcome = await this.nativeAttention.showNotification({
45373
- idempotencyKey: notification.eventUid,
45374
- kind: "chat.message",
45375
- occurredAtMillis: notification.eventAtMillis,
45376
- expiresAtMillis: notification.eventAtMillis + NOTIFICATION_EXPIRY_MILLIS,
45377
- presentation: {
45378
- title: notification.title,
45379
- body: notification.body
45380
- },
45381
- activation: {
45382
- kind: "chat-source",
45383
- sourceRef: notification.sourceRef,
45384
- sourceKey: notification.sourceKey
45700
+ if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45701
+ if (updates.length > 0) {
45702
+ this.source.invalidateSourceListCache(session.userId, "root");
45703
+ this.runtime.invalidateCalendarDates(this.runtime.requestScope(session.userId), updates.flatMap((update) => [update.source.activeAtMillis, ...update.timelineItems.map((item) => item.sendAtMillis)]));
45704
+ this.emitChatClientEvent({
45705
+ type: "sessions-delta",
45706
+ revision: this.nextChatClientRevision(),
45707
+ updates
45708
+ });
45709
+ const emittedAtMillis = Date.now();
45710
+ for (const { candidate } of sidebarNotificationHints) notificationDiagnostic("notification_sidebar_delta_emitted", {
45711
+ eventUid: candidate.hint.eventUid,
45712
+ connectionGeneration: candidate.connectionGeneration,
45713
+ attempt: candidate.attempts,
45714
+ emittedAtMillis,
45715
+ ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, emittedAtMillis - candidate.receivedAtMillis) }
45716
+ });
45717
+ this.refreshAttentionSummary();
45718
+ }
45719
+ if (notifications.length > 0) {
45720
+ const lane = nextNotificationLane++ % notificationLanes.length;
45721
+ notificationLanes[lane] = notificationLanes[lane].then(async () => {
45722
+ const deliveries = [];
45723
+ for (let offset = 0; offset < notifications.length; offset += 1) {
45724
+ const chunk = notifications.slice(offset, offset + 1);
45725
+ deliveries.push(...await Promise.all(chunk.map(async (entry) => {
45726
+ const { notification, candidate } = entry;
45727
+ if (ownerGeneration !== this.attentionOwnerGeneration) return {
45728
+ entry,
45729
+ fallbackToBrowser: false,
45730
+ outcome: "exception"
45731
+ };
45732
+ const dispatchStartedAtMillis = Date.now();
45733
+ try {
45734
+ const outcome = await this.nativeAttention.showNotification({
45735
+ idempotencyKey: notification.eventUid,
45736
+ kind: "chat.message",
45737
+ occurredAtMillis: notification.eventAtMillis,
45738
+ expiresAtMillis: notification.eventAtMillis + NOTIFICATION_EXPIRY_MILLIS,
45739
+ presentation: {
45740
+ title: notification.title,
45741
+ body: notification.body
45742
+ },
45743
+ activation: {
45744
+ kind: "chat-source",
45745
+ sourceRef: notification.sourceRef,
45746
+ sourceKey: notification.sourceKey
45747
+ }
45748
+ });
45749
+ const completedAtMillis = Date.now();
45750
+ notificationDiagnostic("notification_native_dispatch_completed", {
45751
+ eventUid: notification.eventUid,
45752
+ connectionGeneration: candidate.connectionGeneration,
45753
+ attempt: candidate.attempts,
45754
+ outcome: outcome.outcome,
45755
+ completedAtMillis,
45756
+ dispatchDurationMillis: Math.max(0, completedAtMillis - dispatchStartedAtMillis),
45757
+ ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, completedAtMillis - candidate.receivedAtMillis) }
45758
+ });
45759
+ return {
45760
+ entry,
45761
+ fallbackToBrowser: outcome.fallbackToBrowser,
45762
+ outcome: outcome.outcome
45763
+ };
45764
+ } catch {
45765
+ const completedAtMillis = Date.now();
45766
+ notificationDiagnostic("notification_native_dispatch_completed", {
45767
+ eventUid: notification.eventUid,
45768
+ connectionGeneration: candidate.connectionGeneration,
45769
+ attempt: candidate.attempts,
45770
+ outcome: "exception",
45771
+ completedAtMillis,
45772
+ dispatchDurationMillis: Math.max(0, completedAtMillis - dispatchStartedAtMillis),
45773
+ ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, completedAtMillis - candidate.receivedAtMillis) }
45774
+ });
45775
+ return {
45776
+ entry,
45777
+ fallbackToBrowser: false,
45778
+ outcome: "exception"
45779
+ };
45780
+ }
45781
+ })));
45782
+ }
45783
+ if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45784
+ for (const { entry, fallbackToBrowser, outcome } of deliveries) {
45785
+ const { notification, candidate, uid, latestSequence } = entry;
45786
+ if (outcome === "native-failed" || outcome === "rate-limited" || outcome === "exception") {
45787
+ if (!notificationExpired(candidate)) {
45788
+ const attempts = candidate.attempts + 1;
45789
+ this.mergePendingChatProjection(uid, {
45790
+ latestSequence,
45791
+ notificationHints: [{
45792
+ ...candidate,
45793
+ attempts,
45794
+ nextAttemptAtMillis: Date.now() + notificationRetryDelay(attempts)
45795
+ }],
45796
+ refreshSource: false,
45797
+ accountUserId: session.userId,
45798
+ accountOwnerGeneration: ownerGeneration
45799
+ });
45800
+ }
45801
+ continue;
45385
45802
  }
45386
- });
45387
- const completedAtMillis = Date.now();
45388
- notificationDiagnostic("notification_native_dispatch_completed", {
45389
- eventUid: notification.eventUid,
45390
- connectionGeneration: candidate.connectionGeneration,
45391
- attempt: candidate.attempts,
45392
- outcome: outcome.outcome,
45393
- completedAtMillis,
45394
- dispatchDurationMillis: Math.max(0, completedAtMillis - dispatchStartedAtMillis),
45395
- ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, completedAtMillis - candidate.receivedAtMillis) }
45396
- });
45397
- return {
45398
- entry,
45399
- fallbackToBrowser: outcome.fallbackToBrowser,
45400
- outcome: outcome.outcome
45401
- };
45803
+ if (!fallbackToBrowser) continue;
45804
+ this.emitChatClientEvent({
45805
+ type: "message-notification",
45806
+ revision: this.nextChatClientRevision(),
45807
+ notification
45808
+ });
45809
+ }
45810
+ }).then(() => void 0).catch(() => {
45811
+ failed.push([uid, projection]);
45812
+ });
45813
+ }
45814
+ return [];
45815
+ };
45816
+ let next = 0;
45817
+ await Promise.all(Array.from({ length: Math.min(3, pending.length) }, async () => {
45818
+ while (next < pending.length && ownerGeneration === this.attentionOwnerGeneration) {
45819
+ const entry = pending[next++];
45820
+ try {
45821
+ failed.push(...await project(entry));
45402
45822
  } catch {
45403
- const completedAtMillis = Date.now();
45404
- notificationDiagnostic("notification_native_dispatch_completed", {
45405
- eventUid: notification.eventUid,
45406
- connectionGeneration: candidate.connectionGeneration,
45407
- attempt: candidate.attempts,
45408
- outcome: "exception",
45409
- completedAtMillis,
45410
- dispatchDurationMillis: Math.max(0, completedAtMillis - dispatchStartedAtMillis),
45411
- ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, completedAtMillis - candidate.receivedAtMillis) }
45412
- });
45413
- return {
45414
- entry,
45415
- fallbackToBrowser: false,
45416
- outcome: "exception"
45417
- };
45418
- }
45419
- })));
45420
- }
45421
- if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45422
- for (const { entry, fallbackToBrowser, outcome } of deliveries) {
45423
- const { notification, candidate, uid, latestSequence } = entry;
45424
- if (outcome === "native-failed" || outcome === "rate-limited" || outcome === "exception") {
45425
- if (!notificationExpired(candidate)) {
45426
- const attempts = candidate.attempts + 1;
45427
- this.mergePendingChatProjection(uid, {
45428
- latestSequence,
45429
- notificationHints: [{
45430
- ...candidate,
45431
- attempts,
45432
- nextAttemptAtMillis: Date.now() + notificationRetryDelay(attempts)
45433
- }],
45434
- refreshSource: false,
45435
- accountUserId: session.userId,
45436
- accountOwnerGeneration: ownerGeneration
45437
- });
45823
+ failed.push(entry);
45438
45824
  }
45439
- continue;
45440
45825
  }
45441
- if (!fallbackToBrowser) continue;
45442
- this.emitChatClientEvent({
45443
- type: "message-notification",
45444
- revision: this.nextChatClientRevision(),
45445
- notification
45446
- });
45447
- }
45448
- return pending.filter(([uid]) => failedUids.has(uid));
45826
+ }));
45827
+ await Promise.all(notificationLanes);
45828
+ return failed;
45449
45829
  }
45450
45830
  emitChatClientEvent(event) {
45451
45831
  for (const listener of [...this.chatClientListeners]) listener(event);
@@ -49428,12 +49808,13 @@ var ChatService = class {
49428
49808
  if (!options.signal?.aborted && cacheEpoch === (this.runtime.memberCacheEpoch?.() ?? 0)) await this.runtime.stateStore.mergeConversationMembers?.(session.userId, source.ownerRef, result).catch(() => void 0);
49429
49809
  return result;
49430
49810
  }
49431
- async memberRead(session, group, path, body, signal) {
49811
+ async memberRead(session, group, path, body, signal, cacheMs) {
49432
49812
  try {
49433
49813
  return await this.runtime.authenticatedChatPost(path, body, session, signal, {
49434
49814
  lane: path.endsWith("/members/page") ? "interactive-read" : "background-read",
49435
49815
  key: `members:${this.runtime.memberCacheEpoch?.() ?? 0}:${path}:${JSON.stringify(body)}`,
49436
- failureCooldownMs: 2e3
49816
+ failureCooldownMs: 2e3,
49817
+ ...cacheMs === void 0 ? {} : { cacheMs }
49437
49818
  });
49438
49819
  } catch (error) {
49439
49820
  if (invalidatesMemberSnapshot(error)) {
@@ -50555,6 +50936,49 @@ var ChatService = class {
50555
50936
  recordOwnerUserId: reference.recordOwnerUserId
50556
50937
  };
50557
50938
  }
50939
+ async reactionTarget(sourceRef, messageActionRef) {
50940
+ const session = await this.runtime.requireSession();
50941
+ const source = await this.source.openSourceRef(sourceRef.trim(), session.userId);
50942
+ const reference = await this.openMessageActionRef(messageActionRef, session.userId, source);
50943
+ if (reference.sourceKind === "record") return {
50944
+ record_uid: reference.recordUid,
50945
+ owned: true
50946
+ };
50947
+ return {
50948
+ chat_session_uid: reference.chatSessionUid,
50949
+ rel_uid: reference.relationUid
50950
+ };
50951
+ }
50952
+ /** Keep viewer-owned remarks separate from group nicknames and public profiles. */
50953
+ async reactionActorLabels(sourceRef, userIds, signal) {
50954
+ const session = await this.runtime.requireSession();
50955
+ const source = await this.source.openSourceRef(sourceRef.trim(), session.userId);
50956
+ const names = /* @__PURE__ */ new Map();
50957
+ if (source.kind !== "group_chat" && source.kind !== "private_chat") return names;
50958
+ const ids = [...new Set(userIds)].sort((left, right) => left - right);
50959
+ for (let offset = 0; offset < ids.length; offset += 50) {
50960
+ signal?.throwIfAborted();
50961
+ const batch = ids.slice(offset, offset + 50);
50962
+ const data = await this.memberRead(session, source.ownerRef, "/api/v1/chats/members/by-user-ids", {
50963
+ chat_session_uid: source.ownerRef,
50964
+ user_ids: batch,
50965
+ active_only: true,
50966
+ include_stats: false
50967
+ }, signal, 5e3);
50968
+ const items = listValue$16(data.items).map(objectValue$2);
50969
+ if (stringValue$2(data.chat_session_uid) !== source.ownerRef || !Array.isArray(data.items) || items.length > batch.length || items.some((item) => !batch.includes(numberValue$19(item.user_id)) || numberValue$19(item.status) !== 1)) throw new ArkmePluginError("member-presentation-invalid-response", "成员资料响应无效", true, 502);
50970
+ for (const item of items) {
50971
+ const userId = numberValue$19(item.user_id);
50972
+ const groupNickname = source.kind === "group_chat" ? firstUsableChatMemberName([item.display_name_snapshot], userId) : "";
50973
+ const remark = userId === session.userId ? "" : normalizedJoinDisplayName(item.remark);
50974
+ names.set(userId, {
50975
+ remark,
50976
+ groupNickname
50977
+ });
50978
+ }
50979
+ }
50980
+ return names;
50981
+ }
50558
50982
  async sourceMessageExtensionContext(sourceRef, messageActionRef, options = {}) {
50559
50983
  const session = await this.runtime.requireSession();
50560
50984
  const source = await this.source.openSourceRef(sourceRef.trim(), session.userId);
@@ -87358,7 +87782,7 @@ var ProfileService = class {
87358
87782
  async userCard(userId, signal) {
87359
87783
  const session = await this.runtime.requireSession();
87360
87784
  if (!Number.isSafeInteger(userId) || userId <= 0) throw new ArkmePluginError("user-card-target-invalid", "用户信息参数无效", false);
87361
- const profile = (await this.publicProfileSummariesByUserIds([userId], session, signal)).get(userId);
87785
+ const profile = (await this.publicProfileSummariesByUserIds([userId], session, signal, 0)).get(userId);
87362
87786
  return {
87363
87787
  displayName: profile?.displayName ?? "群成员",
87364
87788
  ...profile?.avatarUrl === void 0 ? {} : { avatarRef: await this.sealProfileImageRef(session.userId, userId) }
@@ -87508,7 +87932,7 @@ var ProfileService = class {
87508
87932
  const avatarRef = profile.avatarRef.trim();
87509
87933
  return /^https?:\/\//i.test(avatarRef) || fileAssetAvatarRef(avatarRef) !== void 0;
87510
87934
  }
87511
- async publicProfileSummariesByUserIds(userIds, session, signal) {
87935
+ async publicProfileSummariesByUserIds(userIds, session, signal, maxAgeMillis = PUBLIC_PROFILE_CACHE_TTL_MS) {
87512
87936
  const normalized = [...new Set(userIds.filter((userId) => Number.isSafeInteger(userId) && userId > 0))].sort((left, right) => left - right);
87513
87937
  const profiles = /* @__PURE__ */ new Map();
87514
87938
  const missing = [];
@@ -87516,7 +87940,7 @@ var ProfileService = class {
87516
87940
  for (const [key, cached] of this.publicProfileCache) if (cached.expiresAtMillis <= now) this.publicProfileCache.delete(key);
87517
87941
  for (const userId of normalized) {
87518
87942
  const cached = this.publicProfileCache.get(`${String(session.userId)}:${String(userId)}`);
87519
- if (cached === void 0 || cached.expiresAtMillis <= now) {
87943
+ if (cached === void 0 || cached.expiresAtMillis <= now || cached.expiresAtMillis - (cached.value === null ? PUBLIC_PROFILE_NEGATIVE_CACHE_TTL_MS : PUBLIC_PROFILE_CACHE_TTL_MS) + maxAgeMillis <= now) {
87520
87944
  missing.push(userId);
87521
87945
  continue;
87522
87946
  }
@@ -90219,6 +90643,68 @@ var RecordingService = class {
90219
90643
  }
90220
90644
  };
90221
90645
  //#endregion
90646
+ //#region src/services/recording-import-upload-retry.ts
90647
+ const RECORDING_UPLOAD_RETRY_DELAYS = [
90648
+ 1e3,
90649
+ 2e3,
90650
+ 4e3,
90651
+ 8e3,
90652
+ 16e3
90653
+ ];
90654
+ const RECORDING_UPLOAD_REQUEST_TIMEOUT = 300 * 1e3;
90655
+ const transientStatuses = /* @__PURE__ */ new Set([
90656
+ 408,
90657
+ 429,
90658
+ 500,
90659
+ 502,
90660
+ 503,
90661
+ 504
90662
+ ]);
90663
+ const transientNames = /* @__PURE__ */ new Set([
90664
+ "ResponseTimeoutError",
90665
+ "ConnectionTimeoutError",
90666
+ "SocketAssignTimeoutError"
90667
+ ]);
90668
+ const transientCodes = /* @__PURE__ */ new Set([
90669
+ "ECONNRESET",
90670
+ "ECONNREFUSED",
90671
+ "ECONNABORTED",
90672
+ "ETIMEDOUT",
90673
+ "EPIPE",
90674
+ "EAI_AGAIN",
90675
+ "ENOTFOUND",
90676
+ "ENETUNREACH",
90677
+ "EHOSTUNREACH"
90678
+ ]);
90679
+ function isRetryableRecordingUploadError(error) {
90680
+ if (error === null || typeof error !== "object" || error instanceof RecordingImportContractError) return false;
90681
+ const { status, name, code } = error;
90682
+ if (name === "cancel" || name === "abort" || name === "AbortError") return false;
90683
+ if (typeof status === "number" && status >= 400) return transientStatuses.has(status);
90684
+ if (typeof code === "string" && code !== "" && transientCodes.has(code)) return true;
90685
+ if ((status === -1 || status === -2) && code === name && typeof name === "string" && (transientNames.has(name) || name === "RequestError" || name === "ResponseError")) return true;
90686
+ if (typeof code === "string" && code !== "") return false;
90687
+ return typeof name === "string" && transientNames.has(name);
90688
+ }
90689
+ function throwIfRecordingUploadAborted(signal) {
90690
+ if (signal?.aborted === true) throw new RecordingImportContractError("recording-import-cancelled", "录音导入已取消");
90691
+ }
90692
+ function waitForRecordingUploadRetry(delay, signal) {
90693
+ return new Promise((resolve, reject) => {
90694
+ const abort = () => {
90695
+ clearTimeout(timer);
90696
+ signal?.removeEventListener("abort", abort);
90697
+ reject(new RecordingImportContractError("recording-import-cancelled", "录音导入已取消"));
90698
+ };
90699
+ const timer = setTimeout(() => {
90700
+ signal?.removeEventListener("abort", abort);
90701
+ resolve();
90702
+ }, Math.round(delay * (1 + Math.random() * .3)));
90703
+ signal?.addEventListener("abort", abort, { once: true });
90704
+ if (signal?.aborted === true) abort();
90705
+ });
90706
+ }
90707
+ //#endregion
90222
90708
  //#region src/services/recording-import-gateway.ts
90223
90709
  const AUDIO_SESSION_RECOVERY_LIMIT = 500;
90224
90710
  const AUDIO_IMPORT_DUPLICATE_BATCH_SIZE = 100;
@@ -90572,6 +91058,8 @@ var AudioRecordingImportGateway = class {
90572
91058
  accessKeyId: credentials.accessKeyId,
90573
91059
  accessKeySecret: credentials.accessKeySecret,
90574
91060
  stsToken: credentials.stsToken,
91061
+ timeout: RECORDING_UPLOAD_REQUEST_TIMEOUT,
91062
+ retryMax: 0,
90575
91063
  refreshSTSTokenInterval: 300 * 1e3,
90576
91064
  refreshSTSToken: async () => {
90577
91065
  await this.requireJobSession(job);
@@ -90602,17 +91090,40 @@ var AudioRecordingImportGateway = class {
90602
91090
  signal?.addEventListener("abort", abort, { once: true });
90603
91091
  try {
90604
91092
  const objectPath = `pc_upload/${String(job.userId)}/${job.sessionId}/${remoteFileName(job)}`;
90605
- const upload = client.multipartUpload(objectPath, job.sourceHandle, {
90606
- parallel: 1,
90607
- partSize: 5 * 1024 * 1024,
90608
- checkpoint: job.uploadCheckpoint,
90609
- mime: job.mimeType,
90610
- progress: async (percentage, checkpoint) => {
90611
- if (signal?.aborted === true) throw new RecordingImportContractError("recording-import-cancelled", "录音导入已取消");
90612
- const bounded = Math.min(1, Math.max(0, percentage));
90613
- await onProgress(Math.round(job.fileSize * bounded), checkpoint);
91093
+ let latestCheckpoint = job.uploadCheckpoint;
91094
+ const upload = (async () => {
91095
+ for (let attempt = 0;; attempt += 1) {
91096
+ throwIfRecordingUploadAborted(signal);
91097
+ if (attempt > 0) await this.requireJobSession(job);
91098
+ throwIfRecordingUploadAborted(signal);
91099
+ let progressFailed = false;
91100
+ try {
91101
+ await client.multipartUpload(objectPath, job.sourceHandle, {
91102
+ parallel: 1,
91103
+ partSize: 5 * 1024 * 1024,
91104
+ checkpoint: latestCheckpoint,
91105
+ mime: job.mimeType,
91106
+ progress: async (percentage, checkpoint) => {
91107
+ try {
91108
+ if (signal?.aborted === true) throw new RecordingImportContractError("recording-import-cancelled", "录音导入已取消");
91109
+ const bounded = Math.min(1, Math.max(0, percentage));
91110
+ await onProgress(Math.round(job.fileSize * bounded), checkpoint);
91111
+ if (checkpoint !== void 0) latestCheckpoint = checkpoint;
91112
+ } catch (error) {
91113
+ progressFailed = true;
91114
+ throw error;
91115
+ }
91116
+ }
91117
+ });
91118
+ return;
91119
+ } catch (error) {
91120
+ const delay = RECORDING_UPLOAD_RETRY_DELAYS[attempt];
91121
+ if (signal?.aborted === true || progressFailed || !isRetryableRecordingUploadError(error)) throw error;
91122
+ if (delay === void 0) throw new ArkmePluginError("recording-import-upload-failed", error instanceof Error ? error.message : "录音上传失败", true, 503, { cause: error });
91123
+ await waitForRecordingUploadRetry(delay, signal);
91124
+ }
90614
91125
  }
90615
- });
91126
+ })();
90616
91127
  await (signal === void 0 ? upload : Promise.race([upload, aborted]));
90617
91128
  } finally {
90618
91129
  signal?.removeEventListener("abort", abort);
@@ -94504,6 +95015,15 @@ var WorldService = class {
94504
95015
  media;
94505
95016
  record;
94506
95017
  viewerLabels;
95018
+ /** Called only for a Record-owner-authorized received-reaction response. */
95019
+ async reactionReference(recordUid) {
95020
+ const session = await this.runtime.requireSession();
95021
+ return this.worldRecordRef(session.userId, recordUid, { ownerUserId: session.userId });
95022
+ }
95023
+ async reactionTarget(recordRef) {
95024
+ const session = await this.runtime.requireSession();
95025
+ return { record_uid: this.openWorldRecordRef(recordRef, session.userId).recordUid };
95026
+ }
94507
95027
  worldImageRefs = /* @__PURE__ */ new Map();
94508
95028
  worldAvatarResolutionCache = /* @__PURE__ */ new Map();
94509
95029
  extensionPublicationShareCache = /* @__PURE__ */ new Map();
@@ -96050,6 +96570,7 @@ var ArkmeService = class {
96050
96570
  messageReport: true,
96051
96571
  userBanManagement: true,
96052
96572
  directMessageAdmission: true,
96573
+ reactionsV1: true,
96053
96574
  groupOwnerGovernance: true,
96054
96575
  ...this.config.markdownQuickNotesEnabled === true ? { markdownQuickNotes: true } : {},
96055
96576
  ...this.config.markdownLongArticlesEnabled === true ? { markdownLongArticles: true } : {},
@@ -96841,6 +97362,112 @@ var ArkmeService = class {
96841
97362
  async favoriteStickers(signal) {
96842
97363
  return await this.chat.favoriteStickers(signal);
96843
97364
  }
97365
+ async reactions(input, signal) {
97366
+ const result = await new ReactionService(this.runtime, (target) => target.worldRecordRef !== void 0 ? this.world.reactionTarget(target.worldRecordRef) : this.chat.reactionTarget(target.sourceRef, target.messageActionRef), (uid) => this.world.reactionReference(uid), (viewer, uid) => this.source.chatDirectorySourceKey(viewer, uid), async (items, signal) => {
97367
+ const session = await this.runtime.requireSession();
97368
+ const [sources, profiles] = await Promise.all([this.source.chatSourcesBySessionUids([...new Set(items.map((item) => item.sessionUID).filter(Boolean))], signal), this.profile.publicProfileSummariesByUserIds([...new Set(items.map((item) => item.senderID).filter((id) => id > 0))], session, signal, 5e3)]);
97369
+ const missingAvatars = [...sources].filter(([, source]) => !source.avatarRef && !source.avatarRefs?.length && !source.groupAvatar);
97370
+ if (missingAvatars.length > 0) try {
97371
+ const hydrated = await this.source.hydrateDirectoryPage(missingAvatars.map(([, source]) => source), signal ?? new AbortController().signal);
97372
+ missingAvatars.forEach(([uid], index) => {
97373
+ if (hydrated[index]) sources.set(uid, hydrated[index]);
97374
+ });
97375
+ } catch {
97376
+ signal?.throwIfAborted();
97377
+ }
97378
+ const selfSource = items.some((item) => item.sourceKind === "send_to_self" && item.message) ? {
97379
+ sourceRef: await this.source.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
97380
+ kind: "send_to_self",
97381
+ displayName: "发给自己",
97382
+ activeAtMillis: 0,
97383
+ unreadCount: 0
97384
+ } : void 0;
97385
+ return items.map((item) => {
97386
+ const source = item.sourceKind === "send_to_self" ? selfSource : sources.get(item.sessionUID);
97387
+ return {
97388
+ sourceName: item.sourceKind === "send_to_self" ? "发给自己" : source?.displayName,
97389
+ authorName: item.senderID === session.userId ? "我" : profiles.get(item.senderID)?.displayName,
97390
+ ...source && item.message ? { originalMessage: {
97391
+ source,
97392
+ itemUid: item.message.recordUID,
97393
+ recordOwnerUserId: item.message.ownerUserID,
97394
+ sendAtMillis: item.message.sendAtMillis
97395
+ } } : {},
97396
+ ...source ? { avatar: {
97397
+ ...source.avatarRef ? { avatarRef: source.avatarRef } : {},
97398
+ ...source.avatarRefs ? { avatarRefs: source.avatarRefs } : {},
97399
+ ...source.groupAvatar ? { groupAvatar: source.groupAvatar } : {}
97400
+ } } : {}
97401
+ };
97402
+ });
97403
+ }).request(input, signal);
97404
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
97405
+ const actorLabels = async (sourceRef, ids) => {
97406
+ if (!sourceRef || !ids.length) return /* @__PURE__ */ new Map();
97407
+ try {
97408
+ return await this.chat.reactionActorLabels(sourceRef, ids, signal);
97409
+ } catch (error) {
97410
+ if (signal?.aborted) throw error;
97411
+ return /* @__PURE__ */ new Map();
97412
+ }
97413
+ };
97414
+ const actorAvatars = async (viewer, profiles) => new Map(await Promise.all([...profiles].filter(([, profile]) => profile.avatarUrl).map(async ([id]) => [id, await this.profile.sealProfileImageRef(viewer, id)])));
97415
+ const presentActor = (userId, profileName, label, avatarRef) => ({
97416
+ userId,
97417
+ displayName: label?.remark || profileName?.trim() || "用户",
97418
+ ...label?.groupNickname ? { groupNickname: label.groupNickname } : {},
97419
+ ...avatarRef ? { avatarRef } : {}
97420
+ });
97421
+ if (input.action === "query" || input.action === "groups") {
97422
+ const page = result;
97423
+ const groupPage = result;
97424
+ const groups = input.action === "query" ? page.items.flatMap((item) => item.actors_visible ? item.groups : []) : groupPage.items;
97425
+ const ids = [...new Set(groups.flatMap((group) => group.actorIds))];
97426
+ const session = await this.runtime.requireSession();
97427
+ const sourceByTarget = new Map(input.action === "query" ? input.targets.map((target) => [target.id, target.sourceRef]) : []);
97428
+ const sourceIds = /* @__PURE__ */ new Map();
97429
+ const include = (sourceRef, actorIds) => {
97430
+ if (!sourceRef || !actorIds.length) return;
97431
+ const users = sourceIds.get(sourceRef) ?? /* @__PURE__ */ new Set();
97432
+ actorIds.forEach((id) => users.add(id));
97433
+ sourceIds.set(sourceRef, users);
97434
+ };
97435
+ if (input.action === "query") {
97436
+ for (const item of page.items) if (item.actors_visible) include(sourceByTarget.get(item.target_id), item.groups.flatMap((group) => group.actorIds));
97437
+ } else include(input.target.sourceRef, ids);
97438
+ const [profiles, labels] = await Promise.all([this.profile.publicProfileSummariesByUserIds(ids, session, signal, 5e3), (async () => {
97439
+ const labels = /* @__PURE__ */ new Map();
97440
+ for (const [sourceRef, users] of sourceIds) labels.set(sourceRef, await actorLabels(sourceRef, [...users]));
97441
+ return labels;
97442
+ })()]);
97443
+ const avatars = await actorAvatars(session.userId, profiles);
97444
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
97445
+ const present = ({ actorIds, ...group }, sourceRef, visible = true) => ({
97446
+ ...group,
97447
+ actors: visible ? actorIds.map((userId) => presentActor(userId, profiles.get(userId)?.displayName, labels.get(sourceRef ?? "")?.get(userId), avatars.get(userId))) : []
97448
+ });
97449
+ return input.action === "query" ? {
97450
+ ...page,
97451
+ items: page.items.map((item) => ({
97452
+ ...item,
97453
+ groups: item.groups.map((group) => present(group, sourceByTarget.get(item.target_id), item.actors_visible))
97454
+ }))
97455
+ } : {
97456
+ ...groupPage,
97457
+ items: groupPage.items.map((group) => present(group, input.target.sourceRef))
97458
+ };
97459
+ }
97460
+ if (input.action !== "actors") return result;
97461
+ const page = result;
97462
+ const session = await this.runtime.requireSession();
97463
+ const [profiles, labels] = await Promise.all([this.profile.publicProfileSummariesByUserIds(page.user_ids, session, signal, 5e3), actorLabels(input.target.sourceRef, page.user_ids)]);
97464
+ const avatars = await actorAvatars(session.userId, profiles);
97465
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
97466
+ return {
97467
+ items: page.user_ids.map((userId) => presentActor(userId, profiles.get(userId)?.displayName, labels.get(userId), avatars.get(userId))),
97468
+ has_more: page.has_more
97469
+ };
97470
+ }
96844
97471
  async addFavoriteSticker(item, signal) {
96845
97472
  return await this.chat.addFavoriteSticker(item, signal);
96846
97473
  }