clawdbot-dingtalk 0.1.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 (63) hide show
  1. package/README.md +164 -0
  2. package/clawdbot.plugin.json +11 -0
  3. package/dist/index.d.ts +10 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +15 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/accounts.d.ts +47 -0
  8. package/dist/src/accounts.d.ts.map +1 -0
  9. package/dist/src/accounts.js +152 -0
  10. package/dist/src/accounts.js.map +1 -0
  11. package/dist/src/channel.d.ts +10 -0
  12. package/dist/src/channel.d.ts.map +1 -0
  13. package/dist/src/channel.js +186 -0
  14. package/dist/src/channel.js.map +1 -0
  15. package/dist/src/config-schema.d.ts +279 -0
  16. package/dist/src/config-schema.d.ts.map +1 -0
  17. package/dist/src/config-schema.js +92 -0
  18. package/dist/src/config-schema.js.map +1 -0
  19. package/dist/src/monitor.d.ts +17 -0
  20. package/dist/src/monitor.d.ts.map +1 -0
  21. package/dist/src/monitor.js +183 -0
  22. package/dist/src/monitor.js.map +1 -0
  23. package/dist/src/probe.d.ts +15 -0
  24. package/dist/src/probe.d.ts.map +1 -0
  25. package/dist/src/probe.js +48 -0
  26. package/dist/src/probe.js.map +1 -0
  27. package/dist/src/runtime.d.ts +4 -0
  28. package/dist/src/runtime.d.ts.map +1 -0
  29. package/dist/src/runtime.js +11 -0
  30. package/dist/src/runtime.js.map +1 -0
  31. package/dist/src/send/chunker.d.ts +23 -0
  32. package/dist/src/send/chunker.d.ts.map +1 -0
  33. package/dist/src/send/chunker.js +149 -0
  34. package/dist/src/send/chunker.js.map +1 -0
  35. package/dist/src/send/index.d.ts +9 -0
  36. package/dist/src/send/index.d.ts.map +1 -0
  37. package/dist/src/send/index.js +7 -0
  38. package/dist/src/send/index.js.map +1 -0
  39. package/dist/src/send/markdown.d.ts +13 -0
  40. package/dist/src/send/markdown.d.ts.map +1 -0
  41. package/dist/src/send/markdown.js +44 -0
  42. package/dist/src/send/markdown.js.map +1 -0
  43. package/dist/src/send/reply.d.ts +35 -0
  44. package/dist/src/send/reply.d.ts.map +1 -0
  45. package/dist/src/send/reply.js +100 -0
  46. package/dist/src/send/reply.js.map +1 -0
  47. package/dist/src/stream/client.d.ts +11 -0
  48. package/dist/src/stream/client.d.ts.map +1 -0
  49. package/dist/src/stream/client.js +182 -0
  50. package/dist/src/stream/client.js.map +1 -0
  51. package/dist/src/stream/index.d.ts +7 -0
  52. package/dist/src/stream/index.d.ts.map +1 -0
  53. package/dist/src/stream/index.js +6 -0
  54. package/dist/src/stream/index.js.map +1 -0
  55. package/dist/src/stream/message-parser.d.ts +24 -0
  56. package/dist/src/stream/message-parser.d.ts.map +1 -0
  57. package/dist/src/stream/message-parser.js +211 -0
  58. package/dist/src/stream/message-parser.js.map +1 -0
  59. package/dist/src/stream/types.d.ts +92 -0
  60. package/dist/src/stream/types.d.ts.map +1 -0
  61. package/dist/src/stream/types.js +5 -0
  62. package/dist/src/stream/types.js.map +1 -0
  63. package/package.json +63 -0
