@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
@@ -0,0 +1,111 @@
1
+ import express from "express";
2
+ import { authenticateBearerAppToken } from "./middleware/auth.js";
3
+ import { apnsDigest } from "./storage/apns-repository.js";
4
+ /** Mount before the global API auth gate: PUT requires the app bearer; DELETE has revoke-only authority. */
5
+ export function registerApnsRoutes(app, deps) {
6
+ const router = express.Router();
7
+ let window = 0, requests = 0;
8
+ const bounded = (_req, res, next) => {
9
+ res.set("Cache-Control", "no-store");
10
+ if (Date.now() - window >= 60_000) {
11
+ window = Date.now();
12
+ requests = 0;
13
+ }
14
+ if (++requests > 120) {
15
+ res.set("Retry-After", "60").status(429).json({ error: "推送订阅请求过多。" });
16
+ return;
17
+ }
18
+ next();
19
+ };
20
+ router.use(bounded);
21
+ router.put("/subscription", express.json({ limit: "4kb", inflate: false }), (req, res) => {
22
+ if (!authenticateBearerAppToken(req, deps.storage, deps.config)) {
23
+ res.status(401).json({ error: "需要有效的应用连接凭据。", code: "unauthorized" });
24
+ return;
25
+ }
26
+ if (!deps.service.available) {
27
+ res.status(503).json({ error: "服务器尚未配置 APNs。", code: "apns_unavailable" });
28
+ return;
29
+ }
30
+ const { secret, deviceToken, environment, serverId } = req.body ?? {};
31
+ const config = deps.service.configuration;
32
+ if (typeof secret !== "string" || !/^[a-f0-9]{64}$/.test(secret)
33
+ || typeof deviceToken !== "string" || !/^[a-f0-9]{32,200}$/.test(deviceToken) || deviceToken.length % 2 !== 0
34
+ || environment !== config.environment || typeof serverId !== "string" || !/^[a-zA-Z0-9._:-]{1,128}$/.test(serverId)) {
35
+ res.status(400).json({ error: "推送订阅参数或 APNs 环境不匹配。", code: "invalid_apns_subscription" });
36
+ return;
37
+ }
38
+ // All connected apps currently share one instance-owner credential (not a multi-user account model).
39
+ // Bind to its digest so password/app-secret rotation revokes every existing subscription at send time.
40
+ const appToken = req.headers.authorization.slice("Bearer ".length).trim();
41
+ try {
42
+ deps.storage.apns.register({ secret, token: deviceToken, environment, topic: config.topic, serverId,
43
+ credentialDigest: apnsDigest(appToken) }, Date.now());
44
+ res.status(200).json({ available: true, leaseSeconds: 14 * 86_400 });
45
+ }
46
+ catch {
47
+ res.status(503).json({ error: "推送订阅暂不可用。", code: "apns_storage_unavailable" });
48
+ }
49
+ });
50
+ router.delete("/subscription", (req, res) => {
51
+ const header = req.headers.authorization;
52
+ const secret = typeof header === "string" && header.startsWith("Bearer ") ? header.slice(7) : "";
53
+ if (!/^[a-f0-9]{64}$/.test(secret)) {
54
+ res.status(401).json({ error: "需要设备撤销凭据。" });
55
+ return;
56
+ }
57
+ // This opaque, 256-bit capability can only revoke its own device. No login, listing, registration or sending.
58
+ // It remains usable after the profile/app credential is removed or rotated, allowing durable offline retries.
59
+ try {
60
+ deps.storage.apns.revoke(secret);
61
+ res.status(204).end();
62
+ }
63
+ catch {
64
+ res.status(503).json({ error: "推送撤销暂不可用。" });
65
+ }
66
+ });
67
+ router.put("/live-activity", express.json({ limit: "4kb", inflate: false }), (req, res) => {
68
+ if (!authenticateBearerAppToken(req, deps.storage, deps.config)) {
69
+ res.status(401).json({ error: "需要有效的应用连接凭据。", code: "unauthorized" });
70
+ return;
71
+ }
72
+ if (!deps.liveActivity?.available) {
73
+ res.status(503).json({ error: "服务器尚未配置远程灵动岛。", code: "apns_unavailable" });
74
+ return;
75
+ }
76
+ const { secret, activityId, activityToken, environment, serverId } = req.body ?? {};
77
+ const config = deps.liveActivity.configuration;
78
+ if (typeof secret !== "string" || !/^[a-f0-9]{64}$/.test(secret)
79
+ || typeof activityId !== "string" || !/^[A-Za-z0-9._:-]{1,200}$/.test(activityId)
80
+ || typeof activityToken !== "string" || !/^[a-f0-9]{32,512}$/.test(activityToken) || activityToken.length % 2 !== 0
81
+ || environment !== config.environment || typeof serverId !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(serverId)) {
82
+ res.status(400).json({ error: "灵动岛订阅参数或推送环境不匹配。", code: "invalid_live_activity_subscription" });
83
+ return;
84
+ }
85
+ const appToken = req.headers.authorization.slice(7).trim();
86
+ try {
87
+ deps.storage.liveActivities.register({ secret, activityId, token: activityToken, environment, serverId,
88
+ topic: config.topic, credentialDigest: apnsDigest(appToken) }, Date.now());
89
+ res.json({ available: true, leaseSeconds: 8 * 3600 });
90
+ }
91
+ catch {
92
+ res.status(503).json({ error: "灵动岛订阅暂不可用。", code: "apns_storage_unavailable" });
93
+ }
94
+ });
95
+ router.delete("/live-activity", (req, res) => {
96
+ const header = req.headers.authorization;
97
+ const secret = typeof header === "string" && header.startsWith("Bearer ") ? header.slice(7) : "";
98
+ if (!/^[a-f0-9]{64}$/.test(secret)) {
99
+ res.status(401).json({ error: "需要活动撤销凭据。" });
100
+ return;
101
+ }
102
+ try {
103
+ deps.storage.liveActivities.revoke(secret);
104
+ res.status(204).end();
105
+ }
106
+ catch {
107
+ res.status(503).json({ error: "活动撤销暂不可用。" });
108
+ }
109
+ });
110
+ app.use("/api/push/apns", router);
111
+ }
@@ -0,0 +1,8 @@
1
+ import type { Express, RequestHandler } from "express";
2
+ import type { DingTalkService } from "./dingtalk-service.js";
3
+ /** All discovery, OAuth codes and provisioning controls are admin-scoped, no-store and never public DTO caches. */
4
+ export declare function registerDingTalkRoutes(app: Express, deps: {
5
+ service: DingTalkService;
6
+ requireAdmin: RequestHandler;
7
+ install(): Promise<void>;
8
+ }): void;
@@ -0,0 +1,35 @@
1
+ import { DingTalkError } from "./dingtalk-dws.js";
2
+ import { asyncRoute } from "./express-async.js";
3
+ import { bodyObject, text } from "./server-request.js";
4
+ /** All discovery, OAuth codes and provisioning controls are admin-scoped, no-store and never public DTO caches. */
5
+ export function registerDingTalkRoutes(app, deps) {
6
+ const base = "/api/connectors/dingtalk";
7
+ app.use(base, deps.requireAdmin, (_req, res, next) => { res.setHeader("Cache-Control", "no-store"); next(); });
8
+ const route = (method, path, action, successStatus = 200) => {
9
+ app[method](base + path, asyncRoute(async (req, res) => {
10
+ try {
11
+ res.status(successStatus).json(await action(req));
12
+ }
13
+ catch (error) {
14
+ const safe = error instanceof DingTalkError ? error : new DingTalkError("operation_failed", "钉钉操作未完成,请检查当前接入状态。");
15
+ res.status(safe.code === "unauthorized" ? 401 : safe.code === "not_found" ? 404 : 400).json({ error: safe.message, code: safe.code, unknownDelivery: safe.unknownDelivery });
16
+ }
17
+ }));
18
+ };
19
+ route("get", "", () => deps.service.status());
20
+ route("post", "/install", async () => { await deps.install(); return { installed: true }; });
21
+ route("post", "/auth", () => deps.service.auth.begin());
22
+ route("get", "/auth/:id", req => deps.service.auth.get(req.params.id));
23
+ route("post", "/auth/:id/cancel", req => deps.service.auth.cancel(req.params.id));
24
+ route("get", "/groups", req => deps.service.discover(text(req.query.accountId), text(req.query.cursor)));
25
+ route("post", "/plans", req => {
26
+ const b = bodyObject(req.body);
27
+ return deps.service.plan({ accountId: b.accountId, groupIds: b.groupIds, employeeIds: b.employeeIds, excluded: b.excluded, robotNames: b.robotNames, candidateIndices: b.candidateIndices });
28
+ });
29
+ route("post", "/connections", req => { const b = bodyObject(req.body); return deps.service.connect(text(b.planId), b.confirmed === true); }, 202);
30
+ route("patch", "/bindings/:id", req => deps.service.updateBinding(req.params.id, bodyObject(req.body)));
31
+ route("post", "/bindings/:id/resume", req => deps.service.retryBinding(req.params.id), 202);
32
+ route("post", "/bindings/:id/remove", req => { if (bodyObject(req.body).confirmed !== true)
33
+ throw new DingTalkError("confirmation", "请确认仅移除此群中的员工绑定。"); return deps.service.removeBinding(req.params.id); });
34
+ route("post", "/groups/:id/refresh", req => deps.service.refreshGroup(req.params.id));
35
+ }
package/dist/server.js CHANGED
@@ -24,6 +24,11 @@ import { SessionLogger } from "./session-logger.js";
24
24
  import { coreHarnessAgentDir } from "./harness-engine.js";
