dingtalk-dws-mcp 1.0.0 → 1.0.1
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 +1 -1
- package/dist/src/delivery/webhook.d.ts +1 -1
- package/dist/src/delivery/webhook.js +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/internal/dws-robot-command.d.ts +1 -1
- package/dist/src/internal/dws-robot-command.js +1 -1
- package/dist/src/internal/exec.js +1 -1
- package/dist/src/notify-delivery/dws-robot.d.ts +24 -0
- package/dist/src/notify-delivery/dws-robot.js +129 -0
- package/dist/src/notify-delivery/index.d.ts +5 -0
- package/dist/src/notify-delivery/index.js +4 -0
- package/dist/src/notify-delivery/patrol-message.d.ts +3 -0
- package/dist/src/notify-delivery/patrol-message.js +10 -0
- package/dist/src/notify-delivery/redact.d.ts +1 -0
- package/dist/src/notify-delivery/redact.js +12 -0
- package/dist/src/notify-delivery/result.d.ts +20 -0
- package/dist/src/notify-delivery/result.js +1 -0
- package/dist/src/notify-delivery/webhook.d.ts +14 -0
- package/dist/src/notify-delivery/webhook.js +126 -0
- package/dist/src/patrol/setup.js +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -16,6 +16,6 @@ npm run pack:deploy
|
|
|
16
16
|
可选:`DINGTALK_PATROL_INCLUDE_NODE=0`、`DINGTALK_PATROL_NODE_URL`、`DINGTALK_PATROL_INCLUDE_DWS=0`(默认尝试打入 linux ELF dws;Windows 打包机需 `DINGTALK_PATROL_DWS_PATH` 指向 ELF)。
|
|
17
17
|
`dingtalk_patrol_setup` 只写 staging(含本机已验证投递凭据);传装用 host-execution 或 SSH,目标机跑 `install.sh`。可选 `deploy.sh` 为直连 SSH helper。
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
配置示例(npx):[`mcp/dingtalk-dws-mcp/mcp.example.json`](../../../mcp/dingtalk-dws-mcp/mcp.example.json)。npm:`dingtalk-dws-mcp@1.0.0`。
|
|
20
20
|
|
|
21
21
|
规格(开发者):[RFC-0007](../../../docs/rfc/RFC-0007-dingtalk-dws-mcp.md)、[RFC-0008](../../../docs/rfc/RFC-0008-dingtalk-notify-patrol.md)。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DeliveryResult } from "
|
|
1
|
+
import { type DeliveryResult } from "../notify-delivery/index.js";
|
|
2
2
|
import type { AppConfig, DeliveryRequest } from "../types.js";
|
|
3
3
|
export declare function validateWebhookConfig(config: AppConfig): {
|
|
4
4
|
valid: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sendWebhook as libSendWebhook, validateWebhookUrl, } from "
|
|
1
|
+
import { sendWebhook as libSendWebhook, validateWebhookUrl, } from "../notify-delivery/index.js";
|
|
2
2
|
export function validateWebhookConfig(config) {
|
|
3
3
|
return validateWebhookUrl(config.webhookUrl);
|
|
4
4
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { redact } from "
|
|
2
|
+
import { redact } from "./notify-delivery/index.js";
|
|
3
3
|
import { runStdioServer } from "./server.js";
|
|
4
4
|
runStdioServer().catch((error) => {
|
|
5
5
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommandRunner, type DeliveryResult } from "
|
|
1
|
+
import { type CommandRunner, type DeliveryResult } from "../notify-delivery/index.js";
|
|
2
2
|
import type { AppConfig, DeliveryRequest } from "../types.js";
|
|
3
3
|
export type { CommandRunner };
|
|
4
4
|
export declare function sendRobotMessage(config: AppConfig, request: DeliveryRequest, runner?: CommandRunner, env?: NodeJS.ProcessEnv): Promise<DeliveryResult>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { sendRobotMessage as libSendRobotMessage, } from "
|
|
2
|
+
import { sendRobotMessage as libSendRobotMessage, } from "../notify-delivery/index.js";
|
|
3
3
|
export async function sendRobotMessage(config, request, runner, env = process.env) {
|
|
4
4
|
if (!config.clientId || !config.clientSecret || !config.robotCode) {
|
|
5
5
|
return {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
-
import { redact } from "
|
|
8
|
+
import { redact } from "../notify-delivery/index.js";
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
const USER_DWS_ENV = [
|
|
11
11
|
"HOME", "USERPROFILE", "PATH", "USER", "LOGNAME", "TMPDIR", "TEMP", "TMP",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DeliveryMessage, DeliveryResult } from "./result.js";
|
|
2
|
+
export type CommandRunner = (file: string, args: readonly string[], options: {
|
|
3
|
+
timeout: number;
|
|
4
|
+
maxBuffer: number;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
windowsHide: boolean;
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function robotDwsEnv(configDir: string, clientId: string, clientSecret: string, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
12
|
+
export declare function normalizeRobotResult(value: unknown): DeliveryResult;
|
|
13
|
+
export interface RobotSendOptions {
|
|
14
|
+
dwsCommand: string;
|
|
15
|
+
clientId: string;
|
|
16
|
+
clientSecret: string;
|
|
17
|
+
robotCode: string;
|
|
18
|
+
configDir: string;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
maxOutputBytes: number;
|
|
21
|
+
runner?: CommandRunner;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
}
|
|
24
|
+
export declare function sendRobotMessage(options: RobotSendOptions, message: DeliveryMessage): Promise<DeliveryResult>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { redact } from "./redact.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const GROUP_PATH = "/v1.0/robot/groupMessages/send";
|
|
6
|
+
const USER_PATH = "/v1.0/robot/oToMessages/batchSend";
|
|
7
|
+
const USER_DWS_ENV = [
|
|
8
|
+
"HOME", "USERPROFILE", "PATH", "USER", "LOGNAME", "TMPDIR", "TEMP", "TMP",
|
|
9
|
+
"XDG_CONFIG_HOME", "XDG_DATA_HOME", "BROWSER", "DISPLAY", "WAYLAND_DISPLAY",
|
|
10
|
+
"NODE_EXTRA_CA_CERTS", "DWS_CONFIG_DIR",
|
|
11
|
+
];
|
|
12
|
+
function pickEnv(names, env) {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const name of names) {
|
|
15
|
+
if (env[name] !== undefined)
|
|
16
|
+
result[name] = env[name];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
export function robotDwsEnv(configDir, clientId, clientSecret, env = process.env) {
|
|
21
|
+
return {
|
|
22
|
+
...pickEnv(USER_DWS_ENV, env),
|
|
23
|
+
DWS_CONFIG_DIR: configDir,
|
|
24
|
+
DWS_CLIENT_ID: clientId,
|
|
25
|
+
DWS_CLIENT_SECRET: clientSecret,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function nested(root) {
|
|
29
|
+
const values = [root];
|
|
30
|
+
for (const key of ["result", "body", "payload", "response", "data"]) {
|
|
31
|
+
const candidate = root[key];
|
|
32
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
33
|
+
values.push(candidate);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
38
|
+
function stringField(records, keys) {
|
|
39
|
+
for (const record of records) {
|
|
40
|
+
for (const key of keys) {
|
|
41
|
+
if (typeof record[key] === "string" && record[key])
|
|
42
|
+
return record[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function parseJsonObject(stdout) {
|
|
48
|
+
const trimmed = stdout.trim();
|
|
49
|
+
if (!trimmed)
|
|
50
|
+
throw new Error("Command produced no JSON output");
|
|
51
|
+
const value = JSON.parse(trimmed);
|
|
52
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
53
|
+
throw new Error("Command returned non-object JSON");
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
export function normalizeRobotResult(value) {
|
|
58
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
59
|
+
return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Non-object response" };
|
|
60
|
+
}
|
|
61
|
+
const root = value;
|
|
62
|
+
const records = nested(root);
|
|
63
|
+
const errorRecord = root.error && typeof root.error === "object" ? root.error : undefined;
|
|
64
|
+
const error = typeof errorRecord?.message === "string" ? errorRecord.message : undefined;
|
|
65
|
+
const explicitSuccess = records.find((item) => typeof item.success === "boolean")?.success;
|
|
66
|
+
if (explicitSuccess === false || error) {
|
|
67
|
+
return { status: "failed", errorCode: "ADAPTER_REJECTED", error: error ?? "Request rejected" };
|
|
68
|
+
}
|
|
69
|
+
const messageId = stringField(records, ["processQueryKey", "messageId", "taskId", "id"]);
|
|
70
|
+
if (explicitSuccess === true || messageId) {
|
|
71
|
+
return { status: "sent", messageId };
|
|
72
|
+
}
|
|
73
|
+
return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Delivery was not confirmed" };
|
|
74
|
+
}
|
|
75
|
+
export async function sendRobotMessage(options, message) {
|
|
76
|
+
if (!options.clientId || !options.clientSecret || !options.robotCode) {
|
|
77
|
+
return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Robot application credentials are required" };
|
|
78
|
+
}
|
|
79
|
+
const apiPath = message.routeType === "group" ? GROUP_PATH : USER_PATH;
|
|
80
|
+
const payload = {
|
|
81
|
+
robotCode: options.robotCode,
|
|
82
|
+
msgKey: "sampleMarkdown",
|
|
83
|
+
msgParam: JSON.stringify({ title: message.title, text: message.content }),
|
|
84
|
+
};
|
|
85
|
+
if (message.routeType === "group")
|
|
86
|
+
payload.openConversationId = message.targetId;
|
|
87
|
+
else
|
|
88
|
+
payload.userIds = [message.targetId];
|
|
89
|
+
const runner = options.runner ?? execFileAsync;
|
|
90
|
+
const env = robotDwsEnv(options.configDir, options.clientId, options.clientSecret, options.env);
|
|
91
|
+
try {
|
|
92
|
+
const { stdout } = await runner(options.dwsCommand, ["api", "POST", apiPath, "--data", JSON.stringify(payload), "--format", "json", "--yes"], {
|
|
93
|
+
timeout: options.timeoutMs,
|
|
94
|
+
maxBuffer: options.maxOutputBytes,
|
|
95
|
+
env,
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
});
|
|
98
|
+
return normalizeRobotResult(parseJsonObject(stdout));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
102
|
+
if (/missing app credentials|APP_CREDENTIALS|client.?secret/i.test(text)) {
|
|
103
|
+
return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Application credentials are required" };
|
|
104
|
+
}
|
|
105
|
+
if (/timed out|ETIMEDOUT|abort/i.test(text)) {
|
|
106
|
+
return {
|
|
107
|
+
status: "unknown",
|
|
108
|
+
errorCode: "DELIVERY_UNKNOWN",
|
|
109
|
+
reason: "Robot delivery timed out; delivery status is unknown",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (/ENOENT|ECONNREFUSED/i.test(text)) {
|
|
113
|
+
return { status: "failed", errorCode: "ADAPTER_UNAVAILABLE", error: "dws is unavailable" };
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const stdout = error.stdout;
|
|
117
|
+
if (stdout)
|
|
118
|
+
return normalizeRobotResult(parseJsonObject(stdout));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// fall through
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
status: "failed",
|
|
125
|
+
errorCode: "DELIVERY_FAILED",
|
|
126
|
+
error: redact(text) || "Robot delivery failed",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { DeliveryResult, DeliveryMessage, RouteType } from "./result.js";
|
|
2
|
+
export { normalizeWebhookResult, resolveWebhookUrl, sendWebhook, validateWebhookUrl, type WebhookSendOptions, } from "./webhook.js";
|
|
3
|
+
export { normalizeRobotResult, robotDwsEnv, sendRobotMessage, type CommandRunner, type RobotSendOptions, } from "./dws-robot.js";
|
|
4
|
+
export { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "./patrol-message.js";
|
|
5
|
+
export { redact } from "./redact.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { normalizeWebhookResult, resolveWebhookUrl, sendWebhook, validateWebhookUrl, } from "./webhook.js";
|
|
2
|
+
export { normalizeRobotResult, robotDwsEnv, sendRobotMessage, } from "./dws-robot.js";
|
|
3
|
+
export { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "./patrol-message.js";
|
|
4
|
+
export { redact } from "./redact.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Shared default DingTalk markdown shell for patrol (CLI + MCP setup). */
|
|
2
|
+
export declare const DEFAULT_PATROL_MESSAGE_TITLE = "[{{status}}] {{name}}";
|
|
3
|
+
export declare const DEFAULT_PATROL_MESSAGE_BODY = "### {{name}} \u00B7 {{status}}\n\n{{summary}}\n\n---\n\u4E3B\u673A\uFF1A{{hostname}}\n\u65F6\u95F4\uFF1A{{timestamp}}\n";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Shared default DingTalk markdown shell for patrol (CLI + MCP setup). */
|
|
2
|
+
export const DEFAULT_PATROL_MESSAGE_TITLE = "[{{status}}] {{name}}";
|
|
3
|
+
export const DEFAULT_PATROL_MESSAGE_BODY = `### {{name}} · {{status}}
|
|
4
|
+
|
|
5
|
+
{{summary}}
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
主机:{{hostname}}
|
|
9
|
+
时间:{{timestamp}}
|
|
10
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function redact(value: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [
|
|
2
|
+
/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi,
|
|
3
|
+
/access_token=[^&\s]+/gi,
|
|
4
|
+
/client[_-]?secret["'\s:=]+[^\s"',}]+/gi,
|
|
5
|
+
];
|
|
6
|
+
export function redact(value) {
|
|
7
|
+
let result = value;
|
|
8
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
9
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Discriminated delivery outcome. failed = safe to retry; unknown = do not auto-retry. */
|
|
2
|
+
export type DeliveryResult = {
|
|
3
|
+
status: "sent";
|
|
4
|
+
messageId?: string;
|
|
5
|
+
} | {
|
|
6
|
+
status: "failed";
|
|
7
|
+
error: string;
|
|
8
|
+
errorCode?: string;
|
|
9
|
+
} | {
|
|
10
|
+
status: "unknown";
|
|
11
|
+
reason: string;
|
|
12
|
+
errorCode?: string;
|
|
13
|
+
};
|
|
14
|
+
export type RouteType = "group" | "user";
|
|
15
|
+
export interface DeliveryMessage {
|
|
16
|
+
routeType: RouteType;
|
|
17
|
+
targetId: string;
|
|
18
|
+
title: string;
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DeliveryMessage, DeliveryResult } from "./result.js";
|
|
2
|
+
export declare function resolveWebhookUrl(value: string): URL | undefined;
|
|
3
|
+
export declare function validateWebhookUrl(webhookUrl: string | undefined): {
|
|
4
|
+
valid: boolean;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
export interface WebhookSendOptions {
|
|
8
|
+
webhookUrl: string;
|
|
9
|
+
webhookSecret?: string;
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
}
|
|
13
|
+
export declare function sendWebhook(options: WebhookSendOptions, message: DeliveryMessage): Promise<DeliveryResult>;
|
|
14
|
+
export declare function normalizeWebhookResult(httpStatus: number, value: unknown): DeliveryResult;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { redact } from "./redact.js";
|
|
3
|
+
const WEBHOOK_HOST = "oapi.dingtalk.com";
|
|
4
|
+
const WEBHOOK_PATH = "/robot/send";
|
|
5
|
+
export function resolveWebhookUrl(value) {
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (url.protocol !== "https:"
|
|
9
|
+
|| url.hostname !== WEBHOOK_HOST
|
|
10
|
+
|| url.pathname !== WEBHOOK_PATH
|
|
11
|
+
|| !url.searchParams.get("access_token")
|
|
12
|
+
|| url.username
|
|
13
|
+
|| url.password
|
|
14
|
+
|| url.hash) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function signedUrl(url, secret, timestamp) {
|
|
24
|
+
const result = new URL(url);
|
|
25
|
+
const timestampValue = String(timestamp);
|
|
26
|
+
const signature = createHmac("sha256", secret)
|
|
27
|
+
.update(`${timestampValue}\n${secret}`)
|
|
28
|
+
.digest("base64");
|
|
29
|
+
result.searchParams.set("timestamp", timestampValue);
|
|
30
|
+
result.searchParams.set("sign", signature);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
export function validateWebhookUrl(webhookUrl) {
|
|
34
|
+
if (!webhookUrl)
|
|
35
|
+
return { valid: false, message: "DINGTALK_WEBHOOK_URL is not set" };
|
|
36
|
+
if (!resolveWebhookUrl(webhookUrl)) {
|
|
37
|
+
return { valid: false, message: "DINGTALK_WEBHOOK_URL must be an https://oapi.dingtalk.com/robot/send URL" };
|
|
38
|
+
}
|
|
39
|
+
return { valid: true, message: "Webhook URL is valid" };
|
|
40
|
+
}
|
|
41
|
+
export async function sendWebhook(options, message) {
|
|
42
|
+
if (message.routeType !== "group") {
|
|
43
|
+
return { status: "failed", errorCode: "ADAPTER_UNSUPPORTED", error: "Webhook only supports group routes" };
|
|
44
|
+
}
|
|
45
|
+
const base = resolveWebhookUrl(options.webhookUrl);
|
|
46
|
+
if (!base) {
|
|
47
|
+
return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Webhook URL is invalid" };
|
|
48
|
+
}
|
|
49
|
+
const url = options.webhookSecret
|
|
50
|
+
? signedUrl(base, options.webhookSecret, Date.now())
|
|
51
|
+
: base;
|
|
52
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetchImpl(url, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
msgtype: "markdown",
|
|
59
|
+
markdown: { title: message.title, text: message.content },
|
|
60
|
+
}),
|
|
61
|
+
signal: AbortSignal.timeout(options.timeoutMs),
|
|
62
|
+
});
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
let payload;
|
|
65
|
+
try {
|
|
66
|
+
payload = JSON.parse(text);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
if (response.status === 401 || response.status === 403) {
|
|
70
|
+
return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Webhook authorization failed" };
|
|
71
|
+
}
|
|
72
|
+
return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Webhook returned non-JSON" };
|
|
73
|
+
}
|
|
74
|
+
return normalizeWebhookResult(response.status, payload);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (isTimeout(error)) {
|
|
78
|
+
return {
|
|
79
|
+
status: "unknown",
|
|
80
|
+
errorCode: "DELIVERY_UNKNOWN",
|
|
81
|
+
reason: "Webhook delivery timed out; delivery status is unknown",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
status: "failed",
|
|
86
|
+
errorCode: "ADAPTER_UNAVAILABLE",
|
|
87
|
+
error: error instanceof Error ? redact(error.message) : "Webhook request failed",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isTimeout(error) {
|
|
92
|
+
const name = error && typeof error === "object" && "name" in error ? String(error.name) : "";
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
return name === "TimeoutError" || name === "AbortError" || /aborted due to timeout|The operation was aborted/i.test(message);
|
|
95
|
+
}
|
|
96
|
+
function isRecord(value) {
|
|
97
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
98
|
+
}
|
|
99
|
+
export function normalizeWebhookResult(httpStatus, value) {
|
|
100
|
+
if (httpStatus === 401 || httpStatus === 403) {
|
|
101
|
+
return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Webhook authorization failed" };
|
|
102
|
+
}
|
|
103
|
+
if (!isRecord(value)) {
|
|
104
|
+
return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Webhook returned a non-object response" };
|
|
105
|
+
}
|
|
106
|
+
const errcode = typeof value.errcode === "number" ? value.errcode : undefined;
|
|
107
|
+
const errmsg = typeof value.errmsg === "string" ? redact(value.errmsg) : undefined;
|
|
108
|
+
if (errcode === 310000 || /sign not match|check sign|invalid sign|缺少timestamp|timestamp is expired|加签/i.test(errmsg ?? "")) {
|
|
109
|
+
return {
|
|
110
|
+
status: "failed",
|
|
111
|
+
errorCode: "ADAPTER_UNAUTHORIZED",
|
|
112
|
+
error: "Webhook signing failed; set DINGTALK_WEBHOOK_SECRET for a signed robot",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (httpStatus < 200 || httpStatus >= 300 || (errcode !== undefined && errcode !== 0)) {
|
|
116
|
+
return {
|
|
117
|
+
status: "failed",
|
|
118
|
+
errorCode: "ADAPTER_REJECTED",
|
|
119
|
+
error: errmsg ? `Webhook rejected: ${errmsg}` : "Webhook rejected the request",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (errcode === 0) {
|
|
123
|
+
return { status: "sent" };
|
|
124
|
+
}
|
|
125
|
+
return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Webhook response did not confirm delivery" };
|
|
126
|
+
}
|
package/dist/src/patrol/setup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "
|
|
4
|
+
import { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "../notify-delivery/index.js";
|
|
5
5
|
import { DingtalkDwsError } from "../errors.js";
|
|
6
6
|
import { isRouteConfigured, readTargets } from "../notify/bind.js";
|
|
7
7
|
import { mcpPackageRoot, readPackageVersion } from "../version.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dingtalk-dws-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "DingTalk robot notification MCP with dws readiness handshake",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"notification"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"agent-compose-notify-delivery": "^1.0.0",
|
|
37
36
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|