@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.
- package/LICENSE +21 -0
- package/README.md +796 -42
- package/index.ts +62 -0
- package/package.json +4 -2
- package/src/access-control.ts +83 -0
- package/src/ack-reaction/dynamic-ack-reaction-controller.ts +271 -0
- package/src/ack-reaction/dynamic-ack-reaction-events.ts +123 -0
- package/src/ack-reaction/dynamic-ack-reaction-progress.ts +59 -0
- package/src/ack-reaction-classifier.ts +75 -0
- package/src/ack-reaction-service.ts +182 -0
- package/src/attachment-text-extractor.ts +148 -0
- package/src/card-callback-service.ts +119 -0
- package/src/card-draft-controller.ts +114 -0
- package/src/card-service.ts +666 -26
- package/src/channel.ts +455 -150
- package/src/config-schema.ts +64 -6
- package/src/config.ts +161 -5
- package/src/connection-manager.ts +354 -47
- package/src/dedup.ts +1 -0
- package/src/docs-service.ts +198 -0
- package/src/draft-stream-loop.ts +119 -0
- package/src/feedback-learning-service.ts +643 -0
- package/src/feedback-learning-store.ts +543 -0
- package/src/group-members-store.ts +48 -14
- package/src/inbound-handler.ts +1374 -259
- package/src/learning-command-service.ts +339 -0
- package/src/media-utils.ts +94 -50
- package/src/message-context-store.ts +787 -0
- package/src/message-utils.ts +487 -46
- package/src/messaging/quoted-context.ts +269 -0
- package/src/messaging/quoted-ref.ts +97 -0
- package/src/onboarding.ts +96 -1
- package/src/peer-id-registry.ts +102 -0
- package/src/persistence-store.ts +131 -0
- package/src/quoted-file-service.ts +385 -0
- package/src/reply-strategy-card.ts +225 -0
- package/src/reply-strategy-markdown.ts +55 -0
- package/src/reply-strategy-with-reaction.ts +190 -0
- package/src/reply-strategy.ts +72 -0
- package/src/send-service.ts +267 -45
- package/src/session-command-service.ts +147 -0
- package/src/session-lock.ts +2 -0
- package/src/session-peer-store.ts +77 -0
- package/src/session-routing.ts +33 -0
- package/src/targeting/agent-name-matcher.ts +148 -0
- package/src/targeting/agent-routing.ts +181 -0
- package/src/targeting/target-directory-adapter.ts +151 -0
- package/src/targeting/target-directory-store.ts +396 -0
- package/src/targeting/target-input.ts +62 -0
- package/src/types.ts +261 -28
- package/src/utils.ts +231 -12
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { MessageRecord } from "../message-context-store";
|
|
2
|
+
import type { Logger, QuotedRef } from "../types";
|
|
3
|
+
import { resolveQuotedRecord } from "./quoted-ref";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
6
|
+
const DEFAULT_PER_HOP_BODY_LIMIT = 1200;
|
|
7
|
+
const DEFAULT_TOTAL_BODY_LIMIT = 3600;
|
|
8
|
+
|
|
9
|
+
export interface QuotedChainEntry {
|
|
10
|
+
depth: number;
|
|
11
|
+
direction: MessageRecord["direction"];
|
|
12
|
+
messageType: string;
|
|
13
|
+
sender?: string;
|
|
14
|
+
body: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ResolvedQuotedRuntimeContext {
|
|
19
|
+
replyToId?: string;
|
|
20
|
+
replyToBody: string;
|
|
21
|
+
replyToSender?: string;
|
|
22
|
+
replyToIsQuote: true;
|
|
23
|
+
chain: QuotedChainEntry[];
|
|
24
|
+
untrustedContext?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface QuotedRuntimePreview {
|
|
28
|
+
text?: string;
|
|
29
|
+
messageType?: string;
|
|
30
|
+
senderId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizePositiveInteger(value: number | undefined, fallback: number): number {
|
|
34
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
35
|
+
? Math.floor(value)
|
|
36
|
+
: fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function trimmedString(value: string | undefined): string | undefined {
|
|
40
|
+
if (typeof value !== "string") {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function deriveReplyToId(record: MessageRecord): string | undefined {
|
|
48
|
+
if (record.direction === "inbound") {
|
|
49
|
+
return trimmedString(record.msgId);
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
trimmedString(record.delivery?.processQueryKey) ||
|
|
53
|
+
trimmedString(record.delivery?.messageId) ||
|
|
54
|
+
trimmedString(record.delivery?.outTrackId) ||
|
|
55
|
+
trimmedString(record.delivery?.cardInstanceId) ||
|
|
56
|
+
trimmedString(record.msgId)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function deriveSender(record: MessageRecord): string | undefined {
|
|
61
|
+
return record.direction === "outbound" ? "assistant" : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function deriveMessageType(record: MessageRecord): string {
|
|
65
|
+
return trimmedString(record.messageType) || "unknown";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildBodyPlaceholder(record: MessageRecord): string {
|
|
69
|
+
const messageType = trimmedString(record.messageType) || "message";
|
|
70
|
+
return `[Quoted ${messageType}]`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildPreviewPlaceholder(preview: QuotedRuntimePreview | undefined): string {
|
|
74
|
+
const messageType = trimmedString(preview?.messageType) || "message";
|
|
75
|
+
return `[Quoted ${messageType}]`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveRecordBody(record: MessageRecord): string {
|
|
79
|
+
return (
|
|
80
|
+
trimmedString(record.attachmentText) ||
|
|
81
|
+
trimmedString(record.text) ||
|
|
82
|
+
buildBodyPlaceholder(record)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolvePreviewBody(preview: QuotedRuntimePreview | undefined): string | undefined {
|
|
87
|
+
return trimmedString(preview?.text) || (preview ? buildPreviewPlaceholder(preview) : undefined);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deriveReplyToIdFromQuotedRef(quotedRef: QuotedRef | undefined): string | undefined {
|
|
91
|
+
if (!quotedRef?.key || !quotedRef.value) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return trimmedString(quotedRef.value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function derivePreviewSender(quotedRef?: QuotedRef): string | undefined {
|
|
98
|
+
if (quotedRef?.targetDirection === "outbound") {
|
|
99
|
+
return "assistant";
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function truncateBody(value: string, limit: number): string {
|
|
105
|
+
if (limit <= 0) {
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
108
|
+
return value.length <= limit ? value : value.slice(0, limit);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function fingerprintQuotedRef(quotedRef: QuotedRef | undefined): string | undefined {
|
|
112
|
+
if (!quotedRef) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const direction = quotedRef.targetDirection;
|
|
116
|
+
const key = quotedRef.key || "";
|
|
117
|
+
const value = quotedRef.value || "";
|
|
118
|
+
const fallbackCreatedAt =
|
|
119
|
+
typeof quotedRef.fallbackCreatedAt === "number" && Number.isFinite(quotedRef.fallbackCreatedAt)
|
|
120
|
+
? String(quotedRef.fallbackCreatedAt)
|
|
121
|
+
: "";
|
|
122
|
+
if (!direction) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return JSON.stringify([direction, key, value, fallbackCreatedAt]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildChainEntry(params: {
|
|
129
|
+
record: MessageRecord;
|
|
130
|
+
depth: number;
|
|
131
|
+
remainingBodyBudget: number;
|
|
132
|
+
perHopBodyLimit: number;
|
|
133
|
+
}): QuotedChainEntry | null {
|
|
134
|
+
const limit = Math.min(params.perHopBodyLimit, params.remainingBodyBudget);
|
|
135
|
+
if (limit <= 0) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const body = truncateBody(resolveRecordBody(params.record), limit);
|
|
139
|
+
return {
|
|
140
|
+
depth: params.depth,
|
|
141
|
+
direction: params.record.direction,
|
|
142
|
+
messageType: deriveMessageType(params.record),
|
|
143
|
+
sender: deriveSender(params.record),
|
|
144
|
+
body,
|
|
145
|
+
createdAt: params.record.createdAt,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function resolveQuotedRuntimeContext(params: {
|
|
150
|
+
storePath?: string;
|
|
151
|
+
accountId: string;
|
|
152
|
+
conversationId: string | null;
|
|
153
|
+
quotedRef?: QuotedRef;
|
|
154
|
+
firstRecord?: MessageRecord | null;
|
|
155
|
+
firstPreview?: QuotedRuntimePreview;
|
|
156
|
+
log?: Logger;
|
|
157
|
+
maxDepth?: number;
|
|
158
|
+
perHopBodyLimit?: number;
|
|
159
|
+
totalBodyLimit?: number;
|
|
160
|
+
}): ResolvedQuotedRuntimeContext | null {
|
|
161
|
+
if (!params.quotedRef) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const maxDepth = normalizePositiveInteger(params.maxDepth, DEFAULT_MAX_DEPTH);
|
|
166
|
+
const perHopBodyLimit = normalizePositiveInteger(
|
|
167
|
+
params.perHopBodyLimit,
|
|
168
|
+
DEFAULT_PER_HOP_BODY_LIMIT,
|
|
169
|
+
);
|
|
170
|
+
const totalBodyLimit = normalizePositiveInteger(params.totalBodyLimit, DEFAULT_TOTAL_BODY_LIMIT);
|
|
171
|
+
|
|
172
|
+
const chain: QuotedChainEntry[] = [];
|
|
173
|
+
const seenRecordIds = new Set<string>();
|
|
174
|
+
const seenQuotedRefFingerprints = new Set<string>();
|
|
175
|
+
let firstResolvedRecord: MessageRecord | null = null;
|
|
176
|
+
let remainingBodyBudget = totalBodyLimit;
|
|
177
|
+
let currentQuotedRef: QuotedRef | undefined = params.quotedRef;
|
|
178
|
+
let currentRecord = params.firstRecord ?? null;
|
|
179
|
+
|
|
180
|
+
for (let depth = 1; depth <= maxDepth && currentQuotedRef; depth += 1) {
|
|
181
|
+
if (depth > 1 || !currentRecord) {
|
|
182
|
+
currentRecord = resolveQuotedRecord({
|
|
183
|
+
storePath: params.storePath,
|
|
184
|
+
accountId: params.accountId,
|
|
185
|
+
conversationId: params.conversationId,
|
|
186
|
+
quotedRef: currentQuotedRef,
|
|
187
|
+
log: params.log,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (!currentRecord) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
if (!firstResolvedRecord) {
|
|
194
|
+
firstResolvedRecord = currentRecord;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const recordId = `${currentRecord.direction}:${currentRecord.msgId}`;
|
|
198
|
+
if (seenRecordIds.has(recordId)) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
seenRecordIds.add(recordId);
|
|
202
|
+
|
|
203
|
+
const currentFingerprint = fingerprintQuotedRef(currentQuotedRef);
|
|
204
|
+
if (currentFingerprint) {
|
|
205
|
+
if (seenQuotedRefFingerprints.has(currentFingerprint)) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
seenQuotedRefFingerprints.add(currentFingerprint);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const entry = buildChainEntry({
|
|
212
|
+
record: currentRecord,
|
|
213
|
+
depth,
|
|
214
|
+
remainingBodyBudget,
|
|
215
|
+
perHopBodyLimit,
|
|
216
|
+
});
|
|
217
|
+
if (!entry) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
chain.push(entry);
|
|
221
|
+
remainingBodyBudget -= entry.body.length;
|
|
222
|
+
|
|
223
|
+
const nextQuotedRef = currentRecord.quotedRef;
|
|
224
|
+
if (!nextQuotedRef) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const nextFingerprint = fingerprintQuotedRef(nextQuotedRef);
|
|
229
|
+
if (nextFingerprint && seenQuotedRefFingerprints.has(nextFingerprint)) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
currentQuotedRef = nextQuotedRef;
|
|
234
|
+
currentRecord = null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const replyToRecord = firstResolvedRecord ?? params.firstRecord ?? null;
|
|
238
|
+
const stableReplyToId = replyToRecord ? deriveReplyToId(replyToRecord) : undefined;
|
|
239
|
+
if (replyToRecord && stableReplyToId) {
|
|
240
|
+
return {
|
|
241
|
+
replyToId: stableReplyToId,
|
|
242
|
+
replyToBody: chain[0].body,
|
|
243
|
+
replyToSender: chain[0].sender,
|
|
244
|
+
replyToIsQuote: true,
|
|
245
|
+
chain,
|
|
246
|
+
untrustedContext:
|
|
247
|
+
chain.length > 1
|
|
248
|
+
? JSON.stringify({
|
|
249
|
+
quotedChain: chain.slice(1),
|
|
250
|
+
})
|
|
251
|
+
: undefined,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const previewBody = resolvePreviewBody(params.firstPreview);
|
|
256
|
+
const previewReplyToId = deriveReplyToIdFromQuotedRef(params.quotedRef);
|
|
257
|
+
if (!previewBody) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
replyToId: previewReplyToId,
|
|
263
|
+
replyToBody: truncateBody(previewBody, perHopBodyLimit),
|
|
264
|
+
replyToSender: derivePreviewSender(params.quotedRef),
|
|
265
|
+
replyToIsQuote: true,
|
|
266
|
+
chain: [],
|
|
267
|
+
untrustedContext: undefined,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveByQuotedRef,
|
|
3
|
+
type MessageRecord,
|
|
4
|
+
} from "../message-context-store";
|
|
5
|
+
import type { DingTalkInboundMessage, Logger, MessageContent, QuotedRef } from "../types";
|
|
6
|
+
|
|
7
|
+
function firstTrimmedString(...candidates: Array<string | undefined>): string | undefined {
|
|
8
|
+
for (const candidate of candidates) {
|
|
9
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
10
|
+
return candidate.trim();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function firstFiniteNumber(...candidates: Array<number | undefined>): number | undefined {
|
|
17
|
+
for (const candidate of candidates) {
|
|
18
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildInboundQuotedRef(
|
|
26
|
+
data: DingTalkInboundMessage,
|
|
27
|
+
content: MessageContent,
|
|
28
|
+
): QuotedRef | undefined {
|
|
29
|
+
const repliedMsg = data.text?.repliedMsg;
|
|
30
|
+
const repliedMsgId = firstTrimmedString(repliedMsg?.msgId, data.originalMsgId, content.quoted?.msgId);
|
|
31
|
+
const fallbackCreatedAt = firstFiniteNumber(
|
|
32
|
+
repliedMsg?.createdAt,
|
|
33
|
+
content.quoted?.cardCreatedAt,
|
|
34
|
+
content.quoted?.fileCreatedAt,
|
|
35
|
+
);
|
|
36
|
+
const isOutboundQuoted =
|
|
37
|
+
firstTrimmedString(data.originalProcessQueryKey) !== undefined ||
|
|
38
|
+
repliedMsg?.senderId === data.chatbotUserId ||
|
|
39
|
+
content.quoted?.isQuotedCard === true;
|
|
40
|
+
if (isOutboundQuoted) {
|
|
41
|
+
const processQueryKey = firstTrimmedString(data.originalProcessQueryKey, content.quoted?.processQueryKey);
|
|
42
|
+
if (processQueryKey) {
|
|
43
|
+
return {
|
|
44
|
+
targetDirection: "outbound",
|
|
45
|
+
key: "processQueryKey",
|
|
46
|
+
value: processQueryKey,
|
|
47
|
+
fallbackCreatedAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!fallbackCreatedAt) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
targetDirection: "outbound",
|
|
55
|
+
fallbackCreatedAt,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!repliedMsgId) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
targetDirection: "inbound",
|
|
63
|
+
key: "msgId",
|
|
64
|
+
value: repliedMsgId,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createReplyQuotedRef(msgId: string | undefined): QuotedRef | undefined {
|
|
69
|
+
const value = firstTrimmedString(msgId);
|
|
70
|
+
if (!value) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
targetDirection: "inbound",
|
|
75
|
+
key: "msgId",
|
|
76
|
+
value,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveQuotedRecord(params: {
|
|
81
|
+
storePath?: string;
|
|
82
|
+
accountId: string;
|
|
83
|
+
conversationId: string | null;
|
|
84
|
+
quotedRef?: QuotedRef;
|
|
85
|
+
log?: Logger;
|
|
86
|
+
}): MessageRecord | null {
|
|
87
|
+
if (!params.quotedRef) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return resolveByQuotedRef({
|
|
91
|
+
storePath: params.storePath,
|
|
92
|
+
accountId: params.accountId,
|
|
93
|
+
conversationId: params.conversationId,
|
|
94
|
+
quotedRef: params.quotedRef,
|
|
95
|
+
log: params.log,
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/onboarding.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OpenClawConfig, ChannelOnboardingAdapter, WizardPrompter } from "openclaw/plugin-sdk";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, formatDocsLink } from "openclaw/plugin-sdk";
|
|
3
|
+
import { DEFAULT_MESSAGE_CONTEXT_TTL_DAYS } from "./message-context-store.js";
|
|
3
4
|
import type { DingTalkConfig, DingTalkChannelConfig } from "./types.js";
|
|
4
5
|
import { listDingTalkAccountIds, resolveDingTalkAccount } from "./types.js";
|
|
5
6
|
|
|
@@ -103,6 +104,10 @@ function applyAccountConfig(params: {
|
|
|
103
104
|
...(input.dmPolicy ? { dmPolicy: input.dmPolicy } : {}),
|
|
104
105
|
...(input.groupPolicy ? { groupPolicy: input.groupPolicy } : {}),
|
|
105
106
|
...(input.allowFrom && input.allowFrom.length > 0 ? { allowFrom: input.allowFrom } : {}),
|
|
107
|
+
...(input.groupAllowFrom && input.groupAllowFrom.length > 0 ? { groupAllowFrom: input.groupAllowFrom } : {}),
|
|
108
|
+
...(input.displayNameResolution
|
|
109
|
+
? { displayNameResolution: input.displayNameResolution }
|
|
110
|
+
: {}),
|
|
106
111
|
...(input.messageType ? { messageType: input.messageType } : {}),
|
|
107
112
|
...(input.cardTemplateId ? { cardTemplateId: input.cardTemplateId } : {}),
|
|
108
113
|
...(input.cardTemplateKey ? { cardTemplateKey: input.cardTemplateKey } : {}),
|
|
@@ -113,6 +118,7 @@ function applyAccountConfig(params: {
|
|
|
113
118
|
? { useConnectionManager: input.useConnectionManager }
|
|
114
119
|
: {}),
|
|
115
120
|
...(typeof input.mediaMaxMb === "number" ? { mediaMaxMb: input.mediaMaxMb } : {}),
|
|
121
|
+
...(typeof input.journalTTLDays === "number" ? { journalTTLDays: input.journalTTLDays } : {}),
|
|
116
122
|
};
|
|
117
123
|
|
|
118
124
|
if (useDefault) {
|
|
@@ -315,10 +321,61 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
315
321
|
options: [
|
|
316
322
|
{ label: "Open - any group can use bot", value: "open" },
|
|
317
323
|
{ label: "Allowlist - only allowed groups", value: "allowlist" },
|
|
324
|
+
{ label: "Disabled - block all group messages", value: "disabled" },
|
|
318
325
|
],
|
|
319
326
|
initialValue: resolved.groupPolicy ?? "open",
|
|
320
327
|
});
|
|
321
328
|
|
|
329
|
+
if (groupPolicyValue === "allowlist") {
|
|
330
|
+
await prompter.note(
|
|
331
|
+
[
|
|
332
|
+
'groupPolicy=allowlist requires "groups" config to specify allowed group IDs.',
|
|
333
|
+
"After setup, manually add group conversationIds to your config:",
|
|
334
|
+
"",
|
|
335
|
+
' "groups": { "cidXXX": {}, "cidYYY": { "systemPrompt": "..." } }',
|
|
336
|
+
"",
|
|
337
|
+
"Groups not listed will be blocked. Use \"*\" as key to allow all groups.",
|
|
338
|
+
].join("\n"),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let groupAllowFrom: string[] | undefined;
|
|
343
|
+
if (groupPolicyValue !== "disabled") {
|
|
344
|
+
const groupAllowFromEntry = await prompter.text({
|
|
345
|
+
message: "Group sender allowlist - user IDs allowed in groups (comma-separated, optional)",
|
|
346
|
+
placeholder: "user1, user2",
|
|
347
|
+
initialValue: (resolved.groupAllowFrom || []).join(", ") || undefined,
|
|
348
|
+
});
|
|
349
|
+
const parsedGroupAllowFrom = parseList(String(groupAllowFromEntry ?? ""));
|
|
350
|
+
groupAllowFrom = parsedGroupAllowFrom.length > 0 ? parsedGroupAllowFrom : undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await prompter.note(
|
|
354
|
+
[
|
|
355
|
+
"Enabling learned displayName target resolution has tradeoffs:",
|
|
356
|
+
"- learned names come from observed inbound messages and can become stale",
|
|
357
|
+
"- duplicate display names can resolve to the wrong group or user",
|
|
358
|
+
"- current upstream target resolution does not provide requester authz, so \"all\" applies to every caller that can reach the send flow",
|
|
359
|
+
"Use explicit IDs for sensitive or high-risk deliveries.",
|
|
360
|
+
].join("\n"),
|
|
361
|
+
"displayName resolution risk",
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const displayNameResolutionValue = await prompter.select({
|
|
365
|
+
message: "Learned displayName target resolution",
|
|
366
|
+
options: [
|
|
367
|
+
{
|
|
368
|
+
label: "Disabled - require explicit IDs",
|
|
369
|
+
value: "disabled",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
label: "All - learned lookup for all callers (higher risk)",
|
|
373
|
+
value: "all",
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
initialValue: resolved.displayNameResolution ?? "disabled",
|
|
377
|
+
});
|
|
378
|
+
|
|
322
379
|
let maxReconnectCycles: number | undefined;
|
|
323
380
|
const wantsReconnectLimits = await prompter.confirm({
|
|
324
381
|
message: "Configure runtime reconnect cycle limit? (recommended)",
|
|
@@ -378,6 +435,41 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
378
435
|
mediaMaxMb = Number.isInteger(parsedMediaMax) && parsedMediaMax > 0 ? parsedMediaMax : 20;
|
|
379
436
|
}
|
|
380
437
|
|
|
438
|
+
let journalTTLDays: number | undefined;
|
|
439
|
+
const wantsJournalTTL = await prompter.confirm({
|
|
440
|
+
message: "Configure quote journal retention in days?",
|
|
441
|
+
initialValue: typeof resolved.journalTTLDays === "number",
|
|
442
|
+
});
|
|
443
|
+
if (wantsJournalTTL) {
|
|
444
|
+
const parsedJournalTTL = Number(
|
|
445
|
+
String(
|
|
446
|
+
await prompter.text({
|
|
447
|
+
message: "Quote journal retention days",
|
|
448
|
+
placeholder: String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
|
|
449
|
+
initialValue:
|
|
450
|
+
typeof resolved.journalTTLDays === "number"
|
|
451
|
+
? String(resolved.journalTTLDays)
|
|
452
|
+
: String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
|
|
453
|
+
validate: (value) => {
|
|
454
|
+
const raw = String(value ?? "").trim();
|
|
455
|
+
const num = Number(raw);
|
|
456
|
+
if (!raw) {
|
|
457
|
+
return "Required";
|
|
458
|
+
}
|
|
459
|
+
if (!Number.isInteger(num) || num < 1) {
|
|
460
|
+
return "Must be an integer >= 1";
|
|
461
|
+
}
|
|
462
|
+
return undefined;
|
|
463
|
+
},
|
|
464
|
+
}),
|
|
465
|
+
).trim(),
|
|
466
|
+
);
|
|
467
|
+
journalTTLDays =
|
|
468
|
+
Number.isInteger(parsedJournalTTL) && parsedJournalTTL > 0
|
|
469
|
+
? parsedJournalTTL
|
|
470
|
+
: DEFAULT_MESSAGE_CONTEXT_TTL_DAYS;
|
|
471
|
+
}
|
|
472
|
+
|
|
381
473
|
const next = applyAccountConfig({
|
|
382
474
|
cfg,
|
|
383
475
|
accountId,
|
|
@@ -388,14 +480,17 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
388
480
|
corpId,
|
|
389
481
|
agentId,
|
|
390
482
|
dmPolicy: dmPolicyValue as "open" | "allowlist",
|
|
391
|
-
groupPolicy: groupPolicyValue as "open" | "allowlist",
|
|
483
|
+
groupPolicy: groupPolicyValue as "open" | "allowlist" | "disabled",
|
|
392
484
|
allowFrom,
|
|
485
|
+
groupAllowFrom,
|
|
486
|
+
displayNameResolution: displayNameResolutionValue as "disabled" | "all",
|
|
393
487
|
mediaUrlAllowlist,
|
|
394
488
|
messageType,
|
|
395
489
|
cardTemplateId,
|
|
396
490
|
cardTemplateKey,
|
|
397
491
|
maxReconnectCycles,
|
|
398
492
|
mediaMaxMb,
|
|
493
|
+
journalTTLDays,
|
|
399
494
|
},
|
|
400
495
|
});
|
|
401
496
|
|
package/src/peer-id-registry.ts
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
* can be delivered correctly.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { readFileSync, readdirSync } from "fs";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { getLogger } from "./logger-context";
|
|
15
|
+
|
|
11
16
|
const peerIdMap = new Map<string, string>();
|
|
17
|
+
let preloaded = false;
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Register an original peer ID, keyed by its lowercased form.
|
|
@@ -20,14 +26,31 @@ export function registerPeerId(originalId: string): void {
|
|
|
20
26
|
peerIdMap.set(originalId.toLowerCase(), originalId);
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
function maybeRegisterDingTalkGroupPeerId(value: unknown): void {
|
|
30
|
+
// DingTalk group openConversationId values are base64-like and consistently start with "cid".
|
|
31
|
+
// User IDs in DM contexts do not follow this pattern and do not require case restoration.
|
|
32
|
+
if (typeof value === "string" && value.startsWith("cid")) {
|
|
33
|
+
registerPeerId(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* Resolve a possibly-lowercased peer ID back to its original casing.
|
|
39
|
+
*
|
|
40
|
+
* If registry is empty at first outbound call (for example, cron/delivery queue
|
|
41
|
+
* fires before inbound callbacks), perform a one-time lazy preload from sessions.
|
|
42
|
+
*
|
|
25
43
|
* Returns the original if found, otherwise returns the input as-is.
|
|
26
44
|
*/
|
|
27
45
|
export function resolveOriginalPeerId(id: string): string {
|
|
28
46
|
if (!id) {
|
|
29
47
|
return id;
|
|
30
48
|
}
|
|
49
|
+
|
|
50
|
+
if (!preloaded) {
|
|
51
|
+
preloadPeerIdsFromSessions();
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
return peerIdMap.get(id.toLowerCase()) || id;
|
|
32
55
|
}
|
|
33
56
|
|
|
@@ -36,4 +59,83 @@ export function resolveOriginalPeerId(id: string): string {
|
|
|
36
59
|
*/
|
|
37
60
|
export function clearPeerIdRegistry(): void {
|
|
38
61
|
peerIdMap.clear();
|
|
62
|
+
preloaded = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Preload known peer IDs from all agents' sessions.json files.
|
|
67
|
+
*
|
|
68
|
+
* Safe to call repeatedly:
|
|
69
|
+
* - without explicit homeDir: runs once, then no-ops;
|
|
70
|
+
* - with explicit homeDir: always runs (useful for tests).
|
|
71
|
+
*/
|
|
72
|
+
export function preloadPeerIdsFromSessions(homeDir?: string): void {
|
|
73
|
+
if (!homeDir && preloaded) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const home = homeDir || os.homedir();
|
|
78
|
+
const agentsDir = join(home, ".openclaw", "agents");
|
|
79
|
+
const log = getLogger();
|
|
80
|
+
let preloadCompleted = false;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const agentDirs = readdirSync(agentsDir, { withFileTypes: true })
|
|
84
|
+
.filter((entry) => entry.isDirectory())
|
|
85
|
+
.map((entry) => entry.name);
|
|
86
|
+
|
|
87
|
+
for (const agentName of agentDirs) {
|
|
88
|
+
const sessionsPath = join(agentsDir, agentName, "sessions", "sessions.json");
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const raw = readFileSync(sessionsPath, "utf-8");
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
if (!parsed || typeof parsed !== "object") {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const session of Object.values(parsed as Record<string, unknown>)) {
|
|
98
|
+
if (!session || typeof session !== "object") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sessionRecord = session as Record<string, unknown>;
|
|
103
|
+
maybeRegisterDingTalkGroupPeerId(sessionRecord.lastTo);
|
|
104
|
+
|
|
105
|
+
const origin = sessionRecord.origin;
|
|
106
|
+
if (!origin || typeof origin !== "object") {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const originRecord = origin as Record<string, unknown>;
|
|
111
|
+
maybeRegisterDingTalkGroupPeerId(originRecord.from);
|
|
112
|
+
maybeRegisterDingTalkGroupPeerId(originRecord.to);
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
// sessions.json may be missing or malformed for some agents; ignore per file.
|
|
116
|
+
log?.debug?.(
|
|
117
|
+
`[DingTalk][PeerIdRegistry] Failed to parse preload sessions file ${sessionsPath}: ${
|
|
118
|
+
err instanceof Error ? err.message : String(err)
|
|
119
|
+
}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
preloadCompleted = true;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const errorCode = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
126
|
+
if (errorCode === "ENOENT") {
|
|
127
|
+
// agents directory may not exist yet; this is a stable no-op state.
|
|
128
|
+
preloadCompleted = true;
|
|
129
|
+
} else {
|
|
130
|
+
log?.debug?.(
|
|
131
|
+
`[DingTalk][PeerIdRegistry] Failed to scan preload directory ${agentsDir}: ${
|
|
132
|
+
err instanceof Error ? err.message : String(err)
|
|
133
|
+
}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!homeDir && preloadCompleted) {
|
|
139
|
+
preloaded = true;
|
|
140
|
+
}
|
|
39
141
|
}
|