25
25
  import { SessionRegistry } from "./session-registry.js";
26
26
  import { SessionCompletionTracker } from "./session-completion.js";
27
+ import { ApnsClient, loadApnsConfiguration } from "./apns-client.js";
28
+ import { ApnsService } from "./apns-service.js";
29
+ import { LiveActivityService } from "./live-activity-service.js";
30
+ import { apnsDigest } from "./storage/apns-repository.js";
31
+ import { registerApnsRoutes } from "./server-apns-routes.js";
27
32
  import { resolveSystemAiContext } from "./session-ai-context.js";
28
33
  import { systemAiSessions } from "./system-ai-session-recorder.js";
29
34
  import { StructuredSessionManager } from "./structured-session-manager.js";
@@ -41,7 +46,7 @@ import { registerStorageCleanupRoutes } from "./server-storage-cleanup-routes.js
41
46
  import { LocalModelSetupService } from "./local-model-setup.js";
42
47
  import { DecisionExpertService } from "./decision-expert-service.js";
43
48
  import { DECISION_EXPERT_KEY } from "./decision-expert-identity.js";
44
- import { buildStructuredChatPersonaPayload, resolveAppConnectCode, } from "./server-app-connect.js";
49
+ import { buildStructuredChatPersonaPayload, generateAppToken, resolveAppConnectCode, } from "./server-app-connect.js";
45
50
  import { resolveTrustProxy, sendRouteError } from "./server-request.js";
