@soimy/dingtalk 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +796 -42
  3. package/index.ts +62 -0
  4. package/package.json +4 -2
  5. package/src/access-control.ts +83 -0
  6. package/src/ack-reaction/dynamic-ack-reaction-controller.ts +271 -0
  7. package/src/ack-reaction/dynamic-ack-reaction-events.ts +123 -0
  8. package/src/ack-reaction/dynamic-ack-reaction-progress.ts +59 -0
  9. package/src/ack-reaction-classifier.ts +75 -0
  10. package/src/ack-reaction-service.ts +182 -0
  11. package/src/attachment-text-extractor.ts +148 -0
  12. package/src/card-callback-service.ts +119 -0
  13. package/src/card-draft-controller.ts +114 -0
  14. package/src/card-service.ts +666 -26
  15. package/src/channel.ts +455 -150
  16. package/src/config-schema.ts +64 -6
  17. package/src/config.ts +161 -5
  18. package/src/connection-manager.ts +354 -47
  19. package/src/dedup.ts +1 -0
  20. package/src/docs-service.ts +198 -0
  21. package/src/draft-stream-loop.ts +119 -0
  22. package/src/feedback-learning-service.ts +643 -0
  23. package/src/feedback-learning-store.ts +543 -0
  24. package/src/group-members-store.ts +48 -14
  25. package/src/inbound-handler.ts +1374 -259
  26. package/src/learning-command-service.ts +339 -0
  27. package/src/media-utils.ts +94 -50
  28. package/src/message-context-store.ts +787 -0
  29. package/src/message-utils.ts +487 -46
  30. package/src/messaging/quoted-context.ts +269 -0
  31. package/src/messaging/quoted-ref.ts +97 -0
  32. package/src/onboarding.ts +96 -1
  33. package/src/peer-id-registry.ts +102 -0
  34. package/src/persistence-store.ts +131 -0
  35. package/src/quoted-file-service.ts +385 -0
  36. package/src/reply-strategy-card.ts +225 -0
  37. package/src/reply-strategy-markdown.ts +55 -0
  38. package/src/reply-strategy-with-reaction.ts +190 -0
  39. package/src/reply-strategy.ts +72 -0
  40. package/src/send-service.ts +267 -45
  41. package/src/session-command-service.ts +147 -0
  42. package/src/session-lock.ts +2 -0
  43. package/src/session-peer-store.ts +77 -0
  44. package/src/session-routing.ts +33 -0
  45. package/src/targeting/agent-name-matcher.ts +148 -0
  46. package/src/targeting/agent-routing.ts +181 -0
  47. package/src/targeting/target-directory-adapter.ts +151 -0
  48. package/src/targeting/target-directory-store.ts +396 -0
  49. package/src/targeting/target-input.ts +62 -0
  50. package/src/types.ts +261 -28
  51. package/src/utils.ts +231 -12
