@co0ontty/wand 4.102.0 → 4.103.0

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 (212) hide show
  1. package/dist/ai-team-live.js +1 -1
  2. package/dist/apns-client.d.ts +45 -0
  3. package/dist/apns-client.js +156 -0
  4. package/dist/apns-service.d.ts +29 -0
  5. package/dist/apns-service.js +156 -0
  6. package/dist/build-info.json +3 -3
  7. package/dist/conversation-service.d.ts +2 -0
  8. package/dist/conversation-service.js +23 -3
  9. package/dist/conversation-types.d.ts +2 -0
  10. package/dist/dingtalk-auth.d.ts +41 -0
  11. package/dist/dingtalk-auth.js +127 -0
  12. package/dist/dingtalk-dws.d.ts +45 -0
  13. package/dist/dingtalk-dws.js +240 -0
  14. package/dist/dingtalk-service.d.ts +100 -0
  15. package/dist/dingtalk-service.js +865 -0
  16. package/dist/dingtalk-stream.d.ts +24 -0
  17. package/dist/dingtalk-stream.js +115 -0
  18. package/dist/dingtalk-types.d.ts +138 -0
  19. package/dist/dingtalk-types.js +1 -0
  20. package/dist/employee-knowledge.js +8 -0
  21. package/dist/live-activity-service.d.ts +26 -0
  22. package/dist/live-activity-service.js +158 -0
  23. package/dist/live-activity-state.d.ts +37 -0
  24. package/dist/live-activity-state.js +108 -0
  25. package/dist/middleware/api-scope.js +2 -0
  26. package/dist/pi-cli-checkpoint.d.ts +17 -0
  27. package/dist/pi-cli-checkpoint.js +10 -0
  28. package/dist/pi-event-projection.js +13 -0
  29. package/dist/pi-execution.js +2 -1
  30. package/dist/pi-protocol-reader.d.ts +6 -2
  31. package/dist/pi-protocol-reader.js +36 -12
  32. package/dist/pi-tmux-host.d.ts +1 -1
  33. package/dist/pi-tmux-host.js +28 -8
  34. package/dist/pi-tool-result-preview.d.ts +23 -0
  35. package/dist/pi-tool-result-preview.js +185 -0
  36. package/dist/render-daemon-client.js +1 -1
  37. package/dist/render-host.d.ts +3 -3
  38. package/dist/render-host.js +3 -3
  39. package/dist/server-apns-routes.d.ts +12 -0
  40. package/dist/server-apns-routes.js +111 -0
  41. package/dist/server-dingtalk-routes.d.ts +8 -0
  42. package/dist/server-dingtalk-routes.js +35 -0
  43. package/dist/server.js +49 -3
  44. package/dist/session-completion.d.ts +2 -1
  45. package/dist/session-completion.js +6 -2
  46. package/dist/session-registry.d.ts +3 -1
  47. package/dist/session-registry.js +3 -1
  48. package/dist/storage/apns-repository.d.ts +52 -0
  49. package/dist/storage/apns-repository.js +108 -0
  50. package/dist/storage/conversations-repository.d.ts +2 -0
  51. package/dist/storage/conversations-repository.js +10 -0
  52. package/dist/storage/dingtalk-repository.d.ts +20 -0
  53. package/dist/storage/dingtalk-repository.js +75 -0
  54. package/dist/storage/live-activity-repository.d.ts +68 -0
  55. package/dist/storage/live-activity-repository.js +117 -0
  56. package/dist/storage/migrations.js +22 -0
  57. package/dist/storage/sessions-repository.d.ts +5 -0
  58. package/dist/storage/sessions-repository.js +43 -1
  59. package/dist/storage-schema.js +2 -0
  60. package/dist/storage.d.ts +7 -1
  61. package/dist/storage.js +10 -0
  62. package/dist/structured-exec-host.d.ts +9 -1
  63. package/dist/structured-exec-pump.d.ts +11 -0
  64. package/dist/structured-exec-pump.js +27 -8
  65. package/dist/structured-pi-adapter.js +5 -0
  66. package/dist/structured-runner.d.ts +2 -0
  67. package/dist/structured-session-manager.d.ts +2 -0
  68. package/dist/structured-session-manager.js +51 -4
  69. package/dist/types.d.ts +7 -0
  70. package/dist/web-ui/content/module-manifest.json +1 -1
  71. package/dist/web-ui/content/modules/{4f30a913cc5b3f89 → 02fad74223254019}/chunk.js +1 -1
  72. package/dist/web-ui/content/modules/{acb21c7e7fd6cb67 → 033e0a741edd2ba1}/page.js +1 -1
  73. package/dist/web-ui/content/modules/{e61136c22675ec7d → 0c04f94bbfd39a2f}/chunk.js +1 -1
  74. package/dist/web-ui/content/modules/{119874145414688d → 12e91deb96ace24d}/shell-file-panel-drawer.js +1 -1
  75. package/dist/web-ui/content/modules/15279373f0e1461f/host.js +1 -0
  76. package/dist/web-ui/content/modules/{78a46adbf9807f89 → 17603a34d545c784}/chunk.js +1 -1
  77. package/dist/web-ui/content/modules/1f041aa8d3673d93/chunk.js +1 -0
  78. package/dist/web-ui/content/modules/{96dd661559b67ebf → 20483d6e57cfe9d7}/chunk.js +1 -1
  79. package/dist/web-ui/content/modules/{f55031a02775c89e → 217595f72a1c3fc5}/chunk.js +1 -1
  80. package/dist/web-ui/content/modules/2230c51655163cf3/runtime.js +279 -0
  81. package/dist/web-ui/content/modules/{82e40fdf0abfb8cd → 2460e872ab3a1f37}/chunk.js +1 -1
  82. package/dist/web-ui/content/modules/{9e754b01fa4207fb → 2a47853ca7752c1f}/chunk.js +1 -1
  83. package/dist/web-ui/content/modules/{e95b60ab18a79499 → 2afbc201474adacc}/login-visual.js +1 -1
  84. package/dist/web-ui/content/modules/{38917c19e2cf1702 → 30169d15cea782f0}/chunk.js +1 -1
  85. package/dist/web-ui/content/modules/{5513bcbacdf44588 → 37abb74d1a0a0791}/notice-view.js +1 -1
  86. package/dist/web-ui/content/modules/{b62294e53dcb3147 → 38bb024e09cb5d5d}/host.js +1 -1
  87. package/dist/web-ui/content/modules/39faef0cffbad28e/home.js +6 -0
  88. package/dist/web-ui/content/modules/3a2fa12c8b537964/chunk.js +1 -0
  89. package/dist/web-ui/content/modules/{35a8fd2cfd31d7a0 → 3a72b79418f0f80a}/chunk.js +1 -1
  90. package/dist/web-ui/content/modules/{321bd9c755bc30fc → 3bd3c84c4257ba0d}/chunk.js +1 -1
  91. package/dist/web-ui/content/modules/{2981483671797b92 → 3d10e3f6f6f92b55}/chunk.js +1 -1
  92. package/dist/web-ui/content/modules/{6f341fe3e8aee844 → 3d4451bc069c1498}/chunk.js +1 -1
  93. package/dist/web-ui/content/modules/{4a9a3e1f87b2c32b → 3d5c498522b48225}/dialog-modal.js +1 -1
  94. package/dist/web-ui/content/modules/426b8133daebff78/host.js +1 -0
  95. package/dist/web-ui/content/modules/455436229dc57d7f/sidebar-peek.js +1 -0
  96. package/dist/web-ui/content/modules/46de3c2e2ca680c1/host.js +1 -0
  97. package/dist/web-ui/content/modules/47af6b35e1d01ea7/host.js +30 -0
  98. package/dist/web-ui/content/modules/4841e45cb7c5cc67/queue-view-adapter.js +3 -0
  99. package/dist/web-ui/content/modules/{47419445e0aaa3cc → 497789a07177ed7b}/chunk.js +1 -1
  100. package/dist/web-ui/content/modules/{32c461164a1b364c → 5472191457a926d3}/chunk.js +1 -1
  101. package/dist/web-ui/content/modules/{c152782274c04fad → 55a47b6e2c2016f2}/chunk.js +1 -1
  102. package/dist/web-ui/content/modules/5743d8116d15696b/employee-card.js +1 -0
  103. package/dist/web-ui/content/modules/{c81a3d95b14cdcd4 → 578703196f82222d}/chunk.js +1 -1
  104. package/dist/web-ui/content/modules/{8ce71665ae55ccf1 → 57d68f0a47e8fd7c}/toast-region.js +1 -1
  105. package/dist/web-ui/content/modules/58866143a3365521/chunk.js +1 -0
  106. package/dist/web-ui/content/modules/{3601736ae2d678ef → 5bb5db89a343817f}/chunk.js +1 -1
  107. package/dist/web-ui/content/modules/{a098c7b1aa4e2f6b → 5c9e8245d4a8a312}/sha256.js +1 -1
  108. package/dist/web-ui/content/modules/{8198bfd269c0caca → 5de35db482fb1a42}/chunk.js +1 -1
  109. package/dist/web-ui/content/modules/{aab92f2990a7185f → 5e7104e569359449}/chunk.js +1 -1
  110. package/dist/web-ui/content/modules/{4b9deeeb77570db3 → 601a2767ae24d1cb}/chunk.js +1 -1
  111. package/dist/web-ui/content/modules/6687ad7aa57f443b/task-board-host.js +22 -0
  112. package/dist/web-ui/content/modules/{c422f47fd13dc4bc → 684db83476dcba20}/chunk.js +1 -1
  113. package/dist/web-ui/content/modules/{cf715621edb1d12e → 6b0bafa239dfe4e2}/chunk.js +1 -1
  114. package/dist/web-ui/content/modules/{cd22aa55f6cc5276 → 6bd6ee3c9f05e2f5}/chunk.js +1 -1
  115. package/dist/web-ui/content/modules/{09ef1bb1d195d727 → 79b0c18f2bf35740}/host.js +1 -1
  116. package/dist/web-ui/content/modules/{905f8bc3f8a3efb8 → 7ae83a3f06f2daa3}/chunk.js +1 -1
  117. package/dist/web-ui/content/modules/{84b79a33c64e146c → 7f32261c1627c3cd}/chunk.js +1 -1
  118. package/dist/web-ui/content/modules/{eece3c2d2374e8f7 → 805b859b1b76ce4d}/chunk.js +1 -1
  119. package/dist/web-ui/content/modules/{eecaebc5482ee203 → 84358b5b729cbe48}/chunk.js +1 -1
  120. package/dist/web-ui/content/modules/85181d84d5ce62b3/employee-profile.js +1 -0
  121. package/dist/web-ui/content/modules/87edd7cc79811bb8/composer-surface-adapter.js +1 -0
  122. package/dist/web-ui/content/modules/{b0254a2d3652a925 → 8ea498cf092dba30}/chunk.js +1 -1
  123. package/dist/web-ui/content/modules/{a4af270960a39ea6 → 8f0d4e22c18a58b0}/chunk.js +1 -1
  124. package/dist/web-ui/content/modules/{c72c4f1d47c3726d → 907dcf0ea5b5f23f}/chunk.js +1 -1
  125. package/dist/web-ui/content/modules/{9f33acc1c4284e03 → 916021ce252882ca}/chunk.js +1 -1
  126. package/dist/web-ui/content/modules/{98c14fc0474455a5 → 91ee32135a3582b0}/chunk.js +1 -1
  127. package/dist/web-ui/content/modules/92df279c40bc4218/host.js +1 -0
  128. package/dist/web-ui/content/modules/{544dc812abdfbbb8 → 9661b076f08e64b4}/chunk.js +1 -1
  129. package/dist/web-ui/content/modules/{5c94fb2b7abcd1a5 → 97ef63ff1f0784d9}/chunk.js +1 -1
  130. package/dist/web-ui/content/modules/99e48e07517d3f83/dropdown-surface.js +1 -0
  131. package/dist/web-ui/content/modules/9eb83751622e2b97/chunk.js +10 -0
  132. package/dist/web-ui/content/modules/{7375574a2ea42740 → a1715be0f97c2479}/chunk.js +1 -1
  133. package/dist/web-ui/content/modules/{456634525d44777d → a2be1fd01cf06837}/chunk.js +1 -1
  134. package/dist/web-ui/content/modules/a33666b143331cd6/workspace-window.js +1 -0
  135. package/dist/web-ui/content/modules/{b38924f45bfa71d6 → a41351130003e6bb}/chunk.js +1 -1
  136. package/dist/web-ui/content/modules/a59c83b5b5b85da2/host.js +1 -0
  137. package/dist/web-ui/content/modules/{a4dca2ffaf656d3c → a63f0b3d73d97e0b}/chunk.js +1 -1
  138. package/dist/web-ui/content/modules/a685793c5d9b90d1/chunk.js +1 -0
  139. package/dist/web-ui/content/modules/a750f4868304b089/host.js +1 -0
  140. package/dist/web-ui/content/modules/abfe4b88dcd89d88/host.js +1 -0
  141. package/dist/web-ui/content/modules/{15bcc550ef9fd5b7 → acda148b7423bdab}/host.js +1 -1
  142. package/dist/web-ui/content/modules/{ad1136919a65fc6e → af6ff78aeb782cf2}/host.js +1 -1
  143. package/dist/web-ui/content/modules/b32c9addcd31f54b/panel-host.js +1 -0
  144. package/dist/web-ui/content/modules/{b937f1a939618be9 → b358eaf2e45849a6}/host.js +1 -1
  145. package/dist/web-ui/content/modules/{476ff7911bce1a38 → b7aeeb22bf604ec7}/chunk.js +1 -1
  146. package/dist/web-ui/content/modules/bad2c42c7ddeab94/host.js +1 -0
  147. package/dist/web-ui/content/modules/{622a8d12c4c622bf → bb59e8cb4323bf06}/host.js +1 -1
  148. package/dist/web-ui/content/modules/{08c7b047195f138d → be497f1d39753979}/chunk.js +1 -1
  149. package/dist/web-ui/content/modules/bfa8e2c827201374/chunk.js +1 -0
  150. package/dist/web-ui/content/modules/{5395fa57a47e9b96 → bfe51130a84795fb}/chunk.js +1 -1
  151. package/dist/web-ui/content/modules/{8e29056fb56439f7 → c03e0f84f798d33a}/chunk.js +1 -1
  152. package/dist/web-ui/content/modules/c118e0c96bc023ee/host-bridge.js +1 -0
  153. package/dist/web-ui/content/modules/c3512b18c4c0e4ac/brand-identity.js +1 -0
  154. package/dist/web-ui/content/modules/c3e0de0a61f0db34/chunk.js +1 -0
  155. package/dist/web-ui/content/modules/{9d48d1ea3c9228bd → c3e1f083e07d6840}/library-bridge.js +1 -1
  156. package/dist/web-ui/content/modules/cd5e90a37ccdf2dc/group-editor.js +1 -0
  157. package/dist/web-ui/content/modules/{2edc19639ac1bd4b → cdb8281bb89da303}/chunk.js +1 -1
  158. package/dist/web-ui/content/modules/{f6f86d43c12b0864 → ce90d0b55bbe6430}/chunk.js +1 -1
  159. package/dist/web-ui/content/modules/{72b13a7486b6d437 → d274491ffc8ee646}/chunk.js +1 -1
  160. package/dist/web-ui/content/modules/{b1d43ed06a5d1a57 → d3cc94cf4a47a12a}/chunk.js +1 -1
  161. package/dist/web-ui/content/modules/{b0c4eed46c1b6ad4 → d4b07cef43d36e79}/chat-render.js +5 -4
  162. package/dist/web-ui/content/modules/{8a4f0ef2c0812af6 → d7b540917fb90755}/chunk.js +1 -1
  163. package/dist/web-ui/content/modules/{ef7f76b551fdc619 → d856bfc56bea5f20}/chunk.js +1 -1
  164. package/dist/web-ui/content/modules/{42c439d3acfe22bd → d857ea8244b1026e}/chunk.js +1 -1
  165. package/dist/web-ui/content/modules/{36ce8123a2079807 → d9019922118b2c37}/chunk.js +1 -1
  166. package/dist/web-ui/content/modules/{58836e616e6d12ff → d90e77f9f8d4f1ec}/workspaces-panel.js +1 -1
  167. package/dist/web-ui/content/modules/{4d4870f20eb1c52d → da1aa5fa0186fea7}/chunk.js +1 -1
  168. package/dist/web-ui/content/modules/de977ff0b3ad3882/progress.js +1 -0
  169. package/dist/web-ui/content/modules/{222f48948517167e → e01769df89873feb}/chunk.js +1 -1
  170. package/dist/web-ui/content/modules/{5df1b522f5d3c99d → e0ab9fa5a8882433}/host.js +1 -1
  171. package/dist/web-ui/content/modules/e25dea52e13504b6/markdown.js +1 -0
  172. package/dist/web-ui/content/modules/{36bd608366c56b72 → e43af4aa407885fd}/chunk.js +1 -1
  173. package/dist/web-ui/content/modules/ee893f06cdb2b4f7/panel.js +1 -0
  174. package/dist/web-ui/content/modules/{f90f80d7207dff1f → f2bdbdc4a1141826}/host.js +1 -1
  175. package/dist/web-ui/content/modules/{e33f6d66a02ec26c → f36b4d3d44491852}/chunk.js +1 -1
  176. package/dist/web-ui/content/modules/{665e81fe210f1b7a → fa79ed5d045c24dd}/chunk.js +1 -1
  177. package/dist/web-ui/content/styles.css +1 -1
  178. package/dist/web-ui/embedded-assets.d.ts +412 -406
  179. package/dist/web-ui/embedded-assets.js +4 -4
  180. package/package.json +1 -1
  181. package/scripts/install-render-binary.js +1 -1
  182. package/dist/web-ui/content/modules/1a7303b87763fd43/employee-profile.js +0 -1
  183. package/dist/web-ui/content/modules/1d35739f634745f6/task-board-host.js +0 -22
  184. package/dist/web-ui/content/modules/2099d37d57856cbd/brand-identity.js +0 -1
  185. package/dist/web-ui/content/modules/2d6339b2f6c2e2d0/host.js +0 -1
  186. package/dist/web-ui/content/modules/2f88fc37c6105f7e/sidebar-peek.js +0 -1
  187. package/dist/web-ui/content/modules/31124fb7ee6939df/host.js +0 -1
  188. package/dist/web-ui/content/modules/31b28c5d0019c363/host-bridge.js +0 -1
  189. package/dist/web-ui/content/modules/36e5c3a6d6705358/employee-card.js +0 -1
  190. package/dist/web-ui/content/modules/3700d99c54d3c400/host.js +0 -1
  191. package/dist/web-ui/content/modules/3ae29086b227f9d9/markdown.js +0 -1
  192. package/dist/web-ui/content/modules/42db858e4417c702/chunk.js +0 -10
  193. package/dist/web-ui/content/modules/443dc81eac29f233/host.js +0 -1
  194. package/dist/web-ui/content/modules/4af10e1f8a789204/runtime.js +0 -275
  195. package/dist/web-ui/content/modules/55b75ebcf6924001/host.js +0 -30
  196. package/dist/web-ui/content/modules/626c7ce0aa1714b0/host.js +0 -1
  197. package/dist/web-ui/content/modules/76adde4019731df1/chunk.js +0 -1
  198. package/dist/web-ui/content/modules/7925b65867b7aa4a/queue-view-adapter.js +0 -3
  199. package/dist/web-ui/content/modules/8a6844a4251a1271/chunk.js +0 -1
  200. package/dist/web-ui/content/modules/8bd214570df4a7a0/host.js +0 -1
  201. package/dist/web-ui/content/modules/8bff85e108320aab/panel.js +0 -1
  202. package/dist/web-ui/content/modules/988e01b67e6f1883/composer-surface-adapter.js +0 -1
  203. package/dist/web-ui/content/modules/a975ff38c3d43ba8/workspace-window.js +0 -1
  204. package/dist/web-ui/content/modules/b550260fc9c5363a/dropdown-surface.js +0 -1
  205. package/dist/web-ui/content/modules/bce467c603299460/chunk.js +0 -1
  206. package/dist/web-ui/content/modules/c138b442c1682189/panel-host.js +0 -1
  207. package/dist/web-ui/content/modules/c32d14632487364f/group-editor.js +0 -1
  208. package/dist/web-ui/content/modules/c6d0a1b2753a4883/host.js +0 -1
  209. package/dist/web-ui/content/modules/cdff6e7f72879c88/progress.js +0 -1
  210. package/dist/web-ui/content/modules/cfee7b0d79587d59/home.js +0 -6
  211. package/dist/web-ui/content/modules/e5e93fe5304838a1/chunk.js +0 -1
  212. package/dist/web-ui/content/modules/fbe35e7ddd7384fa/host.js +0 -1