46
51
  import { registerAuthRoutes } from "./server-auth-routes.js";
47
52
  import { WebLoginTickets } from "./web-login-tickets.js";
@@ -66,6 +71,9 @@ import { registerSiliconEmployeeRoutes } from "./server-employee-routes.js";
66
71
  import { EmployeeImService } from "./employee-im.js";
67
72
  import { EmployeeImLifecycle } from "./employee-im-lifecycle.js";
68
73
  import { registerEmployeeImRoutes } from "./server-employee-im-routes.js";
74
+ import { DwsClient } from "./dingtalk-dws.js";
75
+ import { DingTalkService } from "./dingtalk-service.js";
76
+ import { registerDingTalkRoutes } from "./server-dingtalk-routes.js";
69
77
  import { UserMemoryService } from "./user-memory.js";
70
78
  import { registerUserMemoryRoutes, userMemoryOperationLog } from "./server-user-memory.js";
71
79
  import { registerAttentionRoutes } from "./server-attention-routes.js";
@@ -414,7 +422,17 @@ export async function startServer(config, configPath, options = {}) {
414
422
  canStart: (session, scheduling) => taskExecutionQueue.canStartSession(session, scheduling),
415
423
  schedule: employeeId => taskExecutionQueue.scheduleDrain(employeeId),
416
424
  });
417
- const sessionCompletions = new SessionCompletionTracker(storage, (id) => sessionRegistry.get(id));
425
+ let apnsConfiguration = null;
426
+ try {
427
+ apnsConfiguration = loadApnsConfiguration(process.env, configDir);
428
+ }
429
+ catch (error) {
430
+ wandWarn(error instanceof Error ? error.message : "APNs 配置不可用。");
431
+ }
432
+ const apns = new ApnsService(storage, apnsConfiguration, apnsConfiguration ? new ApnsClient(apnsConfiguration) : null, () => apnsDigest(generateAppToken(getEffectivePassword(storage, config), config.appSecret ?? "")), id => sessionRegistry.ownerOf(id) === "storage" ? null : sessionRegistry.getSlim(id), message => wandWarn(message));
433
+ let liveActivityFactsReady = false;
434
+ const liveActivities = new LiveActivityService(storage, apnsConfiguration, apnsConfiguration ? new ApnsClient(apnsConfiguration) : null, () => apnsDigest(generateAppToken(getEffectivePassword(storage, config), config.appSecret ?? "")), () => liveActivityFactsReady ? sessionRegistry.listSlim({ runtimeOnly: true }) : null, message => wandWarn(message));
435
+ const sessionCompletions = new SessionCompletionTracker(storage, (id) => sessionRegistry.get(id), (id, revision) => apns.completed(id, revision));
418
436
  let nextEmployeeImWarningAt = 0;
