@soimy/dingtalk 3.5.1 → 3.5.2
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 +7 -1
- package/package.json +8 -3
- package/src/ack-reaction-service.ts +1 -1
- package/src/auth.ts +1 -1
- package/src/card/card-action-handler.ts +1 -1
- package/src/card/card-stop-handler.ts +1 -1
- package/src/card/reasoning-block-assembler.ts +157 -0
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +32 -0
- package/src/card-service.ts +1 -1
- package/src/channel.ts +71 -55
- package/src/command/card-stop-command.ts +4 -22
- package/src/command/inbound-command-dispatch-service.ts +464 -0
- package/src/docs-service.ts +5 -5
- package/src/http-client.ts +20 -0
- package/src/inbound-handler.ts +120 -463
- package/src/logger-context.ts +16 -2
- package/src/media-utils.ts +3 -3
- package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
- package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +5 -5
- package/src/reply-strategy-card.ts +64 -13
- package/src/reply-strategy-markdown.ts +123 -18
- package/src/reply-strategy.ts +4 -0
- package/src/send-service.ts +68 -3
- package/src/targeting/agent-routing.ts +11 -4
- package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
- package/src/utils.ts +165 -0
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<a href="https://www.npmjs.com/package/@soimy/dingtalk"><img alt="npm downloads" src="https://img.shields.io/npm/dm/%40soimy%2Fdingtalk"></a>
|
|
11
11
|
<a href="https://github.com/soimy/openclaw-channel-dingtalk/actions/workflows/docs-pages.yml"><img alt="Docs" src="https://img.shields.io/github/actions/workflow/status/soimy/openclaw-channel-dingtalk/docs-pages.yml?branch=main&label=Docs"></a>
|
|
12
12
|
<a href="https://github.com/soimy/openclaw-channel-dingtalk/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/soimy/openclaw-channel-dingtalk"></a>
|
|
13
|
+
<a href="https://github.com/soimy/openclaw-channel-dingtalk/blob/main/CITATION.cff"><img alt="Citation" src="https://img.shields.io/badge/Citation-CITATION.cff-1277B5"></a>
|
|
13
14
|
</p>
|
|
14
15
|
|
|
15
16
|
针对 OpenClaw 的钉钉企业内部机器人 Channel 渠道插件,使用 Stream 模式,无需公网 IP。
|
|
@@ -33,6 +34,11 @@
|
|
|
33
34
|
- 发布记录:[docs/releases/index.md](docs/releases/index.md)
|
|
34
35
|
- 英文入口:[docs/en/index.md](docs/en/index.md)
|
|
35
36
|
|
|
37
|
+
## 引用与署名
|
|
38
|
+
|
|
39
|
+
- GitHub / 机器可读引用元数据:[CITATION.cff](https://github.com/soimy/openclaw-channel-dingtalk/blob/main/CITATION.cff)
|
|
40
|
+
- 维护者对复用、引用与 AI 协作场景的署名请求:[docs/contributor/citation-and-attribution.md](docs/contributor/citation-and-attribution.md)
|
|
41
|
+
|
|
36
42
|
## 安装
|
|
37
43
|
|
|
38
44
|
> [!IMPORTANT]
|
|
@@ -180,4 +186,4 @@ pnpm test
|
|
|
180
186
|
|
|
181
187
|
## 许可
|
|
182
188
|
|
|
183
|
-
[MIT](LICENSE)
|
|
189
|
+
[MIT](https://github.com/soimy/openclaw-channel-dingtalk/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soimy/dingtalk",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.2",
|
|
4
4
|
"description": "DingTalk (钉钉) channel plugin for OpenClaw",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bot",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"type-check": "tsc -p tsconfig.json"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"axios": "
|
|
49
|
+
"axios": "1.13.6",
|
|
50
50
|
"dingtalk-stream": "^2.1.4",
|
|
51
51
|
"form-data": "^4.0.0",
|
|
52
52
|
"mammoth": "^1.12.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@vitest/coverage-v8": "^3.2.4",
|
|
59
59
|
"oxfmt": "0.34.0",
|
|
60
60
|
"oxlint": "^1.49.0",
|
|
61
|
-
"oxlint-tsgolint": "^0.
|
|
61
|
+
"oxlint-tsgolint": "^0.18.0",
|
|
62
62
|
"typescript": "^5.3.0",
|
|
63
63
|
"vitepress": "1.6.4",
|
|
64
64
|
"vitest": "^3.2.4"
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"openclaw": ">=2026.3.24"
|
|
68
68
|
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"openclaw": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
69
74
|
"openclaw": {
|
|
70
75
|
"compat": {
|
|
71
76
|
"pluginApi": ">=2026.3.24"
|
package/src/auth.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
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
4
|
import { resolveCardRun } from "./card-run-registry";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { getAccessToken } from "../auth";
|
|
3
3
|
import { finishStoppedAICard, hideCardStopButton } from "../card-service";
|
|
4
4
|
import { dispatchDingTalkCardStopCommand } from "../command/card-stop-command";
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export interface ReasoningBlockAssembler {
|
|
2
|
+
ingestSnapshot: (text: string | undefined) => string[];
|
|
3
|
+
flushPendingAtBoundary: () => string[];
|
|
4
|
+
reset: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function stripReasoningPrefix(text: string): string {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
if (trimmed.startsWith("Reasoning:")) {
|
|
10
|
+
return trimmed.slice("Reasoning:".length).trimStart();
|
|
11
|
+
}
|
|
12
|
+
return trimmed;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cleanReasoningLine(line: string): string {
|
|
16
|
+
return line.trim().replace(/^_/, "").replace(/_$/, "").trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isClosedReasoningLine(line: string): boolean {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
return trimmed.startsWith("_") && trimmed.endsWith("_") && trimmed.length >= 2;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function startsReasonBlock(line: string): boolean {
|
|
25
|
+
return cleanReasoningLine(line).startsWith("Reason:");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function blocksStartWithPrefix(blocks: string[], prefix: string[]): boolean {
|
|
29
|
+
if (prefix.length > blocks.length) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return prefix.every((entry, index) => blocks[index] === entry);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseReasoningSnapshot(text: string | undefined): {
|
|
36
|
+
completeBlocks: string[];
|
|
37
|
+
pendingBlock: string;
|
|
38
|
+
} {
|
|
39
|
+
const normalized = typeof text === "string" ? stripReasoningPrefix(text) : "";
|
|
40
|
+
if (!normalized.trim()) {
|
|
41
|
+
return {
|
|
42
|
+
completeBlocks: [],
|
|
43
|
+
pendingBlock: "",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines = normalized
|
|
48
|
+
.split("\n")
|
|
49
|
+
.map((line) => line.trim())
|
|
50
|
+
.filter((line) => line.length > 0);
|
|
51
|
+
|
|
52
|
+
const completeBlocks: string[] = [];
|
|
53
|
+
let currentLines: string[] = [];
|
|
54
|
+
let currentMode: "explicit" | "implicit" | null = null;
|
|
55
|
+
let currentComplete = true;
|
|
56
|
+
|
|
57
|
+
const finalizeCurrent = () => {
|
|
58
|
+
if (currentLines.length === 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (currentMode === "explicit" && currentComplete) {
|
|
62
|
+
completeBlocks.push(currentLines.join("\n"));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (startsReasonBlock(line)) {
|
|
68
|
+
finalizeCurrent();
|
|
69
|
+
currentLines = [cleanReasoningLine(line)];
|
|
70
|
+
currentMode = "explicit";
|
|
71
|
+
currentComplete = isClosedReasoningLine(line);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (currentLines.length === 0) {
|
|
76
|
+
const trimmedLine = line.trim();
|
|
77
|
+
const cleanedLine = cleanReasoningLine(line);
|
|
78
|
+
if (!cleanedLine) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!trimmedLine.startsWith("_")) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
currentLines = [cleanedLine];
|
|
85
|
+
currentMode = "implicit";
|
|
86
|
+
currentComplete = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
currentLines.push(cleanReasoningLine(line));
|
|
91
|
+
currentComplete = currentMode === "explicit"
|
|
92
|
+
? currentComplete && isClosedReasoningLine(line)
|
|
93
|
+
: false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (currentLines.length === 0) {
|
|
97
|
+
return {
|
|
98
|
+
completeBlocks,
|
|
99
|
+
pendingBlock: "",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (currentMode === "explicit" && currentComplete) {
|
|
104
|
+
completeBlocks.push(currentLines.join("\n"));
|
|
105
|
+
return {
|
|
106
|
+
completeBlocks,
|
|
107
|
+
pendingBlock: "",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
completeBlocks,
|
|
113
|
+
pendingBlock: currentLines.join("\n").trim(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createReasoningBlockAssembler(): ReasoningBlockAssembler {
|
|
118
|
+
let emittedBlocks: string[] = [];
|
|
119
|
+
let pendingBlock = "";
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
ingestSnapshot(text: string | undefined): string[] {
|
|
123
|
+
const parsed = parseReasoningSnapshot(text);
|
|
124
|
+
pendingBlock = parsed.pendingBlock;
|
|
125
|
+
|
|
126
|
+
if (parsed.completeBlocks.length === 0) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (blocksStartWithPrefix(parsed.completeBlocks, emittedBlocks)) {
|
|
131
|
+
const nextBlocks = parsed.completeBlocks.slice(emittedBlocks.length);
|
|
132
|
+
emittedBlocks = [...parsed.completeBlocks];
|
|
133
|
+
return nextBlocks;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (blocksStartWithPrefix(emittedBlocks, parsed.completeBlocks)) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return [];
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
flushPendingAtBoundary(): string[] {
|
|
144
|
+
if (!pendingBlock.trim()) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
const flushed = pendingBlock;
|
|
148
|
+
pendingBlock = "";
|
|
149
|
+
return [flushed];
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
reset(): void {
|
|
153
|
+
emittedBlocks = [];
|
|
154
|
+
pendingBlock = "";
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -25,6 +25,7 @@ export interface CardDraftController {
|
|
|
25
25
|
updateAnswer: (text: string) => Promise<void>;
|
|
26
26
|
updateReasoning: (text: string) => Promise<void>;
|
|
27
27
|
updateThinking: (text: string) => Promise<void>;
|
|
28
|
+
appendThinkingBlock: (text: string) => Promise<void>;
|
|
28
29
|
updateTool: (text: string) => Promise<void>;
|
|
29
30
|
appendTool: (text: string) => Promise<void>;
|
|
30
31
|
/** Signal that a new assistant turn has started (e.g. after a tool call). */
|
|
@@ -116,6 +117,10 @@ export function createCardDraftController(params: {
|
|
|
116
117
|
return timelineEntries.length - 1;
|
|
117
118
|
};
|
|
118
119
|
|
|
120
|
+
const findCurrentSegmentAnswerIndex = (): number | null => {
|
|
121
|
+
return activeAnswerIndex;
|
|
122
|
+
};
|
|
123
|
+
|
|
119
124
|
const renderTimeline = (options: {
|
|
120
125
|
fallbackAnswer?: string;
|
|
121
126
|
overrideAnswer?: string;
|
|
@@ -292,6 +297,32 @@ export function createCardDraftController(params: {
|
|
|
292
297
|
queueRender();
|
|
293
298
|
};
|
|
294
299
|
|
|
300
|
+
const appendThinkingBlock = async (text: string) => {
|
|
301
|
+
await waitForPendingBoundary();
|
|
302
|
+
if (stopped || failed) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const normalized = normalizeProcessText(text);
|
|
306
|
+
if (!normalized) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (timelineEntries.length > 0) {
|
|
310
|
+
await flushBoundaryFrame();
|
|
311
|
+
}
|
|
312
|
+
sealLiveThinking();
|
|
313
|
+
const currentSegmentAnswerIndex = findCurrentSegmentAnswerIndex();
|
|
314
|
+
if (currentSegmentAnswerIndex !== null) {
|
|
315
|
+
timelineEntries.splice(currentSegmentAnswerIndex, 0, { kind: "thinking", text: normalized });
|
|
316
|
+
if (activeAnswerIndex !== null && activeAnswerIndex >= currentSegmentAnswerIndex) {
|
|
317
|
+
activeAnswerIndex += 1;
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
sealCurrentAnswer();
|
|
321
|
+
appendTimelineEntry("thinking", normalized);
|
|
322
|
+
}
|
|
323
|
+
queueRender();
|
|
324
|
+
};
|
|
325
|
+
|
|
295
326
|
const notifyNewAssistantTurn = async () => {
|
|
296
327
|
if (stopped || failed) {
|
|
297
328
|
return;
|
|
@@ -311,6 +342,7 @@ export function createCardDraftController(params: {
|
|
|
311
342
|
updateAnswer,
|
|
312
343
|
updateReasoning,
|
|
313
344
|
updateThinking: updateReasoning,
|
|
345
|
+
appendThinkingBlock,
|
|
314
346
|
updateTool,
|
|
315
347
|
appendTool: updateTool,
|
|
316
348
|
notifyNewAssistantTurn,
|
package/src/card-service.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import axios from "
|
|
4
|
+
import axios from "./http-client";
|
|
5
5
|
import { getAccessToken } from "./auth";
|
|
6
6
|
import { updateCardVariables } from "./card-callback-service";
|
|
7
7
|
import { DINGTALK_CARD_TEMPLATE, STOP_ACTION_VISIBLE, STOP_ACTION_HIDDEN } from "./card/card-template";
|