@@ -91,7 +91,7 @@ function safeTailStart(output, start) {
91
91
  * 终端原始输出的尾部:先按尾部截窗,再剥 ANSI / 换行归一 / 压缩空行 / 截尾。
92
92
  * live 每 500ms 就可能算一遍,而 PTY 的 output 能长到几 MB —— 顺序反过来等于每半秒对全量
93
93
  * 跑一遍正则。丢掉的前缀计入 omittedChars,是**估算值(可能偏大也可能偏小,用于量级提示)**:
94
- * 按截窗点的原始字数计,含 ANSI 与控制字符,不精确到账。三条边界写进 docs/ai-teams.md。
94
+ * 按截窗点的原始字数计,含 ANSI 与控制字符,只用于量级提示,不精确到账。
95
95
  */
96
96
  function outputTailText(output) {
97
97
  const rawStart = Math.max(0, output.length - LIVE_OUTPUT_WINDOW_CHARS);
@@ -0,0 +1,45 @@
1
+ import { type KeyObject } from "node:crypto";
2
+ import { type ClientHttp2Session } from "node:http2";
3
+ export type ApnsEnvironment = "production" | "sandbox";
4
+ export interface ApnsConfiguration {
5
+ keyId: string;
6
+ teamId: string;
7
+ topic: string;
8
+ environment: ApnsEnvironment;
9
+ key: KeyObject;
10
+ }
11
+ export interface ApnsMessage {
12
+ id: string;
13
+ token: string;
14
+ payload: string;
15
+ expiresAt: number;
16
+ collapseId: string;
17
+ /** ActivityKit has a distinct token and derived topic; never substitute an ordinary device token. */
18
+ pushType?: "alert" | "liveactivity";
19
+ priority?: 5 | 10;
20
+ }
21
+ export interface ApnsResult {
22
+ outcome: "accepted" | "invalid-device" | "retry" | "rejected";
23
+ /** Bounded, provider-defined diagnostic only. Never include a token, payload or raw error. */
24
+ reason: string;
25
+ }
26
+ export interface ApnsTransport {
27
+ send(message: ApnsMessage): Promise<ApnsResult>;
28
+ close(): void;
29
+ }
30
+ /** Deployment-owned paths/IDs only; the p8 itself never enters config.json, DTOs or logs. */
31
+ export declare function loadApnsConfiguration(env?: NodeJS.ProcessEnv, configDirectory?: string): ApnsConfiguration | null;
32
+ export declare function createApnsJwt(config: ApnsConfiguration, now?: number): string;
33
+ export declare function classifyApnsResponse(status: number, reason: unknown): ApnsResult;
34
+ /** Low-volume alerts: bounded HTTP/2 request per delivery, only Apple's fixed TLS endpoints. */
35
+ export declare class ApnsClient implements ApnsTransport {
36
+ private readonly config;
37
+ private readonly open;
38
+ private jwt;
39
+ private jwtAt;
40
+ private closed;
41
+ private readonly connections;
42
+ constructor(config: ApnsConfiguration, open?: (url: string) => ClientHttp2Session);
43
+ send(message: ApnsMessage): Promise<ApnsResult>;
44
+ close(): void;
45
+ }
@@ -0,0 +1,156 @@
1
+ import { createPrivateKey, sign } from "node:crypto";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { connect } from "node:http2";
5
+ /** Deployment-owned paths/IDs only; the p8 itself never enters config.json, DTOs or logs. */
6
+ export function loadApnsConfiguration(env = process.env, configDirectory) {
7
+ // A deployment-local file is deliberately separate from public/runtime WandConfig projections.
8
+ // Explicit environment configuration wins as a whole; incomplete overrides fail closed.
9
+ const names = ["WAND_APNS_KEY_PATH", "WAND_APNS_KEY_ID", "WAND_APNS_TEAM_ID", "WAND_APNS_TOPIC"];
10
+ const localFile = configDirectory ? path.join(configDirectory, "apns.json") : null;
11
+ if (!names.some(name => env[name]) && !env.WAND_APNS_ENVIRONMENT && localFile && existsSync(localFile)) {
12
+ try {
13
+ const stat = statSync(localFile);
14
+ if (!stat.isFile() || stat.size > 4096 || (stat.mode & 0o077) !== 0)
15
+ throw new Error();
16
+ const local = JSON.parse(readFileSync(localFile, "utf8"));
17
+ if (!local || typeof local !== "object" || Array.isArray(local))
18
+ throw new Error();
19
+ const fields = ["keyPath", "keyId", "teamId", "topic", "environment"];
20
+ if (fields.some(field => typeof local[field] !== "string"))
21
+ throw new Error();
22
+ env = Object.fromEntries([...names, "WAND_APNS_ENVIRONMENT"].map((name, index) => [name, local[fields[index]]]));
23
+ }
24
+ catch {
25
+ throw new Error("本机 apns.json 不可用:需要权限0600的有效推送配置文件。");
26
+ }
27
+ }
28
+ const values = [env.WAND_APNS_KEY_PATH, env.WAND_APNS_KEY_ID, env.WAND_APNS_TEAM_ID, env.WAND_APNS_TOPIC];
29
+ if (values.every(value => !value))
30
+ return null;
31
+ if (values.some(value => !value))
32
+ throw new Error("APNs 配置不完整:需要 KEY_PATH、KEY_ID、TEAM_ID 和 TOPIC。");
33
+ const [keyPath, keyId, teamId, topic] = values;
34
+ if (!/^[A-Z0-9]{10}$/.test(keyId) || !/^[A-Z0-9]{10}$/.test(teamId)
35
+ || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(topic) || topic.length > 255) {
36
+ throw new Error("APNs 的 key/team/topic 配置格式无效。");
37
+ }
38
+ const environment = env.WAND_APNS_ENVIRONMENT ?? "production";
39
+ if (environment !== "production" && environment !== "sandbox")
40
+ throw new Error("APNs environment 必须是 production 或 sandbox。");
41
+ try {
42
+ if (!path.isAbsolute(keyPath))
43
+ throw new Error();
44
+ const stat = statSync(keyPath);
45
+ if (!stat.isFile() || stat.size > 16_384 || (stat.mode & 0o077) !== 0)
46
+ throw new Error();
47
+ const key = createPrivateKey(readFileSync(keyPath));
48
+ if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1")
49
+ throw new Error();
50
+ return { keyId: keyId, teamId: teamId, topic: topic, environment, key };
51
+ }
52
+ catch {
53
+ throw new Error("APNs 私钥不可用:需要本机可读、权限0600的P-256 p8文件。");
54
+ }
55
+ }
56
+ export function createApnsJwt(config, now = Date.now()) {
57
+ const header = Buffer.from(JSON.stringify({ alg: "ES256", kid: config.keyId })).toString("base64url");
58
+ const claims = Buffer.from(JSON.stringify({ iss: config.teamId, iat: Math.floor(now / 1000) })).toString("base64url");
59
+ const input = `${header}.${claims}`;
60
+ return `${input}.${sign("sha256", Buffer.from(input), { key: config.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
61
+ }
62
+ export function classifyApnsResponse(status, reason) {
63
+ const safe = typeof reason === "string" && /^[A-Za-z]{1,64}$/.test(reason) ? reason : `HTTP${status}`;
64
+ if (status === 200)
65
+ return { outcome: "accepted", reason: "AcceptedByAPNs" };
66
+ if (status === 410 || (status === 400 && ["BadDeviceToken", "DeviceTokenNotForTopic", "Unregistered"].includes(safe))) {
67
+ return { outcome: "invalid-device", reason: safe };
68
+ }
69
+ return { outcome: status === 0 || status === 429 || status >= 500 || safe === "ExpiredProviderToken" ? "retry" : "rejected", reason: safe };
70
+ }
71
+ /** Low-volume alerts: bounded HTTP/2 request per delivery, only Apple's fixed TLS endpoints. */
72
+ export class ApnsClient {
73
+ config;
74
+ open;
75
+ jwt = "";
76
+ jwtAt = 0;
77
+ closed = false;
78
+ connections = new Set();
79
+ constructor(config, open = connect) {
80
+ this.config = config;
81
+ this.open = open;
82
+ }
83
+ send(message) {
84
+ if (this.closed)
85
+ return Promise.resolve({ outcome: "retry", reason: "Stopped" });
86
+ const now = Date.now();
87
+ if (!this.jwt || now - this.jwtAt > 50 * 60_000) {
88
+ this.jwt = createApnsJwt(this.config, now);
89
+ this.jwtAt = now;
90
+ }
91
+ return new Promise(resolve => {
92
+ const endpoint = this.config.environment === "sandbox" ? "https://api.sandbox.push.apple.com" : "https://api.push.apple.com";
93
+ const connection = this.open(endpoint);
94
+ this.connections.add(connection);
95
+ let settled = false;
96
+ const finish = (result) => {
97
+ if (settled)
98
+ return;
99
+ settled = true;
100
+ clearTimeout(timer);
101
+ this.connections.delete(connection);
102
+ connection.destroy();
103
+ resolve(result);
104
+ };
105
+ const timer = setTimeout(() => finish({ outcome: "retry", reason: "Timeout" }), 10_000);
106
+ timer.unref();
107
+ connection.on("error", () => finish({ outcome: "retry", reason: "ConnectionError" }));
108
+ connection.on("close", () => finish({ outcome: "retry", reason: "ConnectionClosed" }));
109
+ connection.once("connect", () => {
110
+ if (settled)
111
+ return;
112
+ try {
113
+ const request = connection.request({
114
+ ":method": "POST", ":path": `/3/device/${message.token}`,
115
+ authorization: `bearer ${this.jwt}`,
116
+ "apns-topic": message.pushType === "liveactivity" ? `${this.config.topic}.push-type.liveactivity` : this.config.topic,
117
+ "apns-push-type": message.pushType ?? "alert", "apns-priority": String(message.priority ?? (message.pushType === "liveactivity" ? 5 : 10)), "apns-id": message.id,
118
+ "apns-expiration": String(Math.floor(message.expiresAt / 1000)), "apns-collapse-id": message.collapseId,
119
+ "content-type": "application/json",
120
+ });
121
+ let status = 0;
122
+ let body = "";
123
+ request.setEncoding("utf8");
124
+ request.on("response", headers => { status = Number(headers[":status"] ?? 0); });
125
+ request.on("data", (chunk) => {
126
+ body += chunk;
127
+ if (Buffer.byteLength(body) > 4096)
128
+ finish({ outcome: "retry", reason: "ResponseTooLarge" });
129
+ });
130
+ request.on("error", () => finish({ outcome: "retry", reason: "StreamError" }));
131
+ request.on("end", () => {
132
+ let reason;
133
+ try {
134
+ reason = JSON.parse(body).reason;
135
+ }
136
+ catch { /* empty success body */ }
137
+ const result = classifyApnsResponse(status, reason);
138
+ if (result.reason === "ExpiredProviderToken")
139
+ this.jwt = "";
140
+ finish(result);
141
+ });
142
+ request.end(message.payload);
143
+ }
144
+ catch {
145
+ finish({ outcome: "retry", reason: "RequestError" });
146
+ }
147
+ });
148
+ });
149
+ }
150
+ close() {
151
+ this.closed = true;
152
+ for (const connection of this.connections)
153
+ connection.destroy();
154
+ this.connections.clear();
155
+ }
156
+ }
@@ -0,0 +1,29 @@
1
+ import type { ApnsConfiguration, ApnsTransport } from "./apns-client.js";
2
+ import type { WandStorage } from "./storage.js";
3
+ import type { ProcessEvent, SessionSnapshot } from "./types.js";
4
+ export declare class ApnsService {
5
+ private readonly storage;
6
+ readonly configuration: ApnsConfiguration | null;
7
+ private readonly transport;
8
+ private readonly credentialDigest;
9
+ private readonly getSession;
10
+ private readonly warn;
11
+ private readonly now;
12
+ private stopped;
13
+ private running;
14
+ private timer;
15
+ private lastSweep;
16
+ private nextWarningAt;
17
+ private report;
18
+ constructor(storage: WandStorage, configuration: ApnsConfiguration | null, transport: ApnsTransport | null, credentialDigest: () => string, getSession: (id: string) => SessionSnapshot | null, warn?: (message: string) => void, now?: () => number);
19
+ get available(): boolean;
20
+ start(): void;
21
+ /** Called only by SessionCompletionTracker when it creates a NEW successful completion fact. */
22
+ completed(sessionId: string, revision: number): void;
23
+ ingest(event: ProcessEvent): void;
24
+ private enqueue;
25
+ /** A single bounded consumer, independent from the lifetime of PTY/Render/Core. */
26
+ flush(): Promise<void>;
27
+ private drain;
28
+ dispose(): void;
29
+ }
@@ -0,0 +1,156 @@
1
+ import { apnsDigest } from "./storage/apns-repository.js";
2
+ export class ApnsService {
3
+ storage;
4
+ configuration;
5
+ transport;
6
+ credentialDigest;
7
+ getSession;
8
+ warn;
9
+ now;
10
+ stopped = false;
11
+ running = null;
12
+ timer = null;
13
+ lastSweep = 0;
14
+ nextWarningAt = 0;
15
+ report(message) {
16
+ if (this.now() < this.nextWarningAt)
17
+ return;
18
+ this.nextWarningAt = this.now() + 60_000;
19
+ this.warn(message);
20
+ }
21
+ constructor(storage, configuration, transport, credentialDigest, getSession, warn = () => { }, now = Date.now) {
22
+ this.storage = storage;
23
+ this.configuration = configuration;
24
+ this.transport = transport;
25
+ this.credentialDigest = credentialDigest;
26
+ this.getSession = getSession;
27
+ this.warn = warn;
28
+ this.now = now;
29
+ storage.onClose(() => this.dispose());
30
+ }
31
+ get available() { return !!this.configuration && !!this.transport && !this.stopped; }
32
+ start() {
33
+ if (!this.available || this.timer)
34
+ return;
35
+ this.timer = setInterval(() => { void this.flush(); }, 2000);
36
+ this.timer.unref();
37
+ void this.flush(); // includes persisted, still-actionable outbox entries after a restart.
38
+ }
39
+ /** Called only by SessionCompletionTracker when it creates a NEW successful completion fact. */
40
+ completed(sessionId, revision) {
41
+ if (!this.available)
42
+ return;
43
+ try {
44
+ const session = this.getSession(sessionId);
45
+ if (!session || session.archived)
46
+ return;
47
+ this.enqueue(sessionId, "completed", String(revision));
48
+ }
49
+ catch {
50
+ this.report("APNs 无法读取会话提醒事实;不影响执行。");
51
+ }
52
+ }
53
+ ingest(event) {
54
+ if (!this.available || (event.type !== "status" && event.type !== "ended"))
55
+ return;
56
+ try {
57
+ const session = this.getSession(event.sessionId);
58
+ if (!session || session.archived)
59
+ return;
60
+ // The durable request ID is the fact. Never hash permission contents/tool arguments into a payload.
61
+ const requestId = session.pendingEscalation?.requestId;
62
+ if (requestId)
63
+ this.enqueue(session.id, "permission", requestId);
64
+ }
65
+ catch {
66
+ this.report("APNs 无法读取会话提醒事实;不影响执行。");
67
+ }
68
+ }
69
+ enqueue(sessionId, kind, revision) {
70
+ try {
71
+ const config = this.configuration;
72
+ const capacityAvailable = this.storage.apns.enqueue(sessionId, kind, revision, this.credentialDigest(), config.topic, config.environment, this.now());
73
+ if (!capacityAvailable)
74
+ this.report("APNs 提醒队列已满,部分提醒可能被丢弃;执行与历史不受影响。");
75
+ void this.flush();
76
+ }
77
+ catch {
78
+ this.report("APNs 提醒未能入队;执行与历史不受影响。");
79
+ }
80
+ }
81
+ /** A single bounded consumer, independent from the lifetime of PTY/Render/Core. */
82
+ flush() {
83
+ if (!this.available)
84
+ return Promise.resolve();
85
+ if (!this.running) {
86
+ this.running = this.drain().catch(() => {
87
+ if (!this.stopped)
88
+ this.report("APNs 队列暂不可用,将稍后重试。");
89
+ }).finally(() => { this.running = null; });
90
+ }
91
+ return this.running;
92
+ }
93
+ async drain() {
94
+ if (this.now() - this.lastSweep >= 60_000) {
95
+ this.storage.apns.sweep(this.now());
96
+ this.lastSweep = this.now();
97
+ }
98
+ // Bound work per pump even under an event burst. Pending work remains in SQLite.
99
+ for (let i = 0; i < 32 && !this.stopped; i++) {
100
+ const job = this.storage.apns.due(this.now());
101
+ if (!job)
102
+ return;
103
+ const device = this.storage.apns.device(job.device_id);
104
+ const session = this.getSession(job.session_id);
105
+ const stale = !device || !device.enabled || device.generation !== job.generation
106
+ || device.expires_at <= this.now() || device.credential_digest !== this.credentialDigest()
107
+ || device.topic !== this.configuration.topic || device.environment !== this.configuration.environment
108
+ || job.expires_at <= this.now() || !session || session.archived
109
+ || (job.kind === "permission" && session.pendingEscalation?.requestId !== job.revision)
110
+ || (job.kind === "completed" && ((session.viewedCompletionRevision ?? 0) >= Number(job.revision)
111
+ || (session.completionRevision ?? 0) !== Number(job.revision) || !!session.pendingEscalation
112
+ || session.ptyBusy === true || session.structuredState?.inFlight === true));
113
+ if (stale) {
114
+ this.storage.apns.finish(job.id, "canceled", "NoLongerActionable");
115
+ continue;
116
+ }
117
+ const payload = JSON.stringify({
118
+ aps: { alert: {
119
+ title: job.kind === "permission" ? "需要你的授权" : "回复已完成",
120
+ body: job.kind === "permission" ? "Wand 会话正在等待确认,点击查看。" : "Wand 会话已有新结果,点击查看。",
121
+ }, sound: "default", "thread-id": apnsDigest(`${device.server_id}:${job.session_id}`).slice(0, 32) },
122
+ eventId: job.event_id, sessionId: job.session_id, serverId: device.server_id, kind: job.kind,
123
+ });
124
+ let result;
125
+ try {
126
+ result = await this.transport.send({ id: job.id, token: device.token, payload,
127
+ expiresAt: job.expires_at, collapseId: job.event_id });
128
+ }
129
+ catch {
130
+ result = { outcome: "retry", reason: "TransportFailure" };
131
+ }
132
+ if (this.stopped)
133
+ return; // storage may have closed while the HTTP/2 request was in flight.
134
+ if (result.outcome === "accepted")
135
+ this.storage.apns.finish(job.id, "accepted", result.reason);
136
+ else if (result.outcome === "invalid-device") {
137
+ this.storage.apns.finish(job.id, "failed", result.reason);
138
+ this.storage.apns.invalidateDevice(job.device_id, job.generation);
139
+ }
140
+ else if (result.outcome === "retry" && job.attempts < 4) {
141
+ this.storage.apns.retry(job.id, job.attempts + 1, this.now() + Math.min(60_000, 2000 * 2 ** job.attempts), result.reason);
142
+ }
143
+ else {
144
+ this.storage.apns.finish(job.id, "failed", result.reason);
145
+ this.report(`APNs 未接受提醒(${result.reason}),不影响会话执行。`);
146
+ }
147
+ }
148
+ }
149
+ dispose() {
150
+ this.stopped = true;
151
+ if (this.timer)
152
+ clearInterval(this.timer);
153
+ this.timer = null;
154
+ this.transport?.close();
155
+ }
156
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "9fbc1da896aa6d8c29b8da1f602a3017956e89c5",
3
- "builtAt": "2026-10-22T00:06:11.299Z",
4
- "version": "4.102.0",
2
+ "commit": "54a477b44332156a4ccebc55c1879bff23fa78a5",
3
+ "builtAt": "2026-10-24T13:20:55.647Z",
4
+ "version": "4.103.0",
5
5
  "channel": "stable"
6
6
  }
@@ -31,6 +31,8 @@ interface ConversationExecutionTarget {
31
31
  /** Business coordination only; all execution remains in StructuredSessionManager / AiTeamRunner. */
32
32
  export declare class ConversationService {
33
33
  private readonly deps;
34
+ private external?;
35
+ attachExternal(service: NonNullable<ConversationService["external"]>): void;
34
36
  constructor(deps: {
35
37
  storage: WandStorage;
36
38
  structured: Pick<StructuredSessionManager, "createSession" | "createRelaySession" | "get" | "sendMessage" | "appendRelayTurns">;
@@ -50,6 +50,8 @@ function stableJson(value) {
50
50
  /** Business coordination only; all execution remains in StructuredSessionManager / AiTeamRunner. */
51
51
  export class ConversationService {
52
52
  deps;
53
+ external;
54
+ attachExternal(service) { this.external = service; }
53
55
  constructor(deps) {
54
56
  this.deps = deps;
55
57
  }
@@ -281,6 +283,9 @@ export class ConversationService {
281
283
  return candidates.reduce((latest, turn) => latest ? this.laterTurn(instance, latest, turn) : turn, undefined);
282
284
  }
283
285
  summary(instance, knownTurns) {
286
+ const externalGroup = this.external?.groupForConversation(instance.id);
287
+ if (externalGroup)
288
+ instance = { ...instance, external: this.external.metadata(externalGroup) };
284
289
  const tasks = this.storage.conversationTasks(instance.id).flatMap((link) => {
285
290
  const task = this.storage.getWandTask(link.taskId);
286
291
  const receipt = task ? this.storage.conversationTaskStartup(task.id) : null;
@@ -290,7 +295,7 @@ export class ConversationService {
290
295
  });
291
296
  const employee = instance.peerEmployeeId ? this.storage.getSiliconEmployee(instance.peerEmployeeId) : null;
292
297
  const lifecycle = this.storage.conversationListState(instance.id);
293
- let unavailableReason = lifecycle.deleting ? "对话正在删除,请稍后重试。" : lifecycle.dissolvedAt ? "群聊已解散,恢复后可以继续聊天。" : null;
298
+ let unavailableReason = lifecycle.deleting ? "对话正在删除,请稍后重试。" : lifecycle.dissolvedAt ? instance.external ? "此钉钉群镜像已暂停接收新输入,不影响真实钉钉群。" : "群聊已解散,恢复后可以继续聊天。" : null;
294
299
  if (instance.kind === "dm") {
295
300
  try {
296
301
  requireTeamEmployee(this.storage, instance.peerEmployeeId ?? "");
@@ -306,7 +311,7 @@ export class ConversationService {
306
311
  const title = instance.kind === "dm" ? employee?.name ?? instance.name
307
312
  : instance.nameSource === "custom" ? instance.name : tasks.at(-1)?.task.title ?? instance.name;
308
313
  // 私聊标题已说明对方身份;只保留自己的署名,群聊仍需区分发言人。
309
- const previewAuthor = last?.role === "user" ? `${this.selfName()}:`
314
+ const previewAuthor = last?.role === "user" ? `${instance.external ? last.author?.name ?? "钉钉成员" : this.selfName()}:`
310
315
  : instance.kind === "group" && last ? `${last.author?.name ?? "系统"}:` : "";
311
316
  const team = instance.team ? projectTeamEmployees(instance.team, this.storage) : null;
312
317
  return { ...instance, ...lifecycle, team: team ? { ...team, members: team.members.map((member) => ({
@@ -314,7 +319,7 @@ export class ConversationService {
314
319
  })) } : null,
315
320
  title, unavailableReason, memberUnavailableReasons: this.memberReasons(instance), tasks,
316
321
  preview: last ? `${previewAuthor}${lastText.slice(0, 160)}`
317
- : instance.kind === "group" ? "群已建立,尚未派任务" : "还没有消息", messageAt };
322
+ : instance.external ? "钉钉群已接入,等待消息同步" : instance.kind === "group" ? "群已建立,尚未派任务" : "还没有消息", messageAt };
318
323
  }
319
324
  list() {
320
325
  return this.storage.listConversations().map((instance) => this.summary(instance))
@@ -403,6 +408,8 @@ export class ConversationService {
403
408
  for (const taskId of taskIds)
404
409
  this.storage.deleteWandTask(taskId);
405
410
  this.storage.deleteConversation(id);
411
+ if (instance.external)
412
+ this.external?.removeMirror(id);
406
413
  }
407
414
  history(id) { return this.turns(this.requireInstance(id)); }
408
415
  detail(id, select = turns => turns) {
@@ -633,6 +640,10 @@ export class ConversationService {
633
640
  prepare(id, requestId, explicitCwd) {
634
641
  return this.request(requestId, "prepare", { explicitCwd }, id, (receipt) => {
635
642
  const instance = this.requireInstance(id);
643
+ if (instance.external) {
644
+ receipt.state = "accepted";
645
+ return;
646
+ }
636
647
  const session = this.communicationChannel(instance, this.cwd(explicitCwd));
637
648
  receipt.sessionId = session.id;
638
649
  receipt.state = "accepted";
@@ -671,6 +682,13 @@ export class ConversationService {
671
682
  }
672
683
  send(id, requestId, input, target, explicitCwd, echo = true, executionTarget, executionPreference) {
673
684
  const preference = parseConversationExecutionPreference(executionPreference);
685
+ if (this.storage.getConversation(id)?.external) {
686
+ if (!this.external)
687
+ throw new Error("钉钉连接器尚未启动。");
688
+ if (target || executionTarget || preference)
689
+ throw new Error("钉钉群消息不能作为内部任务或临时员工配置派发。");
690
+ return this.external.send(id, requestId, input);
691
+ }
674
692
  return this.request(requestId, "send", { input, target, explicitCwd, echo,
675
693
  ...(preference ? { executionPreference: preference } : {}),
676
694
  ...(executionTarget?.workspaceId ? { workspaceId: executionTarget.workspaceId } : {}),
@@ -812,6 +830,8 @@ export class ConversationService {
812
830
  dispatch(id, requestId, input) {
813
831
  return this.request(requestId, "dispatch", input, id, (receipt, commit) => {
814
832
  const instance = this.requireInstance(id);
833
+ if (instance.external)
834
+ throw new Error("钉钉群不是内部团队任务群,请在群内明确 @已接入的员工。");
815
835
  // Older clients may still use the task form in a DM. It now starts a session, never a task group.
816
836
  if (instance.kind === "dm") {
817
837
  if (input.continueTaskId)
@@ -13,6 +13,8 @@ export type ConversationTarget = {
13
13
  export interface ConversationInstance {
14
14
  id: string;
15
15
  owner: string;
16
+ /** Real external group mirror; ordinary messages never enter the internal team dispatcher. */
17
+ external?: import("./dingtalk-types.js").DingTalkExternalConversation;
16
18
  kind: "dm" | "group";
17
19
  peerEmployeeId: string | null;
18
20
  name: string;
@@ -0,0 +1,41 @@
1
+ import { boundedFetch, type DwsTransport } from "./dingtalk-dws.js";
2
+ import type { DingTalkAccount } from "./dingtalk-types.js";
3
+ /** Structured device OAuth, not scraping localized CLI stderr. Codes stay in memory and are exchanged over stdin. */
4
+ export declare class DingTalkAuth {
5
+ private readonly dws;
6
+ private readonly request;
7
+ private attempts;
8
+ constructor(dws: DwsTransport, request?: typeof boundedFetch);
9
+ accounts(): Promise<DingTalkAccount[]>;
10
+ private json;
11
+ begin(): Promise<{
12
+ id: string;
13
+ state: "failed" | "cancelled" | "connected" | "waiting" | "exchanging";
14
+ url: string | undefined;
15
+ userCode: string | undefined;
16
+ expiresAt: string;
17
+ error: string | undefined;
18
+ account: DingTalkAccount | undefined;
19
+ }>;
20
+ get(id: string): {
21
+ id: string;
22
+ state: "failed" | "cancelled" | "connected" | "waiting" | "exchanging";
23
+ url: string | undefined;
24
+ userCode: string | undefined;
25
+ expiresAt: string;
26
+ error: string | undefined;
27
+ account: DingTalkAccount | undefined;
28
+ };
29
+ cancel(id: string): {
30
+ id: string;
31
+ state: "failed" | "cancelled" | "connected" | "waiting" | "exchanging";
32
+ url: string | undefined;
33
+ userCode: string | undefined;
34
+ expiresAt: string;
35
+ error: string | undefined;
36
+ account: DingTalkAccount | undefined;
37
+ };
38
+ private public;
39
+ private poll;
40
+ dispose(): void;
41
+ }