@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
package/dist/ai-team-live.js
CHANGED
|
@@ -91,7 +91,7 @@ function safeTailStart(output, start) {
|
|
|
91
91
|
* 终端原始输出的尾部:先按尾部截窗,再剥 ANSI / 换行归一 / 压缩空行 / 截尾。
|
|
92
92
|
* live 每 500ms 就可能算一遍,而 PTY 的 output 能长到几 MB —— 顺序反过来等于每半秒对全量
|
|
93
93
|
* 跑一遍正则。丢掉的前缀计入 omittedChars,是**估算值(可能偏大也可能偏小,用于量级提示)**:
|
|
94
|
-
* 按截窗点的原始字数计,含 ANSI
|
|
94
|
+
* 按截窗点的原始字数计,含 ANSI 与控制字符,只用于量级提示,不精确到账。
|
|
95
95
|
*/
|
|
96
96
|
function outputTailText(output) {
|
|
97
97
|
const rawStart = Math.max(0, output.length - LIVE_OUTPUT_WINDOW_CHARS);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type KeyObject } from "node:crypto";
|
|
2
|
+
import { type ClientHttp2Session } from "node:http2";
|
|
3
|
+
export type ApnsEnvironment = "production" | "sandbox";
|
|
4
|
+
export interface ApnsConfiguration {
|
|
5
|
+
keyId: string;
|
|
6
|
+
teamId: string;
|
|
7
|
+
topic: string;
|
|
8
|
+
environment: ApnsEnvironment;
|
|
9
|
+
key: KeyObject;
|
|
10
|
+
}
|
|
11
|
+
export interface ApnsMessage {
|
|
12
|
+
id: string;
|
|
13
|
+
token: string;
|
|
14
|
+
payload: string;
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
collapseId: string;
|
|
17
|
+
/** ActivityKit has a distinct token and derived topic; never substitute an ordinary device token. */
|
|
18
|
+
pushType?: "alert" | "liveactivity";
|
|
19
|
+
priority?: 5 | 10;
|
|
20
|
+
}
|
|
21
|
+
export interface ApnsResult {
|
|
22
|
+
outcome: "accepted" | "invalid-device" | "retry" | "rejected";
|
|
23
|
+
/** Bounded, provider-defined diagnostic only. Never include a token, payload or raw error. */
|
|
24
|
+
reason: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ApnsTransport {
|
|
27
|
+
send(message: ApnsMessage): Promise<ApnsResult>;
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
/** Deployment-owned paths/IDs only; the p8 itself never enters config.json, DTOs or logs. */
|
|
31
|
+
export declare function loadApnsConfiguration(env?: NodeJS.ProcessEnv, configDirectory?: string): ApnsConfiguration | null;
|
|
32
|
+
export declare function createApnsJwt(config: ApnsConfiguration, now?: number): string;
|
|
33
|
+
export declare function classifyApnsResponse(status: number, reason: unknown): ApnsResult;
|
|
34
|
+
/** Low-volume alerts: bounded HTTP/2 request per delivery, only Apple's fixed TLS endpoints. */
|
|
35
|
+
export declare class ApnsClient implements ApnsTransport {
|
|
36
|
+
private readonly config;
|
|
37
|
+
private readonly open;
|
|
38
|
+
private jwt;
|
|
39
|
+
private jwtAt;
|
|
40
|
+
private closed;
|
|
41
|
+
private readonly connections;
|
|
42
|
+
constructor(config: ApnsConfiguration, open?: (url: string) => ClientHttp2Session);
|
|
43
|
+
send(message: ApnsMessage): Promise<ApnsResult>;
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createPrivateKey, sign } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { connect } from "node:http2";
|
|
5
|
+
/** Deployment-owned paths/IDs only; the p8 itself never enters config.json, DTOs or logs. */
|
|
6
|
+
export function loadApnsConfiguration(env = process.env, configDirectory) {
|
|
7
|
+
// A deployment-local file is deliberately separate from public/runtime WandConfig projections.
|
|
8
|
+
// Explicit environment configuration wins as a whole; incomplete overrides fail closed.
|
|
9
|
+
const names = ["WAND_APNS_KEY_PATH", "WAND_APNS_KEY_ID", "WAND_APNS_TEAM_ID", "WAND_APNS_TOPIC"];
|
|
10
|
+
const localFile = configDirectory ? path.join(configDirectory, "apns.json") : null;
|
|
11
|
+
if (!names.some(name => env[name]) && !env.WAND_APNS_ENVIRONMENT && localFile && existsSync(localFile)) {
|
|
12
|
+
try {
|
|
13
|
+
const stat = statSync(localFile);
|
|
14
|
+
if (!stat.isFile() || stat.size > 4096 || (stat.mode & 0o077) !== 0)
|
|
15
|
+
throw new Error();
|
|
16
|
+
const local = JSON.parse(readFileSync(localFile, "utf8"));
|
|
17
|
+
if (!local || typeof local !== "object" || Array.isArray(local))
|
|
18
|
+
throw new Error();
|
|
19
|
+
const fields = ["keyPath", "keyId", "teamId", "topic", "environment"];
|
|
20
|
+
if (fields.some(field => typeof local[field] !== "string"))
|
|
21
|
+
throw new Error();
|
|
22
|
+
env = Object.fromEntries([...names, "WAND_APNS_ENVIRONMENT"].map((name, index) => [name, local[fields[index]]]));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error("本机 apns.json 不可用:需要权限0600的有效推送配置文件。");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const values = [env.WAND_APNS_KEY_PATH, env.WAND_APNS_KEY_ID, env.WAND_APNS_TEAM_ID, env.WAND_APNS_TOPIC];
|
|
29
|
+
if (values.every(value => !value))
|
|
30
|
+
return null;
|
|
31
|
+
if (values.some(value => !value))
|
|
32
|
+
throw new Error("APNs 配置不完整:需要 KEY_PATH、KEY_ID、TEAM_ID 和 TOPIC。");
|
|
33
|
+
const [keyPath, keyId, teamId, topic] = values;
|
|
34
|
+
if (!/^[A-Z0-9]{10}$/.test(keyId) || !/^[A-Z0-9]{10}$/.test(teamId)
|
|
35
|
+
|| !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(topic) || topic.length > 255) {
|
|
36
|
+
throw new Error("APNs 的 key/team/topic 配置格式无效。");
|
|
37
|
+
}
|
|
38
|
+
const environment = env.WAND_APNS_ENVIRONMENT ?? "production";
|
|
39
|
+
if (environment !== "production" && environment !== "sandbox")
|
|
40
|
+
throw new Error("APNs environment 必须是 production 或 sandbox。");
|
|
41
|
+
try {
|
|
42
|
+
if (!path.isAbsolute(keyPath))
|
|
43
|
+
throw new Error();
|
|
44
|
+
const stat = statSync(keyPath);
|
|
45
|
+
if (!stat.isFile() || stat.size > 16_384 || (stat.mode & 0o077) !== 0)
|
|
46
|
+
throw new Error();
|
|
47
|
+
const key = createPrivateKey(readFileSync(keyPath));
|
|
48
|
+
if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1")
|
|
49
|
+
throw new Error();
|
|
50
|
+
return { keyId: keyId, teamId: teamId, topic: topic, environment, key };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error("APNs 私钥不可用:需要本机可读、权限0600的P-256 p8文件。");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function createApnsJwt(config, now = Date.now()) {
|
|
57
|
+
const header = Buffer.from(JSON.stringify({ alg: "ES256", kid: config.keyId })).toString("base64url");
|
|
58
|
+
const claims = Buffer.from(JSON.stringify({ iss: config.teamId, iat: Math.floor(now / 1000) })).toString("base64url");
|
|
59
|
+
const input = `${header}.${claims}`;
|
|
60
|
+
return `${input}.${sign("sha256", Buffer.from(input), { key: config.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
61
|
+
}
|
|
62
|
+
export function classifyApnsResponse(status, reason) {
|
|
63
|
+
const safe = typeof reason === "string" && /^[A-Za-z]{1,64}$/.test(reason) ? reason : `HTTP${status}`;
|
|
64
|
+
if (status === 200)
|
|
65
|
+
return { outcome: "accepted", reason: "AcceptedByAPNs" };
|
|
66
|
+
if (status === 410 || (status === 400 && ["BadDeviceToken", "DeviceTokenNotForTopic", "Unregistered"].includes(safe))) {
|
|
67
|
+
return { outcome: "invalid-device", reason: safe };
|
|
68
|
+
}
|
|
69
|
+
return { outcome: status === 0 || status === 429 || status >= 500 || safe === "ExpiredProviderToken" ? "retry" : "rejected", reason: safe };
|
|
70
|
+
}
|
|
71
|
+
/** Low-volume alerts: bounded HTTP/2 request per delivery, only Apple's fixed TLS endpoints. */
|
|
72
|
+
export class ApnsClient {
|
|
73
|
+
config;
|
|
74
|
+
open;
|
|
75
|
+
jwt = "";
|
|
76
|
+
jwtAt = 0;
|
|
77
|
+
closed = false;
|
|
78
|
+
connections = new Set();
|
|
79
|
+
constructor(config, open = connect) {
|
|
80
|
+
this.config = config;
|
|
81
|
+
this.open = open;
|
|
82
|
+
}
|
|
83
|
+
send(message) {
|
|
84
|
+
if (this.closed)
|
|
85
|
+
return Promise.resolve({ outcome: "retry", reason: "Stopped" });
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (!this.jwt || now - this.jwtAt > 50 * 60_000) {
|
|
88
|
+
this.jwt = createApnsJwt(this.config, now);
|
|
89
|
+
this.jwtAt = now;
|
|
90
|
+
}
|
|
91
|
+
return new Promise(resolve => {
|
|
92
|
+
const endpoint = this.config.environment === "sandbox" ? "https://api.sandbox.push.apple.com" : "https://api.push.apple.com";
|
|
93
|
+
const connection = this.open(endpoint);
|
|
94
|
+
this.connections.add(connection);
|
|
95
|
+
let settled = false;
|
|
96
|
+
const finish = (result) => {
|
|
97
|
+
if (settled)
|
|
98
|
+
return;
|
|
99
|
+
settled = true;
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
this.connections.delete(connection);
|
|
102
|
+
connection.destroy();
|
|
103
|
+
resolve(result);
|
|
104
|
+
};
|
|
105
|
+
const timer = setTimeout(() => finish({ outcome: "retry", reason: "Timeout" }), 10_000);
|
|
106
|
+
timer.unref();
|
|
107
|
+
connection.on("error", () => finish({ outcome: "retry", reason: "ConnectionError" }));
|
|
108
|
+
connection.on("close", () => finish({ outcome: "retry", reason: "ConnectionClosed" }));
|
|
109
|
+
connection.once("connect", () => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
const request = connection.request({
|
|
114
|
+
":method": "POST", ":path": `/3/device/${message.token}`,
|
|
115
|
+
authorization: `bearer ${this.jwt}`,
|
|
116
|
+
"apns-topic": message.pushType === "liveactivity" ? `${this.config.topic}.push-type.liveactivity` : this.config.topic,
|
|
117
|
+
"apns-push-type": message.pushType ?? "alert", "apns-priority": String(message.priority ?? (message.pushType === "liveactivity" ? 5 : 10)), "apns-id": message.id,
|
|
118
|
+
"apns-expiration": String(Math.floor(message.expiresAt / 1000)), "apns-collapse-id": message.collapseId,
|
|
119
|
+
"content-type": "application/json",
|
|
120
|
+
});
|
|
121
|
+
let status = 0;
|
|
122
|
+
let body = "";
|
|
123
|
+
request.setEncoding("utf8");
|
|
124
|
+
request.on("response", headers => { status = Number(headers[":status"] ?? 0); });
|
|
125
|
+
request.on("data", (chunk) => {
|
|
126
|
+
body += chunk;
|
|
127
|
+
if (Buffer.byteLength(body) > 4096)
|
|
128
|
+
finish({ outcome: "retry", reason: "ResponseTooLarge" });
|
|
129
|
+
});
|
|
130
|
+
request.on("error", () => finish({ outcome: "retry", reason: "StreamError" }));
|
|
131
|
+
request.on("end", () => {
|
|
132
|
+
let reason;
|
|
133
|
+
try {
|
|
134
|
+
reason = JSON.parse(body).reason;
|
|
135
|
+
}
|
|
136
|
+
catch { /* empty success body */ }
|
|
137
|
+
const result = classifyApnsResponse(status, reason);
|
|
138
|
+
if (result.reason === "ExpiredProviderToken")
|
|
139
|
+
this.jwt = "";
|
|
140
|
+
finish(result);
|
|
141
|
+
});
|
|
142
|
+
request.end(message.payload);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
finish({ outcome: "retry", reason: "RequestError" });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
close() {
|
|
151
|
+
this.closed = true;
|
|
152
|
+
for (const connection of this.connections)
|
|
153
|
+
connection.destroy();
|
|
154
|
+
this.connections.clear();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ApnsConfiguration, ApnsTransport } from "./apns-client.js";
|
|
2
|
+
import type { WandStorage } from "./storage.js";
|
|
3
|
+
import type { ProcessEvent, SessionSnapshot } from "./types.js";
|
|
4
|
+
export declare class ApnsService {
|
|
5
|
+
private readonly storage;
|
|
6
|
+
readonly configuration: ApnsConfiguration | null;
|
|
7
|
+
private readonly transport;
|
|
8
|
+
private readonly credentialDigest;
|
|
9
|
+
private readonly getSession;
|
|
10
|
+
private readonly warn;
|
|
11
|
+
private readonly now;
|
|
12
|
+
private stopped;
|
|
13
|
+
private running;
|
|
14
|
+
private timer;
|
|
15
|
+
private lastSweep;
|
|
16
|
+
private nextWarningAt;
|
|
17
|
+
private report;
|
|
18
|
+
constructor(storage: WandStorage, configuration: ApnsConfiguration | null, transport: ApnsTransport | null, credentialDigest: () => string, getSession: (id: string) => SessionSnapshot | null, warn?: (message: string) => void, now?: () => number);
|
|
19
|
+
get available(): boolean;
|
|
20
|
+
start(): void;
|
|
21
|
+
/** Called only by SessionCompletionTracker when it creates a NEW successful completion fact. */
|
|
22
|
+
completed(sessionId: string, revision: number): void;
|
|
23
|
+
ingest(event: ProcessEvent): void;
|
|
24
|
+
private enqueue;
|
|
25
|
+
/** A single bounded consumer, independent from the lifetime of PTY/Render/Core. */
|
|
26
|
+
flush(): Promise<void>;
|
|
27
|
+
private drain;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { apnsDigest } from "./storage/apns-repository.js";
|
|
2
|
+
export class ApnsService {
|
|
3
|
+
storage;
|
|
4
|
+
configuration;
|
|
5
|
+
transport;
|
|
6
|
+
credentialDigest;
|
|
7
|
+
getSession;
|
|
8
|
+
warn;
|
|
9
|
+
now;
|
|
10
|
+
stopped = false;
|
|
11
|
+
running = null;
|
|
12
|
+
timer = null;
|
|
13
|
+
lastSweep = 0;
|
|
14
|
+
nextWarningAt = 0;
|
|
15
|
+
report(message) {
|
|
16
|
+
if (this.now() < this.nextWarningAt)
|
|
17
|
+
return;
|
|
18
|
+
this.nextWarningAt = this.now() + 60_000;
|
|
19
|
+
this.warn(message);
|
|
20
|
+
}
|
|
21
|
+
constructor(storage, configuration, transport, credentialDigest, getSession, warn = () => { }, now = Date.now) {
|
|
22
|
+
this.storage = storage;
|
|
23
|
+
this.configuration = configuration;
|
|
24
|
+
this.transport = transport;
|
|
25
|
+
this.credentialDigest = credentialDigest;
|
|
26
|
+
this.getSession = getSession;
|
|
27
|
+
this.warn = warn;
|
|
28
|
+
this.now = now;
|
|
29
|
+
storage.onClose(() => this.dispose());
|
|
30
|
+
}
|
|
31
|
+
get available() { return !!this.configuration && !!this.transport && !this.stopped; }
|
|
32
|
+
start() {
|
|
33
|
+
if (!this.available || this.timer)
|
|
34
|
+
return;
|
|
35
|
+
this.timer = setInterval(() => { void this.flush(); }, 2000);
|
|
36
|
+
this.timer.unref();
|
|
37
|
+
void this.flush(); // includes persisted, still-actionable outbox entries after a restart.
|
|
38
|
+
}
|
|
39
|
+
/** Called only by SessionCompletionTracker when it creates a NEW successful completion fact. */
|
|
40
|
+
completed(sessionId, revision) {
|
|
41
|
+
if (!this.available)
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
const session = this.getSession(sessionId);
|
|
45
|
+
if (!session || session.archived)
|
|
46
|
+
return;
|
|
47
|
+
this.enqueue(sessionId, "completed", String(revision));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
this.report("APNs 无法读取会话提醒事实;不影响执行。");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
ingest(event) {
|
|
54
|
+
if (!this.available || (event.type !== "status" && event.type !== "ended"))
|
|
55
|
+
return;
|
|
56
|
+
try {
|
|
57
|
+
const session = this.getSession(event.sessionId);
|
|
58
|
+
if (!session || session.archived)
|
|
59
|
+
return;
|
|
60
|
+
// The durable request ID is the fact. Never hash permission contents/tool arguments into a payload.
|
|
61
|
+
const requestId = session.pendingEscalation?.requestId;
|
|
62
|
+
if (requestId)
|
|
63
|
+
this.enqueue(session.id, "permission", requestId);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
this.report("APNs 无法读取会话提醒事实;不影响执行。");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
enqueue(sessionId, kind, revision) {
|
|
70
|
+
try {
|
|
71
|
+
const config = this.configuration;
|
|
72
|
+
const capacityAvailable = this.storage.apns.enqueue(sessionId, kind, revision, this.credentialDigest(), config.topic, config.environment, this.now());
|
|
73
|
+
if (!capacityAvailable)
|
|
74
|
+
this.report("APNs 提醒队列已满,部分提醒可能被丢弃;执行与历史不受影响。");
|
|
75
|
+
void this.flush();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
this.report("APNs 提醒未能入队;执行与历史不受影响。");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** A single bounded consumer, independent from the lifetime of PTY/Render/Core. */
|
|
82
|
+
flush() {
|
|
83
|
+
if (!this.available)
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
if (!this.running) {
|
|
86
|
+
this.running = this.drain().catch(() => {
|
|
87
|
+
if (!this.stopped)
|
|
88
|
+
this.report("APNs 队列暂不可用,将稍后重试。");
|
|
89
|
+
}).finally(() => { this.running = null; });
|
|
90
|
+
}
|
|
91
|
+
return this.running;
|
|
92
|
+
}
|
|
93
|
+
async drain() {
|
|
94
|
+
if (this.now() - this.lastSweep >= 60_000) {
|
|
95
|
+
this.storage.apns.sweep(this.now());
|
|
96
|
+
this.lastSweep = this.now();
|
|
97
|
+
}
|
|
98
|
+
// Bound work per pump even under an event burst. Pending work remains in SQLite.
|
|
99
|
+
for (let i = 0; i < 32 && !this.stopped; i++) {
|
|
100
|
+
const job = this.storage.apns.due(this.now());
|
|
101
|
+
if (!job)
|
|
102
|
+
return;
|
|
103
|
+
const device = this.storage.apns.device(job.device_id);
|
|
104
|
+
const session = this.getSession(job.session_id);
|
|
105
|
+
const stale = !device || !device.enabled || device.generation !== job.generation
|
|
106
|
+
|| device.expires_at <= this.now() || device.credential_digest !== this.credentialDigest()
|
|
107
|
+
|| device.topic !== this.configuration.topic || device.environment !== this.configuration.environment
|
|
108
|
+
|| job.expires_at <= this.now() || !session || session.archived
|
|
109
|
+
|| (job.kind === "permission" && session.pendingEscalation?.requestId !== job.revision)
|
|
110
|
+
|| (job.kind === "completed" && ((session.viewedCompletionRevision ?? 0) >= Number(job.revision)
|
|
111
|
+
|| (session.completionRevision ?? 0) !== Number(job.revision) || !!session.pendingEscalation
|
|
112
|
+
|| session.ptyBusy === true || session.structuredState?.inFlight === true));
|
|
113
|
+
if (stale) {
|
|
114
|
+
this.storage.apns.finish(job.id, "canceled", "NoLongerActionable");
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const payload = JSON.stringify({
|
|
118
|
+
aps: { alert: {
|
|
119
|
+
title: job.kind === "permission" ? "需要你的授权" : "回复已完成",
|
|
120
|
+
body: job.kind === "permission" ? "Wand 会话正在等待确认,点击查看。" : "Wand 会话已有新结果,点击查看。",
|
|
121
|
+
}, sound: "default", "thread-id": apnsDigest(`${device.server_id}:${job.session_id}`).slice(0, 32) },
|
|
122
|
+
eventId: job.event_id, sessionId: job.session_id, serverId: device.server_id, kind: job.kind,
|
|
123
|
+
});
|
|
124
|
+
let result;
|
|
125
|
+
try {
|
|
126
|
+
result = await this.transport.send({ id: job.id, token: device.token, payload,
|
|
127
|
+
expiresAt: job.expires_at, collapseId: job.event_id });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
result = { outcome: "retry", reason: "TransportFailure" };
|
|
131
|
+
}
|
|
132
|
+
if (this.stopped)
|
|
133
|
+
return; // storage may have closed while the HTTP/2 request was in flight.
|
|
134
|
+
if (result.outcome === "accepted")
|
|
135
|
+
this.storage.apns.finish(job.id, "accepted", result.reason);
|
|
136
|
+
else if (result.outcome === "invalid-device") {
|
|
137
|
+
this.storage.apns.finish(job.id, "failed", result.reason);
|
|
138
|
+
this.storage.apns.invalidateDevice(job.device_id, job.generation);
|
|
139
|
+
}
|
|
140
|
+
else if (result.outcome === "retry" && job.attempts < 4) {
|
|
141
|
+
this.storage.apns.retry(job.id, job.attempts + 1, this.now() + Math.min(60_000, 2000 * 2 ** job.attempts), result.reason);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
this.storage.apns.finish(job.id, "failed", result.reason);
|
|
145
|
+
this.report(`APNs 未接受提醒(${result.reason}),不影响会话执行。`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
dispose() {
|
|
150
|
+
this.stopped = true;
|
|
151
|
+
if (this.timer)
|
|
152
|
+
clearInterval(this.timer);
|
|
153
|
+
this.timer = null;
|
|
154
|
+
this.transport?.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-10-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "54a477b44332156a4ccebc55c1879bff23fa78a5",
|
|
3
|
+
"builtAt": "2026-10-24T13:20:55.647Z",
|
|
4
|
+
"version": "4.103.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -31,6 +31,8 @@ interface ConversationExecutionTarget {
|
|
|
31
31
|
/** Business coordination only; all execution remains in StructuredSessionManager / AiTeamRunner. */
|
|
32
32
|
export declare class ConversationService {
|
|
33
33
|
private readonly deps;
|
|
34
|
+
private external?;
|
|
35
|
+
attachExternal(service: NonNullable<ConversationService["external"]>): void;
|
|
34
36
|
constructor(deps: {
|
|
35
37
|
storage: WandStorage;
|
|
36
38
|
structured: Pick<StructuredSessionManager, "createSession" | "createRelaySession" | "get" | "sendMessage" | "appendRelayTurns">;
|
|
@@ -50,6 +50,8 @@ function stableJson(value) {
|
|
|
50
50
|
/** Business coordination only; all execution remains in StructuredSessionManager / AiTeamRunner. */
|
|
51
51
|
export class ConversationService {
|
|
52
52
|
deps;
|
|
53
|
+
external;
|
|
54
|
+
attachExternal(service) { this.external = service; }
|
|
53
55
|
constructor(deps) {
|
|
54
56
|
this.deps = deps;
|
|
55
57
|
}
|
|
@@ -281,6 +283,9 @@ export class ConversationService {
|
|
|
281
283
|
return candidates.reduce((latest, turn) => latest ? this.laterTurn(instance, latest, turn) : turn, undefined);
|
|
282
284
|
}
|
|
283
285
|
summary(instance, knownTurns) {
|
|
286
|
+
const externalGroup = this.external?.groupForConversation(instance.id);
|
|
287
|
+
if (externalGroup)
|
|
288
|
+
instance = { ...instance, external: this.external.metadata(externalGroup) };
|
|
284
289
|
const tasks = this.storage.conversationTasks(instance.id).flatMap((link) => {
|
|
285
290
|
const task = this.storage.getWandTask(link.taskId);
|
|
286
291
|
const receipt = task ? this.storage.conversationTaskStartup(task.id) : null;
|
|
@@ -290,7 +295,7 @@ export class ConversationService {
|
|
|
290
295
|
});
|
|
291
296
|
const employee = instance.peerEmployeeId ? this.storage.getSiliconEmployee(instance.peerEmployeeId) : null;
|
|
292
297
|
const lifecycle = this.storage.conversationListState(instance.id);
|
|
293
|
-
let unavailableReason = lifecycle.deleting ? "对话正在删除,请稍后重试。" : lifecycle.dissolvedAt ? "群聊已解散,恢复后可以继续聊天。" : null;
|
|
298
|
+
let unavailableReason = lifecycle.deleting ? "对话正在删除,请稍后重试。" : lifecycle.dissolvedAt ? instance.external ? "此钉钉群镜像已暂停接收新输入,不影响真实钉钉群。" : "群聊已解散,恢复后可以继续聊天。" : null;
|
|
294
299
|
if (instance.kind === "dm") {
|
|
295
300
|
try {
|
|
296
301
|
requireTeamEmployee(this.storage, instance.peerEmployeeId ?? "");
|
|
@@ -306,7 +311,7 @@ export class ConversationService {
|
|
|
306
311
|
const title = instance.kind === "dm" ? employee?.name ?? instance.name
|
|
307
312
|
: instance.nameSource === "custom" ? instance.name : tasks.at(-1)?.task.title ?? instance.name;
|
|
308
313
|
// 私聊标题已说明对方身份;只保留自己的署名,群聊仍需区分发言人。
|
|
309
|
-
const previewAuthor = last?.role === "user" ? `${this.selfName()}:`
|
|
314
|
+
const previewAuthor = last?.role === "user" ? `${instance.external ? last.author?.name ?? "钉钉成员" : this.selfName()}:`
|
|
310
315
|
: instance.kind === "group" && last ? `${last.author?.name ?? "系统"}:` : "";
|
|
311
316
|
const team = instance.team ? projectTeamEmployees(instance.team, this.storage) : null;
|
|
312
317
|
return { ...instance, ...lifecycle, team: team ? { ...team, members: team.members.map((member) => ({
|
|
@@ -314,7 +319,7 @@ export class ConversationService {
|
|
|
314
319
|
})) } : null,
|
|
315
320
|
title, unavailableReason, memberUnavailableReasons: this.memberReasons(instance), tasks,
|
|
316
321
|
preview: last ? `${previewAuthor}${lastText.slice(0, 160)}`
|
|
317
|
-
: instance.kind === "group" ? "群已建立,尚未派任务" : "还没有消息", messageAt };
|
|
322
|
+
: instance.external ? "钉钉群已接入,等待消息同步" : instance.kind === "group" ? "群已建立,尚未派任务" : "还没有消息", messageAt };
|
|
318
323
|
}
|
|
319
324
|
list() {
|
|
320
325
|
return this.storage.listConversations().map((instance) => this.summary(instance))
|
|
@@ -403,6 +408,8 @@ export class ConversationService {
|
|
|
403
408
|
for (const taskId of taskIds)
|
|
404
409
|
this.storage.deleteWandTask(taskId);
|
|
405
410
|
this.storage.deleteConversation(id);
|
|
411
|
+
if (instance.external)
|
|
412
|
+
this.external?.removeMirror(id);
|
|
406
413
|
}
|
|
407
414
|
history(id) { return this.turns(this.requireInstance(id)); }
|
|
408
415
|
detail(id, select = turns => turns) {
|
|
@@ -633,6 +640,10 @@ export class ConversationService {
|
|
|
633
640
|
prepare(id, requestId, explicitCwd) {
|
|
634
641
|
return this.request(requestId, "prepare", { explicitCwd }, id, (receipt) => {
|
|
635
642
|
const instance = this.requireInstance(id);
|
|
643
|
+
if (instance.external) {
|
|
644
|
+
receipt.state = "accepted";
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
636
647
|
const session = this.communicationChannel(instance, this.cwd(explicitCwd));
|
|
637
648
|
receipt.sessionId = session.id;
|
|
638
649
|
receipt.state = "accepted";
|
|
@@ -671,6 +682,13 @@ export class ConversationService {
|
|
|
671
682
|
}
|
|
672
683
|
send(id, requestId, input, target, explicitCwd, echo = true, executionTarget, executionPreference) {
|
|
673
684
|
const preference = parseConversationExecutionPreference(executionPreference);
|
|
685
|
+
if (this.storage.getConversation(id)?.external) {
|
|
686
|
+
if (!this.external)
|
|
687
|
+
throw new Error("钉钉连接器尚未启动。");
|
|
688
|
+
if (target || executionTarget || preference)
|
|
689
|
+
throw new Error("钉钉群消息不能作为内部任务或临时员工配置派发。");
|
|
690
|
+
return this.external.send(id, requestId, input);
|
|
691
|
+
}
|
|
674
692
|
return this.request(requestId, "send", { input, target, explicitCwd, echo,
|
|
675
693
|
...(preference ? { executionPreference: preference } : {}),
|
|
676
694
|
...(executionTarget?.workspaceId ? { workspaceId: executionTarget.workspaceId } : {}),
|
|
@@ -812,6 +830,8 @@ export class ConversationService {
|
|
|
812
830
|
dispatch(id, requestId, input) {
|
|
813
831
|
return this.request(requestId, "dispatch", input, id, (receipt, commit) => {
|
|
814
832
|
const instance = this.requireInstance(id);
|
|
833
|
+
if (instance.external)
|
|
834
|
+
throw new Error("钉钉群不是内部团队任务群,请在群内明确 @已接入的员工。");
|
|
815
835
|
// Older clients may still use the task form in a DM. It now starts a session, never a task group.
|
|
816
836
|
if (instance.kind === "dm") {
|
|
817
837
|
if (input.continueTaskId)
|
|
@@ -13,6 +13,8 @@ export type ConversationTarget = {
|
|
|
13
13
|
export interface ConversationInstance {
|
|
14
14
|
id: string;
|
|
15
15
|
owner: string;
|
|
16
|
+
/** Real external group mirror; ordinary messages never enter the internal team dispatcher. */
|
|
17
|
+
external?: import("./dingtalk-types.js").DingTalkExternalConversation;
|
|
16
18
|
kind: "dm" | "group";
|
|
17
19
|
peerEmployeeId: string | null;
|
|
18
20
|
name: string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { boundedFetch, type DwsTransport } from "./dingtalk-dws.js";
|
|
2
|
+
import type { DingTalkAccount } from "./dingtalk-types.js";
|
|
3
|
+
/** Structured device OAuth, not scraping localized CLI stderr. Codes stay in memory and are exchanged over stdin. */
|
|
4
|
+
export declare class DingTalkAuth {
|
|
5
|
+
private readonly dws;
|
|
6
|
+
private readonly request;
|
|
7
|
+
private attempts;
|
|
8
|
+
constructor(dws: DwsTransport, request?: typeof boundedFetch);
|
|
9
|
+
accounts(): Promise<DingTalkAccount[]>;
|
|
10
|
+
private json;
|
|
11
|
+
begin(): Promise<{
|
|
12
|
+
id: string;
|
|
13
|
+
state: "failed" | "cancelled" | "connected" | "waiting" | "exchanging";
|
|
14
|
+
url: string | undefined;
|
|
15
|
+
userCode: string | undefined;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
error: string | undefined;
|
|
18
|
+
account: DingTalkAccount | undefined;
|
|
19
|
+
}>;
|
|
20
|
+
get(id: string): {
|
|
21
|
+
id: string;
|
|
22
|
+
state: "failed" | "cancelled" | "connected" | "waiting" | "exchanging";
|
|
23
|
+
url: string | undefined;
|
|
24
|
+
userCode: string | undefined;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
error: string | undefined;
|
|
27
|
+
account: DingTalkAccount | undefined;
|
|
28
|
+
};
|
|
29
|
+
cancel(id: string): {
|
|
30
|
+
id: string;
|
|
31
|
+
state: "failed" | "cancelled" | "connected" | "waiting" | "exchanging";
|
|
32
|
+
url: string | undefined;
|
|
33
|
+
userCode: string | undefined;
|
|
34
|
+
expiresAt: string;
|
|
35
|
+
error: string | undefined;
|
|
36
|
+
account: DingTalkAccount | undefined;
|
|
37
|
+
};
|
|
38
|
+
private public;
|
|
39
|
+
private poll;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|