@senguoyun/dsh-arkme 0.1.78 → 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 (143) 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 +15141 -10588
  4. package/lib/client.js.map +1 -1
  5. package/lib/harness-model-client.js +19 -3
  6. package/lib/harness-native-selection-client.js +19 -3
  7. package/lib/harness-sidebar-client.js +165 -16
  8. package/lib/index.js +112 -1
  9. package/lib/persistent-extension.js +1 -1
  10. package/lib/{persistent-runtime-aiHgSAmj.js → persistent-runtime-CGclHTN1.js} +1196 -272
  11. package/lib/sdk.js +8 -0
  12. package/lib/types/arkme-service.d.ts +10 -0
  13. package/lib/types/arkme-service.d.ts.map +1 -1
  14. package/lib/types/client/ArkmeAccountCancellation.d.ts +8 -0
  15. package/lib/types/client/ArkmeAccountCancellation.d.ts.map +1 -0
  16. package/lib/types/client/ArkmeDataManagementSettings.d.ts.map +1 -1
  17. package/lib/types/client/ArkmeDayTimeline.d.ts +2 -0
  18. package/lib/types/client/ArkmeDayTimeline.d.ts.map +1 -1
  19. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  20. package/lib/types/client/ArkmeLogin.d.ts +3 -0
  21. package/lib/types/client/ArkmeLogin.d.ts.map +1 -1
  22. package/lib/types/client/ArkmePersonalDayCalendar.d.ts.map +1 -1
  23. package/lib/types/client/ArkmeReactionActorCard.d.ts +7 -0
  24. package/lib/types/client/ArkmeReactionActorCard.d.ts.map +1 -0
  25. package/lib/types/client/ArkmeReactionHistorySettings.d.ts +7 -0
  26. package/lib/types/client/ArkmeReactionHistorySettings.d.ts.map +1 -0
  27. package/lib/types/client/ArkmeReactionNotification.d.ts +11 -0
  28. package/lib/types/client/ArkmeReactionNotification.d.ts.map +1 -0
  29. package/lib/types/client/ArkmeReactionPhrases.d.ts +8 -0
  30. package/lib/types/client/ArkmeReactionPhrases.d.ts.map +1 -0
  31. package/lib/types/client/ArkmeReactionPreview.d.ts +42 -0
  32. package/lib/types/client/ArkmeReactionPreview.d.ts.map +1 -0
  33. package/lib/types/client/ArkmeRecordingSurface.d.ts +2 -1
  34. package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
  35. package/lib/types/client/ArkmeRichComposerInput.d.ts.map +1 -1
  36. package/lib/types/client/ArkmeSearchSurface.d.ts.map +1 -1
  37. package/lib/types/client/ArkmeSettingsSurface.d.ts +5 -0
  38. package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -1
  39. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  40. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +2 -1
  41. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  42. package/lib/types/client/ReactionLabel.d.ts +8 -0
  43. package/lib/types/client/ReactionLabel.d.ts.map +1 -0
  44. package/lib/types/client/ReactionPhraseDialog.d.ts +14 -0
  45. package/lib/types/client/ReactionPhraseDialog.d.ts.map +1 -0
  46. package/lib/types/client/arkme-auth-flow.d.ts.map +1 -1
  47. package/lib/types/client/arkme-login-locales.d.ts +6 -0
  48. package/lib/types/client/arkme-login-locales.d.ts.map +1 -1
  49. package/lib/types/client/auth-store.d.ts.map +1 -1
  50. package/lib/types/client/calendar-activity-model.d.ts +1 -1
  51. package/lib/types/client/calendar-activity-model.d.ts.map +1 -1
  52. package/lib/types/client/composer-draft-store.d.ts +6 -1
  53. package/lib/types/client/composer-draft-store.d.ts.map +1 -1
  54. package/lib/types/client/conversation-window-sync.d.ts.map +1 -1
  55. package/lib/types/client/harness-account-sessions.d.ts +6 -0
  56. package/lib/types/client/harness-account-sessions.d.ts.map +1 -1
  57. package/lib/types/client/harness-session-dropdown.d.ts.map +1 -1
  58. package/lib/types/client/harness-session-origin.d.ts +2 -1
  59. package/lib/types/client/harness-session-origin.d.ts.map +1 -1
  60. package/lib/types/client/harness-session-summary.d.ts +11 -2
  61. package/lib/types/client/harness-session-summary.d.ts.map +1 -1
  62. package/lib/types/client/locales/actions-en.d.ts.map +1 -1
  63. package/lib/types/client/outgoing-call-runtime.d.ts.map +1 -1
  64. package/lib/types/client/reaction-expression.d.ts +5 -0
  65. package/lib/types/client/reaction-expression.d.ts.map +1 -0
  66. package/lib/types/client/reaction-label-content.d.ts +20 -0
  67. package/lib/types/client/reaction-label-content.d.ts.map +1 -0
  68. package/lib/types/client/reaction-library.d.ts +26 -0
  69. package/lib/types/client/reaction-library.d.ts.map +1 -0
  70. package/lib/types/client/reaction-locate-layout.d.ts +5 -0
  71. package/lib/types/client/reaction-locate-layout.d.ts.map +1 -0
  72. package/lib/types/client/reaction-notifications.d.ts +49 -0
  73. package/lib/types/client/reaction-notifications.d.ts.map +1 -0
  74. package/lib/types/client/reaction-phrase-palette.d.ts +99 -0
  75. package/lib/types/client/reaction-phrase-palette.d.ts.map +1 -0
  76. package/lib/types/client/reaction-phrases.d.ts +7 -0
  77. package/lib/types/client/reaction-phrases.d.ts.map +1 -0
  78. package/lib/types/client/reaction-preview-store.d.ts +70 -0
  79. package/lib/types/client/reaction-preview-store.d.ts.map +1 -0
  80. package/lib/types/client/reaction-visible-target.d.ts +3 -0
  81. package/lib/types/client/reaction-visible-target.d.ts.map +1 -0
  82. package/lib/types/client/record-input-capture.d.ts +2 -0
  83. package/lib/types/client/record-input-capture.d.ts.map +1 -1
  84. package/lib/types/client/recordings/RecordingSearchRow.d.ts +5 -0
  85. package/lib/types/client/recordings/RecordingSearchRow.d.ts.map +1 -0
  86. package/lib/types/client/recordings/recording-search-target.d.ts +4 -0
  87. package/lib/types/client/recordings/recording-search-target.d.ts.map +1 -0
  88. package/lib/types/client/redesign/styles.d.ts.map +1 -1
  89. package/lib/types/client/ui-controller.d.ts +5 -6
  90. package/lib/types/client/ui-controller.d.ts.map +1 -1
  91. package/lib/types/client/use-phrase-layout-motion.d.ts +2 -0
  92. package/lib/types/client/use-phrase-layout-motion.d.ts.map +1 -0
  93. package/lib/types/client/use-reaction-history.d.ts +10 -0
  94. package/lib/types/client/use-reaction-history.d.ts.map +1 -0
  95. package/lib/types/client/use-reaction-phrase-drag.d.ts +19 -0
  96. package/lib/types/client/use-reaction-phrase-drag.d.ts.map +1 -0
  97. package/lib/types/host-api.d.ts.map +1 -1
  98. package/lib/types/index.d.ts +1 -1
  99. package/lib/types/index.d.ts.map +1 -1
  100. package/lib/types/local-database.d.ts +2 -0
  101. package/lib/types/local-database.d.ts.map +1 -1
  102. package/lib/types/reaction-contract.d.ts +217 -0
  103. package/lib/types/reaction-contract.d.ts.map +1 -0
  104. package/lib/types/recording-presentation.d.ts +1 -0
  105. package/lib/types/recording-presentation.d.ts.map +1 -1
  106. package/lib/types/recording-search-result.d.ts +3 -0
  107. package/lib/types/recording-search-result.d.ts.map +1 -0
  108. package/lib/types/recording-search-version.d.ts +17 -0
  109. package/lib/types/recording-search-version.d.ts.map +1 -0
  110. package/lib/types/sdk/index.d.ts +2 -0
  111. package/lib/types/sdk/index.d.ts.map +1 -1
  112. package/lib/types/services/auth-service.d.ts +20 -1
  113. package/lib/types/services/auth-service.d.ts.map +1 -1
  114. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  115. package/lib/types/services/chat-service.d.ts +6 -0
  116. package/lib/types/services/chat-service.d.ts.map +1 -1
  117. package/lib/types/services/profile-service.d.ts +1 -1
  118. package/lib/types/services/profile-service.d.ts.map +1 -1
  119. package/lib/types/services/reaction-service.d.ts +28 -0
  120. package/lib/types/services/reaction-service.d.ts.map +1 -0
  121. package/lib/types/services/recording-import-gateway.d.ts +3 -1
  122. package/lib/types/services/recording-import-gateway.d.ts.map +1 -1
  123. package/lib/types/services/recording-import-upload-retry.d.ts +6 -0
  124. package/lib/types/services/recording-import-upload-retry.d.ts.map +1 -0
  125. package/lib/types/services/recording-service.d.ts.map +1 -1
  126. package/lib/types/services/search-service.d.ts +3 -1
  127. package/lib/types/services/search-service.d.ts.map +1 -1
  128. package/lib/types/services/service.d.ts +3 -0
  129. package/lib/types/services/service.d.ts.map +1 -1
  130. package/lib/types/services/world-service.d.ts +3 -0
  131. package/lib/types/services/world-service.d.ts.map +1 -1
  132. package/lib/types/state-store.d.ts +7 -0
  133. package/lib/types/state-store.d.ts.map +1 -1
  134. package/lib/types/tools/business/conversation/index.d.ts.map +1 -1
  135. package/lib/types/tools/business/conversation/reactions.d.ts +3 -0
  136. package/lib/types/tools/business/conversation/reactions.d.ts.map +1 -0
  137. package/lib/types/tools/business/index.d.ts.map +1 -1
  138. package/lib/types/tools/ports/conversations.d.ts +1 -0
  139. package/lib/types/tools/ports/conversations.d.ts.map +1 -1
  140. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  141. package/lib/types/types.d.ts +46 -2
  142. package/lib/types/types.d.ts.map +1 -1
  143. package/package.json +5 -5
