@soimy/dingtalk 3.6.10 → 3.7.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/index.d.ts +1 -1
- package/dist/index.js +11655 -10324
- package/dist/src/{ack-reaction-service.d.ts → ack-reaction/ack-reaction-service.d.ts} +1 -1
- package/dist/src/ack-reaction/dynamic-ack-reaction-controller.d.ts +3 -3
- package/dist/src/card/ask-user-question-context.d.ts +1 -1
- package/dist/src/card/ask-user-question-store.d.ts +1 -1
- package/dist/src/card/ask-user-question.d.ts +1 -1
- package/dist/src/card/card-action-handler.d.ts +2 -2
- package/dist/src/{card-draft-controller.d.ts → card/card-draft-controller.d.ts} +5 -1
- package/dist/src/card/card-run-registry.d.ts +2 -2
- package/dist/src/{card-service.d.ts → card/card-service.d.ts} +1 -1
- package/dist/src/card/card-stop-handler.d.ts +1 -1
- package/dist/src/card/card-streaming-mode.d.ts +1 -1
- package/dist/src/card/card-task-progress.d.ts +60 -0
- package/dist/src/channel.d.ts +6 -6
- package/dist/src/command/card-stop-command.d.ts +1 -1
- package/dist/src/{feedback-learning-service.d.ts → command/feedback-learning-service.d.ts} +1 -1
- package/dist/src/command/inbound-command-dispatch-service.d.ts +2 -2
- package/dist/src/{learning-command-service.d.ts → command/learning-command-service.d.ts} +1 -1
- package/dist/src/gateway/channel-gateway.d.ts +1 -1
- package/dist/src/{connection-manager.d.ts → gateway/connection-manager.d.ts} +1 -1
- package/dist/src/{docs-service.d.ts → gateway/docs-service.d.ts} +5 -5
- package/dist/src/{inbound-handler.d.ts → gateway/inbound-handler.d.ts} +1 -1
- package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +1 -1
- package/dist/src/messaging/attachment-text-extractor.d.ts +1 -1
- package/dist/src/messaging/btw-deliver.d.ts +1 -1
- package/dist/src/messaging/channel-outbound.d.ts +1 -1
- package/dist/src/messaging/inline-directives.d.ts +42 -0
- package/dist/src/{media-utils.d.ts → messaging/media-utils.d.ts} +1 -1
- package/dist/src/{message-context-store.d.ts → messaging/message-context-store.d.ts} +7 -1
- package/dist/src/{message-utils.d.ts → messaging/message-utils.d.ts} +1 -1
- package/dist/src/messaging/quoted-context.d.ts +2 -2
- package/dist/src/messaging/quoted-file-service.d.ts +1 -1
- package/dist/src/messaging/quoted-ref.d.ts +2 -2
- package/dist/src/{reply-strategy-card.d.ts → messaging/reply-strategy-card.d.ts} +1 -1
- package/dist/src/{reply-strategy-types.d.ts → messaging/reply-strategy-types.d.ts} +10 -1
- package/dist/src/{reply-strategy-with-reaction.d.ts → messaging/reply-strategy-with-reaction.d.ts} +1 -1
- package/dist/src/{reply-strategy.d.ts → messaging/reply-strategy.d.ts} +1 -1
- package/dist/src/{send-service.d.ts → messaging/send-service.d.ts} +1 -1
- package/dist/src/platform/channel-status.d.ts +1 -1
- package/dist/src/platform/config.d.ts +110 -0
- package/dist/src/platform/runtime-events.d.ts +67 -0
- package/dist/src/{types.d.ts → platform/types.d.ts} +75 -2
- package/dist/src/{persistence-store.d.ts → shared/persistence-store.d.ts} +1 -1
- package/dist/src/{utils.d.ts → shared/utils.d.ts} +1 -1
- package/dist/src/targeting/agent-name-matcher.d.ts +1 -1
- package/dist/src/targeting/agent-routing.d.ts +3 -3
- package/dist/src/{session-routing.d.ts → targeting/session-routing.d.ts} +1 -1
- package/index.ts +193 -69
- package/openclaw.plugin.json +158 -2
- package/package.json +16 -16
- package/src/{ack-reaction-classifier.ts → ack-reaction/ack-reaction-classifier.ts} +85 -13
- package/src/{ack-reaction-service.ts → ack-reaction/ack-reaction-service.ts} +13 -7
- package/src/ack-reaction/dynamic-ack-reaction-controller.ts +46 -31
- package/src/ack-reaction/dynamic-ack-reaction-progress.ts +4 -1
- package/src/card/ask-user-question-context.ts +1 -1
- package/src/card/ask-user-question-store.ts +2 -2
- package/src/card/ask-user-question.ts +11 -7
- package/src/card/card-action-handler.ts +2 -2
- package/src/{card-callback-service.ts → card/card-callback-service.ts} +7 -9
- package/src/card/card-draft-controller.ts +770 -0
- package/src/card/card-markdown-image-reroute.ts +8 -2
- package/src/card/card-run-registry.ts +17 -7
- package/src/{card-service.ts → card/card-service.ts} +91 -61
- package/src/card/card-stop-handler.ts +12 -8
- package/src/card/card-streaming-mode.ts +1 -1
- package/src/card/card-task-progress.ts +330 -0
- package/src/card/draft-stream-loop.ts +119 -0
- package/src/card/reasoning-answer-split.ts +125 -121
- package/src/card/reasoning-block-assembler.ts +122 -123
- package/src/{run-usage-store.ts → card/run-usage-store.ts} +18 -6
- package/src/card/statusline-renderer.ts +14 -5
- package/src/card/task-model-metadata.ts +8 -3
- package/src/channel.ts +14 -16
- package/src/command/card-stop-command.ts +2 -2
- package/src/{feedback-learning-service.ts → command/feedback-learning-service.ts} +36 -16
- package/src/{feedback-learning-store.ts → command/feedback-learning-store.ts} +133 -81
- package/src/command/inbound-command-dispatch-service.ts +12 -6
- package/src/{learning-command-service.ts → command/learning-command-service.ts} +36 -29
- package/src/{session-command-service.ts → command/session-command-service.ts} +3 -1
- package/src/gateway/channel-gateway.ts +106 -28
- package/src/{connection-manager.ts → gateway/connection-manager.ts} +45 -30
- package/src/{docs-service.ts → gateway/docs-service.ts} +5 -5
- package/src/{inbound-handler.ts → gateway/inbound-handler.ts} +144 -85
- package/src/gateway/inbound-session-queue-dispatcher.ts +18 -14
- package/src/{session-lock.ts → gateway/session-lock.ts} +14 -14
- package/src/messaging/attachment-text-extractor.ts +21 -7
- package/src/messaging/btw-deliver.ts +2 -2
- package/src/messaging/channel-actions.ts +15 -7
- package/src/messaging/channel-outbound.ts +16 -11
- package/src/messaging/inline-directives.ts +342 -0
- package/src/{media-utils.ts → messaging/media-utils.ts} +276 -72
- package/src/{message-context-store.ts → messaging/message-context-store.ts} +11 -2
- package/src/{message-utils.ts → messaging/message-utils.ts} +46 -11
- package/src/messaging/quoted-context.ts +2 -2
- package/src/messaging/quoted-file-service.ts +311 -294
- package/src/messaging/quoted-ref.ts +11 -7
- package/src/{reply-strategy-card.ts → messaging/reply-strategy-card.ts} +178 -101
- package/src/{reply-strategy-markdown.ts → messaging/reply-strategy-markdown.ts} +8 -2
- package/src/{reply-strategy-types.ts → messaging/reply-strategy-types.ts} +11 -1
- package/src/{reply-strategy-with-reaction.ts → messaging/reply-strategy-with-reaction.ts} +6 -1
- package/src/{reply-strategy.ts → messaging/reply-strategy.ts} +2 -2
- package/src/{send-service.ts → messaging/send-service.ts} +100 -65
- package/src/{auth.ts → platform/auth.ts} +2 -2
- package/src/platform/channel-status.ts +3 -3
- package/src/{config-schema.ts → platform/config-schema.ts} +81 -4
- package/src/{config.ts → platform/config.ts} +167 -19
- package/src/{device-registration.ts → platform/device-registration.ts} +5 -4
- package/src/{onboarding.ts → platform/onboarding.ts} +1 -4
- package/src/{plugin-sdk-channel-actions-augment.ts → platform/plugin-sdk-channel-actions-augment.ts} +6 -5
- package/src/platform/runtime-events.ts +202 -0
- package/src/{secret-input.ts → platform/secret-input.ts} +117 -11
- package/src/{types.ts → platform/types.ts} +77 -2
- package/src/{http-client.ts → shared/http-client.ts} +1 -3
- package/src/{persistence-store.ts → shared/persistence-store.ts} +6 -6
- package/src/{utils.ts → shared/utils.ts} +30 -13
- package/src/targeting/agent-name-matcher.ts +1 -1
- package/src/targeting/agent-routing.ts +7 -7
- package/src/targeting/group-members-store.ts +1 -1
- package/src/{session-peer-store.ts → targeting/session-peer-store.ts} +9 -4
- package/src/{session-routing.ts → targeting/session-routing.ts} +1 -1
- package/src/targeting/target-directory-adapter.ts +3 -3
- package/src/targeting/target-directory-store.ts +1 -1
- package/src/targeting/target-input.ts +1 -1
- package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts +0 -35
- package/dist/src/config.d.ts +0 -59
- package/src/ack-reaction/dynamic-ack-reaction-events.ts +0 -123
- package/src/card-draft-controller.ts +0 -637
- package/src/draft-stream-loop.ts +0 -119
- /package/dist/src/{ack-reaction-classifier.d.ts → ack-reaction/ack-reaction-classifier.d.ts} +0 -0
- /package/dist/src/{card-callback-service.d.ts → card/card-callback-service.d.ts} +0 -0
- /package/dist/src/{draft-stream-loop.d.ts → card/draft-stream-loop.d.ts} +0 -0
- /package/dist/src/{run-usage-store.d.ts → card/run-usage-store.d.ts} +0 -0
- /package/dist/src/{feedback-learning-store.d.ts → command/feedback-learning-store.d.ts} +0 -0
- /package/dist/src/{session-command-service.d.ts → command/session-command-service.d.ts} +0 -0
- /package/dist/src/{session-lock.d.ts → gateway/session-lock.d.ts} +0 -0
- /package/dist/src/{proactive-risk-registry.d.ts → messaging/proactive-risk-registry.d.ts} +0 -0
- /package/dist/src/{reply-strategy-markdown.d.ts → messaging/reply-strategy-markdown.d.ts} +0 -0
- /package/dist/src/{access-control.d.ts → platform/access-control.d.ts} +0 -0
- /package/dist/src/{auth.d.ts → platform/auth.d.ts} +0 -0
- /package/dist/src/{config-schema.d.ts → platform/config-schema.d.ts} +0 -0
- /package/dist/src/{device-registration.d.ts → platform/device-registration.d.ts} +0 -0
- /package/dist/src/{logger-context.d.ts → platform/logger-context.d.ts} +0 -0
- /package/dist/src/{onboarding.d.ts → platform/onboarding.d.ts} +0 -0
- /package/dist/src/{plugin-sdk-channel-actions-augment.d.ts → platform/plugin-sdk-channel-actions-augment.d.ts} +0 -0
- /package/dist/src/{runtime.d.ts → platform/runtime.d.ts} +0 -0
- /package/dist/src/{secret-input.d.ts → platform/secret-input.d.ts} +0 -0
- /package/dist/src/{session-state.d.ts → platform/session-state.d.ts} +0 -0
- /package/dist/src/{signature.d.ts → platform/signature.d.ts} +0 -0
- /package/dist/src/{dedup.d.ts → shared/dedup.d.ts} +0 -0
- /package/dist/src/{http-client.d.ts → shared/http-client.d.ts} +0 -0
- /package/dist/src/{path-utils.d.ts → shared/path-utils.d.ts} +0 -0
- /package/dist/src/{peer-id-registry.d.ts → targeting/peer-id-registry.d.ts} +0 -0
- /package/dist/src/{session-peer-store.d.ts → targeting/session-peer-store.d.ts} +0 -0
- /package/src/{proactive-risk-registry.ts → messaging/proactive-risk-registry.ts} +0 -0
- /package/src/{access-control.ts → platform/access-control.ts} +0 -0
- /package/src/{logger-context.ts → platform/logger-context.ts} +0 -0
- /package/src/{runtime.ts → platform/runtime.ts} +0 -0
- /package/src/{session-state.ts → platform/session-state.ts} +0 -0
- /package/src/{signature.ts → platform/signature.ts} +0 -0
- /package/src/{dedup.ts → shared/dedup.ts} +0 -0
- /package/src/{path-utils.ts → shared/path-utils.ts} +0 -0
- /package/src/{peer-id-registry.ts → targeting/peer-id-registry.ts} +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import https from "node:https";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import { formatDingTalkErrorPayload, formatDingTalkErrorPayloadLog } from "../utils";
|
|
3
|
+
import { getAccessToken } from "../platform/auth";
|
|
4
|
+
import { getDingTalkRuntime } from "../platform/runtime";
|
|
5
|
+
import type { DingTalkConfig, Logger, MediaFile } from "../platform/types";
|
|
6
|
+
import axios from "../shared/http-client";
|
|
7
|
+
import { formatDingTalkErrorPayload, formatDingTalkErrorPayloadLog } from "../shared/utils";
|
|
8
8
|
|
|
9
9
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return value as Record<string, unknown>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const ipv4OnlyHttpAgent = new http.Agent({ family: 4 });
|
|
@@ -27,26 +27,28 @@ const UNION_ID_CACHE_MAX = 5000;
|
|
|
27
27
|
const SPACE_ID_CACHE_MAX = 500;
|
|
28
28
|
|
|
29
29
|
function describeResolveError(err: unknown): string {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
if (err instanceof Error) {
|
|
43
|
-
return `${err.name || "Error"} message=${err.message || "unknown error"}`;
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
return JSON.stringify(err);
|
|
47
|
-
} catch {
|
|
48
|
-
return String(err);
|
|
30
|
+
if (axios.isAxiosError(err)) {
|
|
31
|
+
const status = err.response?.status;
|
|
32
|
+
const statusText = err.response?.statusText;
|
|
33
|
+
const statusLabel = status
|
|
34
|
+
? `status=${status}${statusText ? ` ${statusText}` : ""}`
|
|
35
|
+
: "status=unknown";
|
|
36
|
+
const code = typeof err.code === "string" && err.code ? ` code=${err.code}` : "";
|
|
37
|
+
const hasRequest = err.request ? " request=yes" : " request=no";
|
|
38
|
+
const hasResponse = err.response ? " response=yes" : " response=no";
|
|
39
|
+
if (err.response?.data !== undefined) {
|
|
40
|
+
return `${statusLabel}${code}${hasRequest}${hasResponse} ${formatDingTalkErrorPayload(err.response.data)}`;
|
|
49
41
|
}
|
|
42
|
+
return `${statusLabel}${code}${hasRequest}${hasResponse} message=${err.message || "unknown axios error"}`;
|
|
43
|
+
}
|
|
44
|
+
if (err instanceof Error) {
|
|
45
|
+
return `${err.name || "Error"} message=${err.message || "unknown error"}`;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(err);
|
|
49
|
+
} catch {
|
|
50
|
+
return String(err);
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// ============ LRU caches ============
|
|
@@ -55,336 +57,351 @@ const unionIdCache = new Map<string, string>();
|
|
|
55
57
|
const spaceIdCache = new Map<string, string>();
|
|
56
58
|
|
|
57
59
|
function lruSet<V>(map: Map<string, V>, key: string, value: V, maxSize: number): void {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
60
|
+
if (map.has(key)) {
|
|
61
|
+
map.delete(key);
|
|
62
|
+
}
|
|
63
|
+
map.set(key, value);
|
|
64
|
+
if (map.size > maxSize) {
|
|
65
|
+
const oldest = map.keys().next().value;
|
|
66
|
+
if (oldest !== undefined) {
|
|
67
|
+
map.delete(oldest);
|
|
67
68
|
}
|
|
69
|
+
}
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
function lruGet<V>(map: Map<string, V>, key: string): V | undefined {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
const value = map.get(key);
|
|
74
|
+
if (value === undefined) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
map.delete(key);
|
|
78
|
+
map.set(key, value);
|
|
79
|
+
return value;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
// ============ Time parsing ============
|
|
81
83
|
|
|
82
84
|
export function parseDingTalkFileTime(timeStr: string): number {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const normalized = timeStr.replace(/\bCST\b/, "+0800");
|
|
86
|
+
const ms = new Date(normalized).getTime();
|
|
87
|
+
if (Number.isNaN(ms)) {
|
|
88
|
+
throw new Error(`Cannot parse DingTalk file time: ${timeStr}`);
|
|
89
|
+
}
|
|
90
|
+
return ms;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
// ============ API helpers ============
|
|
92
94
|
|
|
93
95
|
export async function getUnionIdByStaffId(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
config: DingTalkConfig,
|
|
97
|
+
staffId: string,
|
|
98
|
+
log?: Logger,
|
|
97
99
|
): Promise<string> {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
100
|
+
const cacheKey = `${config.clientId}:${staffId}`;
|
|
101
|
+
const cached = lruGet(unionIdCache, cacheKey);
|
|
102
|
+
if (cached) {
|
|
103
|
+
return cached;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const token = await getAccessToken(config, log);
|
|
107
|
+
const resp = await axios.post(`${DINGTALK_OAPI}/topapi/v2/user/get?access_token=${token}`, {
|
|
108
|
+
userid: staffId,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const payload = asRecord(resp.data) ?? {};
|
|
112
|
+
const errcode =
|
|
113
|
+
typeof payload.errcode === "number" || typeof payload.errcode === "string"
|
|
114
|
+
? String(payload.errcode)
|
|
115
|
+
: "unknown";
|
|
116
|
+
const errmsg = typeof payload.errmsg === "string" ? payload.errmsg : "unknown";
|
|
117
|
+
if (payload.errcode !== 0) {
|
|
118
|
+
throw new Error(`topapi/v2/user/get failed: errcode=${errcode} errmsg=${errmsg}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const unionIdValue = asRecord(payload.result)?.unionid;
|
|
122
|
+
const unionId = typeof unionIdValue === "string" ? unionIdValue : undefined;
|
|
123
|
+
if (!unionId) {
|
|
124
|
+
throw new Error(`topapi/v2/user/get returned no unionid for staffId=${staffId}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
lruSet(unionIdCache, cacheKey, unionId, UNION_ID_CACHE_MAX);
|
|
128
|
+
return unionId;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
export async function getGroupFileSpaceId(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
config: DingTalkConfig,
|
|
133
|
+
openConversationId: string,
|
|
134
|
+
unionId: string,
|
|
135
|
+
log?: Logger,
|
|
134
136
|
): Promise<string> {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
const cacheKey = `${config.clientId}:${openConversationId}`;
|
|
138
|
+
const cached = lruGet(spaceIdCache, cacheKey);
|
|
139
|
+
if (cached) {
|
|
140
|
+
return cached;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const token = await getAccessToken(config, log);
|
|
144
|
+
const resp = await axios.post(
|
|
145
|
+
`${DINGTALK_API}/v1.0/convFile/conversations/spaces/query`,
|
|
146
|
+
{ openConversationId, unionId },
|
|
147
|
+
{ headers: { "x-acs-dingtalk-access-token": token } },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const spaceIdValue = asRecord(asRecord(resp.data)?.space)?.spaceId;
|
|
151
|
+
const spaceId = typeof spaceIdValue === "string" ? spaceIdValue : undefined;
|
|
152
|
+
if (!spaceId) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`convFile spaces/query returned no spaceId for conversationId=${openConversationId}`,
|
|
146
155
|
);
|
|
156
|
+
}
|
|
147
157
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!spaceId) {
|
|
151
|
-
throw new Error(`convFile spaces/query returned no spaceId for conversationId=${openConversationId}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
lruSet(spaceIdCache, cacheKey, spaceId, SPACE_ID_CACHE_MAX);
|
|
155
|
-
return spaceId;
|
|
158
|
+
lruSet(spaceIdCache, cacheKey, spaceId, SPACE_ID_CACHE_MAX);
|
|
159
|
+
return spaceId;
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
interface DentryMatch {
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
dentryId: string;
|
|
164
|
+
name: string;
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
export interface ResolvedQuotedFile {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
media: MediaFile;
|
|
169
|
+
spaceId: string;
|
|
170
|
+
fileId: string;
|
|
171
|
+
name?: string;
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
export async function findFileByTimestamp(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
config: DingTalkConfig,
|
|
176
|
+
spaceId: string,
|
|
177
|
+
unionId: string,
|
|
178
|
+
createdAt: number,
|
|
179
|
+
log?: Logger,
|
|
176
180
|
): Promise<DentryMatch | null> {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
181
|
+
const token = await getAccessToken(config, log);
|
|
182
|
+
|
|
183
|
+
let bestMatch: DentryMatch | null = null;
|
|
184
|
+
let bestDelta = Infinity;
|
|
185
|
+
let nextToken: string | undefined;
|
|
186
|
+
|
|
187
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
188
|
+
const body: Record<string, unknown> = {
|
|
189
|
+
option: { maxResults: PAGE_SIZE },
|
|
190
|
+
};
|
|
191
|
+
if (nextToken) {
|
|
192
|
+
(body.option as { nextToken?: string }).nextToken = nextToken;
|
|
193
|
+
}
|
|
190
194
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
const resp = await axios.post(
|
|
196
|
+
`${DINGTALK_API}/v1.0/storage/spaces/${spaceId}/dentries/listAll?unionId=${unionId}`,
|
|
197
|
+
body,
|
|
198
|
+
{ headers: { "x-acs-dingtalk-access-token": token } },
|
|
199
|
+
);
|
|
196
200
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const createTime = typeof entry.createTime === "string" ? entry.createTime : JSON.stringify(entry.createTime);
|
|
213
|
-
log?.debug?.(`[DingTalk][QuotedFile] Failed to parse createTime: ${createTime}`);
|
|
214
|
-
}
|
|
201
|
+
const data = asRecord(resp.data) ?? {};
|
|
202
|
+
const dentries = Array.isArray(data.dentries)
|
|
203
|
+
? (data.dentries as Array<Record<string, unknown>>)
|
|
204
|
+
: [];
|
|
205
|
+
|
|
206
|
+
for (const entry of dentries) {
|
|
207
|
+
if (entry.type !== "FILE" || !entry.createTime) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const fileTime = parseDingTalkFileTime(entry.createTime as string);
|
|
212
|
+
const delta = Math.abs(fileTime - createdAt);
|
|
213
|
+
if (delta <= MATCH_WINDOW_MS && delta < bestDelta) {
|
|
214
|
+
bestDelta = delta;
|
|
215
|
+
bestMatch = { dentryId: entry.id as string, name: entry.name as string };
|
|
215
216
|
}
|
|
217
|
+
} catch {
|
|
218
|
+
const createTime =
|
|
219
|
+
typeof entry.createTime === "string"
|
|
220
|
+
? entry.createTime
|
|
221
|
+
: JSON.stringify(entry.createTime);
|
|
222
|
+
log?.debug?.(`[DingTalk][QuotedFile] Failed to parse createTime: ${createTime}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
216
225
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
226
|
+
nextToken = typeof data.nextToken === "string" ? data.nextToken : undefined;
|
|
227
|
+
if (!nextToken) {
|
|
228
|
+
break;
|
|
221
229
|
}
|
|
230
|
+
}
|
|
222
231
|
|
|
223
|
-
|
|
232
|
+
return bestMatch;
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
export async function downloadGroupFile(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
config: DingTalkConfig,
|
|
237
|
+
spaceId: string,
|
|
238
|
+
dentryId: string,
|
|
239
|
+
unionId: string,
|
|
240
|
+
log?: Logger,
|
|
241
|
+
originalFilename?: string,
|
|
233
242
|
): Promise<MediaFile | null> {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
const rt = getDingTalkRuntime();
|
|
244
|
+
const token = await getAccessToken(config, log);
|
|
245
|
+
let resourceUrl = "";
|
|
246
|
+
let contentType = "application/octet-stream";
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const infoResp = await axios.post(
|
|
250
|
+
`${DINGTALK_API}/v1.0/storage/spaces/${spaceId}/dentries/${dentryId}/downloadInfos/query?unionId=${unionId}`,
|
|
251
|
+
{},
|
|
252
|
+
{ headers: { "x-acs-dingtalk-access-token": token } },
|
|
253
|
+
);
|
|
238
254
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
255
|
+
const info = asRecord(infoResp.data) ?? {};
|
|
256
|
+
const headerSig = asRecord(info.headerSignatureInfo);
|
|
257
|
+
const resourceUrls =
|
|
258
|
+
headerSig && Array.isArray(headerSig.resourceUrls) ? headerSig.resourceUrls : undefined;
|
|
259
|
+
resourceUrl = typeof resourceUrls?.[0] === "string" ? resourceUrls[0] : "";
|
|
260
|
+
if (!resourceUrl) {
|
|
261
|
+
log?.warn?.("[DingTalk][QuotedFile] downloadInfos/query returned no resourceUrl");
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
245
264
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
return null;
|
|
265
|
+
const sigHeaders: Record<string, string> = {};
|
|
266
|
+
const headerMap = asRecord(headerSig?.headers);
|
|
267
|
+
if (headerMap) {
|
|
268
|
+
for (const [k, v] of Object.entries(headerMap)) {
|
|
269
|
+
if (typeof v === "string") {
|
|
270
|
+
sigHeaders[k] = v;
|
|
253
271
|
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
let fileResp;
|
|
275
|
+
try {
|
|
276
|
+
fileResp = await axios.get(resourceUrl, {
|
|
277
|
+
headers: sigHeaders,
|
|
278
|
+
responseType: "arraybuffer",
|
|
279
|
+
timeout: 15000,
|
|
280
|
+
});
|
|
281
|
+
} catch (firstErr: unknown) {
|
|
282
|
+
log?.warn?.(
|
|
283
|
+
`[DingTalk][QuotedFile] CDN download failed on default network path, retrying with IPv4-only: ${describeResolveError(firstErr)}`,
|
|
284
|
+
);
|
|
285
|
+
try {
|
|
286
|
+
fileResp = await axios.get(resourceUrl, {
|
|
287
|
+
headers: sigHeaders,
|
|
288
|
+
responseType: "arraybuffer",
|
|
289
|
+
httpAgent: ipv4OnlyHttpAgent,
|
|
290
|
+
httpsAgent: ipv4OnlyHttpsAgent,
|
|
291
|
+
timeout: 15000,
|
|
292
|
+
});
|
|
293
|
+
} catch (retryErr: unknown) {
|
|
294
|
+
const host = resourceUrl ? new URL(resourceUrl).host : "(unknown-host)";
|
|
295
|
+
throw new Error(
|
|
296
|
+
`download-resource failed host=${host} detail=${describeResolveError(retryErr)}`,
|
|
297
|
+
{ cause: retryErr },
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
254
301
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (headerMap) {
|
|
258
|
-
for (const [k, v] of Object.entries(headerMap)) {
|
|
259
|
-
if (typeof v === "string") {
|
|
260
|
-
sigHeaders[k] = v;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
let fileResp;
|
|
265
|
-
try {
|
|
266
|
-
fileResp = await axios.get(resourceUrl, {
|
|
267
|
-
headers: sigHeaders,
|
|
268
|
-
responseType: "arraybuffer",
|
|
269
|
-
timeout: 15000,
|
|
270
|
-
});
|
|
271
|
-
} catch (firstErr: unknown) {
|
|
272
|
-
log?.warn?.(
|
|
273
|
-
`[DingTalk][QuotedFile] CDN download failed on default network path, retrying with IPv4-only: ${describeResolveError(firstErr)}`,
|
|
274
|
-
);
|
|
275
|
-
try {
|
|
276
|
-
fileResp = await axios.get(resourceUrl, {
|
|
277
|
-
headers: sigHeaders,
|
|
278
|
-
responseType: "arraybuffer",
|
|
279
|
-
httpAgent: ipv4OnlyHttpAgent,
|
|
280
|
-
httpsAgent: ipv4OnlyHttpsAgent,
|
|
281
|
-
timeout: 15000,
|
|
282
|
-
});
|
|
283
|
-
} catch (retryErr: unknown) {
|
|
284
|
-
const host = resourceUrl ? new URL(resourceUrl).host : "(unknown-host)";
|
|
285
|
-
throw new Error(
|
|
286
|
-
`download-resource failed host=${host} detail=${describeResolveError(retryErr)}`,
|
|
287
|
-
{ cause: retryErr },
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
302
|
+
contentType = (fileResp.headers["content-type"] as string) || "application/octet-stream";
|
|
303
|
+
const buffer = Buffer.from(fileResp.data as ArrayBuffer);
|
|
291
304
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
return { path: saved.path, mimeType: saved.contentType ?? contentType };
|
|
307
|
-
} catch (err: unknown) {
|
|
308
|
-
throw new Error(
|
|
309
|
-
`save-buffer failed contentType=${contentType} detail=${describeResolveError(err)}`,
|
|
310
|
-
{ cause: err },
|
|
311
|
-
);
|
|
312
|
-
}
|
|
305
|
+
const maxBytes =
|
|
306
|
+
config.mediaMaxMb && config.mediaMaxMb > 0 ? config.mediaMaxMb * 1024 * 1024 : undefined;
|
|
307
|
+
try {
|
|
308
|
+
const saved = await rt.channel.media.saveMediaBuffer(
|
|
309
|
+
buffer,
|
|
310
|
+
contentType,
|
|
311
|
+
"inbound",
|
|
312
|
+
maxBytes,
|
|
313
|
+
originalFilename,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
return { path: saved.path, mimeType: saved.contentType ?? contentType };
|
|
313
317
|
} catch (err: unknown) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
throw new Error(
|
|
319
|
+
`save-buffer failed contentType=${contentType} detail=${describeResolveError(err)}`,
|
|
320
|
+
{ cause: err },
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
} catch (err: unknown) {
|
|
324
|
+
if (axios.isAxiosError(err) && err.response?.data !== undefined) {
|
|
325
|
+
log?.warn?.(formatDingTalkErrorPayloadLog("quotedFile.downloadGroupFile", err.response.data));
|
|
321
326
|
}
|
|
327
|
+
log?.warn?.(
|
|
328
|
+
`[DingTalk][QuotedFile] downloadGroupFile failed: spaceId=${spaceId} dentryId=${dentryId} resourceUrl=${resourceUrl || "(none)"} contentType=${contentType} error=${describeResolveError(err)}`,
|
|
329
|
+
);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
322
332
|
}
|
|
323
333
|
|
|
324
334
|
// ============ Composite entry point ============
|
|
325
335
|
|
|
326
336
|
export interface ResolveQuotedFileParams {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
337
|
+
openConversationId: string;
|
|
338
|
+
senderStaffId?: string;
|
|
339
|
+
fileCreatedAt?: number;
|
|
330
340
|
}
|
|
331
341
|
|
|
332
342
|
export async function resolveQuotedFile(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
343
|
+
config: DingTalkConfig,
|
|
344
|
+
params: ResolveQuotedFileParams,
|
|
345
|
+
log?: Logger,
|
|
336
346
|
): Promise<ResolvedQuotedFile | null> {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
const { openConversationId, senderStaffId, fileCreatedAt } = params;
|
|
348
|
+
let stage = "init";
|
|
349
|
+
|
|
350
|
+
if (!senderStaffId || !fileCreatedAt) {
|
|
351
|
+
log?.warn?.("[DingTalk][QuotedFile] Missing senderStaffId or fileCreatedAt, skipping");
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
stage = "resolve-unionId";
|
|
357
|
+
const unionId = await getUnionIdByStaffId(config, senderStaffId, log);
|
|
358
|
+
stage = "resolve-spaceId";
|
|
359
|
+
const spaceId = await getGroupFileSpaceId(config, openConversationId, unionId, log);
|
|
360
|
+
stage = "list-and-match";
|
|
361
|
+
const match = await findFileByTimestamp(config, spaceId, unionId, fileCreatedAt, log);
|
|
362
|
+
|
|
363
|
+
if (!match) {
|
|
364
|
+
log?.warn?.(
|
|
365
|
+
`[DingTalk][QuotedFile] No file matched within ±${MATCH_WINDOW_MS}ms window for createdAt=${fileCreatedAt}`,
|
|
366
|
+
);
|
|
367
|
+
return null;
|
|
343
368
|
}
|
|
344
369
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
stage = "download-file";
|
|
361
|
-
const media = await downloadGroupFile(config, spaceId, match.dentryId, unionId, log, match.name);
|
|
362
|
-
if (!media) {
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
370
|
+
stage = "download-file";
|
|
371
|
+
const media = await downloadGroupFile(
|
|
372
|
+
config,
|
|
373
|
+
spaceId,
|
|
374
|
+
match.dentryId,
|
|
375
|
+
unionId,
|
|
376
|
+
log,
|
|
377
|
+
match.name,
|
|
378
|
+
);
|
|
379
|
+
if (!media) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
365
382
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
return null;
|
|
383
|
+
return {
|
|
384
|
+
media,
|
|
385
|
+
spaceId,
|
|
386
|
+
fileId: match.dentryId,
|
|
387
|
+
name: match.name,
|
|
388
|
+
};
|
|
389
|
+
} catch (err: unknown) {
|
|
390
|
+
if (log?.warn) {
|
|
391
|
+
if (axios.isAxiosError(err) && err.response?.data !== undefined) {
|
|
392
|
+
log.warn(formatDingTalkErrorPayloadLog("quotedFile.resolve", err.response.data));
|
|
393
|
+
}
|
|
394
|
+
log.warn(
|
|
395
|
+
`[DingTalk][QuotedFile] Failed to resolve quoted file: stage=${stage} conversationId=${openConversationId} senderStaffId=${senderStaffId} fileCreatedAt=${fileCreatedAt} error=${describeResolveError(err)}`,
|
|
396
|
+
);
|
|
382
397
|
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
383
400
|
}
|
|
384
401
|
|
|
385
402
|
// ============ Test helpers ============
|
|
386
403
|
|
|
387
404
|
export function clearQuotedFileServiceCachesForTest(): void {
|
|
388
|
-
|
|
389
|
-
|
|
405
|
+
unionIdCache.clear();
|
|
406
|
+
spaceIdCache.clear();
|
|
390
407
|
}
|