@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,75 @@
|
|
|
1
|
+
/** Additive domain records. Secrets belong to the existing encrypted connector vault, never this table. */
|
|
2
|
+
export const DINGTALK_SCHEMA = `CREATE TABLE IF NOT EXISTS dingtalk_records (
|
|
3
|
+
kind TEXT NOT NULL, id TEXT NOT NULL, data TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
4
|
+
PRIMARY KEY(kind, id)
|
|
5
|
+
); CREATE INDEX IF NOT EXISTS dingtalk_records_kind_updated ON dingtalk_records(kind, updated_at);
|
|
6
|
+
CREATE INDEX IF NOT EXISTS dingtalk_records_state ON dingtalk_records(kind, json_extract(data, '$.state'), id);
|
|
7
|
+
CREATE INDEX IF NOT EXISTS dingtalk_records_delivery ON dingtalk_records(kind, json_extract(data, '$.delivery'), updated_at, id);
|
|
8
|
+
CREATE INDEX IF NOT EXISTS dingtalk_records_binding_delivery ON dingtalk_records(kind, json_extract(data, '$.bindingId'), json_extract(data, '$.delivery'));`;
|
|
9
|
+
export class DingTalkRepository {
|
|
10
|
+
db;
|
|
11
|
+
constructor(db) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
}
|
|
14
|
+
getDingTalkRecord(kind, id) {
|
|
15
|
+
const row = this.db.prepare("SELECT data FROM dingtalk_records WHERE kind = ? AND id = ?").get(kind, id);
|
|
16
|
+
return row ? JSON.parse(row.data) : null;
|
|
17
|
+
}
|
|
18
|
+
listDingTalkRecords(kind) {
|
|
19
|
+
return this.db.prepare("SELECT data FROM dingtalk_records WHERE kind = ? ORDER BY updated_at, id LIMIT 10000").all(kind).map(row => JSON.parse(row.data));
|
|
20
|
+
}
|
|
21
|
+
saveDingTalkRecord(kind, id, value) {
|
|
22
|
+
const data = JSON.stringify(value);
|
|
23
|
+
if (Buffer.byteLength(data) > 512 * 1024)
|
|
24
|
+
throw new Error("钉钉记录过大。");
|
|
25
|
+
this.db.prepare(`INSERT INTO dingtalk_records(kind,id,data,updated_at) VALUES(?,?,?,?)
|
|
26
|
+
ON CONFLICT(kind,id) DO UPDATE SET data=excluded.data,updated_at=excluded.updated_at`).run(kind, id, data, new Date().toISOString());
|
|
27
|
+
}
|
|
28
|
+
/** Explicit mirror deletion removes body copies but retains content-free admission tombstones. */
|
|
29
|
+
clearDingTalkGroupHistory(groupId) {
|
|
30
|
+
this.db.prepare(`UPDATE dingtalk_records SET data = json_remove(json_set(data, '$.state', 'unknown', '$.delivery', 'none'), '$.text', '$.error'), updated_at = ?
|
|
31
|
+
WHERE kind = 'run' AND json_extract(data, '$.groupId') = ?`).run(new Date().toISOString(), groupId);
|
|
32
|
+
}
|
|
33
|
+
countDingTalkRecords(kind) {
|
|
34
|
+
return this.db.prepare("SELECT COUNT(*) AS n FROM dingtalk_records WHERE kind = ?").get(kind).n;
|
|
35
|
+
}
|
|
36
|
+
pendingDingTalkDeliveries() {
|
|
37
|
+
return this.db.prepare(`SELECT data FROM dingtalk_records WHERE kind = 'run'
|
|
38
|
+
AND json_extract(data, '$.delivery') = 'pending' ORDER BY updated_at, id LIMIT 4`).all()
|
|
39
|
+
.map(row => JSON.parse(row.data));
|
|
40
|
+
}
|
|
41
|
+
runningDingTalkPage(after) {
|
|
42
|
+
return this.db.prepare(`SELECT data FROM dingtalk_records WHERE kind = 'run'
|
|
43
|
+
AND json_extract(data, '$.state') = 'running' AND id > ? ORDER BY id LIMIT 100`).all(after)
|
|
44
|
+
.map(row => JSON.parse(row.data));
|
|
45
|
+
}
|
|
46
|
+
/** Recovery never replays a model or a resource write. A request claim is the boundary before sending. */
|
|
47
|
+
recoverDingTalkIntents() {
|
|
48
|
+
this.db.exec(`UPDATE dingtalk_records SET data = json_set(data, '$.state', 'unknown')
|
|
49
|
+
WHERE kind = 'job' AND json_extract(data, '$.state') = 'running';
|
|
50
|
+
UPDATE dingtalk_records SET data = json_set(data, '$.state', 'unknown', '$.error', '服务重启,原接入步骤待核实;请继续原任务,不会重复创建资源。')
|
|
51
|
+
WHERE kind = 'binding' AND json_extract(data, '$.state') IN ('creating', 'configuring', 'joining');
|
|
52
|
+
UPDATE dingtalk_records SET data = json_set(data, '$.state', 'unknown', '$.error', '服务重启前已接受,请核对原执行,不会重复执行。')
|
|
53
|
+
WHERE kind = 'run' AND json_extract(data, '$.state') = 'accepted';
|
|
54
|
+
UPDATE dingtalk_records AS r SET data = json_set(data,
|
|
55
|
+
'$.delivery', CASE
|
|
56
|
+
WHEN EXISTS(SELECT 1 FROM dingtalk_records q WHERE q.kind = 'request' AND q.id = 'final:' || r.id AND json_extract(q.data, '$.state') = 'sent') THEN 'sent'
|
|
57
|
+
WHEN EXISTS(SELECT 1 FROM dingtalk_records q WHERE q.kind = 'request' AND q.id = 'final:' || r.id) THEN 'unknown'
|
|
58
|
+
ELSE 'pending' END,
|
|
59
|
+
'$.state', CASE
|
|
60
|
+
WHEN EXISTS(SELECT 1 FROM dingtalk_records q WHERE q.kind = 'request' AND q.id = 'final:' || r.id AND json_extract(q.data, '$.state') = 'sent') THEN CASE WHEN json_extract(r.data, '$.state') = 'failed' THEN 'failed' ELSE 'delivered' END
|
|
61
|
+
WHEN EXISTS(SELECT 1 FROM dingtalk_records q WHERE q.kind = 'request' AND q.id = 'final:' || r.id) THEN 'unknown'
|
|
62
|
+
ELSE CASE WHEN json_extract(r.data, '$.state') = 'failed' THEN 'failed' ELSE 'completed' END END)
|
|
63
|
+
WHERE kind = 'run' AND json_extract(data, '$.delivery') = 'sending';
|
|
64
|
+
UPDATE dingtalk_records AS b SET data = json_set(data, '$.state', 'unknown', '$.error', '此前回复送达未确认,请核实原群消息后恢复,不会重放旧任务。')
|
|
65
|
+
WHERE kind = 'binding' AND json_extract(data, '$.state') != 'paused' AND json_extract(data, '$.removedAt') IS NULL
|
|
66
|
+
AND EXISTS(SELECT 1 FROM dingtalk_records r WHERE r.kind = 'run' AND json_extract(r.data, '$.bindingId') = b.id
|
|
67
|
+
AND json_extract(r.data, '$.delivery') = 'unknown'
|
|
68
|
+
AND coalesce(json_extract(r.data, '$.bindingGeneration'), 'legacy') = coalesce(json_extract(b.data, '$.generation'), 'legacy'));`);
|
|
69
|
+
}
|
|
70
|
+
/** Claim before any I/O. An unknown outcome is retained across restarts, never automatically replayed. */
|
|
71
|
+
claimDingTalkRecord(kind, id, value) {
|
|
72
|
+
return this.db.prepare("INSERT OR IGNORE INTO dingtalk_records(kind,id,data,updated_at) VALUES(?,?,?,?)")
|
|
73
|
+
.run(kind, id, JSON.stringify(value), new Date().toISOString()).changes > 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { WandStorage } from "../storage.js";
|
|
3
|
+
import type { ApnsEnvironment } from "../apns-client.js";
|
|
4
|
+
import type { LiveActivityCachedEntry } from "../live-activity-state.js";
|
|
5
|
+
export declare const LIVE_ACTIVITY_SCHEMA = "\n CREATE TABLE IF NOT EXISTS apns_live_activities (\n id TEXT PRIMARY KEY, activity_id TEXT NOT NULL, token TEXT NOT NULL,\n server_id TEXT NOT NULL, credential_digest TEXT NOT NULL, topic TEXT NOT NULL, environment TEXT NOT NULL,\n generation INTEGER NOT NULL DEFAULT 1, enabled INTEGER NOT NULL DEFAULT 1, blocked INTEGER NOT NULL DEFAULT 0,\n fact_revision INTEGER NOT NULL DEFAULT 0, prepared_revision INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL,\n content_json TEXT NOT NULL DEFAULT '[]', signature TEXT NOT NULL DEFAULT '', last_timestamp INTEGER NOT NULL DEFAULT 0,\n last_sent_at INTEGER NOT NULL DEFAULT 0, next_at INTEGER NOT NULL,\n pending_id TEXT, pending_payload TEXT, pending_signature TEXT, pending_event TEXT, pending_priority INTEGER,\n pending_next_at INTEGER, attempts INTEGER NOT NULL DEFAULT 0, outcome TEXT, reason TEXT\n );\n CREATE INDEX IF NOT EXISTS apns_live_due ON apns_live_activities(enabled, blocked, next_at);\n";
|
|
6
|
+
export interface LiveActivityRecord {
|
|
7
|
+
id: string;
|
|
8
|
+
activity_id: string;
|
|
9
|
+
token: string;
|
|
10
|
+
server_id: string;
|
|
11
|
+
credential_digest: string;
|
|
12
|
+
topic: string;
|
|
13
|
+
environment: ApnsEnvironment;
|
|
14
|
+
generation: number;
|
|
15
|
+
enabled: number;
|
|
16
|
+
blocked: number;
|
|
17
|
+
fact_revision: number;
|
|
18
|
+
prepared_revision: number;
|
|
19
|
+
created_at: number;
|
|
20
|
+
expires_at: number;
|
|
21
|
+
content_json: string;
|
|
22
|
+
signature: string;
|
|
23
|
+
last_timestamp: number;
|
|
24
|
+
last_sent_at: number;
|
|
25
|
+
next_at: number;
|
|
26
|
+
pending_id: string | null;
|
|
27
|
+
pending_payload: string | null;
|
|
28
|
+
pending_signature: string | null;
|
|
29
|
+
pending_event: "update" | "end" | null;
|
|
30
|
+
pending_priority: 5 | 10 | null;
|
|
31
|
+
pending_next_at: number | null;
|
|
32
|
+
attempts: number;
|
|
33
|
+
outcome: string | null;
|
|
34
|
+
reason: string | null;
|
|
35
|
+
}
|
|
36
|
+
export declare class LiveActivityRepository {
|
|
37
|
+
private readonly db;
|
|
38
|
+
private readonly storage;
|
|
39
|
+
constructor(db: DatabaseSync, storage: WandStorage);
|
|
40
|
+
register(input: {
|
|
41
|
+
secret: string;
|
|
42
|
+
activityId: string;
|
|
43
|
+
token: string;
|
|
44
|
+
serverId: string;
|
|
45
|
+
credentialDigest: string;
|
|
46
|
+
topic: string;
|
|
47
|
+
environment: ApnsEnvironment;
|
|
48
|
+
}, now: number): void;
|
|
49
|
+
get(id: string): LiveActivityRecord | null;
|
|
50
|
+
revoke(secret: string): void;
|
|
51
|
+
disable(id: string, reason: string, generation?: number): void;
|
|
52
|
+
wake(now: number): void;
|
|
53
|
+
dirty(now: number): void;
|
|
54
|
+
due(now: number): LiveActivityRecord | null;
|
|
55
|
+
defer(id: string, generation: number, next: number): void;
|
|
56
|
+
prepare(record: LiveActivityRecord, update: {
|
|
57
|
+
payload: string;
|
|
58
|
+
signature: string;
|
|
59
|
+
entries: LiveActivityCachedEntry[];
|
|
60
|
+
event: "update" | "end";
|
|
61
|
+
priority: 5 | 10;
|
|
62
|
+
timestamp: number;
|
|
63
|
+
nextAt: number;
|
|
64
|
+
}, now: number): LiveActivityRecord | null;
|
|
65
|
+
finish(record: LiveActivityRecord, outcome: "accepted" | "failed" | "rejected", reason: string, now: number): void;
|
|
66
|
+
retry(record: LiveActivityRecord, reason: string, now: number): void;
|
|
67
|
+
sweep(now: number): void;
|
|
68
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { apnsDigest } from "./apns-repository.js";
|
|
3
|
+
export const LIVE_ACTIVITY_SCHEMA = `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS apns_live_activities (
|
|
5
|
+
id TEXT PRIMARY KEY, activity_id TEXT NOT NULL, token TEXT NOT NULL,
|
|
6
|
+
server_id TEXT NOT NULL, credential_digest TEXT NOT NULL, topic TEXT NOT NULL, environment TEXT NOT NULL,
|
|
7
|
+
generation INTEGER NOT NULL DEFAULT 1, enabled INTEGER NOT NULL DEFAULT 1, blocked INTEGER NOT NULL DEFAULT 0,
|
|
8
|
+
fact_revision INTEGER NOT NULL DEFAULT 0, prepared_revision INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL,
|
|
10
|
+
content_json TEXT NOT NULL DEFAULT '[]', signature TEXT NOT NULL DEFAULT '', last_timestamp INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
last_sent_at INTEGER NOT NULL DEFAULT 0, next_at INTEGER NOT NULL,
|
|
12
|
+
pending_id TEXT, pending_payload TEXT, pending_signature TEXT, pending_event TEXT, pending_priority INTEGER,
|
|
13
|
+
pending_next_at INTEGER, attempts INTEGER NOT NULL DEFAULT 0, outcome TEXT, reason TEXT
|
|
14
|
+
);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS apns_live_due ON apns_live_activities(enabled, blocked, next_at);
|
|
16
|
+
`;
|
|
17
|
+
export class LiveActivityRepository {
|
|
18
|
+
db;
|
|
19
|
+
storage;
|
|
20
|
+
constructor(db, storage) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
this.storage = storage;
|
|
23
|
+
}
|
|
24
|
+
register(input, now) {
|
|
25
|
+
this.storage.transaction(() => {
|
|
26
|
+
const id = apnsDigest(input.secret);
|
|
27
|
+
const previous = this.get(id);
|
|
28
|
+
const aliases = this.db.prepare("SELECT id FROM apns_live_activities WHERE id != ? AND enabled = 1 AND (activity_id = ? OR (token = ? AND topic = ? AND environment = ?))")
|
|
29
|
+
.all(id, input.activityId, input.token, input.topic, input.environment);
|
|
30
|
+
for (const alias of aliases)
|
|
31
|
+
this.disable(alias.id, "Superseded");
|
|
32
|
+
const count = this.db.prepare("SELECT COUNT(*) AS n FROM apns_live_activities WHERE enabled = 1 AND expires_at > ? AND id != ?").get(now, id);
|
|
33
|
+
if (count.n >= 64)
|
|
34
|
+
throw new Error("Live Activity registration limit reached");
|
|
35
|
+
const changed = !previous || !previous.enabled || previous.activity_id !== input.activityId || previous.token !== input.token
|
|
36
|
+
|| previous.server_id !== input.serverId || previous.credential_digest !== input.credentialDigest
|
|
37
|
+
|| previous.topic !== input.topic || previous.environment !== input.environment;
|
|
38
|
+
const created = previous?.activity_id === input.activityId ? previous.created_at : now;
|
|
39
|
+
this.db.prepare(`INSERT INTO apns_live_activities
|
|
40
|
+
(id, activity_id, token, server_id, credential_digest, topic, environment, created_at, expires_at, next_at)
|
|
41
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
42
|
+
ON CONFLICT(id) DO UPDATE SET activity_id = excluded.activity_id, token = excluded.token,
|
|
43
|
+
server_id = excluded.server_id, credential_digest = excluded.credential_digest,
|
|
44
|
+
topic = excluded.topic, environment = excluded.environment, enabled = 1, blocked = 0,
|
|
45
|
+
created_at = excluded.created_at, expires_at = excluded.expires_at, next_at = MIN(apns_live_activities.next_at, excluded.next_at),
|
|
46
|
+
generation = apns_live_activities.generation + ?`)
|
|
47
|
+
.run(id, input.activityId, input.token, input.serverId, input.credentialDigest, input.topic, input.environment, created, created + 8 * 3_600_000, now, changed ? 1 : 0);
|
|
48
|
+
if (changed) {
|
|
49
|
+
this.db.prepare("UPDATE apns_live_activities SET pending_id = NULL, pending_payload = NULL, attempts = 0 WHERE id = ?").run(id);
|
|
50
|
+
if (previous && (previous.activity_id !== input.activityId || previous.server_id !== input.serverId || previous.credential_digest !== input.credentialDigest)) {
|
|
51
|
+
this.db.prepare("UPDATE apns_live_activities SET content_json = '[]', signature = '', last_timestamp = 0, last_sent_at = 0 WHERE id = ?").run(id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
get(id) {
|
|
57
|
+
return this.db.prepare("SELECT * FROM apns_live_activities WHERE id = ?").get(id) ?? null;
|
|
58
|
+
}
|
|
59
|
+
revoke(secret) { this.disable(apnsDigest(secret), "ClientRevoked"); }
|
|
60
|
+
disable(id, reason, generation) {
|
|
61
|
+
this.db.prepare(`UPDATE apns_live_activities SET enabled = 0, token = '', pending_id = NULL, pending_payload = NULL,
|
|
62
|
+
outcome = 'closed', reason = ? WHERE id = ? ${generation === undefined ? "" : "AND generation = ?"}`)
|
|
63
|
+
.run(reason, id, ...(generation === undefined ? [] : [generation]));
|
|
64
|
+
}
|
|
65
|
+
wake(now) {
|
|
66
|
+
this.db.prepare("UPDATE apns_live_activities SET blocked = 0, next_at = MIN(next_at, ?) WHERE enabled = 1").run(now);
|
|
67
|
+
}
|
|
68
|
+
dirty(now) {
|
|
69
|
+
// Coalesce facts, not deliveries. A redundant status heartbeat must not reset a transport retry budget.
|
|
70
|
+
this.db.prepare(`UPDATE apns_live_activities SET next_at = CASE WHEN pending_id IS NULL THEN MIN(next_at, ?) ELSE next_at END, fact_revision = fact_revision + 1
|
|
71
|
+
WHERE enabled = 1 AND blocked = 0`).run(now + 2000);
|
|
72
|
+
}
|
|
73
|
+
due(now) {
|
|
74
|
+
return this.db.prepare("SELECT * FROM apns_live_activities WHERE enabled = 1 AND blocked = 0 AND next_at <= ? ORDER BY next_at, rowid LIMIT 1")
|
|
75
|
+
.get(now) ?? null;
|
|
76
|
+
}
|
|
77
|
+
defer(id, generation, next) {
|
|
78
|
+
this.db.prepare("UPDATE apns_live_activities SET next_at = ? WHERE id = ? AND generation = ? AND enabled = 1").run(next, id, generation);
|
|
79
|
+
}
|
|
80
|
+
prepare(record, update, now) {
|
|
81
|
+
const id = randomUUID();
|
|
82
|
+
const result = this.db.prepare(`UPDATE apns_live_activities SET content_json = ?, last_timestamp = ?,
|
|
83
|
+
pending_id = ?, pending_payload = ?, pending_signature = ?, pending_event = ?, pending_priority = ?, pending_next_at = ?,
|
|
84
|
+
attempts = 0, prepared_revision = fact_revision, next_at = ? WHERE id = ? AND generation = ? AND enabled = 1`)
|
|
85
|
+
.run(JSON.stringify(update.entries), update.timestamp, id, update.payload, update.signature, update.event, update.priority, update.nextAt, now, record.id, record.generation);
|
|
86
|
+
return result.changes ? this.get(record.id) : null;
|
|
87
|
+
}
|
|
88
|
+
finish(record, outcome, reason, now) {
|
|
89
|
+
this.storage.transaction(() => {
|
|
90
|
+
// A route revoke/token rotation/new state may have superseded this HTTP/2 request while it was in flight.
|
|
91
|
+
const current = this.get(record.id);
|
|
92
|
+
if (!current?.enabled || current.generation !== record.generation || current.pending_id !== record.pending_id)
|
|
93
|
+
return;
|
|
94
|
+
if (outcome === "accepted" && record.pending_event === "end") {
|
|
95
|
+
this.db.prepare("UPDATE apns_live_activities SET enabled = 0, token = '', pending_id = NULL, pending_payload = NULL, outcome = 'accepted', reason = 'EndedByAPNs' WHERE id = ?").run(record.id);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const next = outcome === "accepted" ? record.pending_next_at ?? now + 60_000 : now + 60_000;
|
|
99
|
+
const nextWithFacts = current.fact_revision > record.prepared_revision ? Math.min(next, now + 2000) : next;
|
|
100
|
+
this.db.prepare(`UPDATE apns_live_activities SET signature = ?, last_sent_at = ?,
|
|
101
|
+
next_at = ?, pending_id = NULL, pending_payload = NULL, outcome = ?, reason = ?, blocked = ?
|
|
102
|
+
WHERE id = ? AND generation = ? AND pending_id = ?`)
|
|
103
|
+
.run(record.pending_signature ?? "", now, nextWithFacts, outcome, reason, outcome === "rejected" ? 1 : 0, record.id, record.generation, record.pending_id);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
retry(record, reason, now) {
|
|
107
|
+
this.db.prepare(`UPDATE apns_live_activities SET attempts = attempts + 1, next_at = ?, outcome = 'retry', reason = ?
|
|
108
|
+
WHERE id = ? AND generation = ? AND enabled = 1 AND pending_id = ?`)
|
|
109
|
+
.run(now + Math.min(60_000, 2000 * 2 ** record.attempts), reason, record.id, record.generation, record.pending_id);
|
|
110
|
+
}
|
|
111
|
+
sweep(now) {
|
|
112
|
+
this.storage.transaction(() => {
|
|
113
|
+
this.db.prepare("UPDATE apns_live_activities SET enabled = 0, token = '', pending_payload = NULL, pending_id = NULL, reason = 'Expired' WHERE expires_at <= ?").run(now);
|
|
114
|
+
this.db.prepare("DELETE FROM apns_live_activities WHERE expires_at < ?").run(now - 7 * 86_400_000);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { APNS_SCHEMA } from "./apns-repository.js";
|
|
3
|
+
import { LIVE_ACTIVITY_SCHEMA } from "./live-activity-repository.js";
|
|
2
4
|
import { mkdirSync } from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { isUnnamedWorkspaceTaskName } from "../wand-task-sync.js";
|
|
@@ -330,4 +332,24 @@ export const STORAGE_MIGRATIONS = [
|
|
|
330
332
|
{ version: 17, name: "employee-task-concurrency-default", up: db => {
|
|
331
333
|
db.exec("UPDATE silicon_employees SET max_concurrent_tasks = 2 WHERE max_concurrent_tasks IS NULL");
|
|
332
334
|
} },
|
|
335
|
+
{ version: 18, name: "pi-cli-parsed-checkpoints", up: db => db.exec(`
|
|
336
|
+
CREATE TABLE IF NOT EXISTS pi_cli_checkpoints (
|
|
337
|
+
session_id TEXT PRIMARY KEY,
|
|
338
|
+
checkpoint TEXT NOT NULL,
|
|
339
|
+
updated_at TEXT NOT NULL,
|
|
340
|
+
FOREIGN KEY(session_id) REFERENCES command_sessions(id) ON DELETE CASCADE
|
|
341
|
+
)
|
|
342
|
+
`) },
|
|
343
|
+
{ version: 19, name: "dingtalk-group-mirrors", up: db => db.exec(`
|
|
344
|
+
CREATE TABLE IF NOT EXISTS dingtalk_records (
|
|
345
|
+
kind TEXT NOT NULL, id TEXT NOT NULL, data TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
346
|
+
PRIMARY KEY(kind, id)
|
|
347
|
+
);
|
|
348
|
+
CREATE INDEX IF NOT EXISTS dingtalk_records_kind_updated ON dingtalk_records(kind, updated_at);
|
|
349
|
+
CREATE INDEX IF NOT EXISTS dingtalk_records_state ON dingtalk_records(kind, json_extract(data, '$.state'), id);
|
|
350
|
+
CREATE INDEX IF NOT EXISTS dingtalk_records_delivery ON dingtalk_records(kind, json_extract(data, '$.delivery'), updated_at, id);
|
|
351
|
+
CREATE INDEX IF NOT EXISTS dingtalk_records_binding_delivery ON dingtalk_records(kind, json_extract(data, '$.bindingId'), json_extract(data, '$.delivery'));
|
|
352
|
+
`) },
|
|
353
|
+
{ version: 20, name: "apns-device-subscriptions-and-outbox", up: db => db.exec(APNS_SCHEMA) },
|
|
354
|
+
{ version: 21, name: "apns-live-activity-updates", up: db => db.exec(LIVE_ACTIVITY_SCHEMA) },
|
|
333
355
|
];
|
|
@@ -2,6 +2,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
2
2
|
import { ConversationTurn, SessionSnapshot, StructuredSessionState } from "../types.js";
|
|
3
3
|
import { SessionRow } from "./mappers.js";
|
|
4
4
|
import type { WandStorage } from "../storage.js";
|
|
5
|
+
import { type PiCliCheckpoint } from "../pi-cli-checkpoint.js";
|
|
5
6
|
/** Session storage: one domain of the WandStorage facade. Shares the connection and the facade's transaction. */
|
|
6
7
|
export declare class SessionRepository {
|
|
7
8
|
private readonly db;
|
|
@@ -56,5 +57,9 @@ export declare class SessionRepository {
|
|
|
56
57
|
mapSessionRow(row: SessionRow): SessionSnapshot;
|
|
57
58
|
/** Metadata-only indexed scan. Running or ambiguous records never count toward the history cap. */
|
|
58
59
|
excessSystemAiSessionIds(employeeId: string, keep: number): string[];
|
|
60
|
+
/** Cursor and parsed state are a single atomic value. A crash can only leave
|
|
61
|
+
* an older complete checkpoint, never a newer cursor with missing content. */
|
|
62
|
+
savePiCliCheckpoint(sessionId: string, checkpoint: PiCliCheckpoint): boolean;
|
|
63
|
+
getPiCliCheckpoint(sessionId: string): PiCliCheckpoint | null;
|
|
59
64
|
deleteSession(id: string): void;
|
|
60
65
|
}
|
|
@@ -3,6 +3,7 @@ import { sessionRuntimeMetadataValues } from "./mappers.js";
|
|
|
3
3
|
import { nowIso } from "../password-manager.js";
|
|
4
4
|
import { sessionSelectFields, sessionPersistFields, sessionPersistAssignments, sessionRuntimeMetadataAssignments, mapSessionCore, sessionRowQuery } from "./mappers.js";
|
|
5
5
|
import { SYSTEM_AI_AUXILIARY_PURPOSES, SYSTEM_AI_CALL_PREFIX } from "../system-ai-call.js";
|
|
6
|
+
import { validPiCliCheckpoint } from "../pi-cli-checkpoint.js";
|
|
6
7
|
const AUXILIARY_HISTORY_PATTERNS = SYSTEM_AI_AUXILIARY_PURPOSES.map(purpose => `${SYSTEM_AI_CALL_PREFIX}${purpose}:?*`);
|
|
7
8
|
/** Session storage: one domain of the WandStorage facade. Shares the connection and the facade's transaction. */
|
|
8
9
|
export class SessionRepository {
|
|
@@ -229,7 +230,48 @@ export class SessionRepository {
|
|
|
229
230
|
ORDER BY started_at DESC, rowid DESC LIMIT -1 OFFSET ?`).all(...AUXILIARY_HISTORY_PATTERNS, employeeId, keep);
|
|
230
231
|
return rows.map(row => row.id);
|
|
231
232
|
}
|
|
233
|
+
/** Cursor and parsed state are a single atomic value. A crash can only leave
|
|
234
|
+
* an older complete checkpoint, never a newer cursor with missing content. */
|
|
235
|
+
savePiCliCheckpoint(sessionId, checkpoint) {
|
|
236
|
+
if (!validPiCliCheckpoint(checkpoint))
|
|
237
|
+
return false;
|
|
238
|
+
const json = JSON.stringify(checkpoint);
|
|
239
|
+
// Optional acceleration only: an unusually large turn safely replays from
|
|
240
|
+
// its original journal rather than doubling that payload in SQLite.
|
|
241
|
+
if (Buffer.byteLength(json) > 16 * 1024 * 1024)
|
|
242
|
+
return false;
|
|
243
|
+
return this.db.prepare(`INSERT INTO pi_cli_checkpoints (session_id, checkpoint, updated_at)
|
|
244
|
+
SELECT id, ?, ? FROM command_sessions WHERE id = ? AND provider = 'pi'
|
|
245
|
+
AND json_extract(structured_state, '$.activeRequestId') = ?
|
|
246
|
+
AND json_extract(structured_state, '$.inFlight') = 1
|
|
247
|
+
ON CONFLICT(session_id) DO UPDATE SET checkpoint = excluded.checkpoint, updated_at = excluded.updated_at
|
|
248
|
+
WHERE NOT json_valid(pi_cli_checkpoints.checkpoint)
|
|
249
|
+
OR CASE WHEN json_valid(pi_cli_checkpoints.checkpoint) THEN
|
|
250
|
+
json_extract(pi_cli_checkpoints.checkpoint, '$.incarnationId') != json_extract(excluded.checkpoint, '$.incarnationId')
|
|
251
|
+
OR json_extract(pi_cli_checkpoints.checkpoint, '$.offset') <= json_extract(excluded.checkpoint, '$.offset')
|
|
252
|
+
ELSE 1 END`)
|
|
253
|
+
.run(json, nowIso(), sessionId, checkpoint.requestId).changes > 0;
|
|
254
|
+
}
|
|
255
|
+
getPiCliCheckpoint(sessionId) {
|
|
256
|
+
const row = this.db.prepare(`SELECT p.checkpoint, json_extract(s.structured_state, '$.activeRequestId') AS requestId
|
|
257
|
+
FROM pi_cli_checkpoints p JOIN command_sessions s ON s.id = p.session_id
|
|
258
|
+
WHERE p.session_id = ? AND s.provider = 'pi'
|
|
259
|
+
AND json_extract(s.structured_state, '$.inFlight') = 1`)
|
|
260
|
+
.get(sessionId);
|
|
261
|
+
if (!row)
|
|
262
|
+
return null;
|
|
263
|
+
try {
|
|
264
|
+
const value = JSON.parse(row.checkpoint);
|
|
265
|
+
return validPiCliCheckpoint(value) && value.requestId === row.requestId ? value : null;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
232
271
|
deleteSession(id) {
|
|
233
|
-
this.
|
|
272
|
+
this.s.transaction(() => {
|
|
273
|
+
this.db.prepare("DELETE FROM pi_cli_checkpoints WHERE session_id = ?").run(id);
|
|
274
|
+
this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
|
|
275
|
+
});
|
|
234
276
|
}
|
|
235
277
|
}
|
package/dist/storage-schema.js
CHANGED
|
@@ -5,8 +5,10 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
5
5
|
import { STORAGE_MIGRATIONS } from "./storage/migrations.js";
|
|
6
6
|
import { pruneMigrationBackups } from "./migration-backup-retention.js";
|
|
7
7
|
import { SCHEDULED_CLAIM_DATE_SQL } from "./storage-schedules.js";
|
|
8
|
+
import { DINGTALK_SCHEMA } from "./storage/dingtalk-repository.js";
|
|
8
9
|
/** Additive schema only. Domain migrations and transactions remain owned by WandStorage. */
|
|
9
10
|
const INIT_SQL = `
|
|
11
|
+
${DINGTALK_SCHEMA}
|
|
10
12
|
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
11
13
|
token TEXT PRIMARY KEY,
|
|
12
14
|
expires_at INTEGER NOT NULL,
|
package/dist/storage.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ import type { ConnectorMeta } from "./storage/mappers.js";
|
|
|
3
3
|
import { parseWandTaskAgent } from "./storage/mappers.js";
|
|
4
4
|
export * from "./storage/internal.js";
|
|
5
5
|
import { ConversationRepository } from "./storage/conversations-repository.js";
|
|
6
|
+
import { DingTalkRepository } from "./storage/dingtalk-repository.js";
|
|
7
|
+
import { ApnsRepository } from "./storage/apns-repository.js";
|
|
8
|
+
import { LiveActivityRepository } from "./storage/live-activity-repository.js";
|
|
6
9
|
import { ConversationOrganizationRepository } from "./storage/conversation-organization-repository.js";
|
|
7
10
|
import { PreferencesRepository } from "./storage/preferences-repository.js";
|
|
8
11
|
import { WorkspaceRepository } from "./storage/workspaces-repository.js";
|
|
@@ -22,9 +25,12 @@ export { ensureDatabaseFile } from "./storage-schema.js";
|
|
|
22
25
|
type RepositoryMethods<T> = Pick<T, {
|
|
23
26
|
[K in keyof T]: T[K] extends (...args: never[]) => unknown ? K : never;
|
|
24
27
|
}[keyof T]>;
|
|
25
|
-
export interface WandStorage extends RepositoryMethods<ConversationRepository>, RepositoryMethods<ConversationOrganizationRepository>, RepositoryMethods<PreferencesRepository>, RepositoryMethods<WorkspaceRepository>, RepositoryMethods<TaskRepository>, RepositoryMethods<SecretsRepository>, RepositoryMethods<EmployeeRepository>, RepositoryMethods<UserMemoryRepository>, RepositoryMethods<AiTeamRepository>, RepositoryMethods<MissionRepository>, RepositoryMethods<SessionRepository>, RepositoryMethods<UploadRepository>, RepositoryMethods<FeedbackRepository> {
|
|
28
|
+
export interface WandStorage extends RepositoryMethods<DingTalkRepository>, RepositoryMethods<ConversationRepository>, RepositoryMethods<ConversationOrganizationRepository>, RepositoryMethods<PreferencesRepository>, RepositoryMethods<WorkspaceRepository>, RepositoryMethods<TaskRepository>, RepositoryMethods<SecretsRepository>, RepositoryMethods<EmployeeRepository>, RepositoryMethods<UserMemoryRepository>, RepositoryMethods<AiTeamRepository>, RepositoryMethods<MissionRepository>, RepositoryMethods<SessionRepository>, RepositoryMethods<UploadRepository>, RepositoryMethods<FeedbackRepository> {
|
|
26
29
|
}
|
|
27
30
|
export declare class WandStorage {
|
|
31
|
+
readonly dingtalk: DingTalkRepository;
|
|
32
|
+
readonly apns: ApnsRepository;
|
|
33
|
+
readonly liveActivities: LiveActivityRepository;
|
|
28
34
|
readonly conversations: ConversationRepository;
|
|
29
35
|
readonly conversationOrganizations: ConversationOrganizationRepository;
|
|
30
36
|
readonly preferences: PreferencesRepository;
|
package/dist/storage.js
CHANGED
|
@@ -6,6 +6,9 @@ import { openStorageDatabase } from "./storage-schema.js";
|
|
|
6
6
|
import { parseWandTaskAgent } from "./storage/mappers.js";
|
|
7
7
|
export * from "./storage/internal.js";
|
|
8
8
|
import { ConversationRepository } from "./storage/conversations-repository.js";
|
|
9
|
+
import { DingTalkRepository } from "./storage/dingtalk-repository.js";
|
|
10
|
+
import { ApnsRepository } from "./storage/apns-repository.js";
|
|
11
|
+
import { LiveActivityRepository } from "./storage/live-activity-repository.js";
|
|
9
12
|
import { ConversationOrganizationRepository } from "./storage/conversation-organization-repository.js";
|
|
10
13
|
import { PreferencesRepository } from "./storage/preferences-repository.js";
|
|
11
14
|
import { WorkspaceRepository } from "./storage/workspaces-repository.js";
|
|
@@ -21,6 +24,9 @@ import { FeedbackRepository } from "./storage/feedback-repository.js";
|
|
|
21
24
|
export { parseWandTaskAgent };
|
|
22
25
|
export { ensureDatabaseFile } from "./storage-schema.js";
|
|
23
26
|
export class WandStorage {
|
|
27
|
+
dingtalk;
|
|
28
|
+
apns;
|
|
29
|
+
liveActivities;
|
|
24
30
|
conversations;
|
|
25
31
|
conversationOrganizations;
|
|
26
32
|
preferences;
|
|
@@ -84,6 +90,9 @@ export class WandStorage {
|
|
|
84
90
|
chmodSync(dir, 0o700);
|
|
85
91
|
this.dbPath = dbPath;
|
|
86
92
|
this.db = openStorageDatabase(dbPath);
|
|
93
|
+
this.dingtalk = new DingTalkRepository(this.db);
|
|
94
|
+
this.apns = new ApnsRepository(this.db, this);
|
|
95
|
+
this.liveActivities = new LiveActivityRepository(this.db, this);
|
|
87
96
|
this.conversations = new ConversationRepository(this.db, this);
|
|
88
97
|
this.conversationOrganizations = new ConversationOrganizationRepository(this.db);
|
|
89
98
|
this.preferences = new PreferencesRepository(this.db, this);
|
|
@@ -238,6 +247,7 @@ export class WandStorage {
|
|
|
238
247
|
}
|
|
239
248
|
}
|
|
240
249
|
const REPOSITORY_FORWARDS = [
|
|
250
|
+
["dingtalk", DingTalkRepository],
|
|
241
251
|
["conversations", ConversationRepository],
|
|
242
252
|
["conversationOrganizations", ConversationOrganizationRepository],
|
|
243
253
|
["preferences", PreferencesRepository],
|
|
@@ -35,6 +35,8 @@ export interface StructuredRunState {
|
|
|
35
35
|
stderrTruncated: boolean;
|
|
36
36
|
/** Already-file-backed native output; no Server-side stdout capture copy is required. */
|
|
37
37
|
nativeFile?: string;
|
|
38
|
+
/** Validated native replay starts after this durable parsed checkpoint. */
|
|
39
|
+
stdoutFrom?: number;
|
|
38
40
|
}
|
|
39
41
|
export interface StructuredStreamEvent {
|
|
40
42
|
stream: "stdout" | "stderr";
|
|
@@ -51,6 +53,12 @@ export interface StructuredExecProcess {
|
|
|
51
53
|
readonly runId: string;
|
|
52
54
|
readonly incarnationId: string;
|
|
53
55
|
readonly pid: number;
|
|
56
|
+
/** Immutable identity of the native journal, used only by internal checkpoints. */
|
|
57
|
+
readonly protocolSource?: {
|
|
58
|
+
file: string;
|
|
59
|
+
dev: number;
|
|
60
|
+
ino: number;
|
|
61
|
+
};
|
|
54
62
|
interrupt(signal?: string): void;
|
|
55
63
|
onStream(listener: (event: StructuredStreamEvent) => void): {
|
|
56
64
|
dispose(): void;
|
|
@@ -73,7 +81,7 @@ export interface StructuredExecHost {
|
|
|
73
81
|
};
|
|
74
82
|
spawnStructured(request: StructuredSpawnRequest): Promise<StructuredExecProcess>;
|
|
75
83
|
/** Full state incl. replay logs; null when the host does not know the run. */
|
|
76
|
-
attachRun(runId: string): Promise<StructuredRunState | null>;
|
|
84
|
+
attachRun(runId: string, checkpoint?: import("./pi-cli-checkpoint.js").PiCliCheckpoint): Promise<StructuredRunState | null>;
|
|
77
85
|
/** Subscribe to a known running run without spawning anything. */
|
|
78
86
|
adoptRun(runId: string): Promise<StructuredExecProcess | null>;
|
|
79
87
|
listRuns(): Promise<StructuredRunState[]>;
|
|
@@ -36,7 +36,18 @@ export interface StructuredCliPumpOptions<S extends StructuredRunnerTurnState> {
|
|
|
36
36
|
createProtocolReader?(onLine: (line: string) => void): {
|
|
37
37
|
push(text: string): void;
|
|
38
38
|
finish(): void;
|
|
39
|
+
readonly atBoundary?: boolean;
|
|
39
40
|
};
|
|
41
|
+
/** Called only at native-file chunk boundaries after complete parsed events. */
|
|
42
|
+
checkpoint?(source: {
|
|
43
|
+
incarnationId: string;
|
|
44
|
+
source: {
|
|
45
|
+
file: string;
|
|
46
|
+
dev: number;
|
|
47
|
+
ino: number;
|
|
48
|
+
};
|
|
49
|
+
offset: number;
|
|
50
|
+
}): void;
|
|
40
51
|
/** Fresh per-run state (or reducer-owned state) for the pump context. */
|
|
41
52
|
createState(): S;
|
|
42
53
|
processLine(line: string, ctx: StructuredCliPumpContext<S>): void;
|
|
@@ -18,6 +18,9 @@ export function startStructuredCli(options) {
|
|
|
18
18
|
let lineBuffer = "";
|
|
19
19
|
let lineBytes = 0;
|
|
20
20
|
const protocolReader = options.createProtocolReader?.((line) => options.processLine(line, ctx));
|
|
21
|
+
// Compact Pi's transport before journaling it: a single image or redundant
|
|
22
|
+
// history snapshot must not exhaust the raw-file quota before SAX can read it.
|
|
23
|
+
const captureReader = options.stdoutFile ? options.createProtocolReader?.((line) => stdoutFile?.append(line + "\n")) : undefined;
|
|
21
24
|
let settled = false;
|
|
22
25
|
let stopRequested = false;
|
|
23
26
|
let stopDelivered = false;
|
|
@@ -52,18 +55,25 @@ export function startStructuredCli(options) {
|
|
|
52
55
|
return;
|
|
53
56
|
if (!protocolError) {
|
|
54
57
|
try {
|
|
55
|
-
|
|
56
|
-
// File consumption can itself settle a protocol failure.
|
|
57
|
-
if (settled)
|
|
58
|
-
return;
|
|
59
|
-
protocolReader?.finish();
|
|
58
|
+
captureReader?.finish();
|
|
60
59
|
}
|
|
61
60
|
catch (error) {
|
|
62
61
|
protocolError = error instanceof Error ? error : new Error(String(error));
|
|
63
62
|
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// Preserve already validated output even if a later source frame failed.
|
|
66
|
+
stdoutFile?.drain();
|
|
64
67
|
if (settled)
|
|
65
68
|
return;
|
|
69
|
+
if (!protocolError)
|
|
70
|
+
protocolReader?.finish();
|
|
66
71
|
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
protocolError ??= error instanceof Error ? error : new Error(String(error));
|
|
74
|
+
}
|
|
75
|
+
if (settled)
|
|
76
|
+
return;
|
|
67
77
|
settled = true;
|
|
68
78
|
try {
|
|
69
79
|
if (!protocolError && lineBuffer.trim())
|
|
@@ -146,7 +156,10 @@ export function startStructuredCli(options) {
|
|
|
146
156
|
return;
|
|
147
157
|
}
|
|
148
158
|
try {
|
|
149
|
-
|
|
159
|
+
if (captureReader)
|
|
160
|
+
captureReader.push(text);
|
|
161
|
+
else
|
|
162
|
+
stdoutFile.append(text);
|
|
150
163
|
}
|
|
151
164
|
catch (error) {
|
|
152
165
|
fileFailure(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -196,7 +209,12 @@ export function startStructuredCli(options) {
|
|
|
196
209
|
void host.spawnStructured(request).then((handle) => {
|
|
197
210
|
source = { pid: handle.pid, interrupt: () => handle.interrupt() };
|
|
198
211
|
// Replay and subscribe before applying a pending external cancellation.
|
|
199
|
-
wireRemoteHandle(handle, captureStdout, handleStderrText, finish, acknowledgeExit)
|
|
212
|
+
wireRemoteHandle(handle, captureStdout, handleStderrText, finish, acknowledgeExit, (event) => {
|
|
213
|
+
if (!settled && options.nativePi && event.stream === "stdout" && handle.protocolSource
|
|
214
|
+
&& protocolReader?.atBoundary && observer.isActive()) {
|
|
215
|
+
options.checkpoint?.({ incarnationId: handle.incarnationId, source: handle.protocolSource, offset: event.seq });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
200
218
|
if (stopRequested && !settled)
|
|
201
219
|
interruptSource();
|
|
202
220
|
}, unavailable);
|
|
@@ -234,12 +252,13 @@ export function startStructuredCli(options) {
|
|
|
234
252
|
}
|
|
235
253
|
return execution;
|
|
236
254
|
}
|
|
237
|
-
function wireRemoteHandle(handle, onStdout, onStderr, finish, acknowledgeExit) {
|
|
255
|
+
function wireRemoteHandle(handle, onStdout, onStderr, finish, acknowledgeExit, afterStream) {
|
|
238
256
|
const stream = handle.onStream((event) => {
|
|
239
257
|
if (event.stream === "stdout")
|
|
240
258
|
onStdout(event.data);
|
|
241
259
|
else
|
|
242
260
|
onStderr(event.data);
|
|
261
|
+
afterStream?.(event);
|
|
243
262
|
});
|
|
244
263
|
let exit;
|
|
245
264
|
let ended = false;
|
|
@@ -321,6 +321,11 @@ export class PiRunner {
|
|
|
321
321
|
requestId: context.session.structuredState?.activeRequestId ?? undefined,
|
|
322
322
|
createState: () => state,
|
|
323
323
|
createProtocolReader: (onLine) => new PiProtocolReader(onLine),
|
|
324
|
+
checkpoint: (checkpoint) => {
|
|
325
|
+
const requestId = context.session.structuredState?.activeRequestId;
|
|
326
|
+
if (requestId)
|
|
327
|
+
observer.onPiCheckpoint?.({ version: 1, ...checkpoint, requestId, state, primaryError });
|
|
328
|
+
},
|
|
324
329
|
processLine: (line) => {
|
|
325
330
|
if (!observer.isActive() || !line.trim())
|
|
326
331
|
return;
|
|
@@ -57,6 +57,8 @@ export interface StructuredRunnerObserver {
|
|
|
57
57
|
onStdout?(text: string): void;
|
|
58
58
|
onStderr?(text: string): void;
|
|
59
59
|
onEvent?(event: Record<string, unknown>): void;
|
|
60
|
+
/** Internal native Pi materialization; never forwarded to clients. */
|
|
61
|
+
onPiCheckpoint?(checkpoint: import("./pi-cli-checkpoint.js").PiCliCheckpoint): void;
|
|
60
62
|
onUpdate(state: StructuredRunnerTurnState): void;
|
|
61
63
|
}
|
|
62
64
|
export interface StructuredRunnerResult {
|
|
@@ -100,6 +100,7 @@ export declare class StructuredSessionManager {
|
|
|
100
100
|
private inputSequence;
|
|
101
101
|
/** Last wall-clock time (ms) a streaming checkpoint reached SQLite. */
|
|
102
102
|
private readonly lastStreamSaveAt;
|
|
103
|
+
private readonly lastPiCheckpointAt;
|
|
103
104
|
private readonly streamCheckpointTimers;
|
|
104
105
|
private readonly streamCheckpointDirty;
|
|
105
106
|
/**
|
|
@@ -172,6 +173,7 @@ export declare class StructuredSessionManager {
|
|
|
172
173
|
private finalizeRecoveredRun;
|
|
173
174
|
private trackStreamEmitTimer;
|
|
174
175
|
private clearStreamEmitTimer;
|
|
176
|
+
private savePiCheckpoint;
|
|
175
177
|
/** Mark streaming payload dirty and enforce both leading and trailing checkpoints. */
|
|
176
178
|
private saveStreamingSnapshot;
|
|
177
179
|
private flushStreamingCheckpoint;
|