@soimy/dingtalk 3.6.4 → 3.6.5
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/README.md +12 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6592 -5621
- package/dist/index.js.map +4 -4
- package/dist/src/card/ask-user-question-context.d.ts +17 -0
- package/dist/src/card/ask-user-question-context.d.ts.map +1 -0
- package/dist/src/card/ask-user-question.d.ts +241 -0
- package/dist/src/card/ask-user-question.d.ts.map +1 -0
- package/dist/src/card/card-action-handler.d.ts +1 -0
- package/dist/src/card/card-action-handler.d.ts.map +1 -1
- package/dist/src/card/card-template.d.ts +5 -0
- package/dist/src/card/card-template.d.ts.map +1 -1
- package/dist/src/gateway/channel-gateway.d.ts.map +1 -1
- package/dist/src/inbound-handler.d.ts.map +1 -1
- package/index.ts +18 -1
- package/openclaw.plugin.json +3 -0
- package/package.json +1 -1
- package/src/card/ask-user-question-context.ts +31 -0
- package/src/card/ask-user-question.ts +1068 -0
- package/src/card/card-action-handler.ts +14 -0
- package/src/card/card-template.ts +10 -0
- package/src/gateway/channel-gateway.ts +5 -5
- package/src/inbound-handler.ts +103 -9
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import type { CardCallbackAnalysis } from "../card-callback-service";
|
|
3
3
|
import type { DingTalkConfig, Logger } from "../types";
|
|
4
|
+
import { handleDingTalkAskUserCardCallback } from "./ask-user-question";
|
|
4
5
|
import { resolveCardRun } from "./card-run-registry";
|
|
5
6
|
import { stopCardRun } from "./card-stop-handler";
|
|
6
7
|
|
|
@@ -9,12 +10,25 @@ export interface CardActionResult {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export async function handleCardAction(params: {
|
|
13
|
+
payload: unknown;
|
|
12
14
|
analysis: CardCallbackAnalysis;
|
|
13
15
|
cfg: OpenClawConfig;
|
|
14
16
|
accountId: string;
|
|
15
17
|
config: DingTalkConfig;
|
|
16
18
|
log?: Logger;
|
|
17
19
|
}): Promise<CardActionResult> {
|
|
20
|
+
const askUserResult = await handleDingTalkAskUserCardCallback({
|
|
21
|
+
payload: params.payload,
|
|
22
|
+
cfg: params.cfg,
|
|
23
|
+
accountId: params.accountId,
|
|
24
|
+
config: params.config,
|
|
25
|
+
clickerUserId: params.analysis.userId,
|
|
26
|
+
log: params.log,
|
|
27
|
+
});
|
|
28
|
+
if (askUserResult.handled) {
|
|
29
|
+
return askUserResult;
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
if (params.analysis.actionId !== "btn_stop") {
|
|
19
33
|
return { handled: false };
|
|
20
34
|
}
|
|
@@ -8,6 +8,8 @@ export const BUILTIN_DINGTALK_CARD_TEMPLATE_ID =
|
|
|
8
8
|
export const BUILTIN_DINGTALK_CARD_CONTENT_KEY = "content";
|
|
9
9
|
export const BUILTIN_DINGTALK_CARD_BLOCK_LIST_KEY = "blockList";
|
|
10
10
|
export const BUILTIN_DINGTALK_CARD_COPY_CONTENT_KEY = "copy_content";
|
|
11
|
+
export const BUILTIN_DINGTALK_ASK_USER_CARD_TEMPLATE_ID =
|
|
12
|
+
"89d0c6fe-3822-44c8-950e-e950f562546d.schema";
|
|
11
13
|
|
|
12
14
|
export interface DingTalkCardTemplateContract {
|
|
13
15
|
templateId: string;
|
|
@@ -20,6 +22,10 @@ export interface DingTalkCardTemplateContract {
|
|
|
20
22
|
copyContentKey: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
export interface DingTalkAskUserCardTemplateContract {
|
|
26
|
+
templateId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
/** Frozen singleton — no allocation on every call. */
|
|
24
30
|
export const DINGTALK_CARD_TEMPLATE: Readonly<DingTalkCardTemplateContract> = Object.freeze({
|
|
25
31
|
templateId: BUILTIN_DINGTALK_CARD_TEMPLATE_ID,
|
|
@@ -29,3 +35,7 @@ export const DINGTALK_CARD_TEMPLATE: Readonly<DingTalkCardTemplateContract> = Ob
|
|
|
29
35
|
copyContentKey: BUILTIN_DINGTALK_CARD_COPY_CONTENT_KEY,
|
|
30
36
|
});
|
|
31
37
|
|
|
38
|
+
export const DINGTALK_ASK_USER_CARD_TEMPLATE: Readonly<DingTalkAskUserCardTemplateContract> =
|
|
39
|
+
Object.freeze({
|
|
40
|
+
templateId: BUILTIN_DINGTALK_ASK_USER_CARD_TEMPLATE_ID,
|
|
41
|
+
});
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
|
|
2
2
|
import { analyzeCardCallback } from "../card-callback-service";
|
|
3
|
+
import { finalizeActiveCardsForAccount, recoverPendingCardsForAccount } from "../card-service";
|
|
3
4
|
import { handleCardAction } from "../card/card-action-handler";
|
|
4
|
-
import {
|
|
5
|
-
finalizeActiveCardsForAccount,
|
|
6
|
-
recoverPendingCardsForAccount,
|
|
7
|
-
} from "../card-service";
|
|
8
5
|
import { resolveRobotCode, resolveRuntimeConfig } from "../config";
|
|
9
6
|
import { ConnectionManager } from "../connection-manager";
|
|
10
7
|
import { isMessageProcessed, markMessageProcessed } from "../dedup";
|
|
@@ -381,6 +378,7 @@ export function createDingTalkGateway(): NonNullable<DingTalkChannelPlugin["gate
|
|
|
381
378
|
}
|
|
382
379
|
}
|
|
383
380
|
const actionResult = await handleCardAction({
|
|
381
|
+
payload,
|
|
384
382
|
analysis,
|
|
385
383
|
cfg,
|
|
386
384
|
accountId: account.accountId,
|
|
@@ -500,7 +498,9 @@ export function createDingTalkGateway(): NonNullable<DingTalkChannelPlugin["gate
|
|
|
500
498
|
lastStartAt: getCurrentTimestamp(),
|
|
501
499
|
lastError: null,
|
|
502
500
|
});
|
|
503
|
-
pluginLog?.info?.(
|
|
501
|
+
pluginLog?.info?.(
|
|
502
|
+
`[${account.accountId}] DingTalk Stream client connected successfully`,
|
|
503
|
+
);
|
|
504
504
|
await nativeStopPromise;
|
|
505
505
|
}
|
|
506
506
|
} catch (err: any) {
|
package/src/inbound-handler.ts
CHANGED
|
@@ -7,9 +7,19 @@ import { classifyAckReactionEmoji } from "./ack-reaction-classifier";
|
|
|
7
7
|
import { attachNativeAckReaction } from "./ack-reaction-service";
|
|
8
8
|
import { createDynamicAckReactionController } from "./ack-reaction/dynamic-ack-reaction-controller";
|
|
9
9
|
import { getAccessToken } from "./auth";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createAICard,
|
|
12
|
+
commitAICardBlocks,
|
|
13
|
+
isCardInTerminalState,
|
|
14
|
+
recallAICardMessage,
|
|
15
|
+
} from "./card-service";
|
|
16
|
+
import {
|
|
17
|
+
getDingTalkQuestionContext,
|
|
18
|
+
withDingTalkQuestionContext,
|
|
19
|
+
} from "./card/ask-user-question-context";
|
|
11
20
|
import { isCardRunStopRequested, registerCardRun, removeCardRun } from "./card/card-run-registry";
|
|
12
21
|
import { renderStatusLine } from "./card/statusline-renderer";
|
|
22
|
+
import { dispatchDingTalkCardStopCommand } from "./command/card-stop-command";
|
|
13
23
|
import { handleInboundCommandDispatch } from "./command/inbound-command-dispatch-service";
|
|
14
24
|
import {
|
|
15
25
|
resolveAckReactionSetting,
|
|
@@ -548,6 +558,23 @@ export async function downloadMedia(
|
|
|
548
558
|
}
|
|
549
559
|
|
|
550
560
|
export async function handleDingTalkMessage(params: HandleDingTalkMessageParams): Promise<void> {
|
|
561
|
+
// Keep context creation inside this public inbound entry. Ask-user synthetic
|
|
562
|
+
// reinjections call handleDingTalkMessage directly, so moving this wrapper to
|
|
563
|
+
// gateway callbacks would lose per-message isolation for reinjected answers.
|
|
564
|
+
return withDingTalkQuestionContext(
|
|
565
|
+
{
|
|
566
|
+
cfg: params.cfg,
|
|
567
|
+
accountId: params.accountId,
|
|
568
|
+
data: params.data,
|
|
569
|
+
sessionWebhook: params.sessionWebhook,
|
|
570
|
+
log: params.log,
|
|
571
|
+
dingtalkConfig: params.dingtalkConfig,
|
|
572
|
+
},
|
|
573
|
+
() => handleDingTalkMessageInner(params),
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams): Promise<void> {
|
|
551
578
|
const {
|
|
552
579
|
cfg,
|
|
553
580
|
accountId,
|
|
@@ -809,6 +836,10 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
809
836
|
peer: { kind: sessionPeer.kind, id: sessionPeer.peerId },
|
|
810
837
|
});
|
|
811
838
|
}
|
|
839
|
+
const questionContext = getDingTalkQuestionContext();
|
|
840
|
+
if (questionContext) {
|
|
841
|
+
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
842
|
+
}
|
|
812
843
|
|
|
813
844
|
// @Sub-Agent routing: dispatch @mention-targeted messages to their agent(s).
|
|
814
845
|
// Both content (`@agent <message>`) and commands (`@agent /new`) re-enter
|
|
@@ -930,8 +961,52 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
930
961
|
// sending a separate plain-text message.
|
|
931
962
|
let useCardMode = dingtalkConfig.messageType === "card";
|
|
932
963
|
let currentAICard: import("./types").AICardInstance | undefined;
|
|
964
|
+
let questionCardTookOver = false;
|
|
933
965
|
|
|
934
966
|
let cardFlightKey: string | undefined;
|
|
967
|
+
if (questionContext) {
|
|
968
|
+
questionContext.onQuestionCardSent = async ({ questionId, outTrackId }) => {
|
|
969
|
+
questionCardTookOver = true;
|
|
970
|
+
if (cardFlightKey) {
|
|
971
|
+
cardCreationInFlight.delete(cardFlightKey);
|
|
972
|
+
cardFlightKey = undefined;
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
await dispatchDingTalkCardStopCommand({
|
|
976
|
+
cfg,
|
|
977
|
+
accountId,
|
|
978
|
+
agentId: route.agentId,
|
|
979
|
+
targetSessionKey: route.sessionKey,
|
|
980
|
+
clickerUserId: senderId || "unknown",
|
|
981
|
+
log,
|
|
982
|
+
});
|
|
983
|
+
log?.info?.(
|
|
984
|
+
`[DingTalk][AskUser] Dispatched targeted stop after question card sent question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}`,
|
|
985
|
+
);
|
|
986
|
+
} catch (err) {
|
|
987
|
+
log?.warn?.(
|
|
988
|
+
`[DingTalk][AskUser] Question card sent, but targeted stop failed question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}: ${err instanceof Error ? err.message : String(err)}`,
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (!currentAICard) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (isCardInTerminalState(currentAICard.state)) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const recalled = await recallAICardMessage(currentAICard, log);
|
|
998
|
+
if (!recalled) {
|
|
999
|
+
log?.warn?.(
|
|
1000
|
+
`[DingTalk][AskUser] Question card sent, but AI card recall failed; normal replies remain suppressed question=${questionId} outTrackId=${outTrackId}`,
|
|
1001
|
+
);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
log?.info?.(
|
|
1005
|
+
`[DingTalk][AskUser] Recalled empty AI card after question card sent question=${questionId} outTrackId=${outTrackId}`,
|
|
1006
|
+
);
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
935
1010
|
if (useCardMode && !isBtwBypass) {
|
|
936
1011
|
const key = `${accountId}:${to}`;
|
|
937
1012
|
if (cardCreationInFlight.has(key)) {
|
|
@@ -2163,11 +2238,18 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
2163
2238
|
const inlineReplyPayload = parseInlineReplyPayloadText(payload.text);
|
|
2164
2239
|
const mediaUrls = extractMediaUrls(payload, inlineReplyPayload);
|
|
2165
2240
|
const richPayload = payload as ReplyStreamPayload & { isReasoning?: boolean };
|
|
2241
|
+
const replyKind = (info?.kind as DeliverPayload["kind"]) || "block";
|
|
2242
|
+
if (questionCardTookOver) {
|
|
2243
|
+
log?.info?.(
|
|
2244
|
+
`[DingTalk][AskUser] Suppressed ${replyKind} reply after question card took over`,
|
|
2245
|
+
);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2166
2248
|
await strategy.deliver({
|
|
2167
2249
|
text: inlineReplyPayload.text,
|
|
2168
2250
|
mediaUrls,
|
|
2169
2251
|
audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
|
|
2170
|
-
kind:
|
|
2252
|
+
kind: replyKind,
|
|
2171
2253
|
isReasoning: richPayload.isReasoning === true,
|
|
2172
2254
|
});
|
|
2173
2255
|
} catch (err: unknown) {
|
|
@@ -2212,13 +2294,19 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
2212
2294
|
typeof bufferedFinalPayload.text === "string" &&
|
|
2213
2295
|
bufferedFinalPayload.text.trim().length > 0;
|
|
2214
2296
|
if (hasBufferedText || mediaUrls.length > 0) {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2297
|
+
if (questionCardTookOver) {
|
|
2298
|
+
log?.info?.(
|
|
2299
|
+
"[DingTalk][AskUser] Suppressed buffered final after question card took over",
|
|
2300
|
+
);
|
|
2301
|
+
} else {
|
|
2302
|
+
await strategy.deliver({
|
|
2303
|
+
text: inlineReplyPayload.text,
|
|
2304
|
+
mediaUrls,
|
|
2305
|
+
audioAsVoice: extractSharedAudioAsVoice(bufferedFinalPayload, inlineReplyPayload),
|
|
2306
|
+
kind: "final",
|
|
2307
|
+
isReasoning: bufferedFinalPayload.isReasoning === true,
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2222
2310
|
}
|
|
2223
2311
|
}
|
|
2224
2312
|
} catch (dispatchErr: unknown) {
|
|
@@ -2228,6 +2316,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
2228
2316
|
throw dispatchErr;
|
|
2229
2317
|
}
|
|
2230
2318
|
|
|
2319
|
+
if (questionCardTookOver) {
|
|
2320
|
+
log?.info?.(
|
|
2321
|
+
"[DingTalk][AskUser] Skipping normal AI card finalize after question card took over",
|
|
2322
|
+
);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2231
2325
|
await strategy.finalize();
|
|
2232
2326
|
} finally {
|
|
2233
2327
|
// Only remove the registry entry if no stop was requested. When a stop is
|