@@ -0,0 +1,198 @@
1
+ import axios from "axios";
2
+ import { getAccessToken } from "./auth";
3
+ import { getLogger } from "./logger-context";
4
+ import { getProxyBypassOption } from "./utils";
5
+ import type { DingTalkConfig, DocInfo, Logger } from "./types";
6
+
7
+ const DINGTALK_API = "https://api.dingtalk.com";
8
+
9
+ async function buildHeaders(config: DingTalkConfig, log?: Logger): Promise<Record<string, string>> {
10
+ const token = await getAccessToken(config, log);
11
+ return {
12
+ "x-acs-dingtalk-access-token": token,
13
+ "Content-Type": "application/json",
14
+ };
15
+ }
16
+
17
+ type CreateDocResponse = {
18
+ docId?: string;
19
+ title?: string;
20
+ name?: string;
21
+ docType?: string;
22
+ creatorId?: string;
23
+ updatedAt?: number | string;
24
+ };
25
+
26
+ type SearchDocItem = {
27
+ docId?: string;
28
+ title?: string;
29
+ docType?: string;
30
+ creatorId?: string;
31
+ updatedAt?: number | string;
32
+ };
33
+
34
+ type ListDentryItem = {
35
+ dentryUuid?: string;
36
+ name?: string;
37
+ dentryType?: string;
38
+ creatorId?: string;
39
+ updatedAt?: number | string;
40
+ };
41
+
42
+ type AppendDocResponse = {
43
+ success?: boolean;
44
+ };
45
+
46
+ export class DocCreateAppendError extends Error {
47
+ readonly doc: DocInfo;
48
+
49
+ constructor(doc: DocInfo, cause?: unknown) {
50
+ super("initial content append failed after document creation");
51
+ this.name = "DocCreateAppendError";
52
+ this.doc = doc;
53
+ this.cause = cause;
54
+ }
55
+ }
56
+
57
+ function mapCreatedDoc(item: CreateDocResponse): DocInfo {
58
+ return {
59
+ docId: item.docId ?? "",
60
+ title: item.title ?? item.name ?? "",
61
+ docType: item.docType ?? "unknown",
62
+ creatorId: item.creatorId,
63
+ updatedAt: item.updatedAt,
64
+ };
65
+ }
66
+
67
+ function mapSearchDoc(item: SearchDocItem): DocInfo {
68
+ return {
69
+ docId: item.docId ?? "",
70
+ title: item.title ?? "",
71
+ docType: item.docType ?? "unknown",
72
+ creatorId: item.creatorId,
73
+ updatedAt: item.updatedAt,
74
+ };
75
+ }
76
+
77
+ function mapListDentry(item: ListDentryItem): DocInfo {
78
+ return {
79
+ docId: item.dentryUuid ?? "",
80
+ title: item.name ?? "",
81
+ docType: item.dentryType ?? "unknown",
82
+ creatorId: item.creatorId,
83
+ updatedAt: item.updatedAt,
84
+ };
85
+ }
86
+
87
+ export async function createDoc(
88
+ config: DingTalkConfig,
89
+ spaceId: string,
90
+ title: string,
91
+ content?: string,
92
+ log = getLogger(),
93
+ parentId?: string,
94
+ ): Promise<DocInfo> {
95
+ const headers = await buildHeaders(config, log);
96
+ const createResp = await axios.post(
97
+ `${DINGTALK_API}/v1.0/doc/spaces/${spaceId}/docs`,
98
+ {
99
+ spaceId,
100
+ ...(parentId ? { parentDentryId: parentId } : { parentDentryId: "" }),
101
+ name: title,
102
+ docType: "alidoc",
103
+ },
104
+ {
105
+ headers,
106
+ timeout: 10_000,
107
+ ...getProxyBypassOption(config),
108
+ },
109
+ );
110
+ const createdBase = mapCreatedDoc((createResp.data ?? {}) as CreateDocResponse);
111
+ const created = {
112
+ ...createdBase,
113
+ title: createdBase.title || title,
114
+ docType: createdBase.docType || "alidoc",
115
+ };
116
+ if (content?.trim() && created.docId) {
117
+ try {
118
+ await appendToDoc(config, created.docId, content, log);
119
+ } catch (error) {
120
+ throw new DocCreateAppendError(created, error);
121
+ }
122
+ }
123
+ return created;
124
+ }
125
+
126
+ export async function appendToDoc(
127
+ config: DingTalkConfig,
128
+ docId: string,
129
+ content: string,
130
+ log = getLogger(),
131
+ index = -1,
132
+ ): Promise<{ success: true }> {
133
+ const headers = await buildHeaders(config, log);
134
+ // DingTalk document block API accepts `index = -1` to append content at the end.
135
+ const resp = await axios.post(
136
+ `${DINGTALK_API}/v1.0/doc/documents/${docId}/blocks/root/children`,
137
+ {
138
+ blockType: "PARAGRAPH",
139
+ body: { text: content },
140
+ index,
141
+ },
142
+ {
143
+ headers,
144
+ timeout: 10_000,
145
+ ...getProxyBypassOption(config),
146
+ },
147
+ );
148
+ if ((resp.data as AppendDocResponse | undefined)?.success === false) {
149
+ throw new Error("appendToDoc failed");
150
+ }
151
+ return { success: true };
152
+ }
153
+
154
+ export async function searchDocs(
155
+ config: DingTalkConfig,
156
+ keyword: string,
157
+ spaceId?: string,
158
+ log = getLogger(),
159
+ ): Promise<DocInfo[]> {
160
+ const headers = await buildHeaders(config, log);
161
+ const resp = await axios.post(
162
+ `${DINGTALK_API}/v1.0/doc/docs/search`,
163
+ {
164
+ keyword,
165
+ maxResults: 20,
166
+ ...(spaceId ? { spaceId } : {}),
167
+ },
168
+ {
169
+ headers,
170
+ timeout: 10_000,
171
+ ...getProxyBypassOption(config),
172
+ },
173
+ );
174
+ return Array.isArray(resp.data?.items)
175
+ ? (resp.data.items as SearchDocItem[]).map(mapSearchDoc)
176
+ : [];
177
+ }
178
+
179
+ export async function listDocs(
180
+ config: DingTalkConfig,
181
+ spaceId: string,
182
+ parentId?: string,
183
+ log = getLogger(),
184
+ ): Promise<DocInfo[]> {
185
+ const headers = await buildHeaders(config, log);
186
+ const resp = await axios.get(`${DINGTALK_API}/v1.0/doc/spaces/${spaceId}/dentries`, {
187
+ headers,
188
+ params: {
189
+ maxResults: 50,
190
+ ...(parentId ? { parentDentryId: parentId } : {}),
191
+ },
192
+ timeout: 10_000,
193
+ ...getProxyBypassOption(config),
194
+ });
195
+ return Array.isArray(resp.data?.items)
196
+ ? (resp.data.items as ListDentryItem[]).map(mapListDentry)
197
+ : [];
198
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Throttled stream loop for fire-and-forget draft updates.
3
+ *
4
+ * Ported from OpenClaw core (channels/draft-stream-loop.ts) because the module
5
+ * is not exported via the plugin-sdk. Keeps the same interface so that future
6
+ * sync with upstream is straightforward.
7
+ *
8
+ * Key safety properties:
9
+ * - Single-flight: at most one `sendOrEditStreamMessage` is in-flight at any time.
10
+ * - Latest-wins: multiple `update()` calls during in-flight only keep the last text.
11
+ * - Throttle: respects `throttleMs` between consecutive sends.
12
+ * - `flush()` drains all pending + waits for in-flight before returning.
13
+ * - `stop()` clears pending and timers; subsequent `update()` calls are ignored.
14
+ */
15
+
16
+ export type DraftStreamLoop = {
17
+ update: (text: string) => void;
18
+ flush: () => Promise<void>;
19
+ stop: () => void;
20
+ resetPending: () => void;
21
+ resetThrottleWindow: () => void;
22
+ waitForInFlight: () => Promise<void>;
23
+ };
24
+
25
+ export function createDraftStreamLoop(params: {
26
+ throttleMs: number;
27
+ isStopped: () => boolean;
28
+ sendOrEditStreamMessage: (text: string) => Promise<void | boolean>;
29
+ }): DraftStreamLoop {
30
+ let lastSentAt = 0;
31
+ let pendingText = "";
32
+ let inFlightPromise: Promise<void | boolean> | undefined;
33
+ let timer: ReturnType<typeof setTimeout> | undefined;
34
+
35
+ const flush = async () => {
36
+ if (timer) {
37
+ clearTimeout(timer);
38
+ timer = undefined;
39
+ }
40
+ while (!params.isStopped()) {
41
+ if (inFlightPromise) {
42
+ await inFlightPromise;
43
+ continue;
44
+ }
45
+ const text = pendingText;
46
+ if (!text.trim()) {
47
+ pendingText = "";
48
+ return;
49
+ }
50
+ pendingText = "";
51
+ const current = params.sendOrEditStreamMessage(text).finally(() => {
52
+ if (inFlightPromise === current) {
53
+ inFlightPromise = undefined;
54
+ }
55
+ });
56
+ inFlightPromise = current;
57
+ const sent = await current;
58
+ if (sent === false) {
59
+ pendingText = text;
60
+ return;
61
+ }
62
+ lastSentAt = Date.now();
63
+ if (!pendingText) {
64
+ return;
65
+ }
66
+ }
67
+ };
68
+
69
+ const schedule = () => {
70
+ if (timer) {
71
+ return;
72
+ }
73
+ const delay = Math.max(0, params.throttleMs - (Date.now() - lastSentAt));
74
+ timer = setTimeout(() => {
75
+ void flush();
76
+ }, delay);
77
+ };
78
+
79
+ return {
80
+ update: (text: string) => {
81
+ if (params.isStopped()) {
82
+ return;
83
+ }
84
+ pendingText = text;
85
+ if (inFlightPromise) {
86
+ schedule();
87
+ return;
88
+ }
89
+ if (!timer && Date.now() - lastSentAt >= params.throttleMs) {
90
+ void flush();
91
+ return;
92
+ }
93
+ schedule();
94
+ },
95
+ flush,
96
+ stop: () => {
97
+ pendingText = "";
98
+ if (timer) {
99
+ clearTimeout(timer);
100
+ timer = undefined;
101
+ }
102
+ },
103
+ resetPending: () => {
104
+ pendingText = "";
105
+ },
106
+ resetThrottleWindow: () => {
107
+ lastSentAt = 0;
108
+ if (timer) {
109
+ clearTimeout(timer);
110
+ timer = undefined;
111
+ }
112
+ },
113
+ waitForInFlight: async () => {
114
+ if (inFlightPromise) {
115
+ await inFlightPromise;
116
+ }
117
+ },
118
+ };
119
+ }