419
437
  const employeeIm = new EmployeeImService(storage, { onFailure: () => {
420
438
  if (Date.now() < nextEmployeeImWarningAt)
@@ -466,6 +484,14 @@ export async function startServer(config, configPath, options = {}) {
466
484
  notifySession: notifier.conversationSession, notifyChanged: () => notifier.system({ kind: "conversations-changed" }), deleteSession: id => { sessionRegistry.deleteWithProviderHistory(id); } });
467
485
  conversations = conversationService;
468
486
  conversationService.migrateExistingChats();
487
+ const dingtalkDws = new DwsClient(storage.directory());
488
+ const dingtalk = new DingTalkService({ storage, dws: dingtalkDws, config, structured: structuredSessions,
489
+ changed: () => notifier.system({ kind: "conversations-changed" }) });
490
+ conversationService.attachExternal(dingtalk);
491
+ storage.onClose(() => { dingtalk.dispose(); dingtalkDws.dispose(); });
492
+ // Restore only explicitly persisted subscriptions; startup never creates or publishes remote resources.
493
+ if (!testMode)
494
+ void dingtalk.restore().catch(() => wandWarn("钉钉连接恢复未完成,请在连接器检查授权。"));
469
495
  const scheduledTasks = new ScheduledTaskService({ storage, conversations: conversationService,
470
496
  getSession: id => sessionRegistry.get(id), defaultCwd: () => config.defaultCwd });
471
497
  for (const prefix of [CONVERSATION_RELAY_PREFIX, AI_TEAM_CHAT_PREFIX]) {
@@ -519,6 +545,7 @@ export async function startServer(config, configPath, options = {}) {
519
545
  // Route-specific parsers must run before the global parser. Once body-parser
520
546
  // has consumed a request, a later express.json() cannot tighten or widen it.
521
547
  registerFeedbackIngress(app, feedbackReceiver);
548
+ registerApnsRoutes(app, { storage, config, service: apns, liveActivity: liveActivities });
522
549
  app.use("/api/feedback/client", express.json({ limit: "32kb", inflate: false }));
523
550
  app.use("/api/decisions", express.json({ limit: "32kb" }));
524
551
  app.use("/api/optimize-prompt", express.json({ limit: "256kb" }));
@@ -662,6 +689,7 @@ export async function startServer(config, configPath, options = {}) {
662
689
  notifyEmployeeChanged: (employeeId) => notifyAiTeamRun({ kind: "silicon-employee-definition", employeeId }),
663
690
  });
664
691
  registerEmployeeImRoutes(app, { storage, im: employeeIm, requireAdmin });
692
+ registerDingTalkRoutes(app, { service: dingtalk, requireAdmin, install: () => dingtalkDws.install() });
665
693
  registerAttentionRoutes(app, { sessions: sessionRegistry, runner: aiTeams });
666
694
  registerConversationRoutes(app, conversationService);
667
695
  registerScheduledTaskRoutes(app, scheduledTasks);
@@ -821,6 +849,8 @@ export async function startServer(config, configPath, options = {}) {
821
849
  sessionRegistry.invalidateMessageProjection(event.sessionId);
822
850
  taskTitles.ingest(event);
823
851
  sessionCompletions.ingest(event);
852
+ apns.ingest(event);
853
+ liveActivities.ingest(event);
824
854
  missions.ingest(event);
825
855
  aiTeams.ingest(event);
826
856
  employeeImLifecycle.ingest(event);
@@ -832,7 +862,10 @@ export async function startServer(config, configPath, options = {}) {
832
862
  storage.recordTaskCommentSessionEvent(event);
833
863
  conversationService.ingestSessionEvent(event);
834
864
  scheduledTasks.ingestSessionEvent(event);
865
+ dingtalk.ingest(event);
835
866
  sessionCompletions.ingest(event);
867
+ apns.ingest(event);
868
+ liveActivities.ingest(event);
836
869
  missions.ingest(event);
837
870
  aiTeams.ingest(event);
838
871
  employeeImLifecycle.ingest(event);
@@ -847,7 +880,14 @@ export async function startServer(config, configPath, options = {}) {
847
880
  systemCalls.attach({ emit: event => wsManager.emitEvent(event), notifyConversation: notifier.conversationSession });
848
881
  // Re-attach structured CLI runs that kept going inside terminald while the
849
882
  // previous web process was down; fire-and-forget, failures are logged inside.
850
- void structuredSessions.recoverDetachedRuns().then(() => taskExecutionQueue.drainAll());
883
+ apns.start();
884
+ liveActivities.start();
885
+ void structuredSessions.recoverDetachedRuns().then(() => {
886
+ liveActivityFactsReady = true;
887
+ return taskExecutionQueue.drainAll();
888
+ }, () => {
889
+ wandWarn("会话恢复未确认:暂缓灵动岛远程更新,不将旧状态判为完成。");
890
+ });
851
891
  aiTeams.reconcile();
852
892
  structuredSessions.resumeQueuedPiMessages();
853
893
  // ── Restart endpoint (needs server + wss in scope) ──
@@ -922,6 +962,8 @@ export async function startServer(config, configPath, options = {}) {
922
962
  await new Promise((resolve, reject) => {
923
963
  const cleanupFailedListen = async () => {
924
964
  shuttingDown = true;
965
+ dingtalk.dispose();
966
+ dingtalkDws.dispose();
925
967
  employeeImLifecycle.dispose();
926
968
  employeeIm.dispose();
927
969
  await scheduledTasks.dispose();
@@ -1204,6 +1246,10 @@ export async function startServer(config, configPath, options = {}) {
1204
1246
  if (activeFeedback === feedback)
1205
1247
  activeFeedback = null;
1206
1248
  taskExecutionQueue.dispose();
1249
+ apns.dispose();
1250
+ liveActivities.dispose();
1251
+ dingtalk.dispose();
1252
+ dingtalkDws.dispose();
1207
1253
  employeeImLifecycle.dispose();
1208
1254
  employeeIm.dispose();
1209
1255
  taskTitles.dispose();
@@ -4,9 +4,10 @@ import type { ProcessEvent, SessionSnapshot } from "./types.js";
4
4
  export declare class SessionCompletionTracker {
5
5
  private readonly storage;
6
6
  private readonly getSession;
7
+ private readonly onCompleted?;
7
8
  private readonly busyPty;
8
9
  private readonly completedPty;
9
10
  private readonly ended;
10
- constructor(storage: WandStorage, getSession: (id: string) => SessionSnapshot | null);
11
+ constructor(storage: WandStorage, getSession: (id: string) => SessionSnapshot | null, onCompleted?: ((sessionId: string, revision: number) => void) | undefined);
11
12
  ingest(event: ProcessEvent): void;
12
13
  }
@@ -2,12 +2,14 @@
2
2
  export class SessionCompletionTracker {
3
3
  storage;
4
4
  getSession;
5
+ onCompleted;
5
6
  busyPty = new Set();
6
7
  completedPty = new Set();
7
8
  ended = new Map();
8
- constructor(storage, getSession) {
9
+ constructor(storage, getSession, onCompleted) {
9
10
  this.storage = storage;
10
11
  this.getSession = getSession;
12
+ this.onCompleted = onCompleted;
11
13
  }
12
14
  ingest(event) {
13
15
  if (event.type === "started") {
@@ -55,8 +57,10 @@ export class SessionCompletionTracker {
55
57
  }
56
58
  if (completed) {
57
59
  const completion = this.storage.recordSessionCompletion(snapshot.id);
58
- if (completion)
60
+ if (completion) {
59
61
  Object.assign(data, completion);
62
+ this.onCompleted?.(snapshot.id, completion.completionRevision);
63
+ }
60
64
  }
61
65
  }
62
66
  }
@@ -41,7 +41,9 @@ export declare class SessionRegistry {
41
41
  getLatest(id: string): SessionSnapshot | null;
42
42
  /** Runtime facts and membership only; list/policy reads must not hydrate transcripts. */
43
43
  getSlim(id: string): SessionSnapshot | null;
44
- listSlim(): SessionSnapshot[];
44
+ listSlim(options?: {
45
+ runtimeOnly?: boolean;
46
+ }): SessionSnapshot[];
45
47
  /** Membership belongs to storage; both runners only refresh their live projection. */
46
48
  refreshSessionWorkspace(id: string): void;
47
49
  setSessionModel(id: string, model: string | null): SessionSnapshot | null;
@@ -173,7 +173,7 @@ export class SessionRegistry {
173
173
  const snapshot = this.structured.getSlim(id) ?? this.processes.getOwnedSlim(id) ?? this.storage.getSessionSlim(id);
174
174
  return snapshot ? { ...snapshot, ...this.storage.getSessionWorkspace(id), ...this.storage.getSessionCompletion(id) } : null;
175
175
  }
176
- listSlim() {
176
+ listSlim(options) {
177
177
  const byId = new Map();
178
178
  for (const snapshot of this.structured.listSlim())
179
179
  byId.set(snapshot.id, snapshot);
@@ -183,6 +183,8 @@ export class SessionRegistry {
183
183
  }
184
184
  for (const snapshot of this.storage.loadSessionsSlim()) {
185
185
  const live = byId.get(snapshot.id);
186
+ if (options?.runtimeOnly && !live)
187
+ continue;
186
188
  byId.set(snapshot.id, live ? {
187
189
  ...live, workspaceId: snapshot.workspaceId, workspaceTaskId: snapshot.workspaceTaskId,
188
190
  completionRevision: snapshot.completionRevision,
@@ -0,0 +1,52 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { WandStorage } from "../storage.js";
3
+ import type { ApnsEnvironment } from "../apns-client.js";
4
+ export declare const APNS_SCHEMA = "\n CREATE TABLE IF NOT EXISTS apns_devices (\n id TEXT PRIMARY KEY, token TEXT NOT NULL, environment TEXT NOT NULL, topic TEXT NOT NULL,\n server_id TEXT NOT NULL, credential_digest TEXT NOT NULL, generation INTEGER NOT NULL DEFAULT 1,\n enabled INTEGER NOT NULL DEFAULT 1, expires_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS apns_devices_active ON apns_devices(enabled, expires_at);\n CREATE TABLE IF NOT EXISTS apns_events (\n id TEXT PRIMARY KEY, session_id TEXT NOT NULL, kind TEXT NOT NULL,\n revision TEXT NOT NULL, created_at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS apns_deliveries (\n id TEXT PRIMARY KEY, event_id TEXT NOT NULL, device_id TEXT NOT NULL, generation INTEGER NOT NULL,\n state TEXT NOT NULL DEFAULT 'pending', attempts INTEGER NOT NULL DEFAULT 0,\n next_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, reason TEXT,\n UNIQUE(event_id, device_id)\n );\n CREATE INDEX IF NOT EXISTS apns_deliveries_due ON apns_deliveries(state, next_at);\n";
5
+ export type ApnsEventKind = "completed" | "permission";
6
+ export interface ApnsDevice {
7
+ id: string;
8
+ token: string;
9
+ environment: ApnsEnvironment;
10
+ topic: string;
11
+ server_id: string;
12
+ credential_digest: string;
13
+ generation: number;
14
+ enabled: number;
15
+ expires_at: number;
16
+ }
17
+ export interface ApnsDelivery {
18
+ id: string;
19
+ event_id: string;
20
+ device_id: string;
21
+ generation: number;
22
+ attempts: number;
23
+ expires_at: number;
24
+ session_id: string;
25
+ kind: ApnsEventKind;
26
+ revision: string;
27
+ }
28
+ export declare function apnsDigest(value: string): string;
29
+ /** The revocation capability is stored as a digest only, never returned by a read endpoint. */
30
+ export declare class ApnsRepository {
31
+ private readonly db;
32
+ private readonly storage;
33
+ constructor(db: DatabaseSync, storage: WandStorage);
34
+ register(input: {
35
+ secret: string;
36
+ token: string;
37
+ environment: ApnsEnvironment;
38
+ topic: string;
39
+ serverId: string;
40
+ credentialDigest: string;
41
+ }, now: number): void;
42
+ revoke(secret: string): void;
43
+ private revokeDigest;
44
+ private cancelPending;
45
+ device(id: string): ApnsDevice | null;
46
+ enqueue(sessionId: string, kind: ApnsEventKind, revision: string, credentialDigest: string, topic: string, environment: ApnsEnvironment, now: number): boolean;
47
+ due(now: number): ApnsDelivery | null;
48
+ finish(id: string, state: "accepted" | "canceled" | "failed", reason: string): void;
49
+ retry(id: string, attempts: number, next: number, reason: string): void;
50
+ invalidateDevice(id: string, generation: number): void;
51
+ sweep(now: number): void;
52
+ }
@@ -0,0 +1,108 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ export const APNS_SCHEMA = `
3
+ CREATE TABLE IF NOT EXISTS apns_devices (
4
+ id TEXT PRIMARY KEY, token TEXT NOT NULL, environment TEXT NOT NULL, topic TEXT NOT NULL,
5
+ server_id TEXT NOT NULL, credential_digest TEXT NOT NULL, generation INTEGER NOT NULL DEFAULT 1,
6
+ enabled INTEGER NOT NULL DEFAULT 1, expires_at INTEGER NOT NULL
7
+ );
8
+ CREATE INDEX IF NOT EXISTS apns_devices_active ON apns_devices(enabled, expires_at);
9
+ CREATE TABLE IF NOT EXISTS apns_events (
10
+ id TEXT PRIMARY KEY, session_id TEXT NOT NULL, kind TEXT NOT NULL,
11
+ revision TEXT NOT NULL, created_at INTEGER NOT NULL
12
+ );
13
+ CREATE TABLE IF NOT EXISTS apns_deliveries (
14
+ id TEXT PRIMARY KEY, event_id TEXT NOT NULL, device_id TEXT NOT NULL, generation INTEGER NOT NULL,
15
+ state TEXT NOT NULL DEFAULT 'pending', attempts INTEGER NOT NULL DEFAULT 0,
16
+ next_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, reason TEXT,
17
+ UNIQUE(event_id, device_id)
18
+ );
19
+ CREATE INDEX IF NOT EXISTS apns_deliveries_due ON apns_deliveries(state, next_at);
20
+ `;
21
+ export function apnsDigest(value) { return createHash("sha256").update(value).digest("hex"); }
22
+ /** The revocation capability is stored as a digest only, never returned by a read endpoint. */
23
+ export class ApnsRepository {
24
+ db;
25
+ storage;
26
+ constructor(db, storage) {
27
+ this.db = db;
28
+ this.storage = storage;
29
+ }
30
+ register(input, now) {
31
+ this.storage.transaction(() => {
32
+ const id = apnsDigest(input.secret);
33
+ const previous = this.device(id);
34
+ const changed = !previous || previous.token !== input.token || previous.environment !== input.environment
35
+ || previous.topic !== input.topic || previous.server_id !== input.serverId || previous.credential_digest !== input.credentialDigest || !previous.enabled;
36
+ // Same physical device may have saved two origins for this same server. Only the latest binding delivers.
37
+ const duplicates = this.db.prepare("SELECT id FROM apns_devices WHERE token = ? AND environment = ? AND topic = ? AND id != ? AND enabled = 1")
38
+ .all(input.token, input.environment, input.topic, id);
39
+ for (const device of duplicates)
40
+ this.revokeDigest(device.id);
41
+ if (changed)
42
+ this.cancelPending(id);
43
+ this.db.prepare(`INSERT INTO apns_devices (id, token, environment, topic, server_id, credential_digest, generation, enabled, expires_at)
44
+ VALUES (?, ?, ?, ?, ?, ?, 1, 1, ?) ON CONFLICT(id) DO UPDATE SET
45
+ token = excluded.token, environment = excluded.environment, topic = excluded.topic, server_id = excluded.server_id,
46
+ credential_digest = excluded.credential_digest, generation = apns_devices.generation + ?, enabled = 1, expires_at = excluded.expires_at`)
47
+ .run(id, input.token, input.environment, input.topic, input.serverId, input.credentialDigest, now + 14 * 86_400_000, changed ? 1 : 0);
48
+ });
49
+ }
50
+ revoke(secret) { this.storage.transaction(() => this.revokeDigest(apnsDigest(secret))); }
51
+ revokeDigest(id) {
52
+ this.db.prepare("UPDATE apns_devices SET enabled = 0, token = '' WHERE id = ?").run(id);
53
+ this.cancelPending(id);
54
+ }
55
+ cancelPending(id) {
56
+ this.db.prepare("UPDATE apns_deliveries SET state = 'canceled', reason = 'BindingChanged' WHERE device_id = ? AND state = 'pending'").run(id);
57
+ }
58
+ device(id) {
59
+ return this.db.prepare("SELECT * FROM apns_devices WHERE id = ?").get(id) ?? null;
60
+ }
61
+ enqueue(sessionId, kind, revision, credentialDigest, topic, environment, now) {
62
+ return this.storage.transaction(() => {
63
+ const eventId = apnsDigest(JSON.stringify([sessionId, kind, revision]));
64
+ const inserted = this.db.prepare("INSERT OR IGNORE INTO apns_events (id, session_id, kind, revision, created_at) VALUES (?, ?, ?, ?, ?)")
65
+ .run(eventId, sessionId, kind, revision, now);
66
+ if (!inserted.changes)
67
+ return true;
68
+ const count = this.db.prepare("SELECT COUNT(*) AS n FROM apns_deliveries WHERE state = 'pending'").get();
69
+ if (count.n >= 1000)
70
+ return false; // bounded best-effort queue; never delay execution for a notification.
71
+ const devices = this.db.prepare(`SELECT id, generation FROM apns_devices WHERE enabled = 1 AND expires_at > ?
72
+ AND credential_digest = ? AND topic = ? AND environment = ? LIMIT ?`)
73
+ .all(now, credentialDigest, topic, environment, 1000 - count.n);
74
+ const expiresAt = now + (kind === "permission" ? 15 * 60_000 : 60 * 60_000);
75
+ for (const device of devices) {
76
+ this.db.prepare(`INSERT INTO apns_deliveries (id, event_id, device_id, generation, next_at, expires_at)
77
+ VALUES (?, ?, ?, ?, ?, ?)`).run(randomUUID(), eventId, device.id, device.generation, now, expiresAt);
78
+ }
79
+ return devices.length < 1000 - count.n;
80
+ });
81
+ }
82
+ due(now) {
83
+ return this.db.prepare(`SELECT d.id, d.event_id, d.device_id, d.generation, d.attempts, d.expires_at,
84
+ e.session_id, e.kind, e.revision FROM apns_deliveries d JOIN apns_events e ON e.id = d.event_id
85
+ WHERE d.state = 'pending' AND d.next_at <= ? ORDER BY d.next_at, d.rowid LIMIT 1`).get(now) ?? null;
86
+ }
87
+ finish(id, state, reason) {
88
+ this.db.prepare("UPDATE apns_deliveries SET state = ?, reason = ? WHERE id = ? AND state = 'pending'").run(state, reason, id);
89
+ }
90
+ retry(id, attempts, next, reason) {
91
+ this.db.prepare("UPDATE apns_deliveries SET attempts = ?, next_at = ?, reason = ? WHERE id = ? AND state = 'pending'").run(attempts, next, reason, id);
92
+ }
93
+ invalidateDevice(id, generation) {
94
+ // A late APNs rejection for an old token must never disable its replacement.
95
+ const current = this.device(id);
96
+ if (current?.generation === generation)
97
+ this.revokeDigest(id);
98
+ }
99
+ sweep(now) {
100
+ this.storage.transaction(() => {
101
+ this.db.prepare("UPDATE apns_devices SET enabled = 0, token = '' WHERE expires_at <= ?").run(now);
102
+ this.db.prepare("UPDATE apns_deliveries SET state = 'canceled', reason = 'Expired' WHERE expires_at <= ? AND state = 'pending'").run(now);
103
+ this.db.prepare("DELETE FROM apns_deliveries WHERE expires_at < ?").run(now - 7 * 86_400_000);
104
+ this.db.prepare("DELETE FROM apns_events WHERE created_at < ? AND id NOT IN (SELECT event_id FROM apns_deliveries)").run(now - 7 * 86_400_000);
105
+ this.db.prepare("DELETE FROM apns_devices WHERE expires_at < ?").run(now - 7 * 86_400_000);
106
+ });
107
+ }
108
+ }
@@ -48,6 +48,8 @@ export declare class ConversationRepository {
48
48
  conversationSessionIds(id: string): string[];
49
49
  conversationTaskStartup(taskId: string): ConversationRequest["receipt"] | null;
50
50
  appendConversationEvent(id: string, turn: ConversationTurn): void;
51
+ /** Exact message update scoped to its owning conversation; external echoes never create a second transcript. */
52
+ updateConversationEvent(id: string, messageId: string, patch: Partial<ConversationTurn>): boolean;
51
53
  conversationEvents(id: string): ConversationTurn[];
52
54
  /** List preview only needs the newest event, not the whole transcript. */
53
55
  lastConversationEvent(id: string): ConversationTurn | null;
@@ -190,6 +190,16 @@ export class ConversationRepository {
190
190
  this.db.prepare("INSERT INTO conversation_messages(id, conversation_id, turn_json, created_at) VALUES (?, ?, ?, ?)")
191
191
  .run(messageId, id, JSON.stringify({ ...turn, messageId, conversationId: id, createdAt }), createdAt);
192
192
  }
193
+ /** Exact message update scoped to its owning conversation; external echoes never create a second transcript. */
194
+ updateConversationEvent(id, messageId, patch) {
195
+ const row = this.db.prepare("SELECT turn_json FROM conversation_messages WHERE id = ? AND conversation_id = ?").get(messageId, id);
196
+ if (!row)
197
+ return false;
198
+ const turn = JSON.parse(row.turn_json);
199
+ this.db.prepare("UPDATE conversation_messages SET turn_json = ? WHERE id = ? AND conversation_id = ?")
200
+ .run(JSON.stringify({ ...turn, ...patch, messageId, conversationId: id }), messageId, id);
201
+ return true;
202
+ }
193
203
  conversationEvents(id) {
194
204
  return this.db.prepare("SELECT turn_json FROM conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid")
195
205
  .all(id).map((row) => JSON.parse(row.turn_json));
@@ -0,0 +1,20 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { DingTalkRecordKind, DingTalkRun } from "../dingtalk-types.js";
3
+ /** Additive domain records. Secrets belong to the existing encrypted connector vault, never this table. */
4
+ export declare const DINGTALK_SCHEMA = "CREATE TABLE IF NOT EXISTS dingtalk_records (\n kind TEXT NOT NULL, id TEXT NOT NULL, data TEXT NOT NULL, updated_at TEXT NOT NULL,\n PRIMARY KEY(kind, id)\n); CREATE INDEX IF NOT EXISTS dingtalk_records_kind_updated ON dingtalk_records(kind, updated_at);\nCREATE INDEX IF NOT EXISTS dingtalk_records_state ON dingtalk_records(kind, json_extract(data, '$.state'), id);\nCREATE INDEX IF NOT EXISTS dingtalk_records_delivery ON dingtalk_records(kind, json_extract(data, '$.delivery'), updated_at, id);\nCREATE INDEX IF NOT EXISTS dingtalk_records_binding_delivery ON dingtalk_records(kind, json_extract(data, '$.bindingId'), json_extract(data, '$.delivery'));";
5
+ export declare class DingTalkRepository {
6
+ private readonly db;
7
+ constructor(db: DatabaseSync);
8
+ getDingTalkRecord<T>(kind: DingTalkRecordKind, id: string): T | null;
9
+ listDingTalkRecords<T>(kind: DingTalkRecordKind): T[];
10
+ saveDingTalkRecord(kind: DingTalkRecordKind, id: string, value: unknown): void;
11
+ /** Explicit mirror deletion removes body copies but retains content-free admission tombstones. */
12
+ clearDingTalkGroupHistory(groupId: string): void;
13
+ countDingTalkRecords(kind: DingTalkRecordKind): number;
14
+ pendingDingTalkDeliveries(): DingTalkRun[];
15
+ runningDingTalkPage(after: string): DingTalkRun[];
16
+ /** Recovery never replays a model or a resource write. A request claim is the boundary before sending. */
17
+ recoverDingTalkIntents(): void;
18
+ /** Claim before any I/O. An unknown outcome is retained across restarts, never automatically replayed. */
19
+ claimDingTalkRecord(kind: DingTalkRecordKind, id: string, value: unknown): boolean;
20
+ }