@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.
- package/dist/ai-team-live.js +1 -1
- package/dist/apns-client.d.ts +45 -0
- package/dist/apns-client.js +156 -0
- package/dist/apns-service.d.ts +29 -0
- package/dist/apns-service.js +156 -0
- package/dist/build-info.json +3 -3
- package/dist/conversation-service.d.ts +2 -0
- package/dist/conversation-service.js +23 -3
- package/dist/conversation-types.d.ts +2 -0
- package/dist/dingtalk-auth.d.ts +41 -0
- package/dist/dingtalk-auth.js +127 -0
- package/dist/dingtalk-dws.d.ts +45 -0
- package/dist/dingtalk-dws.js +240 -0
- package/dist/dingtalk-service.d.ts +100 -0
- package/dist/dingtalk-service.js +865 -0
- package/dist/dingtalk-stream.d.ts +24 -0
- package/dist/dingtalk-stream.js +115 -0
- package/dist/dingtalk-types.d.ts +138 -0
- package/dist/dingtalk-types.js +1 -0
- package/dist/employee-knowledge.js +8 -0
- package/dist/live-activity-service.d.ts +26 -0
- package/dist/live-activity-service.js +158 -0
- package/dist/live-activity-state.d.ts +37 -0
- package/dist/live-activity-state.js +108 -0
- package/dist/middleware/api-scope.js +2 -0
- package/dist/pi-cli-checkpoint.d.ts +17 -0
- package/dist/pi-cli-checkpoint.js +10 -0
- package/dist/pi-event-projection.js +13 -0
- package/dist/pi-execution.js +2 -1
- package/dist/pi-protocol-reader.d.ts +6 -2
- package/dist/pi-protocol-reader.js +36 -12
- package/dist/pi-tmux-host.d.ts +1 -1
- package/dist/pi-tmux-host.js +28 -8
- package/dist/pi-tool-result-preview.d.ts +23 -0
- package/dist/pi-tool-result-preview.js +185 -0
- package/dist/render-daemon-client.js +1 -1
- package/dist/render-host.d.ts +3 -3
- package/dist/render-host.js +3 -3
- package/dist/server-apns-routes.d.ts +12 -0
- package/dist/server-apns-routes.js +111 -0
- package/dist/server-dingtalk-routes.d.ts +8 -0
- package/dist/server-dingtalk-routes.js +35 -0
- package/dist/server.js +49 -3
- package/dist/session-completion.d.ts +2 -1
- package/dist/session-completion.js +6 -2
- package/dist/session-registry.d.ts +3 -1
- package/dist/session-registry.js +3 -1
- package/dist/storage/apns-repository.d.ts +52 -0
- package/dist/storage/apns-repository.js +108 -0
- package/dist/storage/conversations-repository.d.ts +2 -0
- package/dist/storage/conversations-repository.js +10 -0
- package/dist/storage/dingtalk-repository.d.ts +20 -0
- package/dist/storage/dingtalk-repository.js +75 -0
- package/dist/storage/live-activity-repository.d.ts +68 -0
- package/dist/storage/live-activity-repository.js +117 -0
- package/dist/storage/migrations.js +22 -0
- package/dist/storage/sessions-repository.d.ts +5 -0
- package/dist/storage/sessions-repository.js +43 -1
- package/dist/storage-schema.js +2 -0
- package/dist/storage.d.ts +7 -1
- package/dist/storage.js +10 -0
- package/dist/structured-exec-host.d.ts +9 -1
- package/dist/structured-exec-pump.d.ts +11 -0
- package/dist/structured-exec-pump.js +27 -8
- package/dist/structured-pi-adapter.js +5 -0
- package/dist/structured-runner.d.ts +2 -0
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +51 -4
- package/dist/types.d.ts +7 -0
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/{4f30a913cc5b3f89 → 02fad74223254019}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{acb21c7e7fd6cb67 → 033e0a741edd2ba1}/page.js +1 -1
- package/dist/web-ui/content/modules/{e61136c22675ec7d → 0c04f94bbfd39a2f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{119874145414688d → 12e91deb96ace24d}/shell-file-panel-drawer.js +1 -1
- package/dist/web-ui/content/modules/15279373f0e1461f/host.js +1 -0
- package/dist/web-ui/content/modules/{78a46adbf9807f89 → 17603a34d545c784}/chunk.js +1 -1
- package/dist/web-ui/content/modules/1f041aa8d3673d93/chunk.js +1 -0
- package/dist/web-ui/content/modules/{96dd661559b67ebf → 20483d6e57cfe9d7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f55031a02775c89e → 217595f72a1c3fc5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/2230c51655163cf3/runtime.js +279 -0
- package/dist/web-ui/content/modules/{82e40fdf0abfb8cd → 2460e872ab3a1f37}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{9e754b01fa4207fb → 2a47853ca7752c1f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e95b60ab18a79499 → 2afbc201474adacc}/login-visual.js +1 -1
- package/dist/web-ui/content/modules/{38917c19e2cf1702 → 30169d15cea782f0}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{5513bcbacdf44588 → 37abb74d1a0a0791}/notice-view.js +1 -1
- package/dist/web-ui/content/modules/{b62294e53dcb3147 → 38bb024e09cb5d5d}/host.js +1 -1
- package/dist/web-ui/content/modules/39faef0cffbad28e/home.js +6 -0
- package/dist/web-ui/content/modules/3a2fa12c8b537964/chunk.js +1 -0
- package/dist/web-ui/content/modules/{35a8fd2cfd31d7a0 → 3a72b79418f0f80a}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{321bd9c755bc30fc → 3bd3c84c4257ba0d}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{2981483671797b92 → 3d10e3f6f6f92b55}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{6f341fe3e8aee844 → 3d4451bc069c1498}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4a9a3e1f87b2c32b → 3d5c498522b48225}/dialog-modal.js +1 -1
- package/dist/web-ui/content/modules/426b8133daebff78/host.js +1 -0
- package/dist/web-ui/content/modules/455436229dc57d7f/sidebar-peek.js +1 -0
- package/dist/web-ui/content/modules/46de3c2e2ca680c1/host.js +1 -0
- package/dist/web-ui/content/modules/47af6b35e1d01ea7/host.js +30 -0
- package/dist/web-ui/content/modules/4841e45cb7c5cc67/queue-view-adapter.js +3 -0
- package/dist/web-ui/content/modules/{47419445e0aaa3cc → 497789a07177ed7b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{32c461164a1b364c → 5472191457a926d3}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{c152782274c04fad → 55a47b6e2c2016f2}/chunk.js +1 -1
- package/dist/web-ui/content/modules/5743d8116d15696b/employee-card.js +1 -0
- package/dist/web-ui/content/modules/{c81a3d95b14cdcd4 → 578703196f82222d}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{8ce71665ae55ccf1 → 57d68f0a47e8fd7c}/toast-region.js +1 -1
- package/dist/web-ui/content/modules/58866143a3365521/chunk.js +1 -0
- package/dist/web-ui/content/modules/{3601736ae2d678ef → 5bb5db89a343817f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a098c7b1aa4e2f6b → 5c9e8245d4a8a312}/sha256.js +1 -1
- package/dist/web-ui/content/modules/{8198bfd269c0caca → 5de35db482fb1a42}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{aab92f2990a7185f → 5e7104e569359449}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4b9deeeb77570db3 → 601a2767ae24d1cb}/chunk.js +1 -1
- package/dist/web-ui/content/modules/6687ad7aa57f443b/task-board-host.js +22 -0
- package/dist/web-ui/content/modules/{c422f47fd13dc4bc → 684db83476dcba20}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{cf715621edb1d12e → 6b0bafa239dfe4e2}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{cd22aa55f6cc5276 → 6bd6ee3c9f05e2f5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{09ef1bb1d195d727 → 79b0c18f2bf35740}/host.js +1 -1
- package/dist/web-ui/content/modules/{905f8bc3f8a3efb8 → 7ae83a3f06f2daa3}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{84b79a33c64e146c → 7f32261c1627c3cd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{eece3c2d2374e8f7 → 805b859b1b76ce4d}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{eecaebc5482ee203 → 84358b5b729cbe48}/chunk.js +1 -1
- package/dist/web-ui/content/modules/85181d84d5ce62b3/employee-profile.js +1 -0
- package/dist/web-ui/content/modules/87edd7cc79811bb8/composer-surface-adapter.js +1 -0
- package/dist/web-ui/content/modules/{b0254a2d3652a925 → 8ea498cf092dba30}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a4af270960a39ea6 → 8f0d4e22c18a58b0}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{c72c4f1d47c3726d → 907dcf0ea5b5f23f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{9f33acc1c4284e03 → 916021ce252882ca}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{98c14fc0474455a5 → 91ee32135a3582b0}/chunk.js +1 -1
- package/dist/web-ui/content/modules/92df279c40bc4218/host.js +1 -0
- package/dist/web-ui/content/modules/{544dc812abdfbbb8 → 9661b076f08e64b4}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{5c94fb2b7abcd1a5 → 97ef63ff1f0784d9}/chunk.js +1 -1
- package/dist/web-ui/content/modules/99e48e07517d3f83/dropdown-surface.js +1 -0
- package/dist/web-ui/content/modules/9eb83751622e2b97/chunk.js +10 -0
- package/dist/web-ui/content/modules/{7375574a2ea42740 → a1715be0f97c2479}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{456634525d44777d → a2be1fd01cf06837}/chunk.js +1 -1
- package/dist/web-ui/content/modules/a33666b143331cd6/workspace-window.js +1 -0
- package/dist/web-ui/content/modules/{b38924f45bfa71d6 → a41351130003e6bb}/chunk.js +1 -1
- package/dist/web-ui/content/modules/a59c83b5b5b85da2/host.js +1 -0
- package/dist/web-ui/content/modules/{a4dca2ffaf656d3c → a63f0b3d73d97e0b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/a685793c5d9b90d1/chunk.js +1 -0
- package/dist/web-ui/content/modules/a750f4868304b089/host.js +1 -0
- package/dist/web-ui/content/modules/abfe4b88dcd89d88/host.js +1 -0
- package/dist/web-ui/content/modules/{15bcc550ef9fd5b7 → acda148b7423bdab}/host.js +1 -1
- package/dist/web-ui/content/modules/{ad1136919a65fc6e → af6ff78aeb782cf2}/host.js +1 -1
- package/dist/web-ui/content/modules/b32c9addcd31f54b/panel-host.js +1 -0
- package/dist/web-ui/content/modules/{b937f1a939618be9 → b358eaf2e45849a6}/host.js +1 -1
- package/dist/web-ui/content/modules/{476ff7911bce1a38 → b7aeeb22bf604ec7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/bad2c42c7ddeab94/host.js +1 -0
- package/dist/web-ui/content/modules/{622a8d12c4c622bf → bb59e8cb4323bf06}/host.js +1 -1
- package/dist/web-ui/content/modules/{08c7b047195f138d → be497f1d39753979}/chunk.js +1 -1
- package/dist/web-ui/content/modules/bfa8e2c827201374/chunk.js +1 -0
- package/dist/web-ui/content/modules/{5395fa57a47e9b96 → bfe51130a84795fb}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{8e29056fb56439f7 → c03e0f84f798d33a}/chunk.js +1 -1
- package/dist/web-ui/content/modules/c118e0c96bc023ee/host-bridge.js +1 -0
- package/dist/web-ui/content/modules/c3512b18c4c0e4ac/brand-identity.js +1 -0
- package/dist/web-ui/content/modules/c3e0de0a61f0db34/chunk.js +1 -0
- package/dist/web-ui/content/modules/{9d48d1ea3c9228bd → c3e1f083e07d6840}/library-bridge.js +1 -1
- package/dist/web-ui/content/modules/cd5e90a37ccdf2dc/group-editor.js +1 -0
- package/dist/web-ui/content/modules/{2edc19639ac1bd4b → cdb8281bb89da303}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f6f86d43c12b0864 → ce90d0b55bbe6430}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{72b13a7486b6d437 → d274491ffc8ee646}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{b1d43ed06a5d1a57 → d3cc94cf4a47a12a}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{b0c4eed46c1b6ad4 → d4b07cef43d36e79}/chat-render.js +5 -4
- package/dist/web-ui/content/modules/{8a4f0ef2c0812af6 → d7b540917fb90755}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{ef7f76b551fdc619 → d856bfc56bea5f20}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{42c439d3acfe22bd → d857ea8244b1026e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{36ce8123a2079807 → d9019922118b2c37}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{58836e616e6d12ff → d90e77f9f8d4f1ec}/workspaces-panel.js +1 -1
- package/dist/web-ui/content/modules/{4d4870f20eb1c52d → da1aa5fa0186fea7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/de977ff0b3ad3882/progress.js +1 -0
- package/dist/web-ui/content/modules/{222f48948517167e → e01769df89873feb}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{5df1b522f5d3c99d → e0ab9fa5a8882433}/host.js +1 -1
- package/dist/web-ui/content/modules/e25dea52e13504b6/markdown.js +1 -0
- package/dist/web-ui/content/modules/{36bd608366c56b72 → e43af4aa407885fd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/ee893f06cdb2b4f7/panel.js +1 -0
- package/dist/web-ui/content/modules/{f90f80d7207dff1f → f2bdbdc4a1141826}/host.js +1 -1
- package/dist/web-ui/content/modules/{e33f6d66a02ec26c → f36b4d3d44491852}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{665e81fe210f1b7a → fa79ed5d045c24dd}/chunk.js +1 -1
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +412 -406
- package/dist/web-ui/embedded-assets.js +4 -4
- package/package.json +1 -1
- package/scripts/install-render-binary.js +1 -1
- package/dist/web-ui/content/modules/1a7303b87763fd43/employee-profile.js +0 -1
- package/dist/web-ui/content/modules/1d35739f634745f6/task-board-host.js +0 -22
- package/dist/web-ui/content/modules/2099d37d57856cbd/brand-identity.js +0 -1
- package/dist/web-ui/content/modules/2d6339b2f6c2e2d0/host.js +0 -1
- package/dist/web-ui/content/modules/2f88fc37c6105f7e/sidebar-peek.js +0 -1
- package/dist/web-ui/content/modules/31124fb7ee6939df/host.js +0 -1
- package/dist/web-ui/content/modules/31b28c5d0019c363/host-bridge.js +0 -1
- package/dist/web-ui/content/modules/36e5c3a6d6705358/employee-card.js +0 -1
- package/dist/web-ui/content/modules/3700d99c54d3c400/host.js +0 -1
- package/dist/web-ui/content/modules/3ae29086b227f9d9/markdown.js +0 -1
- package/dist/web-ui/content/modules/42db858e4417c702/chunk.js +0 -10
- package/dist/web-ui/content/modules/443dc81eac29f233/host.js +0 -1
- package/dist/web-ui/content/modules/4af10e1f8a789204/runtime.js +0 -275
- package/dist/web-ui/content/modules/55b75ebcf6924001/host.js +0 -30
- package/dist/web-ui/content/modules/626c7ce0aa1714b0/host.js +0 -1
- package/dist/web-ui/content/modules/76adde4019731df1/chunk.js +0 -1
- package/dist/web-ui/content/modules/7925b65867b7aa4a/queue-view-adapter.js +0 -3
- package/dist/web-ui/content/modules/8a6844a4251a1271/chunk.js +0 -1
- package/dist/web-ui/content/modules/8bd214570df4a7a0/host.js +0 -1
- package/dist/web-ui/content/modules/8bff85e108320aab/panel.js +0 -1
- package/dist/web-ui/content/modules/988e01b67e6f1883/composer-surface-adapter.js +0 -1
- package/dist/web-ui/content/modules/a975ff38c3d43ba8/workspace-window.js +0 -1
- package/dist/web-ui/content/modules/b550260fc9c5363a/dropdown-surface.js +0 -1
- package/dist/web-ui/content/modules/bce467c603299460/chunk.js +0 -1
- package/dist/web-ui/content/modules/c138b442c1682189/panel-host.js +0 -1
- package/dist/web-ui/content/modules/c32d14632487364f/group-editor.js +0 -1
- package/dist/web-ui/content/modules/c6d0a1b2753a4883/host.js +0 -1
- package/dist/web-ui/content/modules/cdff6e7f72879c88/progress.js +0 -1
- package/dist/web-ui/content/modules/cfee7b0d79587d59/home.js +0 -6
- package/dist/web-ui/content/modules/e5e93fe5304838a1/chunk.js +0 -1
- 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
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|
package/dist/session-registry.js
CHANGED
|
@@ -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
|
+
}
|