@@ -0,0 +1,4 @@
1
+ import type { PluginRuntime } from "clawdbot/plugin-sdk";
2
+ export declare function setDingTalkRuntime(next: PluginRuntime): void;
3
+ export declare function getDingTalkRuntime(): PluginRuntime;
4
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAE5D;AAED,wBAAgB,kBAAkB,IAAI,aAAa,CAKlD"}
@@ -0,0 +1,11 @@
1
+ let runtime = null;
2
+ export function setDingTalkRuntime(next) {
3
+ runtime = next;
4
+ }
5
+ export function getDingTalkRuntime() {
6
+ if (!runtime) {
7
+ throw new Error("DingTalk runtime not initialized");
8
+ }
9
+ return runtime;
10
+ }
11
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAEA,IAAI,OAAO,GAAyB,IAAI,CAAC;AAEzC,MAAM,UAAU,kBAAkB,CAAC,IAAmB;IACpD,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Text chunking utilities for DingTalk message limits.
3
+ * Preserves markdown code fences and tries to break on natural boundaries.
4
+ */
5
+ /**
6
+ * Split long text into chunks under maxChars.
7
+ * Tries to split on paragraph boundaries first, then lines, then punctuation.
8
+ */
9
+ export declare function chunkText(text: string, maxChars: number): string[];
10
+ /**
11
+ * Split markdown text into chunks, never breaking inside code fences.
12
+ * Uses same chunking strategy as chunkText() but respects ``` and ~~~ blocks.
13
+ */
14
+ export declare function chunkMarkdownText(text: string, maxChars: number): string[];
15
+ /**
16
+ * Convert value to clean string.
17
+ */
18
+ export declare function toCleanString(v: unknown): string;
19
+ /**
20
+ * Normalize text for DingTalk messages.
21
+ */
22
+ export declare function normalizeForTextMessage(text: string): string;
23
+ //# sourceMappingURL=chunker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../../../src/send/chunker.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyCH;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAsClE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAkD1E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAGhD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Text chunking utilities for DingTalk message limits.
3
+ * Preserves markdown code fences and tries to break on natural boundaries.
4
+ */
5
+ /**
6
+ * Parse fence spans (``` or ~~~ code blocks) in markdown text.
7
+ * Returns array of {start, end} positions for each closed fence.
8
+ */
9
+ function parseFenceSpans(text) {
10
+ const spans = [];
11
+ const fencePattern = /^(`{3,}|~{3,})(\w*)?\s*$/gm;
12
+ let match;
13
+ let openFence = null;
14
+ let openIndex = 0;
15
+ while ((match = fencePattern.exec(text)) !== null) {
16
+ const fence = match[1];
17
+ const matchIndex = match.index;
18
+ if (!openFence) {
19
+ // Opening fence
20
+ openFence = fence[0]; // '`' or '~'
21
+ openIndex = matchIndex;
22
+ }
23
+ else if (fence[0] === openFence && fence.length >= openFence.length) {
24
+ // Matching closing fence
25
+ spans.push({ start: openIndex, end: matchIndex + match[0].length });
26
+ openFence = null;
27
+ }
28
+ }
29
+ return spans;
30
+ }
31
+ /**
32
+ * Check if a position is safe to break (not inside a fence).
33
+ */
34
+ function isSafeBreakpoint(position, fenceSpans) {
35
+ for (const span of fenceSpans) {
36
+ if (position > span.start && position < span.end)
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+ /**
42
+ * Split long text into chunks under maxChars.
43
+ * Tries to split on paragraph boundaries first, then lines, then punctuation.
44
+ */
45
+ export function chunkText(text, maxChars) {
46
+ const s = (text ?? "").toString();
47
+ if (s.length <= maxChars)
48
+ return [s];
49
+ const chunks = [];
50
+ let remaining = s;
51
+ const pushChunk = (c) => {
52
+ const v = c.trim();
53
+ if (v)
54
+ chunks.push(v);
55
+ };
56
+ while (remaining.length > maxChars) {
57
+ // Try split by double newline within limit
58
+ let cut = remaining.lastIndexOf("\n\n", maxChars);
59
+ if (cut < maxChars * 0.5) {
60
+ // Try split by single newline
61
+ cut = remaining.lastIndexOf("\n", maxChars);
62
+ }
63
+ if (cut < maxChars * 0.5) {
64
+ // Try split by punctuation (Chinese + English)
65
+ const punct = ["。", "!", "?", ".", "!", "?", ";", ";"];
66
+ for (const p of punct) {
67
+ const idx = remaining.lastIndexOf(p, maxChars);
68
+ if (idx > cut)
69
+ cut = idx + p.length;
70
+ }
71
+ }
72
+ if (cut < maxChars * 0.4) {
73
+ // Hard cut
74
+ cut = maxChars;
75
+ }
76
+ pushChunk(remaining.slice(0, cut));
77
+ remaining = remaining.slice(cut);
78
+ }
79
+ pushChunk(remaining);
80
+ return chunks;
81
+ }
82
+ /**
83
+ * Split markdown text into chunks, never breaking inside code fences.
84
+ * Uses same chunking strategy as chunkText() but respects ``` and ~~~ blocks.
85
+ */
86
+ export function chunkMarkdownText(text, maxChars) {
87
+ const s = (text ?? "").toString();
88
+ if (s.length <= maxChars)
89
+ return [s];
90
+ const fenceSpans = parseFenceSpans(s);
91
+ const chunks = [];
92
+ let remaining = s;
93
+ while (remaining.length > maxChars) {
94
+ // Try paragraph break
95
+ let cut = remaining.lastIndexOf("\n\n", maxChars);
96
+ if (cut < maxChars * 0.5) {
97
+ // Try single newline
98
+ cut = remaining.lastIndexOf("\n", maxChars);
99
+ }
100
+ if (cut < maxChars * 0.5) {
101
+ // Try punctuation (Chinese + English)
102
+ const punct = ["。", "!", "?", ".", "!", "?", ";", ";"];
103
+ for (const p of punct) {
104
+ const idx = remaining.lastIndexOf(p, maxChars);
105
+ if (idx > cut)
106
+ cut = idx + p.length;
107
+ }
108
+ }
109
+ if (cut < maxChars * 0.4) {
110
+ // Hard cut
111
+ cut = maxChars;
112
+ }
113
+ // Check if safe (not inside fence)
114
+ const globalPos = s.length - remaining.length + cut;
115
+ if (!isSafeBreakpoint(globalPos, fenceSpans)) {
116
+ // Find the end of the fence we're inside
117
+ for (const span of fenceSpans) {
118
+ if (globalPos > span.start && globalPos < span.end) {
119
+ // Move cut to end of fence
120
+ cut = span.end - (s.length - remaining.length);
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ const chunk = remaining.slice(0, cut).trim();
126
+ if (chunk)
127
+ chunks.push(chunk);
128
+ remaining = remaining.slice(cut);
129
+ }
130
+ const final = remaining.trim();
131
+ if (final)
132
+ chunks.push(final);
133
+ return chunks;
134
+ }
135
+ /**
136
+ * Convert value to clean string.
137
+ */
138
+ export function toCleanString(v) {
139
+ if (v === null || v === undefined)
140
+ return "";
141
+ return String(v);
142
+ }
143
+ /**
144
+ * Normalize text for DingTalk messages.
145
+ */
146
+ export function normalizeForTextMessage(text) {
147
+ return toCleanString(text).replace(/\r\n/g, "\n");
148
+ }
149
+ //# sourceMappingURL=chunker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker.js","sourceRoot":"","sources":["../../../src/send/chunker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Send module exports.
3
+ */
4
+ export { sendReplyViaSessionWebhook, resolveResponsePrefix } from "./reply.js";
5
+ export type { ReplyOptions, ReplyResult, ReplyLogger } from "./reply.js";
6
+ export { chunkText, chunkMarkdownText, toCleanString, normalizeForTextMessage } from "./chunker.js";
7
+ export { convertMarkdownForDingTalk } from "./markdown.js";
8
+ export type { MarkdownOptions } from "./markdown.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/send/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,0BAA0B,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAC/E,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Send module exports.
3
+ */
4
+ export { sendReplyViaSessionWebhook, resolveResponsePrefix } from "./reply.js";
5
+ export { chunkText, chunkMarkdownText, toCleanString, normalizeForTextMessage } from "./chunker.js";
6
+ export { convertMarkdownForDingTalk } from "./markdown.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/send/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,0BAA0B,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAE/E,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Markdown table conversion for DingTalk.
3
+ * DingTalk's markdown renderer doesn't support tables well,
4
+ * so we convert them to code blocks.
5
+ */
6
+ export interface MarkdownOptions {
7
+ tableMode?: "code" | "off";
8
+ }
9
+ /**
10
+ * Convert markdown tables to code blocks for DingTalk compatibility.
11
+ */
12
+ export declare function convertMarkdownForDingTalk(text: string, options?: MarkdownOptions): string;
13
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../src/send/markdown.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC5B;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAsC9F"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Markdown table conversion for DingTalk.
3
+ * DingTalk's markdown renderer doesn't support tables well,
4
+ * so we convert them to code blocks.
5
+ */
6
+ /**
7
+ * Convert markdown tables to code blocks for DingTalk compatibility.
8
+ */
9
+ export function convertMarkdownForDingTalk(text, options = {}) {
10
+ const tableMode = options.tableMode ?? "code";
11
+ if (tableMode === "off")
12
+ return text;
13
+ const lines = text.split("\n");
14
+ let inTable = false;
15
+ let tableLines = [];
16
+ const result = [];
17
+ for (const line of lines) {
18
+ const isTableLine = line.trim().startsWith("|") && line.includes("|");
19
+ if (isTableLine) {
20
+ if (!inTable) {
21
+ inTable = true;
22
+ tableLines = [];
23
+ }
24
+ tableLines.push(line);
25
+ }
26
+ else {
27
+ if (inTable) {
28
+ result.push("```");
29
+ result.push(...tableLines);
30
+ result.push("```");
31
+ tableLines = [];
32
+ inTable = false;
33
+ }
34
+ result.push(line);
35
+ }
36
+ }
37
+ if (inTable) {
38
+ result.push("```");
39
+ result.push(...tableLines);
40
+ result.push("```");
41
+ }
42
+ return result.join("\n");
43
+ }
44
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../../src/send/markdown.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,UAAU,0BAA0B,CAAC,IAAY,EAAE,UAA2B,EAAE;IACpF,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;IAE9C,IAAI,SAAS,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAErC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,UAAU,GAAa,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAEtE,IAAI,WAAW,EAAE,CAAC;YAChB,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,GAAG,IAAI,CAAC;gBACf,UAAU,GAAG,EAAE,CAAC;YAClB,CAAC;YACD,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACnB,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;gBAC3B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACnB,UAAU,GAAG,EAAE,CAAC;gBAChB,OAAO,GAAG,KAAK,CAAC;YAClB,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnB,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;QAC3B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * DingTalk reply implementation via sessionWebhook.
3
+ */
4
+ export interface ReplyOptions {
5
+ replyMode?: "text" | "markdown";
6
+ maxChars?: number;
7
+ tableMode?: "code" | "off";
8
+ logger?: ReplyLogger;
9
+ }
10
+ export interface ReplyResult {
11
+ ok: boolean;
12
+ reason?: string;
13
+ status?: number;
14
+ data?: unknown;
15
+ chunks?: number;
16
+ }
17
+ export interface ReplyLogger {
18
+ debug?: (obj: Record<string, unknown>, msg?: string) => void;
19
+ warn?: (obj: Record<string, unknown> | string, msg?: string) => void;
20
+ error?: (obj: Record<string, unknown>, msg?: string) => void;
21
+ }
22
+ /**
23
+ * Send reply to DingTalk via sessionWebhook.
24
+ * Automatically chunks long messages.
25
+ */
26
+ export declare function sendReplyViaSessionWebhook(sessionWebhook: string, text: string, options?: ReplyOptions): Promise<ReplyResult>;
27
+ /**
28
+ * Resolve response prefix template with model context.
29
+ */
30
+ export declare function resolveResponsePrefix(template: string | undefined, context: {
31
+ model?: string;
32
+ provider?: string;
33
+ identity?: string;
34
+ }): string | undefined;
35
+ //# sourceMappingURL=reply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply.d.ts","sourceRoot":"","sources":["../../../src/send/reply.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9D;AAeD;;;GAGG;AACH,wBAAsB,0BAA0B,CAC9C,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,WAAW,CAAC,CAkEtB;AAOD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAChE,MAAM,GAAG,SAAS,CAgBpB"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * DingTalk reply implementation via sessionWebhook.
3
+ */
4
+ import { chunkText, chunkMarkdownText, normalizeForTextMessage } from "./chunker.js";
5
+ import { convertMarkdownForDingTalk } from "./markdown.js";
6
+ /**
7
+ * Mask webhook URL for logging (hide query params).
8
+ */
9
+ function maskWebhook(url) {
10
+ if (!url)
11
+ return "";
12
+ try {
13
+ const u = new URL(url);
14
+ return `${u.origin}${u.pathname}`;
15
+ }
16
+ catch {
17
+ return String(url).slice(0, 64) + "...";
18
+ }
19
+ }
20
+ /**
21
+ * Send reply to DingTalk via sessionWebhook.
22
+ * Automatically chunks long messages.
23
+ */
24
+ export async function sendReplyViaSessionWebhook(sessionWebhook, text, options = {}) {
25
+ const { replyMode = "text", maxChars = 1800, tableMode = "code", logger } = options;
26
+ if (!sessionWebhook) {
27
+ logger?.warn?.("No sessionWebhook, cannot reply");
28
+ return { ok: false, reason: "missing_sessionWebhook" };
29
+ }
30
+ let processedText = text;
31
+ if (replyMode === "markdown" && tableMode !== "off") {
32
+ processedText = convertMarkdownForDingTalk(processedText, { tableMode });
33
+ }
34
+ const cleaned = normalizeForTextMessage(processedText);
35
+ const chunks = replyMode === "markdown"
36
+ ? chunkMarkdownText(cleaned, maxChars)
37
+ : chunkText(cleaned, maxChars);
38
+ for (let i = 0; i < chunks.length; i++) {
39
+ const part = chunks[i];
40
+ const payload = replyMode === "markdown"
41
+ ? {
42
+ msgtype: "markdown",
43
+ markdown: {
44
+ title: "Clawdbot",
45
+ text: part,
46
+ },
47
+ }
48
+ : {
49
+ msgtype: "text",
50
+ text: {
51
+ content: part,
52
+ },
53
+ };
54
+ try {
55
+ const resp = await fetch(sessionWebhook, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify(payload),
59
+ signal: AbortSignal.timeout(30_000),
60
+ });
61
+ if (!resp.ok) {
62
+ const data = await resp.text();
63
+ logger?.error?.({ err: { message: `HTTP ${resp.status}`, status: resp.status, data }, webhook: maskWebhook(sessionWebhook) }, "Failed to reply DingTalk");
64
+ return { ok: false, reason: "http_error", status: resp.status, data };
65
+ }
66
+ logger?.debug?.({ webhook: maskWebhook(sessionWebhook), idx: i + 1, total: chunks.length }, "Replied to DingTalk");
67
+ }
68
+ catch (err) {
69
+ const error = err;
70
+ logger?.error?.({ err: { message: error?.message }, webhook: maskWebhook(sessionWebhook) }, "Failed to reply DingTalk");
71
+ return { ok: false, reason: "fetch_error" };
72
+ }
73
+ }
74
+ return { ok: true, chunks: chunks.length };
75
+ }
76
+ /**
77
+ * Response prefix template variable pattern.
78
+ */
79
+ const TEMPLATE_VAR_PATTERN = /\{([a-zA-Z][a-zA-Z0-9.]*)\}/g;
80
+ /**
81
+ * Resolve response prefix template with model context.
82
+ */
83
+ export function resolveResponsePrefix(template, context) {
84
+ if (template === undefined || template === null)
85
+ return undefined;
86
+ return template.replace(TEMPLATE_VAR_PATTERN, (match, varName) => {
87
+ const normalized = varName.toLowerCase();
88
+ switch (normalized) {
89
+ case "model":
90
+ return context.model ?? match;
91
+ case "provider":
92
+ return context.provider ?? match;
93
+ case "identity":
94
+ return context.identity ?? match;
95
+ default:
96
+ return match;
97
+ }
98
+ });
99
+ }
100
+ //# sourceMappingURL=reply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply.js","sourceRoot":"","sources":["../../../src/send/reply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * DingTalk Stream API client.
3
+ * Connects to DingTalk via WebSocket for receiving bot messages.
4
+ */
5
+ import type { StreamClientHandle, StreamClientOptions } from "./types.js";
6
+ /**
7
+ * Start DingTalk Stream client.
8
+ * Maintains persistent WebSocket connection with auto-reconnect.
9
+ */
10
+ export declare function startDingTalkStreamClient(options: StreamClientOptions): Promise<StreamClientHandle>;
11
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/stream/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAGV,kBAAkB,EAClB,mBAAmB,EAGpB,MAAM,YAAY,CAAC;AAyEpB;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAmI7B"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * DingTalk Stream API client.
3
+ * Connects to DingTalk via WebSocket for receiving bot messages.
4
+ */
5
+ import WebSocket from "ws";
6
+ import { buildAck, extractChatbotMessage } from "./message-parser.js";
7
+ function sleep(ms) {
8
+ return new Promise((r) => setTimeout(r, ms));
9
+ }
10
+ /**
11
+ * Open stream connection via DingTalk API.
12
+ */
13
+ async function openStreamConnection(params) {
14
+ const { apiBase, openPath, openBody, logger } = params;
15
+ const url = `${apiBase.replace(/\/$/, "")}${openPath}`;
16
+ logger?.info?.({ url }, "Opening DingTalk stream connection");
17
+ logger?.debug?.({ openBody: JSON.stringify(openBody).slice(0, 500) }, "Open request body");
18
+ const resp = await fetch(url, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify(openBody),
22
+ signal: AbortSignal.timeout(30_000),
23
+ });
24
+ if (!resp.ok) {
25
+ throw new Error(`Stream open failed: ${resp.status} ${resp.statusText}`);
26
+ }
27
+ const data = (await resp.json());
28
+ logger?.debug?.({ respData: JSON.stringify(data).slice(0, 500) }, "DingTalk stream open response");
29
+ // Response shapes can vary. Try multiple candidates.
30
+ const endpoint = data.endpoint ??
31
+ data.data?.endpoint ??
32
+ data.result?.endpoint ??
33
+ data.socketUrl ??
34
+ data.url;
35
+ const ticket = data.ticket ??
36
+ data.data?.ticket ??
37
+ data.result?.ticket ??
38
+ data.token;
39
+ if (!endpoint) {
40
+ throw new Error(`Stream open response missing endpoint. data=${JSON.stringify(data).slice(0, 500)}`);
41
+ }
42
+ return { endpoint, ticket, raw: data };
43
+ }
44
+ /**
45
+ * Build WebSocket URL with ticket parameter.
46
+ */
47
+ function buildWsUrl(endpoint, ticket) {
48
+ if (!ticket)
49
+ return endpoint;
50
+ try {
51
+ const u = new URL(endpoint);
52
+ if (!u.searchParams.get("ticket"))
53
+ u.searchParams.set("ticket", ticket);
54
+ return u.toString();
55
+ }
56
+ catch {
57
+ // If it's not a valid URL string, just append.
58
+ const sep = endpoint.includes("?") ? "&" : "?";
59
+ return `${endpoint}${sep}ticket=${encodeURIComponent(ticket)}`;
60
+ }
61
+ }
62
+ /**
63
+ * Start DingTalk Stream client.
64
+ * Maintains persistent WebSocket connection with auto-reconnect.
65
+ */
66
+ export async function startDingTalkStreamClient(options) {
67
+ const { clientId, clientSecret, apiBase, openPath, openBody, logger, onChatMessage } = options;
68
+ let stop = false;
69
+ let attempt = 0;
70
+ const baseOpenBody = openBody ?? {
71
+ clientId,
72
+ clientSecret,
73
+ };
74
+ async function loop() {
75
+ while (!stop) {
76
+ attempt += 1;
77
+ let ws = null;
78
+ try {
79
+ const { endpoint, ticket } = await openStreamConnection({
80
+ apiBase,
81
+ openPath,
82
+ openBody: baseOpenBody,
83
+ logger,
84
+ });
85
+ const wsUrl = buildWsUrl(endpoint, ticket);
86
+ logger?.info?.({ wsUrl: wsUrl.replace(/ticket=[^&]+/i, "ticket=***") }, "Connecting WebSocket");
87
+ ws = new WebSocket(wsUrl, ticket ? { headers: { "x-dingtalk-ticket": ticket, ticket } } : undefined);
88
+ ws.on("ping", (data) => {
89
+ logger?.debug?.({ data: data?.toString?.() }, "WebSocket ping received");
90
+ });
91
+ ws.on("pong", (data) => {
92
+ logger?.debug?.({ data: data?.toString?.() }, "WebSocket pong received");
93
+ });
94
+ ws.on("error", (err) => {
95
+ logger?.error?.({ err: err?.message }, "WebSocket error");
96
+ });
97
+ // Wait for connection to open
98
+ await new Promise((resolve, reject) => {
99
+ const onOpen = () => {
100
+ cleanup();
101
+ resolve();
102
+ };
103
+ const onErr = (e) => {
104
+ cleanup();
105
+ reject(e);
106
+ };
107
+ const cleanup = () => {
108
+ ws?.off("open", onOpen);
109
+ ws?.off("error", onErr);
110
+ };
111
+ ws.on("open", onOpen);
112
+ ws.on("error", onErr);
113
+ });
114
+ logger?.info?.("WebSocket connected successfully");
115
+ attempt = 0;
116
+ // Handle incoming messages
117
+ const currentWs = ws;
118
+ ws.on("message", async (data) => {
119
+ logger?.debug?.({ rawLength: data?.length }, "WebSocket raw message received");
120
+ let msg = null;
121
+ try {
122
+ msg = JSON.parse(data.toString());
123
+ }
124
+ catch {
125
+ logger?.debug?.({ data: data.toString().slice(0, 200) }, "Non-JSON stream message (ignored)");
126
+ return;
127
+ }
128
+ // ACK early if possible
129
+ const ack = buildAck(msg);
130
+ if (ack) {
131
+ try {
132
+ currentWs.send(JSON.stringify(ack));
133
+ }
134
+ catch (e) {
135
+ logger?.debug?.({ e: e?.message }, "ACK send failed (ignored)");
136
+ }
137
+ }
138
+ const chat = extractChatbotMessage(msg);
139
+ if (!chat) {
140
+ logger?.debug?.({
141
+ eventType: msg?.headers?.eventType ?? msg?.type,
142
+ msgPreview: JSON.stringify(msg).slice(0, 500),
143
+ }, "Stream event ignored (not chatbot message)");
144
+ return;
145
+ }
146
+ await onChatMessage(chat);
147
+ });
148
+ // Wait for connection to close
149
+ await new Promise((resolve) => {
150
+ ws.on("close", (code, reason) => {
151
+ logger?.warn?.({ code, reason: reason?.toString?.() }, "WebSocket closed");
152
+ resolve();
153
+ });
154
+ });
155
+ }
156
+ catch (err) {
157
+ logger?.error?.({ err: { message: err?.message } }, "Stream loop error");
158
+ }
159
+ finally {
160
+ try {
161
+ ws?.close?.();
162
+ }
163
+ catch {
164
+ // ignore
165
+ }
166
+ }
167
+ if (stop)
168
+ break;
169
+ // Reconnect with backoff
170
+ const backoff = Math.min(30_000, 1000 * Math.pow(2, Math.min(attempt, 5)));
171
+ logger?.info?.({ backoffMs: backoff }, "Reconnecting after backoff");
172
+ await sleep(backoff);
173
+ }
174
+ }
175
+ loop().catch((e) => logger?.error?.({ e: e?.message }, "Fatal stream loop error"));
176
+ return {
177
+ stop: () => {
178
+ stop = true;
179
+ },
180
+ };
181
+ }
182
+ //# sourceMappingURL=client.js.map