@@ -1296,6 +1296,9 @@ var ServiceRuntime = class {
1296
1296
  async deleteSession() {
1297
1297
  await this.accountSessions.delete();
1298
1298
  }
1299
+ async deleteSessionIfCurrent(expected) {
1300
+ return await this.accountSessions.deleteIfCurrent(expected);
1301
+ }
1299
1302
  async moveSessionToPendingBinding(expected) {
1300
1303
  return await this.accountSessions.deleteIfCurrent(expected, async (current) => {
1301
1304
  await this.writePendingBindingSession(current);
@@ -1948,6 +1951,372 @@ var ServiceRuntime = class {
1948
1951
  }
1949
1952
  };
1950
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
1951
2320
  //#region src/managed-ai/image-preparer.ts
1952
2321
  const MAXIMUM_DECODED_IMAGE_PIXELS = 4e7;
1953
2322
  const MAXIMUM_SOURCE_IMAGE_BYTES = 64 << 20;
@@ -3863,7 +4232,7 @@ var ArkmeDesktopScreenshot = class {
3863
4232
  };
3864
4233
  //#endregion
3865
4234
  //#region src/services/record-edit-history-service.ts
3866
- function invalid$9() {
4235
+ function invalid$10() {
3867
4236
  throw new ArkmePluginError("record-edit-history-invalid", "编辑记录暂不可用,请刷新后重试", true, 502);
3868
4237
  }
3869
4238
  var RecordEditHistoryService = class {
@@ -3874,9 +4243,9 @@ var RecordEditHistoryService = class {
3874
4243
  this.projector = projector;
3875
4244
  }
3876
4245
  async page(target, cursorEditAt = 0, signal) {
3877
- if (!Number.isSafeInteger(cursorEditAt) || cursorEditAt < 0 || target.recordUid.trim() === "") invalid$9();
4246
+ if (!Number.isSafeInteger(cursorEditAt) || cursorEditAt < 0 || target.recordUid.trim() === "") invalid$10();
3878
4247
  const session = await this.runtime.requireSession();
3879
- if (session.userId !== target.viewerUserId) invalid$9();
4248
+ if (session.userId !== target.viewerUserId) invalid$10();
3880
4249
  const window = {
3881
4250
  limit: 50,
3882
4251
  ...cursorEditAt === 0 ? {} : { cursor_edit_at: cursorEditAt }
@@ -3889,22 +4258,22 @@ var RecordEditHistoryService = class {
3889
4258
  rel_uid: target.relationUid,
3890
4259
  ...window
3891
4260
  }, session, signal));
3892
- if (signal?.aborted || (await this.runtime.requireSession()).userId !== session.userId) invalid$9();
3893
- if (!Array.isArray(data.items) || typeof data.has_more !== "boolean") invalid$9();
4261
+ if (signal?.aborted || (await this.runtime.requireSession()).userId !== session.userId) invalid$10();
4262
+ if (!Array.isArray(data.items) || typeof data.has_more !== "boolean") invalid$10();
3894
4263
  const next = data.next_cursor_edit_at;
3895
- if (data.has_more && (typeof next !== "number" || !Number.isSafeInteger(next) || next <= 0 || cursorEditAt > 0 && next >= cursorEditAt)) invalid$9();
4264
+ if (data.has_more && (typeof next !== "number" || !Number.isSafeInteger(next) || next <= 0 || cursorEditAt > 0 && next >= cursorEditAt)) invalid$10();
3896
4265
  const seen = /* @__PURE__ */ new Set();
3897
4266
  const snapshots = data.items.flatMap((raw) => {
3898
4267
  const revision = objectValue$2(raw);
3899
- if (revision.record_uid !== target.recordUid || typeof revision.revision_uid !== "string" || revision.revision_uid.trim() === "") invalid$9();
4268
+ if (revision.record_uid !== target.recordUid || typeof revision.revision_uid !== "string" || revision.revision_uid.trim() === "") invalid$10();
3900
4269
  if (revision.revision_type !== 1 && revision.revision_type !== 5) return [];
3901
- if (typeof revision.edit_at !== "number" || !Number.isSafeInteger(revision.edit_at) || revision.edit_at <= 0 || !Number.isFinite(new Date(revision.edit_at).getTime()) || typeof revision.text_content !== "string") invalid$9();
4270
+ if (typeof revision.edit_at !== "number" || !Number.isSafeInteger(revision.edit_at) || revision.edit_at <= 0 || !Number.isFinite(new Date(revision.edit_at).getTime()) || typeof revision.text_content !== "string") invalid$10();
3902
4271
  if (seen.has(revision.revision_uid)) return [];
3903
4272
  seen.add(revision.revision_uid);
3904
4273
  return [revision];
3905
4274
  });
3906
4275
  const contents = snapshots.length === 0 ? [] : await this.projector.projectPage(snapshots, target, signal);
3907
- if (signal?.aborted || (await this.runtime.requireSession()).userId !== session.userId || contents.length !== snapshots.length) invalid$9();
4276
+ if (signal?.aborted || (await this.runtime.requireSession()).userId !== session.userId || contents.length !== snapshots.length) invalid$10();
3908
4277
  const items = snapshots.map((revision, index) => ({
3909
4278
  revisionUid: revision.revision_uid,
3910
4279
  kind: revision.revision_type === 5 ? "original" : "manual",
@@ -22457,7 +22826,7 @@ function arkmeArticleImageReference(value) {
22457
22826
  }
22458
22827
  //#endregion
22459
22828
  //#region src/services/archive-service.ts
22460
- const invalid$8 = () => new ArkmePluginError("archive-contract-invalid", "归档数据无效,请刷新后重试", false, 502);
22829
+ const invalid$9 = () => new ArkmePluginError("archive-contract-invalid", "归档数据无效,请刷新后重试", false, 502);
22461
22830
  /** One Host owner for UI, Tools and SDK. Raw topic identities stay here. */
22462
22831
  var ArchiveService = class {
22463
22832
  runtime;
@@ -22481,17 +22850,17 @@ var ArchiveService = class {
22481
22850
  limit: 50,
22482
22851
  ...cursor === void 0 ? {} : { cursor }
22483
22852
  }, session, readSignal, { lane: "interactive-read" });
22484
- if (!Array.isArray(data.items) || typeof data.has_more !== "boolean") throw invalid$8();
22853
+ if (!Array.isArray(data.items) || typeof data.has_more !== "boolean") throw invalid$9();
22485
22854
  const items = [];
22486
22855
  for (const value of data.items) {
22487
22856
  const raw = objectValue$2(value);
22488
- if (typeof raw.title !== "string" || typeof raw.privacy_locked !== "boolean") throw invalid$8();
22857
+ if (typeof raw.title !== "string" || typeof raw.privacy_locked !== "boolean") throw invalid$9();
22489
22858
  const title = raw.privacy_locked ? "隐私主题" : raw.title;
22490
22859
  const state = await this.state(session, raw, title);
22491
- if (!state.ownerAvailable || !state.effectiveArchived) throw invalid$8();
22860
+ if (!state.ownerAvailable || !state.effectiveArchived) throw invalid$9();
22492
22861
  const inherited = raw.inherited_from_summary === void 0 ? void 0 : objectValue$2(raw.inherited_from_summary);
22493
- if (state.inheritedFrom !== void 0 !== (inherited !== void 0)) throw invalid$8();
22494
- if (inherited !== void 0 && (typeof inherited.title !== "string" || typeof inherited.privacy_locked !== "boolean")) throw invalid$8();
22862
+ if (state.inheritedFrom !== void 0 !== (inherited !== void 0)) throw invalid$9();
22863
+ if (inherited !== void 0 && (typeof inherited.title !== "string" || typeof inherited.privacy_locked !== "boolean")) throw invalid$9();
22495
22864
  items.push({
22496
22865
  ...state,
22497
22866
  privacyLocked: raw.privacy_locked,
@@ -22510,7 +22879,7 @@ var ArchiveService = class {
22510
22879
  }
22511
22880
  });
22512
22881
  }
22513
- if (data.has_more && (typeof data.next_cursor !== "string" || data.next_cursor === "" || data.next_cursor === cursor)) throw invalid$8();
22882
+ if (data.has_more && (typeof data.next_cursor !== "string" || data.next_cursor === "" || data.next_cursor === cursor)) throw invalid$9();
22514
22883
  await this.assertSession(session);
22515
22884
  return {
22516
22885
  items,
@@ -22520,7 +22889,7 @@ var ArchiveService = class {
22520
22889
  }, signal);
22521
22890
  }
22522
22891
  async states(sourceRefs, signal) {
22523
- if (sourceRefs.length === 0 || sourceRefs.length > 200 || new Set(sourceRefs).size !== sourceRefs.length) throw invalid$8();
22892
+ if (sourceRefs.length === 0 || sourceRefs.length > 200 || new Set(sourceRefs).size !== sourceRefs.length) throw invalid$9();
22524
22893
  return this.runtime.runOwnerRead("archives", {
22525
22894
  operation: "states",
22526
22895
  sourceRefs
@@ -22533,12 +22902,12 @@ var ArchiveService = class {
22533
22902
  entity_type: 1,
22534
22903
  entity_uid: ref.ownerRef
22535
22904
  })) }, session, readSignal, { lane: "interactive-read" });
22536
- if (!Array.isArray(data.items) || data.items.length !== refs.length) throw invalid$8();
22905
+ if (!Array.isArray(data.items) || data.items.length !== refs.length) throw invalid$9();
22537
22906
  const byUID = new Map(data.items.map((value) => {
22538
22907
  const raw = objectValue$2(value);
22539
22908
  return [raw.entity_uid, raw];
22540
22909
  }));
22541
- if (byUID.size !== refs.length) throw invalid$8();
22910
+ if (byUID.size !== refs.length) throw invalid$9();
22542
22911
  const states = await Promise.all(refs.map(async (ref, i) => ({
22543
22912
  ...await this.state(session, byUID.get(ref.ownerRef), ""),
22544
22913
  sourceRef: sourceRefs[i]
@@ -22548,7 +22917,7 @@ var ArchiveService = class {
22548
22917
  }, signal);
22549
22918
  }
22550
22919
  async set(input, signal) {
22551
- if (typeof input.selfArchived !== "boolean" || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) throw invalid$8();
22920
+ if (typeof input.selfArchived !== "boolean" || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) throw invalid$9();
22552
22921
  const session = await this.runtime.requireSession();
22553
22922
  const ref = await this.sources.openSourceRef(input.sourceRef, session.userId);
22554
22923
  if (ref.kind !== "topic") throw new ArkmePluginError("archive-target-invalid", "仅支持个人主题归档", false, 400);
@@ -22562,7 +22931,7 @@ var ArchiveService = class {
22562
22931
  self_status: input.selfArchived ? 2 : 1,
22563
22932
  expected_revision: input.expectedRevision
22564
22933
  }, session, signal, { trackWriteOutcome: true });
22565
- if (data.entity_uid !== ref.ownerRef || data.self_status !== (input.selfArchived ? 2 : 1) || typeof data.state_changed !== "boolean" || !Number.isSafeInteger(data.effective_changed_count) || Number(data.effective_changed_count) < 0) throw invalid$8();
22934
+ if (data.entity_uid !== ref.ownerRef || data.self_status !== (input.selfArchived ? 2 : 1) || typeof data.state_changed !== "boolean" || !Number.isSafeInteger(data.effective_changed_count) || Number(data.effective_changed_count) < 0) throw invalid$9();
22566
22935
  const state = await this.state(session, data, "");
22567
22936
  await this.assertSession(session);
22568
22937
  return {
@@ -22578,9 +22947,9 @@ var ArchiveService = class {
22578
22947
  }
22579
22948
  async state(session, value, title) {
22580
22949
  const raw = objectValue$2(value);
22581
- if (raw.entity_type !== 1 || typeof raw.entity_uid !== "string" || raw.entity_uid.trim() === "" || raw.self_status !== 1 && raw.self_status !== 2 || raw.effective_status !== 1 && raw.effective_status !== 2 || typeof raw.owner_available !== "boolean" || !Number.isSafeInteger(raw.revision) || Number(raw.revision) < 0 || !Number.isSafeInteger(raw.display_archive_at)) throw invalid$8();
22950
+ if (raw.entity_type !== 1 || typeof raw.entity_uid !== "string" || raw.entity_uid.trim() === "" || raw.self_status !== 1 && raw.self_status !== 2 || raw.effective_status !== 1 && raw.effective_status !== 2 || typeof raw.owner_available !== "boolean" || !Number.isSafeInteger(raw.revision) || Number(raw.revision) < 0 || !Number.isSafeInteger(raw.display_archive_at)) throw invalid$9();
22582
22951
  const parent = raw.inherited_from === void 0 ? void 0 : objectValue$2(raw.inherited_from);
22583
- if (parent !== void 0 && (parent.entity_type !== 1 || typeof parent.entity_uid !== "string" || parent.entity_uid === "")) throw invalid$8();
22952
+ if (parent !== void 0 && (parent.entity_type !== 1 || typeof parent.entity_uid !== "string" || parent.entity_uid === "")) throw invalid$9();
22584
22953
  return {
22585
22954
  entityType: "topic",
22586
22955
  sourceRef: await this.sources.sealSourceRef(session.userId, "topic", raw.entity_uid, title),
@@ -22599,23 +22968,23 @@ var ArchiveService = class {
22599
22968
  //#endregion
22600
22969
  //#region src/record-deletion-ref.ts
22601
22970
  const prefix$1 = "arkme-record-delete-v1";
22602
- const invalid$7 = () => new ArkmePluginError("record-delete-reference-invalid", "删除引用已失效,请刷新后重新选择", false, 409);
22971
+ const invalid$8 = () => new ArkmePluginError("record-delete-reference-invalid", "删除引用已失效,请刷新后重新选择", false, 409);
22603
22972
  function sealRecordDeletionRef(ref, key) {
22604
22973
  const payload = Buffer.from(JSON.stringify(ref)).toString("base64url");
22605
22974
  return `${prefix$1}.${payload}.${createHmac("sha256", key).update(`${prefix$1}.${payload}`).digest("base64url")}`;
22606
22975
  }
22607
22976
  function openRecordDeletionRef(value, key) {
22608
- if (typeof value !== "string" || value.length > 2048) throw invalid$7();
22977
+ if (typeof value !== "string" || value.length > 2048) throw invalid$8();
22609
22978
  const [kind, payload, signature, extra] = value.split(".");
22610
- if (kind !== prefix$1 || !payload || !signature || extra !== void 0) throw invalid$7();
22979
+ if (kind !== prefix$1 || !payload || !signature || extra !== void 0) throw invalid$8();
22611
22980
  const actual = Buffer.from(signature, "base64url");
22612
22981
  const expected = createHmac("sha256", key).update(`${prefix$1}.${payload}`).digest();
22613
- if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw invalid$7();
22982
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw invalid$8();
22614
22983
  let ref;
22615
22984
  try {
22616
22985
  ref = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
22617
22986
  } catch {
22618
- throw invalid$7();
22987
+ throw invalid$8();
22619
22988
  }
22620
22989
  if (!ref || !Number.isSafeInteger(ref.userId) || ref.userId <= 0 || !Number.isSafeInteger(ref.recordVersion) || ref.recordVersion <= 0 || ![
22621
22990
  "private_chat",
@@ -22623,7 +22992,7 @@ function openRecordDeletionRef(value, key) {
22623
22992
  "send_to_self",
22624
22993
  "default_category",
22625
22994
  "topic"
22626
- ].includes(ref.sourceKind) || typeof ref.sourceOwnerRef !== "string" || !ref.sourceOwnerRef.trim() || typeof ref.recordUid !== "string" || !ref.recordUid.trim()) throw invalid$7();
22995
+ ].includes(ref.sourceKind) || typeof ref.sourceOwnerRef !== "string" || !ref.sourceOwnerRef.trim() || typeof ref.recordUid !== "string" || !ref.recordUid.trim()) throw invalid$8();
22627
22996
  return ref;
22628
22997
  }
22629
22998
  /** Only authoritative Record snapshots can grant deletion; missing versions never fall back to a relation version. */
@@ -22736,31 +23105,31 @@ var RecordDeletionService = class {
22736
23105
  };
22737
23106
  //#endregion
22738
23107
  //#region src/record-topic-assignment-ref.ts
22739
- const invalid$6 = () => new ArkmePluginError("record-topic-reference-invalid", "快记归属引用已失效,请刷新后重新选择", false, 409);
23108
+ const invalid$7 = () => new ArkmePluginError("record-topic-reference-invalid", "快记归属引用已失效,请刷新后重新选择", false, 409);
22740
23109
  const prefix = "arkme-record-topic-v1";
22741
23110
  function sealRecordTopicAssignmentRef(reference, key) {
22742
23111
  const payload = Buffer.from(JSON.stringify(reference)).toString("base64url");
22743
23112
  return `${prefix}.${payload}.${createHmac("sha256", key).update(`${prefix}.${payload}`).digest("base64url")}`;
22744
23113
  }
22745
23114
  function openRecordTopicAssignmentRef(value, key) {
22746
- if (typeof value !== "string" || value.length > 2048) throw invalid$6();
23115
+ if (typeof value !== "string" || value.length > 2048) throw invalid$7();
22747
23116
  const parts = value.split(".");
22748
- if (parts.length !== 3 || parts[0] !== prefix) throw invalid$6();
23117
+ if (parts.length !== 3 || parts[0] !== prefix) throw invalid$7();
22749
23118
  const payload = parts[1];
22750
23119
  const signature = Buffer.from(parts[2], "base64url");
22751
23120
  const expected = createHmac("sha256", key).update(`${prefix}.${payload}`).digest();
22752
- if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) throw invalid$6();
23121
+ if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) throw invalid$7();
22753
23122
  let parsed;
22754
23123
  try {
22755
23124
  parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
22756
23125
  } catch {
22757
- throw invalid$6();
23126
+ throw invalid$7();
22758
23127
  }
22759
23128
  if (!parsed || !Number.isSafeInteger(parsed.userId) || parsed.userId <= 0 || ![
22760
23129
  "send_to_self",
22761
23130
  "default_category",
22762
23131
  "topic"
22763
- ].includes(parsed.sourceKind) || typeof parsed.sourceOwnerRef !== "string" || parsed.sourceOwnerRef.trim() === "" || typeof parsed.recordUid !== "string" || parsed.recordUid.trim() === "" || typeof parsed.sourceTopicUid !== "string") throw invalid$6();
23132
+ ].includes(parsed.sourceKind) || typeof parsed.sourceOwnerRef !== "string" || parsed.sourceOwnerRef.trim() === "" || typeof parsed.recordUid !== "string" || parsed.recordUid.trim() === "" || typeof parsed.sourceTopicUid !== "string") throw invalid$7();
22764
23133
  return parsed;
22765
23134
  }
22766
23135
  //#endregion
@@ -36846,6 +37215,10 @@ var AuthService = class {
36846
37215
  runtime;
36847
37216
  profile;
36848
37217
  lifecycle;
37218
+ cancellationNotice;
37219
+ pendingCancellationLogin;
37220
+ pendingCancellationCleanup;
37221
+ cancellationTask;
36849
37222
  pendingPhoneUnbindSession;
36850
37223
  attempts = /* @__PURE__ */ new Map();
36851
37224
  jiwoAttemptGeneration = 0;
@@ -36860,6 +37233,7 @@ var AuthService = class {
36860
37233
  for (const [attemptId, attempt] of this.attempts) if (attempt.kind === "wechat") this.attempts.delete(attemptId);
36861
37234
  }
36862
37235
  async logout() {
37236
+ this.pendingCancellationLogin = void 0;
36863
37237
  this.pendingPhoneUnbindSession = void 0;
36864
37238
  const activeSession = await this.runtime.sessionStore.read();
36865
37239
  const pendingSession = await this.runtime.readPendingBindingSession();
@@ -36882,7 +37256,24 @@ var AuthService = class {
36882
37256
  };
36883
37257
  }
36884
37258
  async authStatus() {
37259
+ if (this.pendingCancellationLogin !== void 0) return this.cancellationLoginSnapshot();
36885
37260
  const activeSession = await this.runtime.sessionStore.read();
37261
+ const completion = await this.runtime.stateStore.readCancellationCompletion?.();
37262
+ if (completion !== void 0) {
37263
+ if (activeSession === void 0) this.cancellationNotice = completion.result.status === "done" ? "done" : "waiting";
37264
+ else if (completion.userId === activeSession.userId && completion.sessionHash === this.cancellationSessionHash(activeSession)) {
37265
+ this.pendingCancellationCleanup = {
37266
+ session: activeSession,
37267
+ result: completion.result
37268
+ };
37269
+ await this.submitCancellation(activeSession.userId, completion.result.mode);
37270
+ return {
37271
+ status: "logged-out",
37272
+ environment: this.runtime.config.environment,
37273
+ cancellationNotice: completion.result.status === "done" ? "done" : "waiting"
37274
+ };
37275
+ }
37276
+ }
36886
37277
  if (activeSession !== void 0) {
36887
37278
  const cachedProfile = await this.runtime.stateStore.cachedProfile(activeSession.userId);
36888
37279
  const snapshot = cachedProfile.profile === null ? await this.authSnapshotForSession(activeSession) : {
@@ -36900,7 +37291,8 @@ var AuthService = class {
36900
37291
  const pendingSession = await this.runtime.readPendingBindingSession();
36901
37292
  return pendingSession === void 0 ? {
36902
37293
  status: "logged-out",
36903
- environment: this.runtime.config.environment
37294
+ environment: this.runtime.config.environment,
37295
+ ...this.cancellationNotice === void 0 ? {} : { cancellationNotice: this.cancellationNotice }
36904
37296
  } : {
36905
37297
  status: "binding-required",
36906
37298
  environment: this.runtime.config.environment,
@@ -36908,6 +37300,7 @@ var AuthService = class {
36908
37300
  };
36909
37301
  }
36910
37302
  async beginWechatLogin() {
37303
+ this.pendingCancellationLogin = void 0;
36911
37304
  this.jiwoAttemptGeneration += 1;
36912
37305
  await this.cancelAllJiwoLoginAttempts();
36913
37306
  const data = await this.runtime.post(this.runtime.config.authBaseUrl, "/api/public/v1/auth/wechat-oauth-login-qrcode", {}, void 0, [200]);
@@ -36970,10 +37363,11 @@ var AuthService = class {
36970
37363
  userId
36971
37364
  };
36972
37365
  this.attempts.delete(attemptId);
36973
- return await this.acceptLoginSession(session);
37366
+ return await this.acceptLoginSession(session, numberValue$27(data.rest_days_cancel));
36974
37367
  }
36975
37368
  async beginJiwoLogin() {
36976
37369
  if (!jiwoScanLoginAvailable(this.runtime.config)) throw new ArkmePluginError("jiwo-scan-login-disabled", "即我扫码登录当前未启用", false, 403);
37370
+ this.pendingCancellationLogin = void 0;
36977
37371
  const attemptGeneration = ++this.jiwoAttemptGeneration;
36978
37372
  await this.cancelAllJiwoLoginAttempts();
36979
37373
  const data = await this.runtime.post(this.runtime.config.authBaseUrl, "/api/public/v1/auth/app-scan-login/start", {
@@ -37042,7 +37436,7 @@ var AuthService = class {
37042
37436
  accessToken,
37043
37437
  refreshToken,
37044
37438
  userId
37045
- });
37439
+ }, numberValue$27(data.rest_days_cancel));
37046
37440
  }
37047
37441
  async cancelJiwoLogin(attemptId) {
37048
37442
  if (attemptId.trim() === "") return { canceled: true };
@@ -37080,6 +37474,7 @@ var AuthService = class {
37080
37474
  async testLogin(userId) {
37081
37475
  if (this.runtime.config.environment !== "test") throw new ArkmePluginError("test-login-disabled", "测试账号登录仅允许测试环境使用", false, 403);
37082
37476
  if (!Number.isSafeInteger(userId) || userId <= 0) throw new ArkmePluginError("test-user-id-invalid", "请输入有效的测试账号 user_id", false);
37477
+ this.pendingCancellationLogin = void 0;
37083
37478
  const data = await this.runtime.post(this.runtime.config.authBaseUrl, "/api/public/v1/auth/the-best-api-for-testing", {
37084
37479
  user_id: userId,
37085
37480
  unique_code: await this.runtime.stateStore.uniqueCode(),
@@ -37095,7 +37490,30 @@ var AuthService = class {
37095
37490
  userId
37096
37491
  };
37097
37492
  this.attempts.clear();
37098
- return await this.acceptLoginSession(session);
37493
+ return await this.acceptLoginSession(session, numberValue$27(data.rest_days_cancel));
37494
+ }
37495
+ async emailBindingRequest(expectedUserId, email, code) {
37496
+ const session = await this.runtime.requireSession();
37497
+ if (session.userId !== expectedUserId) throw new ArkmePluginError("email-session-changed", "账号已切换,请重新打开账号设置", false);
37498
+ const normalizedEmail = email.trim();
37499
+ if (normalizedEmail.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) throw new ArkmePluginError("email-invalid", "请输入有效的邮箱地址", false);
37500
+ if (code !== void 0 && !/^[0-9]{4}$/.test(code.trim())) throw new ArkmePluginError("email-code-invalid", "请输入4位邮箱验证码", false);
37501
+ const data = await this.runtime.post(this.runtime.config.authBaseUrl, code === void 0 ? "/api/v1/auth/email-bind-send-code" : "/api/v1/auth/email-bind", {
37502
+ email: normalizedEmail,
37503
+ ...code === void 0 ? {} : { code: code.trim() }
37504
+ }, session.accessToken, [200]);
37505
+ const current = await this.runtime.requireSession();
37506
+ if (current.userId !== session.userId || current.refreshToken !== session.refreshToken) throw new ArkmePluginError("email-session-changed", "账号已切换,请重新打开账号设置", false);
37507
+ if (data.result !== 1) throw new ArkmePluginError("email-bind-rejected", data.result === 2 ? "该邮箱已绑定其他账号" : data.result === 3 ? "当前账号已绑定邮箱,请刷新账号信息" : code !== void 0 && data.result === 4 ? "邮箱或验证码错误" : "邮箱绑定响应异常,请重试", false);
37508
+ if (code !== void 0) this.profile.invalidate(session.userId);
37509
+ }
37510
+ async sendEmailBindCode(expectedUserId, email) {
37511
+ await this.emailBindingRequest(expectedUserId, email);
37512
+ return { sent: true };
37513
+ }
37514
+ async bindEmail(expectedUserId, email, code) {
37515
+ await this.emailBindingRequest(expectedUserId, email, code);
37516
+ return { bound: true };
37099
37517
  }
37100
37518
  async sendPhoneCode(phone, captcha) {
37101
37519
  const normalizedPhone = this.normalizedPhone(phone);
@@ -37122,6 +37540,7 @@ var AuthService = class {
37122
37540
  const normalizedPhone = this.normalizedPhone(phone);
37123
37541
  const normalizedCode = code.trim();
37124
37542
  if (!/^[0-9]{6}$/.test(normalizedCode)) throw new ArkmePluginError("phone-code-invalid", "请输入有效的短信验证码", false);
37543
+ this.pendingCancellationLogin = void 0;
37125
37544
  const session = await this.runtime.sessionStore.read() ?? await this.runtime.readPendingBindingSession();
37126
37545
  if (session !== void 0) {
37127
37546
  const result = numberValue$27((await this.runtime.post(this.runtime.config.authBaseUrl, "/api/v1/auth/verify-bind-phone", {
@@ -37155,7 +37574,7 @@ var AuthService = class {
37155
37574
  userId
37156
37575
  };
37157
37576
  this.attempts.clear();
37158
- return await this.acceptLoginSession(sessionAfterPhoneLogin);
37577
+ return await this.acceptLoginSession(sessionAfterPhoneLogin, numberValue$27(data.rest_days_cancel));
37159
37578
  }
37160
37579
  async checkPhoneUnbindEligibility(expectedUserId) {
37161
37580
  const session = await this.runtime.requireSession();
@@ -37250,21 +37669,144 @@ var AuthService = class {
37250
37669
  profileHasBoundPhone(snapshot) {
37251
37670
  return (snapshot.profile?.contact.phoneMasked?.trim() ?? "") !== "";
37252
37671
  }
37253
- async acceptLoginSession(session) {
37672
+ async acceptLoginSession(session, restDaysCancel = 0, assertCurrent) {
37673
+ if (assertCurrent === void 0) this.pendingCancellationLogin = void 0;
37674
+ if (restDaysCancel > 0) {
37675
+ this.pendingCancellationLogin = {
37676
+ session,
37677
+ days: restDaysCancel
37678
+ };
37679
+ return this.cancellationLoginSnapshot();
37680
+ }
37681
+ await this.runtime.stateStore.writeCancellationCompletion?.(void 0);
37682
+ this.cancellationNotice = void 0;
37254
37683
  this.runtime.invalidateScope(this.runtime.requestScope(session.userId));
37255
37684
  this.profile.invalidate(session.userId);
37256
37685
  const snapshot = await this.authSnapshotForSession(session, { forceProfile: true });
37686
+ assertCurrent?.();
37257
37687
  if (snapshot.status === "authenticated") {
37258
37688
  await this.runtime.clearPendingBindingSession();
37689
+ assertCurrent?.();
37259
37690
  await this.runtime.writeSession(session);
37691
+ assertCurrent?.();
37260
37692
  this.lifecycle.reconnectChatRealtime();
37261
37693
  return snapshot;
37262
37694
  }
37263
37695
  await this.runtime.writePendingBindingSession(session);
37696
+ assertCurrent?.();
37264
37697
  await this.runtime.deleteSession();
37698
+ assertCurrent?.();
37265
37699
  this.lifecycle.reconnectChatRealtime();
37266
37700
  return snapshot;
37267
37701
  }
37702
+ cancellationLoginSnapshot() {
37703
+ return {
37704
+ status: "cancellation-pending",
37705
+ environment: this.runtime.config.environment,
37706
+ restDaysCancel: this.pendingCancellationLogin.days
37707
+ };
37708
+ }
37709
+ async resolveCancellationLogin(continueLogin) {
37710
+ const pending = this.pendingCancellationLogin;
37711
+ if (pending === void 0) throw new ArkmePluginError("cancellation-login-expired", "登录确认已失效,请重新登录", false);
37712
+ if (!continueLogin) {
37713
+ this.pendingCancellationLogin = void 0;
37714
+ return {
37715
+ status: "logged-out",
37716
+ environment: this.runtime.config.environment
37717
+ };
37718
+ }
37719
+ await this.runtime.post(this.runtime.config.authBaseUrl, "/api/v1/auth/abort-cancel", {}, pending.session.accessToken, [200]);
37720
+ if (this.pendingCancellationLogin !== pending) throw new ArkmePluginError("cancellation-login-expired", "登录确认已失效,请重新登录", false);
37721
+ const snapshot = await this.acceptLoginSession(pending.session, 0, () => {
37722
+ if (this.pendingCancellationLogin !== pending) throw new ArkmePluginError("cancellation-login-expired", "登录确认已失效,请重新登录", false);
37723
+ });
37724
+ if (this.pendingCancellationLogin === pending) this.pendingCancellationLogin = void 0;
37725
+ return snapshot;
37726
+ }
37727
+ async cancellationSession(expectedUserId) {
37728
+ const session = await this.runtime.requireSession();
37729
+ if (session.userId !== expectedUserId) throw new ArkmePluginError("cancellation-session-changed", "账号已切换,请重新打开账号设置", false);
37730
+ return session;
37731
+ }
37732
+ cancellationResult(data) {
37733
+ if (!data || !["immediate", "waiting"].includes(data.mode) || ![
37734
+ "eligible",
37735
+ "waiting",
37736
+ "done"
37737
+ ].includes(data.status) || !Number.isSafeInteger(data.cancel_at) || data.cancel_at < 0 || typeof data.has_phone !== "boolean") throw new ArkmePluginError("cancellation-contract-invalid", "注销状态未确认,请稍后重试", true, 502);
37738
+ return data;
37739
+ }
37740
+ async previewCancellation(expectedUserId) {
37741
+ const session = await this.cancellationSession(expectedUserId);
37742
+ const data = await this.runtime.post(this.runtime.config.authBaseUrl, "/api/v1/auth/cancellation/preview", {}, session.accessToken, [200]);
37743
+ if ((await this.cancellationSession(expectedUserId)).refreshToken !== session.refreshToken) throw new ArkmePluginError("cancellation-session-changed", "登录状态已变化,请重试", false);
37744
+ return this.cancellationResult(data);
37745
+ }
37746
+ async submitCancellation(expectedUserId, expectedMode) {
37747
+ if (expectedMode !== "immediate" && expectedMode !== "waiting") throw new ArkmePluginError("cancellation-mode-invalid", "请重新确认注销方式", false);
37748
+ if (this.cancellationTask !== void 0) throw new ArkmePluginError("cancellation-busy", "正在处理注销,请稍候", true);
37749
+ const task = this.performCancellation(expectedUserId, expectedMode);
37750
+ this.cancellationTask = task;
37751
+ try {
37752
+ return await task;
37753
+ } finally {
37754
+ if (this.cancellationTask === task) this.cancellationTask = void 0;
37755
+ }
37756
+ }
37757
+ async performCancellation(expectedUserId, expectedMode) {
37758
+ let cleanup = this.pendingCancellationCleanup;
37759
+ if (cleanup === void 0) {
37760
+ const completion = await this.runtime.stateStore.readCancellationCompletion?.();
37761
+ const active = await this.runtime.sessionStore.read();
37762
+ if (completion?.userId === expectedUserId && active === void 0) return completion.result;
37763
+ if (completion?.userId === expectedUserId && active !== void 0 && completion.sessionHash === this.cancellationSessionHash(active)) {
37764
+ cleanup = {
37765
+ session: active,
37766
+ result: completion.result
37767
+ };
37768
+ this.pendingCancellationCleanup = cleanup;
37769
+ }
37770
+ }
37771
+ if (cleanup === void 0 || cleanup.session.userId !== expectedUserId) {
37772
+ const session = await this.cancellationSession(expectedUserId);
37773
+ const result = this.cancellationResult(await this.runtime.post(this.runtime.config.authBaseUrl, "/api/v1/auth/cancellation/submit", { expected_mode: expectedMode }, session.accessToken, [200], void 0, false, this.runtime.authenticatedRequestOptions(session, "auth", "write", { trackWriteOutcome: true })));
37774
+ if (result.changed === true && result.status === "eligible") return result;
37775
+ if (result.status !== "done" && result.status !== "waiting") throw new ArkmePluginError("cancellation-contract-invalid", "注销状态未确认,请稍后重试", true);
37776
+ cleanup = {
37777
+ session,
37778
+ result
37779
+ };
37780
+ this.pendingCancellationCleanup = cleanup;
37781
+ }
37782
+ const current = await this.runtime.sessionStore.read();
37783
+ if (current !== void 0 && (current.userId !== cleanup.session.userId || current.refreshToken !== cleanup.session.refreshToken)) {
37784
+ await this.runtime.stateStore.writeCancellationCompletion?.(void 0);
37785
+ this.pendingCancellationCleanup = void 0;
37786
+ throw new ArkmePluginError("cancellation-session-changed", "账号已切换,原账号注销已处理", false);
37787
+ }
37788
+ await this.runtime.stateStore.writeCancellationCompletion?.({
37789
+ userId: cleanup.session.userId,
37790
+ sessionHash: this.cancellationSessionHash(cleanup.session),
37791
+ result: cleanup.result
37792
+ });
37793
+ this.runtime.invalidateScope(this.runtime.requestScope(cleanup.session.userId));
37794
+ this.runtime.clearRefreshForUser(cleanup.session.userId);
37795
+ if (!await this.runtime.deleteSessionIfCurrent(cleanup.session) && await this.runtime.sessionStore.read() !== void 0) {
37796
+ await this.runtime.stateStore.writeCancellationCompletion?.(void 0);
37797
+ this.pendingCancellationCleanup = void 0;
37798
+ throw new ArkmePluginError("cancellation-session-changed", "账号已切换,原账号注销已处理", false);
37799
+ }
37800
+ this.profile.invalidate(cleanup.session.userId);
37801
+ this.lifecycle.clearAccountState([cleanup.session.userId]);
37802
+ this.lifecycle.reconnectChatRealtime();
37803
+ this.cancellationNotice = cleanup.result.status === "done" ? "done" : "waiting";
37804
+ this.pendingCancellationCleanup = void 0;
37805
+ return cleanup.result;
37806
+ }
37807
+ cancellationSessionHash(session) {
37808
+ return createHash("sha256").update(session.refreshToken).digest("hex");
37809
+ }
37268
37810
  normalizedPhone(phone) {
37269
37811
  const normalized = phone.replace(/[\s-]/g, "");
37270
37812
  if (!/^1[3-9][0-9]{9}$/.test(normalized)) throw new ArkmePluginError("phone-invalid", "请输入有效的中国大陆手机号", false);
@@ -37321,23 +37863,23 @@ var BackgroundSoundMembershipService = class {
37321
37863
  };
37322
37864
  //#endregion
37323
37865
  //#region src/services/membership-service.ts
37324
- function invalid$5() {
37866
+ function invalid$6() {
37325
37867
  throw new ArkmePluginError("membership-contract-invalid", "会员信息暂时无法确认,请稍后重试", true, 502);
37326
37868
  }
37327
37869
  function integer$3(value) {
37328
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return invalid$5();
37870
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return invalid$6();
37329
37871
  return value;
37330
37872
  }
37331
37873
  function timestamp(value) {
37332
37874
  if (value === void 0 || value === null || value === 0) return null;
37333
37875
  const micros = integer$3(value);
37334
37876
  const millis = Math.floor(micros / 1e3);
37335
- if (millis < Date.UTC(2e3, 0) || millis > Date.UTC(3e3, 0)) return invalid$5();
37877
+ if (millis < Date.UTC(2e3, 0) || millis > Date.UTC(3e3, 0)) return invalid$6();
37336
37878
  return millis;
37337
37879
  }
37338
37880
  function parseMembership(raw, userId, now = Date.now()) {
37339
37881
  const memberType = integer$3(raw.member_type);
37340
- if (memberType > 2) return invalid$5();
37882
+ if (memberType > 2) return invalid$6();
37341
37883
  const expireAtMillis = timestamp(raw.expire_at);
37342
37884
  return {
37343
37885
  userId,
@@ -37348,16 +37890,16 @@ function parseMembership(raw, userId, now = Date.now()) {
37348
37890
  };
37349
37891
  }
37350
37892
  function parseMembershipProducts(raw) {
37351
- if (!Array.isArray(raw.data_ls)) return invalid$5();
37893
+ if (!Array.isArray(raw.data_ls)) return invalid$6();
37352
37894
  const products = raw.data_ls.map((entry) => {
37353
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return invalid$5();
37895
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return invalid$6();
37354
37896
  const row = entry;
37355
37897
  const memberType = integer$3(row.spu_code);
37356
- if (memberType !== 1 && memberType !== 2) return invalid$5();
37357
- if (typeof row.name !== "string" || !row.name.trim() || typeof row.is_period !== "boolean") return invalid$5();
37898
+ if (memberType !== 1 && memberType !== 2) return invalid$6();
37899
+ if (typeof row.name !== "string" || !row.name.trim() || typeof row.is_period !== "boolean") return invalid$6();
37358
37900
  const alipayId = integer$3(row.alipay_product_id);
37359
37901
  const wechatId = integer$3(row.wechat_product_id);
37360
- if (alipayId === 0 && wechatId === 0) return invalid$5();
37902
+ if (alipayId === 0 && wechatId === 0) return invalid$6();
37361
37903
  return {
37362
37904
  id: `${memberType}:${alipayId}:${wechatId}:${row.is_period ? "recurring" : "once"}`,
37363
37905
  memberType,
@@ -37368,7 +37910,7 @@ function parseMembershipProducts(raw) {
37368
37910
  monthCount: integer$3(row.month_count)
37369
37911
  };
37370
37912
  });
37371
- if (new Set(products.map((p) => p.id)).size !== products.length) return invalid$5();
37913
+ if (new Set(products.map((p) => p.id)).size !== products.length) return invalid$6();
37372
37914
  return products.sort((a, b) => a.memberType - b.memberType || a.monthCount - b.monthCount || a.priceMinor - b.priceMinor);
37373
37915
  }
37374
37916
  /** Membership is distinct from managed-AI balance billing. Only read endpoints are exposed. */
@@ -37384,7 +37926,7 @@ var MembershipService = class {
37384
37926
  return parseMembership(raw, userId);
37385
37927
  }
37386
37928
  async catalog(expectedUserId) {
37387
- if (!Number.isSafeInteger(expectedUserId) || expectedUserId <= 0) return invalid$5();
37929
+ if (!Number.isSafeInteger(expectedUserId) || expectedUserId <= 0) return invalid$6();
37388
37930
  const session = await this.runtime.requireSession();
37389
37931
  if (session.userId !== expectedUserId) throw new ArkmePluginError("membership-account-changed", "账号已切换,请重新打开会员页面", false, 409);
37390
37932
  const raw = await this.runtime.authenticatedAuthReadPost("/api/v1/payment/get/sale-product", {}, session);
@@ -37499,19 +38041,19 @@ var AccountUsageService = class {
37499
38041
  //#endregion
37500
38042
  //#region src/services/account-usage-details-service.ts
37501
38043
  const monthPattern = /^\d{4}-(0[1-9]|1[0-2])$/;
37502
- function invalid$4() {
38044
+ function invalid$5() {
37503
38045
  return new ArkmePluginError("account-usage-details-invalid", "用量明细暂时无法确认,请重试", true, 502);
37504
38046
  }
37505
38047
  function object$5(value) {
37506
- if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid$4();
38048
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid$5();
37507
38049
  return value;
37508
38050
  }
37509
38051
  function count(value) {
37510
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw invalid$4();
38052
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw invalid$5();
37511
38053
  return value;
37512
38054
  }
37513
38055
  function text$4(value, max = 16384) {
37514
- if (typeof value !== "string" || value.length > max) throw invalid$4();
38056
+ if (typeof value !== "string" || value.length > max) throw invalid$5();
37515
38057
  return value;
37516
38058
  }
37517
38059
  function tokens(value) {
@@ -37540,17 +38082,17 @@ function status(value) {
37540
38082
  }
37541
38083
  function parseTokenSummary(value, accountScope) {
37542
38084
  const raw = object$5(value), monthKey = text$4(raw.month_key, 7);
37543
- if (!monthPattern.test(monthKey) || !Array.isArray(raw.available_months)) throw invalid$4();
38085
+ if (!monthPattern.test(monthKey) || !Array.isArray(raw.available_months)) throw invalid$5();
37544
38086
  const availableMonths = [...new Set(raw.available_months.map((value) => {
37545
38087
  const month = text$4(value, 7);
37546
- if (!monthPattern.test(month)) throw invalid$4();
38088
+ if (!monthPattern.test(month)) throw invalid$5();
37547
38089
  return month;
37548
38090
  }))].sort().reverse();
37549
38091
  const timezone = text$4(raw.timezone, 100);
37550
38092
  try {
37551
38093
  new Intl.DateTimeFormat("en", { timeZone: timezone });
37552
38094
  } catch {
37553
- throw invalid$4();
38095
+ throw invalid$5();
37554
38096
  }
37555
38097
  return {
37556
38098
  accountScope,
@@ -37567,7 +38109,7 @@ function parseTokenSummary(value, accountScope) {
37567
38109
  }
37568
38110
  function parseTokenPage(value, accountScope, kind) {
37569
38111
  const raw = object$5(value);
37570
- if (!Array.isArray(raw.items) || raw.items.length > 100) throw invalid$4();
38112
+ if (!Array.isArray(raw.items) || raw.items.length > 100) throw invalid$5();
37571
38113
  return {
37572
38114
  accountScope,
37573
38115
  items: raw.items.map((value) => {
@@ -37583,7 +38125,7 @@ function parseTokenPage(value, accountScope, kind) {
37583
38125
  usageStatus: status(item.usage_status)
37584
38126
  };
37585
38127
  const operationUid = text$4(item.operation_uid, 256), displayName = text$4(item.display_name, 2048);
37586
- if (!operationUid.trim() || !displayName.trim() || !Array.isArray(item.models)) throw invalid$4();
38128
+ if (!operationUid.trim() || !displayName.trim() || !Array.isArray(item.models)) throw invalid$5();
37587
38129
  return {
37588
38130
  ...common,
37589
38131
  operationUid,
@@ -37610,11 +38152,11 @@ var AccountUsageDetailsService = class {
37610
38152
  async read(scope, kind, query, signal) {
37611
38153
  const session = await this.session(scope);
37612
38154
  const month = text$4(query.monthKey, 7), timezone = text$4(query.timezone, 100) || "Asia/Shanghai";
37613
- if (month && !monthPattern.test(month)) throw invalid$4();
38155
+ if (month && !monthPattern.test(month)) throw invalid$5();
37614
38156
  try {
37615
38157
  new Intl.DateTimeFormat("en", { timeZone: timezone });
37616
38158
  } catch {
37617
- throw invalid$4();
38159
+ throw invalid$5();
37618
38160
  }
37619
38161
  const body = {
37620
38162
  month_key: month,
@@ -37626,7 +38168,7 @@ var AccountUsageDetailsService = class {
37626
38168
  }
37627
38169
  if (kind === "calls") {
37628
38170
  const uid = text$4(query.operationUid, 256), biz = count(query.bizCode);
37629
- if (!uid.trim() || biz > 32767) throw invalid$4();
38171
+ if (!uid.trim() || biz > 32767) throw invalid$5();
37630
38172
  body.operation_uid = uid;
37631
38173
  body.biz_code = biz;
37632
38174
  }
@@ -37636,7 +38178,7 @@ var AccountUsageDetailsService = class {
37636
38178
  }
37637
38179
  async summary(scope, query, signal) {
37638
38180
  const result = parseTokenSummary(await this.read(scope, "summary", query, signal), scope);
37639
- if (query.monthKey && result.monthKey !== query.monthKey) throw invalid$4();
38181
+ if (query.monthKey && result.monthKey !== query.monthKey) throw invalid$5();
37640
38182
  return result;
37641
38183
  }
37642
38184
  async operations(scope, query, signal) {
@@ -37649,17 +38191,17 @@ var AccountUsageDetailsService = class {
37649
38191
  //#endregion
37650
38192
  //#region src/services/data-management-service.ts
37651
38193
  function object$4(value) {
37652
- if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid$3();
38194
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid$4();
37653
38195
  return value;
37654
38196
  }
37655
- function invalid$3() {
38197
+ function invalid$4() {
37656
38198
  return new ArkmePluginError("data-management-contract-invalid", "数据格式暂时无法确认,请重试", true, 502);
37657
38199
  }
37658
38200
  function text$3(value) {
37659
38201
  return typeof value === "string" ? value : "";
37660
38202
  }
37661
38203
  function integer$1(value) {
37662
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw invalid$3();
38204
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw invalid$4();
37663
38205
  return value;
37664
38206
  }
37665
38207
  const DELETED_RETENTION_MILLIS = 720 * 60 * 60 * 1e3;
@@ -37682,7 +38224,7 @@ function lifecycle(value) {
37682
38224
  function parseDeletedRecords(raw, accountScope, nowMillis = Date.now()) {
37683
38225
  const envelope = object$4(raw);
37684
38226
  const rows = envelope.items ?? envelope.records ?? envelope.list ?? envelope.data;
37685
- if (!Array.isArray(rows)) throw invalid$3();
38227
+ if (!Array.isArray(rows)) throw invalid$4();
37686
38228
  let unverifiedCount = 0;
37687
38229
  return {
37688
38230
  accountScope,
@@ -37690,7 +38232,7 @@ function parseDeletedRecords(raw, accountScope, nowMillis = Date.now()) {
37690
38232
  const row = object$4(value);
37691
38233
  const core = object$4(row.record_core ?? row.record ?? row);
37692
38234
  const recordUid = text$3(core.record_uid).trim();
37693
- if (!recordUid) throw invalid$3();
38235
+ if (!recordUid) throw invalid$4();
37694
38236
  const status = lifecycle(core.status);
37695
38237
  if (status === "excluded") return [];
37696
38238
  if (status === "unknown") {
@@ -37753,7 +38295,7 @@ var DataManagementService = class {
37753
38295
  }, session, signal, { lane: "interactive-read" }), this.runtime.authenticatedPost("/api/v1/records/export/session/latest", {}, session, signal, { lane: "interactive-read" })]);
37754
38296
  await this.session(scope);
37755
38297
  const raw = object$4(preflightResponse), latest = object$4(latestResponse);
37756
- if (typeof raw.can_export !== "boolean") throw invalid$3();
38298
+ if (typeof raw.can_export !== "boolean") throw invalid$4();
37757
38299
  return {
37758
38300
  accountScope: scope,
37759
38301
  canExport: raw.can_export,
@@ -37765,7 +38307,7 @@ var DataManagementService = class {
37765
38307
  };
37766
38308
  }
37767
38309
  async recover(scope, recordUid, version) {
37768
- if (!recordUid.trim() || integer$1(version) <= 0) throw invalid$3();
38310
+ if (!recordUid.trim() || integer$1(version) <= 0) throw invalid$4();
37769
38311
  const record = (await this.deleted(scope)).items.find((item) => item.recordUid === recordUid);
37770
38312
  if (!record || record.version !== version || record.recoverableUntilMillis === void 0 || record.recoverableUntilMillis <= Date.now()) throw new ArkmePluginError("data-management-record-changed", "记录状态已变化或无法确认可恢复,请刷新最近删除列表。", false, 409);
37771
38313
  const session = await this.session(scope);
@@ -37775,7 +38317,7 @@ var DataManagementService = class {
37775
38317
  }, session);
37776
38318
  await this.session(scope);
37777
38319
  const envelope = object$4(raw);
37778
- if (object$4(envelope.record_core ?? envelope.record ?? envelope).record_uid !== recordUid) throw invalid$3();
38320
+ if (object$4(envelope.record_core ?? envelope.record ?? envelope).record_uid !== recordUid) throw invalid$4();
37779
38321
  return {
37780
38322
  accountScope: scope,
37781
38323
  recordUid
@@ -44122,7 +44664,6 @@ var ArkmeChatRealtimeRuntime = class {
44122
44664
  //#region src/services/chat-realtime-service.ts
44123
44665
  const MAX_PROJECTION_RETRIES = 5;
44124
44666
  const MAX_ATTENTION_SUMMARY_RETRY_DELAY_MS = 3e4;
44125
- const MAX_NATIVE_NOTIFICATION_DELIVERY_CONCURRENCY = 3;
44126
44667
  const MAX_NOTIFICATION_HINTS_PER_TIMELINE_READ = 50;
44127
44668
  const NOTIFICATION_EXPIRY_MILLIS = 5 * 6e4;
44128
44669
  const CHAT_NOTIFICATION_DIAGNOSTICS = process.env.ARKME_CHAT_NOTIFICATION_DIAGNOSTICS === "1";
@@ -44234,6 +44775,7 @@ var ChatRealtimeService = class {
44234
44775
  }
44235
44776
  dispose() {
44236
44777
  this.disposed = true;
44778
+ this.attentionOwnerGeneration += 1;
44237
44779
  this.attentionController.abort();
44238
44780
  if (this.projectionTimer !== void 0) clearTimeout(this.projectionTimer);
44239
44781
  if (this.connectionBaselineRetryTimer !== void 0) clearTimeout(this.connectionBaselineRetryTimer);
@@ -44842,7 +45384,7 @@ var ChatRealtimeService = class {
44842
45384
  accountUserId: notificationHint?.accountUserId ?? current?.accountUserId,
44843
45385
  accountOwnerGeneration: notificationHint?.accountOwnerGeneration ?? current?.accountOwnerGeneration
44844
45386
  });
44845
- this.scheduleProjectionFlush(200);
45387
+ this.scheduleProjectionFlush(0);
44846
45388
  }
44847
45389
  scheduleProjectionFlush(delayMillis) {
44848
45390
  if (this.projectionInFlight || this.disposed) return;
@@ -44861,7 +45403,7 @@ var ChatRealtimeService = class {
44861
45403
  const now = Date.now();
44862
45404
  let delay = Number.POSITIVE_INFINITY;
44863
45405
  for (const projection of this.pendingChatProjections.values()) {
44864
- if (projection.refreshSource !== false) delay = Math.min(delay, 200);
45406
+ if (projection.refreshSource !== false) delay = 0;
44865
45407
  for (const candidate of projection.notificationHints) {
44866
45408
  const dueAtMillis = candidate.nextAttemptAtMillis ?? now + notificationRetryDelay(candidate.attempts);
44867
45409
  delay = Math.min(delay, Math.max(0, dueAtMillis - now));
@@ -44999,26 +45541,29 @@ var ChatRealtimeService = class {
44999
45541
  if (pending.length === 0) return [];
45000
45542
  for (const [uid, projection] of pending) if (projection.refreshSource === false && this.source.cachedChatSourceByKey(`${String(session.userId)}:${uid}`) === void 0) projection.refreshSource = true;
45001
45543
  const sourcePending = pending.filter(([, projection]) => projection.refreshSource !== false);
45002
- const bundles = /* @__PURE__ */ new Map();
45003
- if (sourcePending.length > 0) {
45004
- const sessionUids = sourcePending.map(([uid]) => uid).sort();
45005
- const projectionBatchKey = sessionUids.join("|");
45006
- const displayData = await this.runtime.authenticatedChatPost("/api/v1/chats/display-snapshots", { chat_session_uids: sessionUids }, session, void 0, {
45007
- lane: sourcePending.some(([, projection]) => projection.notificationHints.length > 0) ? "interactive-read" : "background-read",
45008
- key: `projection:display:${projectionBatchKey}`
45009
- });
45010
- for (const raw of listValue$18(displayData.items)) {
45011
- const bundle = objectValue$2(raw);
45012
- const uid = stringValue$2(objectValue$2(bundle.session).chat_session_uid).trim();
45013
- 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
+ }
45014
45558
  }
45015
- }
45016
- const tailItemsByUid = /* @__PURE__ */ new Map();
45017
- const failedUids = /* @__PURE__ */ new Set();
45018
- const timelinePending = pending.filter(([, projection]) => projection.refreshSource !== false || projection.notificationHints.some((candidate) => candidate.resolvedNotification === void 0));
45019
- for (let offset = 0; offset < timelinePending.length; offset += 3) {
45020
- const chunk = timelinePending.slice(offset, offset + 3);
45021
- (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)) {
45022
45567
  const cached = this.source.cachedChatSourceByKey(`${String(session.userId)}:${uid}`);
45023
45568
  const unresolvedHints = projection.notificationHints.filter((candidate) => candidate.resolvedNotification === void 0);
45024
45569
  const firstHintSequence = unresolvedHints.reduce((minimum, item) => Math.min(minimum, item.hint.latestSequence), projection.latestSequence);
@@ -45033,32 +45578,24 @@ var ChatRealtimeService = class {
45033
45578
  lane: projection.notificationHints.length > 0 ? "interactive-read" : "background-read",
45034
45579
  key: `projection:tail:${uid}:${String(afterSequence)}:${String(notificationAttempt)}`
45035
45580
  });
45036
- 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);
45037
45584
  const sessionKind = numberValue$21(objectValue$2(bundle).session_kind ?? objectValue$2(objectValue$2(bundle).session).session_kind);
45038
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;
45039
- return [uid, {
45586
+ timelineProjection = {
45040
45587
  items: await this.projectionReader.chatTimelineItems(data, session, uid, sourceKind, bundle),
45041
45588
  notificationIdentities: timelineNotificationIdentities(data)
45042
- }];
45043
- }))).forEach((result, index) => {
45044
- const uid = chunk[index]?.[0];
45045
- if (uid === void 0) return;
45046
- if (result.status === "fulfilled") tailItemsByUid.set(result.value[0], result.value[1]);
45047
- else failedUids.add(uid);
45048
- });
45049
- }
45050
- if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45051
- const updates = [];
45052
- const sidebarNotificationHints = [];
45053
- const notifications = [];
45054
- for (const [uid, projection] of pending) {
45055
- const bundle = bundles.get(uid);
45056
- if (projection.refreshSource !== false && bundle === void 0 || failedUids.has(uid)) {
45057
- failedUids.add(uid);
45058
- continue;
45589
+ };
45059
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]];
45060
45598
  const cacheKey = `${String(session.userId)}:${uid}`;
45061
- const timelineProjection = tailItemsByUid.get(uid);
45062
45599
  const timelineItems = timelineProjection?.items ?? [];
45063
45600
  try {
45064
45601
  const cachedSource = this.source.cachedChatSourceByKey(cacheKey);
@@ -45068,8 +45605,7 @@ var ChatRealtimeService = class {
45068
45605
  ...projection,
45069
45606
  refreshSource: true
45070
45607
  });
45071
- failedUids.add(uid);
45072
- continue;
45608
+ return [[uid, projection]];
45073
45609
  }
45074
45610
  if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45075
45611
  if (projection.refreshSource !== false) this.source.setChatSourceByKey(cacheKey, source);
@@ -45159,117 +45695,137 @@ var ChatRealtimeService = class {
45159
45695
  });
45160
45696
  }
45161
45697
  } catch {
45162
- failedUids.add(uid);
45698
+ return [[uid, projection]];
45163
45699
  }
45164
- }
45165
- if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45166
- if (updates.length > 0) {
45167
- this.source.invalidateSourceListCache(session.userId, "root");
45168
- this.runtime.invalidateCalendarDates(this.runtime.requestScope(session.userId), updates.flatMap((update) => [update.source.activeAtMillis, ...update.timelineItems.map((item) => item.sendAtMillis)]));
45169
- this.emitChatClientEvent({
45170
- type: "sessions-delta",
45171
- revision: this.nextChatClientRevision(),
45172
- updates
45173
- });
45174
- const emittedAtMillis = Date.now();
45175
- for (const { candidate } of sidebarNotificationHints) notificationDiagnostic("notification_sidebar_delta_emitted", {
45176
- eventUid: candidate.hint.eventUid,
45177
- connectionGeneration: candidate.connectionGeneration,
45178
- attempt: candidate.attempts,
45179
- emittedAtMillis,
45180
- ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, emittedAtMillis - candidate.receivedAtMillis) }
45181
- });
45182
- this.refreshAttentionSummary();
45183
- }
45184
- const deliveries = [];
45185
- for (let offset = 0; offset < notifications.length; offset += MAX_NATIVE_NOTIFICATION_DELIVERY_CONCURRENCY) {
45186
- const chunk = notifications.slice(offset, offset + MAX_NATIVE_NOTIFICATION_DELIVERY_CONCURRENCY);
45187
- deliveries.push(...await Promise.all(chunk.map(async (entry) => {
45188
- const { notification, candidate } = entry;
45189
- if (ownerGeneration !== this.attentionOwnerGeneration) return {
45190
- entry,
45191
- fallbackToBrowser: false,
45192
- outcome: "exception"
45193
- };
45194
- const dispatchStartedAtMillis = Date.now();
45195
- try {
45196
- const outcome = await this.nativeAttention.showNotification({
45197
- idempotencyKey: notification.eventUid,
45198
- kind: "chat.message",
45199
- occurredAtMillis: notification.eventAtMillis,
45200
- expiresAtMillis: notification.eventAtMillis + NOTIFICATION_EXPIRY_MILLIS,
45201
- presentation: {
45202
- title: notification.title,
45203
- body: notification.body
45204
- },
45205
- activation: {
45206
- kind: "chat-source",
45207
- sourceRef: notification.sourceRef,
45208
- 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;
45209
45802
  }
45210
- });
45211
- const completedAtMillis = Date.now();
45212
- notificationDiagnostic("notification_native_dispatch_completed", {
45213
- eventUid: notification.eventUid,
45214
- connectionGeneration: candidate.connectionGeneration,
45215
- attempt: candidate.attempts,
45216
- outcome: outcome.outcome,
45217
- completedAtMillis,
45218
- dispatchDurationMillis: Math.max(0, completedAtMillis - dispatchStartedAtMillis),
45219
- ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, completedAtMillis - candidate.receivedAtMillis) }
45220
- });
45221
- return {
45222
- entry,
45223
- fallbackToBrowser: outcome.fallbackToBrowser,
45224
- outcome: outcome.outcome
45225
- };
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));
45226
45822
  } catch {
45227
- const completedAtMillis = Date.now();
45228
- notificationDiagnostic("notification_native_dispatch_completed", {
45229
- eventUid: notification.eventUid,
45230
- connectionGeneration: candidate.connectionGeneration,
45231
- attempt: candidate.attempts,
45232
- outcome: "exception",
45233
- completedAtMillis,
45234
- dispatchDurationMillis: Math.max(0, completedAtMillis - dispatchStartedAtMillis),
45235
- ...candidate.receivedAtMillis === void 0 ? {} : { durationFromHintMillis: Math.max(0, completedAtMillis - candidate.receivedAtMillis) }
45236
- });
45237
- return {
45238
- entry,
45239
- fallbackToBrowser: false,
45240
- outcome: "exception"
45241
- };
45242
- }
45243
- })));
45244
- }
45245
- if (ownerGeneration !== this.attentionOwnerGeneration) return [];
45246
- for (const { entry, fallbackToBrowser, outcome } of deliveries) {
45247
- const { notification, candidate, uid, latestSequence } = entry;
45248
- if (outcome === "native-failed" || outcome === "rate-limited" || outcome === "exception") {
45249
- if (!notificationExpired(candidate)) {
45250
- const attempts = candidate.attempts + 1;
45251
- this.mergePendingChatProjection(uid, {
45252
- latestSequence,
45253
- notificationHints: [{
45254
- ...candidate,
45255
- attempts,
45256
- nextAttemptAtMillis: Date.now() + notificationRetryDelay(attempts)
45257
- }],
45258
- refreshSource: false,
45259
- accountUserId: session.userId,
45260
- accountOwnerGeneration: ownerGeneration
45261
- });
45823
+ failed.push(entry);
45262
45824
  }
45263
- continue;
45264
45825
  }
45265
- if (!fallbackToBrowser) continue;
45266
- this.emitChatClientEvent({
45267
- type: "message-notification",
45268
- revision: this.nextChatClientRevision(),
45269
- notification
45270
- });
45271
- }
45272
- return pending.filter(([uid]) => failedUids.has(uid));
45826
+ }));
45827
+ await Promise.all(notificationLanes);
45828
+ return failed;
45273
45829
  }
45274
45830
  emitChatClientEvent(event) {
45275
45831
  for (const listener of [...this.chatClientListeners]) listener(event);
@@ -46119,7 +46675,7 @@ var RecordReeditSubmissions = class {
46119
46675
  };
46120
46676
  //#endregion
46121
46677
  //#region src/services/record-reedit-attachments.ts
46122
- const invalid$2 = () => new ArkmePluginError("record-reedit-attachment-invalid", "附件引用无效或不属于这条快记,请重新读取后选择", false, 409);
46678
+ const invalid$3 = () => new ArkmePluginError("record-reedit-attachment-invalid", "附件引用无效或不属于这条快记,请重新读取后选择", false, 409);
46123
46679
  const refs = (payload) => Array.isArray(payload?.media_refs) ? payload.media_refs.map(objectValue$2) : [];
46124
46680
  function recordReeditIsBackgroundSound(ref) {
46125
46681
  return ref.content_file_role === 4;
@@ -46141,14 +46697,14 @@ function recordReeditAttachmentSelection(value, payload) {
46141
46697
  try {
46142
46698
  selection = parseArkmeRecordReeditAttachments(value);
46143
46699
  } catch {
46144
- throw invalid$2();
46700
+ throw invalid$3();
46145
46701
  }
46146
46702
  const groups = recordReeditMediaGroups(payload);
46147
46703
  const supported = new Set(groups.map((group) => group.fileAssetUid));
46148
- if (selection.some((item) => item.fileAssetUid !== void 0 && !supported.has(item.fileAssetUid))) throw invalid$2();
46704
+ if (selection.some((item) => item.fileAssetUid !== void 0 && !supported.has(item.fileAssetUid))) throw invalid$3();
46149
46705
  const grouped = new Set(groups.flatMap((group) => group.refs));
46150
- if (refs(payload).some((ref) => !recordReeditIsBackgroundSound(ref) && ref.render_role === 4 && !groups.some((group) => group.refs.some((item) => item.file_asset_uid === ref.file_asset_uid)))) throw invalid$2();
46151
- if (groups.some((group) => group.fileAssetUid === "") || grouped.size !== groups.reduce((sum, group) => sum + group.refs.length, 0)) throw invalid$2();
46706
+ if (refs(payload).some((ref) => !recordReeditIsBackgroundSound(ref) && ref.render_role === 4 && !groups.some((group) => group.refs.some((item) => item.file_asset_uid === ref.file_asset_uid)))) throw invalid$3();
46707
+ if (groups.some((group) => group.fileAssetUid === "") || grouped.size !== groups.reduce((sum, group) => sum + group.refs.length, 0)) throw invalid$3();
46152
46708
  return selection;
46153
46709
  }
46154
46710
  function recordReeditAttachmentChanges(payload, selection) {
@@ -46170,11 +46726,11 @@ function recordReeditWithAttachments(payload, selection, uploads) {
46170
46726
  const media = selection.flatMap((item) => {
46171
46727
  if (item.fileAssetUid !== void 0) {
46172
46728
  const group = groups.find((value) => value.fileAssetUid === item.fileAssetUid);
46173
- if (group === void 0) throw invalid$2();
46729
+ if (group === void 0) throw invalid$3();
46174
46730
  return group.refs.map((ref) => structuredClone(ref));
46175
46731
  }
46176
46732
  const asset = uploads.get(item.fileRef);
46177
- if (asset === void 0 || asset.fileAssetUid.trim() === "") throw invalid$2();
46733
+ if (asset === void 0 || asset.fileAssetUid.trim() === "") throw invalid$3();
46178
46734
  return [{
46179
46735
  file_asset_uid: asset.fileAssetUid,
46180
46736
  render_role: 1,
@@ -47701,21 +48257,21 @@ var RecordService = class {
47701
48257
  const LOCAL_FILE_REF = /^arkme-file-v1\.[0-9a-f-]{36}$/;
47702
48258
  const ASSET_REF = /^[A-Za-z0-9._:-]{8,256}$/;
47703
48259
  const MAX_AMPLITUDES = 4096;
47704
- function invalid$1(message) {
48260
+ function invalid$2(message) {
47705
48261
  return new ArkmePluginError("background-sound-invalid", message, false, 400);
47706
48262
  }
47707
48263
  function invalidFile(message) {
47708
48264
  return new ArkmePluginError("background-sound-file-invalid", message, false, 400);
47709
48265
  }
47710
48266
  function arkmeBackgroundSoundAmplitudes(values) {
47711
- if (!Array.isArray(values) || values.length > MAX_AMPLITUDES || values.some((value) => typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1)) throw invalid$1("背景音振幅数量不能超过 4096,且每项必须是 0 到 1 之间的有限数字");
48267
+ if (!Array.isArray(values) || values.length > MAX_AMPLITUDES || values.some((value) => typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1)) throw invalid$2("背景音振幅数量不能超过 4096,且每项必须是 0 到 1 之间的有限数字");
47712
48268
  return [...values];
47713
48269
  }
47714
48270
  function arkmeFileBackgroundSound(value, allFileRefs) {
47715
48271
  if (value === void 0) return void 0;
47716
- if (!Array.isArray(value.fileRefs) || value.fileRefs.length === 0 || value.fileRefs.some((ref) => typeof ref !== "string" || !LOCAL_FILE_REF.test(ref)) || new Set(value.fileRefs).size !== value.fileRefs.length) throw invalid$1("背景音文件引用为空、重复或无效");
48272
+ if (!Array.isArray(value.fileRefs) || value.fileRefs.length === 0 || value.fileRefs.some((ref) => typeof ref !== "string" || !LOCAL_FILE_REF.test(ref)) || new Set(value.fileRefs).size !== value.fileRefs.length) throw invalid$2("背景音文件引用为空、重复或无效");
47717
48273
  const allowed = new Set(allFileRefs);
47718
- if (value.fileRefs.some((ref) => !allowed.has(ref))) throw invalid$1("背景音文件必须属于同一发送任务");
48274
+ if (value.fileRefs.some((ref) => !allowed.has(ref))) throw invalid$2("背景音文件必须属于同一发送任务");
47719
48275
  return {
47720
48276
  fileRefs: [...value.fileRefs],
47721
48277
  amplitudes: arkmeBackgroundSoundAmplitudes(value.amplitudes)
@@ -47745,10 +48301,10 @@ function normalizedBackgroundAsset(asset) {
47745
48301
  }
47746
48302
  function arkmeRichBackgroundSound(value) {
47747
48303
  if (value === void 0) return void 0;
47748
- if (!Array.isArray(value.assets) || value.assets.length === 0 || value.assets.length > 20) throw invalid$1("背景音资产为空或数量超限");
48304
+ if (!Array.isArray(value.assets) || value.assets.length === 0 || value.assets.length > 20) throw invalid$2("背景音资产为空或数量超限");
47749
48305
  const assets = value.assets.map(normalizedBackgroundAsset);
47750
48306
  const identities = assets.map((asset) => asset.fileAssetUid);
47751
- if (new Set(identities).size !== identities.length) throw invalid$1("背景音资产不能重复");
48307
+ if (new Set(identities).size !== identities.length) throw invalid$2("背景音资产不能重复");
47752
48308
  return {
47753
48309
  assets,
47754
48310
  amplitudes: arkmeBackgroundSoundAmplitudes(value.amplitudes)
@@ -49252,12 +49808,13 @@ var ChatService = class {
49252
49808
  if (!options.signal?.aborted && cacheEpoch === (this.runtime.memberCacheEpoch?.() ?? 0)) await this.runtime.stateStore.mergeConversationMembers?.(session.userId, source.ownerRef, result).catch(() => void 0);
49253
49809
  return result;
49254
49810
  }
49255
- async memberRead(session, group, path, body, signal) {
49811
+ async memberRead(session, group, path, body, signal, cacheMs) {
49256
49812
  try {
49257
49813
  return await this.runtime.authenticatedChatPost(path, body, session, signal, {
49258
49814
  lane: path.endsWith("/members/page") ? "interactive-read" : "background-read",
49259
49815
  key: `members:${this.runtime.memberCacheEpoch?.() ?? 0}:${path}:${JSON.stringify(body)}`,
49260
- failureCooldownMs: 2e3
49816
+ failureCooldownMs: 2e3,
49817
+ ...cacheMs === void 0 ? {} : { cacheMs }
49261
49818
  });
49262
49819
  } catch (error) {
49263
49820
  if (invalidatesMemberSnapshot(error)) {
@@ -50379,6 +50936,49 @@ var ChatService = class {
50379
50936
  recordOwnerUserId: reference.recordOwnerUserId
50380
50937
  };
50381
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
+ }
50382
50982
  async sourceMessageExtensionContext(sourceRef, messageActionRef, options = {}) {
50383
50983
  const session = await this.runtime.requireSession();
50384
50984
  const source = await this.source.openSourceRef(sourceRef.trim(), session.userId);
@@ -83924,13 +84524,13 @@ var GroupService = class {
83924
84524
  };
83925
84525
  //#endregion
83926
84526
  //#region src/services/common-group-service.ts
83927
- const invalid = () => new ArkmePluginError("common-groups-response-invalid", "共同群聊响应不完整,请重试", true, 502);
84527
+ const invalid$1 = () => new ArkmePluginError("common-groups-response-invalid", "共同群聊响应不完整,请重试", true, 502);
83928
84528
  function commonGroupChanges(value, known, after) {
83929
84529
  const raw = objectValue$2(value);
83930
- if (!Array.isArray(raw.items) || !Array.isArray(raw.removed) || typeof raw.has_more !== "boolean" || raw.items.length > 20 || raw.removed.length > 20) throw invalid();
84530
+ if (!Array.isArray(raw.items) || !Array.isArray(raw.removed) || typeof raw.has_more !== "boolean" || raw.items.length > 20 || raw.removed.length > 20) throw invalid$1();
83931
84531
  const items = raw.items.map((value) => {
83932
84532
  const row = objectValue$2(value);
83933
- if (typeof row.chat_session_uid !== "string" || !row.chat_session_uid || row.chat_session_uid.length > 128 || typeof row.title !== "string" || row.title.length > 4096 || !Number.isSafeInteger(row.member_count) || Number(row.member_count) < 0) throw invalid();
84533
+ if (typeof row.chat_session_uid !== "string" || !row.chat_session_uid || row.chat_session_uid.length > 128 || typeof row.title !== "string" || row.title.length > 4096 || !Number.isSafeInteger(row.member_count) || Number(row.member_count) < 0) throw invalid$1();
83934
84534
  return {
83935
84535
  uid: row.chat_session_uid,
83936
84536
  title: row.title,
@@ -83938,14 +84538,14 @@ function commonGroupChanges(value, known, after) {
83938
84538
  };
83939
84539
  });
83940
84540
  const removed = raw.removed;
83941
- if (removed.some((uid) => typeof uid !== "string" || !known.includes(uid))) throw invalid();
84541
+ if (removed.some((uid) => typeof uid !== "string" || !known.includes(uid))) throw invalid$1();
83942
84542
  const identities = [...items.map((row) => row.uid), ...removed];
83943
- if (new Set(identities).size !== identities.length) throw invalid();
84543
+ if (new Set(identities).size !== identities.length) throw invalid$1();
83944
84544
  if (known.length) {
83945
- if (raw.has_more || identities.length !== known.length || identities.some((uid) => !known.includes(String(uid)))) throw invalid();
83946
- } else if (removed.length || items.some((row, i) => row.uid <= (items[i - 1]?.uid ?? after))) throw invalid();
84545
+ if (raw.has_more || identities.length !== known.length || identities.some((uid) => !known.includes(String(uid)))) throw invalid$1();
84546
+ } else if (removed.length || items.some((row, i) => row.uid <= (items[i - 1]?.uid ?? after))) throw invalid$1();
83947
84547
  const next = raw.has_more ? raw.next_after : "";
83948
- if (typeof next !== "string" || raw.has_more && (!items.length || next !== items.at(-1)?.uid || next <= after)) throw invalid();
84548
+ if (typeof next !== "string" || raw.has_more && (!items.length || next !== items.at(-1)?.uid || next <= after)) throw invalid$1();
83949
84549
  return {
83950
84550
  items,
83951
84551
  removed,
@@ -87182,7 +87782,7 @@ var ProfileService = class {
87182
87782
  async userCard(userId, signal) {
87183
87783
  const session = await this.runtime.requireSession();
87184
87784
  if (!Number.isSafeInteger(userId) || userId <= 0) throw new ArkmePluginError("user-card-target-invalid", "用户信息参数无效", false);
87185
- const profile = (await this.publicProfileSummariesByUserIds([userId], session, signal)).get(userId);
87785
+ const profile = (await this.publicProfileSummariesByUserIds([userId], session, signal, 0)).get(userId);
87186
87786
  return {
87187
87787
  displayName: profile?.displayName ?? "群成员",
87188
87788
  ...profile?.avatarUrl === void 0 ? {} : { avatarRef: await this.sealProfileImageRef(session.userId, userId) }
@@ -87332,7 +87932,7 @@ var ProfileService = class {
87332
87932
  const avatarRef = profile.avatarRef.trim();
87333
87933
  return /^https?:\/\//i.test(avatarRef) || fileAssetAvatarRef(avatarRef) !== void 0;
87334
87934
  }
87335
- async publicProfileSummariesByUserIds(userIds, session, signal) {
87935
+ async publicProfileSummariesByUserIds(userIds, session, signal, maxAgeMillis = PUBLIC_PROFILE_CACHE_TTL_MS) {
87336
87936
  const normalized = [...new Set(userIds.filter((userId) => Number.isSafeInteger(userId) && userId > 0))].sort((left, right) => left - right);
87337
87937
  const profiles = /* @__PURE__ */ new Map();
87338
87938
  const missing = [];
@@ -87340,7 +87940,7 @@ var ProfileService = class {
87340
87940
  for (const [key, cached] of this.publicProfileCache) if (cached.expiresAtMillis <= now) this.publicProfileCache.delete(key);
87341
87941
  for (const userId of normalized) {
87342
87942
  const cached = this.publicProfileCache.get(`${String(session.userId)}:${String(userId)}`);
87343
- 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) {
87344
87944
  missing.push(userId);
87345
87945
  continue;
87346
87946
  }
@@ -87884,6 +88484,28 @@ var RelatedQuickNoteService = class {
87884
88484
  }
87885
88485
  };
87886
88486
  //#endregion
88487
+ //#region src/recording-search-version.ts
88488
+ /** Public Audio contract: hash raw decrypted text, retaining whitespace and Unicode. */
88489
+ function recordingSearchVersion(item) {
88490
+ return createHash("sha256").update([
88491
+ item.sessionId,
88492
+ item.childId,
88493
+ item.asrItemIndex,
88494
+ item.transcriptSource,
88495
+ item.startAtMillis,
88496
+ item.endAtMillis,
88497
+ item.text
88498
+ ].join("\n"), "utf8").digest("hex");
88499
+ }
88500
+ function recordingSearchIdentityHash(item) {
88501
+ return createHash("sha256").update(JSON.stringify([
88502
+ item.sessionId,
88503
+ item.transcriptSource,
88504
+ item.childId,
88505
+ item.itemIndex
88506
+ ]), "utf8").digest("hex");
88507
+ }
88508
+ //#endregion
87887
88509
  //#region src/recording-import-coordinator.ts
87888
88510
  function importFailure(error) {
87889
88511
  if (error instanceof RecordingImportContractError) return {
@@ -88417,6 +89039,7 @@ function projectRecordingTranscripts(response, speakerResponse, profilesByUserId
88417
89039
  isSelf,
88418
89040
  isBackground,
88419
89041
  text,
89042
+ rawTranscriptText: stringValue(row.t ?? row.text),
88420
89043
  ...generationText === text ? {} : { generationText },
88421
89044
  ...stringValue(row.p).trim() === "" ? {} : { event: stringValue(row.p).trim() },
88422
89045
  sourceIndex,
@@ -89817,6 +90440,18 @@ var RecordingService = class {
89817
90440
  return {
89818
90441
  itemId: item.itemId,
89819
90442
  itemRef: this.sealRecordingRefWithKey("arkme-recording-item-v1", payload, refKey),
90443
+ ...item.transcriptSource !== "system" ? {} : {
90444
+ searchIdentityHash: recordingSearchIdentityHash({
90445
+ sessionId: item.sessionId,
90446
+ childId: item.childId,
90447
+ itemIndex: item.asrItemIndex,
90448
+ transcriptSource: item.transcriptSource
90449
+ }),
90450
+ searchVersion: recordingSearchVersion({
90451
+ ...item,
90452
+ text: item.rawTranscriptText ?? item.text
90453
+ })
90454
+ },
89820
90455
  transcriptSource: item.transcriptSource,
89821
90456
  sessionKey: createHmac("sha256", refKey).update(`recording-session:${String(viewerUserId)}:${item.sessionId}`).digest("base64url"),
89822
90457
  startAtMillis: item.startAtMillis,
@@ -90008,6 +90643,68 @@ var RecordingService = class {
90008
90643
  }
90009
90644
  };
90010
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
90011
90708
  //#region src/services/recording-import-gateway.ts
90012
90709
  const AUDIO_SESSION_RECOVERY_LIMIT = 500;
90013
90710
  const AUDIO_IMPORT_DUPLICATE_BATCH_SIZE = 100;
@@ -90361,6 +91058,8 @@ var AudioRecordingImportGateway = class {
90361
91058
  accessKeyId: credentials.accessKeyId,
90362
91059
  accessKeySecret: credentials.accessKeySecret,
90363
91060
  stsToken: credentials.stsToken,
91061
+ timeout: RECORDING_UPLOAD_REQUEST_TIMEOUT,
91062
+ retryMax: 0,
90364
91063
  refreshSTSTokenInterval: 300 * 1e3,
90365
91064
  refreshSTSToken: async () => {
90366
91065
  await this.requireJobSession(job);
@@ -90391,17 +91090,40 @@ var AudioRecordingImportGateway = class {
90391
91090
  signal?.addEventListener("abort", abort, { once: true });
90392
91091
  try {
90393
91092
  const objectPath = `pc_upload/${String(job.userId)}/${job.sessionId}/${remoteFileName(job)}`;
90394
- const upload = client.multipartUpload(objectPath, job.sourceHandle, {
90395
- parallel: 1,
90396
- partSize: 5 * 1024 * 1024,
90397
- checkpoint: job.uploadCheckpoint,
90398
- mime: job.mimeType,
90399
- progress: async (percentage, checkpoint) => {
90400
- if (signal?.aborted === true) throw new RecordingImportContractError("recording-import-cancelled", "录音导入已取消");
90401
- const bounded = Math.min(1, Math.max(0, percentage));
90402
- 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
+ }
90403
91125
  }
90404
- });
91126
+ })();
90405
91127
  await (signal === void 0 ? upload : Promise.race([upload, aborted]));
90406
91128
  } finally {
90407
91129
  signal?.removeEventListener("abort", abort);
@@ -90789,6 +91511,60 @@ var RelatedRecordingService = class {
90789
91511
  }
90790
91512
  };
90791
91513
  //#endregion
91514
+ //#region src/recording-search-result.ts
91515
+ function invalid() {
91516
+ throw new ArkmePluginError("recording-search-invalid", "录音搜索结果已失效,请重试", true);
91517
+ }
91518
+ function segment(raw) {
91519
+ const value = objectValue$2(raw);
91520
+ if (!stringValue$2(value.session_id).trim() || !stringValue$2(value.child_id).trim() || !Number.isSafeInteger(value.item_index) || Number(value.item_index) < 0 || value.transcript_source !== "system" || !stringValue$2(value.transcript_version).trim() || typeof value.text !== "string" || !Number.isSafeInteger(value.start_at) || !Number.isSafeInteger(value.end_at)) invalid();
91521
+ const speaker = objectValue$2(value.speaker);
91522
+ return {
91523
+ sessionId: String(value.session_id),
91524
+ childId: String(value.child_id),
91525
+ itemIndex: Number(value.item_index),
91526
+ transcriptSource: "system",
91527
+ transcriptVersion: String(value.transcript_version),
91528
+ startAtMillis: Number(value.start_at),
91529
+ endAtMillis: Number(value.end_at),
91530
+ text: String(value.text),
91531
+ ...value.speaker === void 0 ? {} : { speaker: {
91532
+ speakerId: stringValue$2(speaker.speaker_id),
91533
+ label: stringValue$2(speaker.label) || "未知说话人",
91534
+ ...typeof speaker.user_id === "number" && speaker.user_id > 0 ? { userId: speaker.user_id } : {},
91535
+ ...typeof speaker.color_index === "number" ? { colorIndex: speaker.color_index } : {}
91536
+ } }
91537
+ };
91538
+ }
91539
+ function parseRecordingSearchItem(raw) {
91540
+ const value = objectValue$2(raw);
91541
+ const match = segment(value.match);
91542
+ const previous = value.previous == null ? void 0 : segment(value.previous);
91543
+ const next = value.next == null ? void 0 : segment(value.next);
91544
+ if (match.sessionId !== value.session_id) invalid();
91545
+ const day = Number(value.date_stamp);
91546
+ if ([previous, next].some((item) => item && item.sessionId !== match.sessionId && (!Number.isSafeInteger(day) || day <= 0 || item.startAtMillis < day || item.startAtMillis >= day + 864e5))) invalid();
91547
+ if (previous && previous.startAtMillis > match.startAtMillis || next && next.startAtMillis < match.startAtMillis) invalid();
91548
+ const ranges = Array.isArray(value.highlight_ranges) ? value.highlight_ranges : [];
91549
+ const length = Array.from(match.text).length;
91550
+ const highlightRanges = ranges.map(objectValue$2).map((range) => ({
91551
+ start: Number(range.start_index),
91552
+ length: Number(range.length)
91553
+ })).filter((range) => Number.isSafeInteger(range.start) && Number.isSafeInteger(range.length) && range.start >= 0 && range.length > 0 && range.start + range.length <= length).sort((a, b) => a.start - b.start).filter((range, index, all) => index === 0 || range.start >= all[index - 1].start + all[index - 1].length);
91554
+ return {
91555
+ sessionId: match.sessionId,
91556
+ dateStamp: Number(value.date_stamp) || 0,
91557
+ startAtMillis: match.startAtMillis,
91558
+ snippet: match.text,
91559
+ score: Number(value.score) || 0,
91560
+ match,
91561
+ highlightRanges,
91562
+ ...stringValue$2(value.record_uid) ? { recordUid: String(value.record_uid) } : {},
91563
+ ...previous ? { previous } : {},
91564
+ ...next ? { next } : {}
91565
+ };
91566
+ }
91567
+ //#endregion
90792
91568
  //#region src/dsh-search-origins.ts
90793
91569
  /** Resolve sync identity using the public index and exact deterministic record IDs. */
90794
91570
  async function resolveDshSearchOrigins(provider, items, signal) {
@@ -91115,13 +91891,15 @@ var SearchService = class {
91115
91891
  media;
91116
91892
  source;
91117
91893
  privacy;
91894
+ profile;
91118
91895
  localDshQuery;
91119
- constructor(runtime, record, media, source, privacy = new ArkmePrivacyVisibilityService(runtime)) {
91896
+ constructor(runtime, record, media, source, privacy = new ArkmePrivacyVisibilityService(runtime), profile) {
91120
91897
  this.runtime = runtime;
91121
91898
  this.record = record;
91122
91899
  this.media = media;
91123
91900
  this.source = source;
91124
91901
  this.privacy = privacy;
91902
+ this.profile = profile;
91125
91903
  }
91126
91904
  async searchRecords(options) {
91127
91905
  const query = options.query.trim();
@@ -91483,22 +92261,37 @@ var SearchService = class {
91483
92261
  const session = await this.runtime.requireSession();
91484
92262
  const data = await this.runtime.authenticatedPost("/api/v1/search/recordings/query", {
91485
92263
  keyword: query,
92264
+ result_mode: "segments",
91486
92265
  limit: Math.min(50, Math.max(1, Math.trunc(options.limit))),
91487
92266
  ...options.cursor?.trim() ? { cursor: options.cursor.trim() } : {}
91488
92267
  }, session, options.signal);
92268
+ const items = listValue$3(data.items).map(parseRecordingSearchItem);
92269
+ const segments = items.flatMap((item) => [
92270
+ item.previous,
92271
+ item.match,
92272
+ item.next
92273
+ ].filter((value) => value !== void 0));
92274
+ const userIds = [...new Set(segments.flatMap((item) => item.speaker?.userId === void 0 ? [] : [item.speaker.userId]))];
92275
+ if (this.profile !== void 0 && userIds.length > 0) try {
92276
+ const profiles = await this.profile.publicProfileSummariesByUserIds(userIds, session, options.signal);
92277
+ const avatars = /* @__PURE__ */ new Map();
92278
+ await Promise.all([...profiles].map(async ([userId, profile]) => {
92279
+ if (profile.avatarUrl !== void 0) avatars.set(userId, await this.profile.sealProfileImageRef(session.userId, userId));
92280
+ }));
92281
+ for (const segment of segments) {
92282
+ const speaker = segment.speaker;
92283
+ if (speaker?.userId === void 0) continue;
92284
+ const profile = profiles.get(speaker.userId);
92285
+ const avatarRef = avatars.get(speaker.userId);
92286
+ if (avatarRef !== void 0) speaker.avatarRef = avatarRef;
92287
+ if (profile && (speaker.userId === session.userId || speaker.label === "未知说话人")) speaker.label = profile.displayName.trim() || profile.nickname.trim() || speaker.label;
92288
+ }
92289
+ } catch (error) {
92290
+ if (options.signal?.aborted) throw error;
92291
+ }
91489
92292
  const guard = objectValue$2(data.query_guard);
91490
92293
  return {
91491
- items: listValue$3(data.items).map((raw) => {
91492
- const item = objectValue$2(raw);
91493
- return {
91494
- sessionId: stringValue$2(item.session_id).trim(),
91495
- ...stringValue$2(item.record_uid).trim() === "" ? {} : { recordUid: stringValue$2(item.record_uid).trim() },
91496
- dateStamp: numberValue$2(item.date_stamp),
91497
- startAtMillis: numberValue$2(item.start_at),
91498
- snippet: clippedText(item.snippet, 1e3),
91499
- score: numberValue$2(item.score)
91500
- };
91501
- }).filter((item) => item.sessionId !== "" && item.snippet !== ""),
92294
+ items,
91502
92295
  hasMore: booleanValue$1(data.has_more),
91503
92296
  ...stringValue$2(data.next_cursor).trim() === "" ? {} : { nextCursor: stringValue$2(data.next_cursor).trim() },
91504
92297
  queryGuard: {
@@ -94222,6 +95015,15 @@ var WorldService = class {
94222
95015
  media;
94223
95016
  record;
94224
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
+ }
94225
95027
  worldImageRefs = /* @__PURE__ */ new Map();
94226
95028
  worldAvatarResolutionCache = /* @__PURE__ */ new Map();
94227
95029
  extensionPublicationShareCache = /* @__PURE__ */ new Map();
@@ -95342,7 +96144,7 @@ var ArkmeService = class {
95342
96144
  await this.realtime.invalidateRecordProjection();
95343
96145
  }, async (source, text, humans, bots, session, textFormat) => await this.chat.resolveMentions(source, text, text, humans, bots, session, void 0, textFormat));
95344
96146
  this.calendar = new CalendarService(this.runtime, this.privacy, this.media, this.record, this.source, this.callHistory);
95345
- this.search = new SearchService(this.runtime, this.record, this.media, this.source, this.privacy);
96147
+ this.search = new SearchService(this.runtime, this.record, this.media, this.source, this.privacy, this.profile);
95346
96148
  if (localDshQuery !== void 0) this.search.localDshQuery = localDshQuery;
95347
96149
  this.bot = new BotService(this.runtime, this.source);
95348
96150
  this.messageActions = new MessageActionService(new ArkmeMessageActionGateway(this.runtime, this.source, async (target, sequence) => {
@@ -95768,6 +96570,7 @@ var ArkmeService = class {
95768
96570
  messageReport: true,
95769
96571
  userBanManagement: true,
95770
96572
  directMessageAdmission: true,
96573
+ reactionsV1: true,
95771
96574
  groupOwnerGovernance: true,
95772
96575
  ...this.config.markdownQuickNotesEnabled === true ? { markdownQuickNotes: true } : {},
95773
96576
  ...this.config.markdownLongArticlesEnabled === true ? { markdownLongArticles: true } : {},
@@ -96559,6 +97362,112 @@ var ArkmeService = class {
96559
97362
  async favoriteStickers(signal) {
96560
97363
  return await this.chat.favoriteStickers(signal);
96561
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
+ }
96562
97471
  async addFavoriteSticker(item, signal) {
96563
97472
  return await this.chat.addFavoriteSticker(item, signal);
96564
97473
  }
@@ -96777,6 +97686,12 @@ var ArkmeService = class {
96777
97686
  async testLogin(userId) {
96778
97687
  return await this.auth.testLogin(userId);
96779
97688
  }
97689
+ async sendEmailBindCode(expectedUserId, email) {
97690
+ return await this.auth.sendEmailBindCode(expectedUserId, email);
97691
+ }
97692
+ async bindEmail(expectedUserId, email, code) {
97693
+ return await this.auth.bindEmail(expectedUserId, email, code);
97694
+ }
96780
97695
  async sendPhoneCode(phone, captcha) {
96781
97696
  return await this.auth.sendPhoneCode(phone, captcha);
96782
97697
  }
@@ -96792,6 +97707,15 @@ var ArkmeService = class {
96792
97707
  async verifyPhoneCode(phone, code) {
96793
97708
  return await this.auth.verifyPhoneCode(phone, code);
96794
97709
  }
97710
+ async previewCancellation(expectedUserId) {
97711
+ return await this.auth.previewCancellation(expectedUserId);
97712
+ }
97713
+ async submitCancellation(expectedUserId, expectedMode) {
97714
+ return await this.auth.submitCancellation(expectedUserId, expectedMode);
97715
+ }
97716
+ async resolveCancellationLogin(continueLogin) {
97717
+ return await this.auth.resolveCancellationLogin(continueLogin);
97718
+ }
96795
97719
  async logout() {
96796
97720
  return await this.auth.logout();
96797
97721
  }