@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 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.1",
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": "^1.6.0",
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.15.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"
@@ -1,4 +1,4 @@
1
- import axios from "axios";
1
+ import axios from "./http-client";
2
2
  import { getAccessToken } from "./auth";
3
3
  import type { DingTalkConfig } from "./types";
4
4
  import { formatDingTalkErrorPayloadLog, getErrorMessage, getProxyBypassOption } from "./utils";
package/src/auth.ts CHANGED
@@ -1,4 +1,4 @@
1
- import axios from "axios";
1
+ import axios from "./http-client";
2
2
  import type { DingTalkConfig, Logger, TokenInfo } from "./types";
3
3
  import { retryWithBackoff } from "./utils";
4
4
 
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import axios from "axios";
1
+ import axios from "./http-client";
2
2
  import { getProxyBypassOption } from "./utils";
3
3
 
4
4
  const DINGTALK_API = "https://api.dingtalk.com";
@@ -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,
@@ -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 "axios";
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";