@insightsuen/dingdoc 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.
- package/README.md +200 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +178 -0
- package/dist/auth.js.map +1 -0
- package/dist/browser.d.ts +10 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +192 -0
- package/dist/browser.js.map +1 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +68 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +788 -0
- package/dist/index.js.map +1 -0
- package/dist/stream.d.ts +26 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +114 -0
- package/dist/stream.js.map +1 -0
- package/dist/tools/get-document.d.ts +3 -0
- package/dist/tools/get-document.d.ts.map +1 -0
- package/dist/tools/get-document.js +117 -0
- package/dist/tools/get-document.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/list-nodes.d.ts +3 -0
- package/dist/tools/list-nodes.d.ts.map +1 -0
- package/dist/tools/list-nodes.js +73 -0
- package/dist/tools/list-nodes.js.map +1 -0
- package/dist/tools/list-spaces.d.ts +3 -0
- package/dist/tools/list-spaces.d.ts.map +1 -0
- package/dist/tools/list-spaces.js +31 -0
- package/dist/tools/list-spaces.js.map +1 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../packages/shared/src/server.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
|
|
6
|
+
// ../../packages/shared/src/logger.ts
|
|
7
|
+
var LEVELS = {
|
|
8
|
+
debug: 0,
|
|
9
|
+
info: 1,
|
|
10
|
+
warn: 2,
|
|
11
|
+
error: 3
|
|
12
|
+
};
|
|
13
|
+
var currentLevel = process.env["LOG_LEVEL"] ?? "info";
|
|
14
|
+
function log(level, message, meta) {
|
|
15
|
+
if (LEVELS[level] < LEVELS[currentLevel]) return;
|
|
16
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
17
|
+
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
18
|
+
const output = meta !== void 0 ? `${prefix} ${message} ${JSON.stringify(meta)}` : `${prefix} ${message}`;
|
|
19
|
+
process.stderr.write(output + "\n");
|
|
20
|
+
}
|
|
21
|
+
var logger = {
|
|
22
|
+
debug: (message, meta) => log("debug", message, meta),
|
|
23
|
+
info: (message, meta) => log("info", message, meta),
|
|
24
|
+
warn: (message, meta) => log("warn", message, meta),
|
|
25
|
+
error: (message, meta) => log("error", message, meta)
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ../../packages/shared/src/server.ts
|
|
29
|
+
function createServer(config) {
|
|
30
|
+
logger.info(`Creating MCP server: ${config.name} v${config.version}`);
|
|
31
|
+
return new McpServer({
|
|
32
|
+
name: config.name,
|
|
33
|
+
version: config.version
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ../../packages/shared/src/transport.ts
|
|
38
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
39
|
+
async function launch(server2, options) {
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const useHttp = options?.transport === "http" || args.includes("--http");
|
|
42
|
+
if (useHttp) {
|
|
43
|
+
await launchHttp(server2, options);
|
|
44
|
+
} else {
|
|
45
|
+
await launchStdio(server2);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function launchStdio(server2) {
|
|
49
|
+
logger.info("Starting MCP server in stdio mode");
|
|
50
|
+
const transport = new StdioServerTransport();
|
|
51
|
+
await server2.connect(transport);
|
|
52
|
+
logger.info("MCP server connected via stdio");
|
|
53
|
+
}
|
|
54
|
+
async function launchHttp(server2, options) {
|
|
55
|
+
const { createServer: createServer2 } = await import("node:http");
|
|
56
|
+
const { randomUUID } = await import("node:crypto");
|
|
57
|
+
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
58
|
+
const port = options?.port ?? parseInt(process.env["PORT"] ?? "3000", 10);
|
|
59
|
+
const host = options?.host ?? process.env["HOST"] ?? "localhost";
|
|
60
|
+
const transport = new StreamableHTTPServerTransport({
|
|
61
|
+
sessionIdGenerator: () => randomUUID()
|
|
62
|
+
});
|
|
63
|
+
await server2.connect(transport);
|
|
64
|
+
const httpServer = createServer2((req, res) => {
|
|
65
|
+
transport.handleRequest(req, res);
|
|
66
|
+
});
|
|
67
|
+
httpServer.listen(port, host, () => {
|
|
68
|
+
logger.info(`MCP server listening on http://${host}:${port}`);
|
|
69
|
+
});
|
|
70
|
+
process.on("SIGINT", () => {
|
|
71
|
+
logger.info("Shutting down MCP server...");
|
|
72
|
+
httpServer.close();
|
|
73
|
+
process.exit(0);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ../../packages/shared/src/errors.ts
|
|
78
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
79
|
+
function toToolError(err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
return {
|
|
82
|
+
isError: true,
|
|
83
|
+
content: [{ type: "text", text: message }]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ../../packages/shared/src/index.ts
|
|
88
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
89
|
+
|
|
90
|
+
// src/auth.ts
|
|
91
|
+
var OAUTH_URL = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
|
|
92
|
+
var USER_GET_URL = "https://oapi.dingtalk.com/topapi/v2/user/get";
|
|
93
|
+
var LEAD_TIME_MS = 10 * 60 * 1e3;
|
|
94
|
+
var cachedToken = null;
|
|
95
|
+
var tokenExpiresAt = 0;
|
|
96
|
+
var cachedOperatorId = null;
|
|
97
|
+
function parseCliArgs() {
|
|
98
|
+
const map = /* @__PURE__ */ new Map();
|
|
99
|
+
for (const arg of process.argv.slice(2)) {
|
|
100
|
+
if (arg.startsWith("--")) {
|
|
101
|
+
const eqIdx = arg.indexOf("=");
|
|
102
|
+
if (eqIdx > 2) {
|
|
103
|
+
const key = arg.slice(2, eqIdx);
|
|
104
|
+
const val = arg.slice(eqIdx + 1);
|
|
105
|
+
map.set(key, val);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return map;
|
|
110
|
+
}
|
|
111
|
+
var CLI_ARGS = parseCliArgs();
|
|
112
|
+
function resolveCredential(argKey, defaultEnvKey) {
|
|
113
|
+
const direct = CLI_ARGS.get(argKey);
|
|
114
|
+
if (direct) return direct;
|
|
115
|
+
const customEnvName = CLI_ARGS.get(`${argKey}-env`);
|
|
116
|
+
if (customEnvName) {
|
|
117
|
+
const val = process.env[customEnvName];
|
|
118
|
+
if (val) return val;
|
|
119
|
+
logger.warn(`--${argKey}-env \u6307\u5B9A\u4E86 ${customEnvName}\uFF0C\u4F46\u8BE5\u73AF\u5883\u53D8\u91CF\u672A\u8BBE\u7F6E`);
|
|
120
|
+
}
|
|
121
|
+
return process.env[defaultEnvKey];
|
|
122
|
+
}
|
|
123
|
+
async function getAccessToken() {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
if (cachedToken && now < tokenExpiresAt) {
|
|
126
|
+
return cachedToken;
|
|
127
|
+
}
|
|
128
|
+
const appKey = resolveCredential("app-key", "DINGTALK_APP_KEY");
|
|
129
|
+
const appSecret = resolveCredential("app-secret", "DINGTALK_APP_SECRET");
|
|
130
|
+
if (!appKey || !appSecret) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
[
|
|
133
|
+
"\u7F3A\u5C11\u9489\u9489\u5E94\u7528\u51ED\u8BC1\uFF0C\u8BF7\u901A\u8FC7\u4EE5\u4E0B\u4EFB\u4E00\u65B9\u5F0F\u63D0\u4F9B\uFF1A",
|
|
134
|
+
" \u2022 \u73AF\u5883\u53D8\u91CF\uFF1ADINGTALK_APP_KEY / DINGTALK_APP_SECRET",
|
|
135
|
+
" \u2022 CLI \u53C2\u6570\uFF1A--app-key=xxx --app-secret=xxx",
|
|
136
|
+
" \u2022 \u81EA\u5B9A\u4E49 env \u540D\uFF1A--app-key-env=MY_VAR --app-secret-env=MY_VAR",
|
|
137
|
+
" AppKey/AppSecret \u53EF\u5728\u9489\u9489\u5F00\u53D1\u8005\u540E\u53F0\u300C\u51ED\u8BC1\u4E0E\u57FA\u7840\u4FE1\u606F\u300D\u9875\u9762\u83B7\u53D6\u3002"
|
|
138
|
+
].join("\n")
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
logger.debug("Refreshing DingTalk access token...");
|
|
142
|
+
const resp = await fetch(OAUTH_URL, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify({ appKey, appSecret, grantType: "client_credentials" })
|
|
146
|
+
});
|
|
147
|
+
if (!resp.ok) {
|
|
148
|
+
throw new Error(`\u83B7\u53D6 access token \u5931\u8D25\uFF1AHTTP ${resp.status}`);
|
|
149
|
+
}
|
|
150
|
+
const data = await resp.json();
|
|
151
|
+
cachedToken = data.accessToken;
|
|
152
|
+
tokenExpiresAt = now + data.expireIn * 1e3 - LEAD_TIME_MS;
|
|
153
|
+
logger.debug("DingTalk access token refreshed");
|
|
154
|
+
return cachedToken;
|
|
155
|
+
}
|
|
156
|
+
async function fetchUnionId(userId) {
|
|
157
|
+
const token = await getAccessToken();
|
|
158
|
+
const resp = await fetch(`${USER_GET_URL}?access_token=${token}`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify({ userid: userId })
|
|
162
|
+
});
|
|
163
|
+
if (!resp.ok) {
|
|
164
|
+
throw new Error(`\u83B7\u53D6 unionId \u5931\u8D25\uFF1AHTTP ${resp.status}`);
|
|
165
|
+
}
|
|
166
|
+
const data = await resp.json();
|
|
167
|
+
if (data.errcode !== 0 || !data.result?.unionid) {
|
|
168
|
+
throw new Error(`\u83B7\u53D6 unionId \u5931\u8D25\uFF1A${data.errmsg}\uFF08errcode: ${data.errcode}\uFF09`);
|
|
169
|
+
}
|
|
170
|
+
return data.result.unionid;
|
|
171
|
+
}
|
|
172
|
+
async function getOperatorId() {
|
|
173
|
+
if (cachedOperatorId) return cachedOperatorId;
|
|
174
|
+
const unionId = resolveCredential("union-id", "DINGTALK_UNION_ID");
|
|
175
|
+
if (unionId) {
|
|
176
|
+
cachedOperatorId = unionId;
|
|
177
|
+
return cachedOperatorId;
|
|
178
|
+
}
|
|
179
|
+
const userId = resolveCredential("user-id", "DINGTALK_USER_ID");
|
|
180
|
+
if (userId) {
|
|
181
|
+
logger.info(
|
|
182
|
+
[
|
|
183
|
+
`\u68C0\u6D4B\u5230 DINGTALK_USER_ID\uFF0C\u6B63\u5728\u67E5\u8BE2\u5BF9\u5E94\u7684 unionId...`,
|
|
184
|
+
` \u63D0\u793A\uFF1A\u4F60\u4E5F\u53EF\u4EE5\u76F4\u63A5\u8BBE\u7F6E DINGTALK_UNION_ID \u8DF3\u8FC7\u6B64\u6B65\u9AA4\uFF0C`,
|
|
185
|
+
` unionId \u53EF\u4ECE\u9489\u9489\u5F00\u53D1\u8005\u540E\u53F0\u300C\u7528\u6237\u7BA1\u7406\u300D\u6216\u9996\u6B21\u8FD0\u884C\u65E5\u5FD7\u4E2D\u83B7\u53D6\u3002`
|
|
186
|
+
].join("\n")
|
|
187
|
+
);
|
|
188
|
+
const resolved = await fetchUnionId(userId);
|
|
189
|
+
logger.info(` \u2713 unionId \u89E3\u6790\u6210\u529F\uFF1A${resolved}`);
|
|
190
|
+
logger.info(` \u5EFA\u8BAE\u5C06 DINGTALK_UNION_ID=${resolved} \u5199\u5165\u73AF\u5883\u53D8\u91CF\u4EE5\u52A0\u901F\u540E\u7EED\u542F\u52A8\u3002`);
|
|
191
|
+
cachedOperatorId = resolved;
|
|
192
|
+
return cachedOperatorId;
|
|
193
|
+
}
|
|
194
|
+
throw new Error(
|
|
195
|
+
[
|
|
196
|
+
"\u7F3A\u5C11\u64CD\u4F5C\u4EBA\u8EAB\u4EFD\uFF08unionId\uFF09\uFF0C\u8BF7\u901A\u8FC7\u4EE5\u4E0B\u4EFB\u4E00\u65B9\u5F0F\u63D0\u4F9B\uFF1A",
|
|
197
|
+
" \u2022 \u73AF\u5883\u53D8\u91CF\uFF08\u63A8\u8350\uFF09\uFF1ADINGTALK_UNION_ID=xxx",
|
|
198
|
+
" \u2022 \u73AF\u5883\u53D8\u91CF\uFF08\u81EA\u52A8\u89E3\u6790\uFF09\uFF1ADINGTALK_USER_ID=xxx \uFF08\u5C06\u81EA\u52A8\u67E5\u8BE2 unionId\uFF09",
|
|
199
|
+
" \u2022 CLI \u53C2\u6570\uFF1A--union-id=xxx \u6216 --user-id=xxx",
|
|
200
|
+
" \u2022 \u81EA\u5B9A\u4E49 env \u540D\uFF1A--union-id-env=MY_VAR",
|
|
201
|
+
"",
|
|
202
|
+
" unionId \u83B7\u53D6\u65B9\u5F0F\uFF1A",
|
|
203
|
+
" 1. \u9489\u9489\u5F00\u53D1\u8005\u540E\u53F0 \u2192 \u7528\u6237\u7BA1\u7406 \u2192 \u70B9\u51FB\u7528\u6237 \u2192 \u67E5\u770B unionId",
|
|
204
|
+
" 2. \u6216\u5148\u8BBE\u7F6E DINGTALK_USER_ID\uFF08\u5982\u5DE5\u53F7 BG012345\uFF09\uFF0C\u670D\u52A1\u542F\u52A8\u65F6\u4F1A\u81EA\u52A8\u663E\u793A unionId"
|
|
205
|
+
].join("\n")
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
function getAppCredentials() {
|
|
209
|
+
return {
|
|
210
|
+
appKey: resolveCredential("app-key", "DINGTALK_APP_KEY"),
|
|
211
|
+
appSecret: resolveCredential("app-secret", "DINGTALK_APP_SECRET")
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function isApiConfigured() {
|
|
215
|
+
const appKey = resolveCredential("app-key", "DINGTALK_APP_KEY");
|
|
216
|
+
const appSecret = resolveCredential("app-secret", "DINGTALK_APP_SECRET");
|
|
217
|
+
const hasUser = !!resolveCredential("union-id", "DINGTALK_UNION_ID") || !!resolveCredential("user-id", "DINGTALK_USER_ID");
|
|
218
|
+
return !!(appKey && appSecret && hasUser);
|
|
219
|
+
}
|
|
220
|
+
var _apiAvailable = false;
|
|
221
|
+
function setApiAvailable(val) {
|
|
222
|
+
_apiAvailable = val;
|
|
223
|
+
}
|
|
224
|
+
function isApiAvailable() {
|
|
225
|
+
return _apiAvailable;
|
|
226
|
+
}
|
|
227
|
+
async function validateCredentials() {
|
|
228
|
+
logger.info("\u6B63\u5728\u9A8C\u8BC1\u9489\u9489\u51ED\u8BC1...");
|
|
229
|
+
try {
|
|
230
|
+
await getAccessToken();
|
|
231
|
+
await getOperatorId();
|
|
232
|
+
logger.info("\u2713 \u9489\u9489\u51ED\u8BC1\u9A8C\u8BC1\u6210\u529F");
|
|
233
|
+
} catch (err) {
|
|
234
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
235
|
+
throw new Error(`\u9489\u9489\u51ED\u8BC1\u9A8C\u8BC1\u5931\u8D25\uFF1A
|
|
236
|
+
${msg}`, { cause: err });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/client.ts
|
|
241
|
+
var BASE_URL = "https://api.dingtalk.com";
|
|
242
|
+
var DingTalkApiError = class extends Error {
|
|
243
|
+
constructor(status, code, message) {
|
|
244
|
+
super(`[${code}] ${message}`);
|
|
245
|
+
this.status = status;
|
|
246
|
+
this.code = code;
|
|
247
|
+
this.name = "DingTalkApiError";
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
async function dingtalkGet(path2, params = {}) {
|
|
251
|
+
const token = await getAccessToken();
|
|
252
|
+
const operatorId = await getOperatorId();
|
|
253
|
+
const query = new URLSearchParams();
|
|
254
|
+
query.set("operatorId", operatorId);
|
|
255
|
+
for (const [k, v] of Object.entries(params)) {
|
|
256
|
+
query.set(k, String(v));
|
|
257
|
+
}
|
|
258
|
+
const url = `${BASE_URL}${path2}?${query.toString()}`;
|
|
259
|
+
const resp = await fetch(url, {
|
|
260
|
+
headers: { "x-acs-dingtalk-access-token": token }
|
|
261
|
+
});
|
|
262
|
+
if (!resp.ok) {
|
|
263
|
+
let code = "unknown";
|
|
264
|
+
let message = `HTTP ${resp.status}`;
|
|
265
|
+
try {
|
|
266
|
+
const body = await resp.json();
|
|
267
|
+
code = body.code ?? code;
|
|
268
|
+
message = body.message ?? message;
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
throw new DingTalkApiError(resp.status, code, message);
|
|
272
|
+
}
|
|
273
|
+
return resp.json();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/tools/list-spaces.ts
|
|
277
|
+
function registerListSpaces(server2) {
|
|
278
|
+
server2.tool(
|
|
279
|
+
"dingtalk_list_spaces",
|
|
280
|
+
"\u5217\u51FA\u5F53\u524D\u7528\u6237\u6709\u6743\u9650\u7684\u6240\u6709\u9489\u9489\u77E5\u8BC6\u5E93\u7A7A\u95F4\uFF0C\u8FD4\u56DE spaceId\u3001\u540D\u79F0\u3001rootNodeId \u548C\u8BBF\u95EE\u94FE\u63A5",
|
|
281
|
+
{},
|
|
282
|
+
async () => {
|
|
283
|
+
try {
|
|
284
|
+
const spaces = [];
|
|
285
|
+
let nextToken;
|
|
286
|
+
do {
|
|
287
|
+
const params = { maxResults: 30 };
|
|
288
|
+
if (nextToken) params["nextToken"] = nextToken;
|
|
289
|
+
const resp = await dingtalkGet("/v2.0/wiki/workspaces", params);
|
|
290
|
+
spaces.push(...resp.workspaces ?? []);
|
|
291
|
+
nextToken = resp.nextToken;
|
|
292
|
+
} while (nextToken);
|
|
293
|
+
const result = spaces.map((s) => ({
|
|
294
|
+
spaceId: s.workspaceId,
|
|
295
|
+
name: s.name,
|
|
296
|
+
rootNodeId: s.rootNodeId,
|
|
297
|
+
url: s.url ?? null
|
|
298
|
+
}));
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
301
|
+
};
|
|
302
|
+
} catch (err) {
|
|
303
|
+
return toToolError(err);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/tools/list-nodes.ts
|
|
310
|
+
import { z } from "zod";
|
|
311
|
+
async function fetchNodes(parentNodeId) {
|
|
312
|
+
const nodes = [];
|
|
313
|
+
let nextToken;
|
|
314
|
+
do {
|
|
315
|
+
const params = {
|
|
316
|
+
parentNodeId,
|
|
317
|
+
maxResults: 50
|
|
318
|
+
};
|
|
319
|
+
if (nextToken) params["nextToken"] = nextToken;
|
|
320
|
+
const resp = await dingtalkGet("/v2.0/wiki/nodes", params);
|
|
321
|
+
nodes.push(...resp.nodes ?? []);
|
|
322
|
+
nextToken = resp.nextToken;
|
|
323
|
+
} while (nextToken);
|
|
324
|
+
return nodes;
|
|
325
|
+
}
|
|
326
|
+
async function fetchNodesRecursive(parentNodeId) {
|
|
327
|
+
const nodes = await fetchNodes(parentNodeId);
|
|
328
|
+
const results = [];
|
|
329
|
+
for (const node of nodes) {
|
|
330
|
+
const result = {
|
|
331
|
+
nodeId: node.nodeId,
|
|
332
|
+
name: node.name,
|
|
333
|
+
type: node.type,
|
|
334
|
+
category: node.category ?? null,
|
|
335
|
+
url: node.url ?? null,
|
|
336
|
+
hasChildren: node.hasChildren
|
|
337
|
+
};
|
|
338
|
+
if (node.hasChildren && node.type === "FOLDER") {
|
|
339
|
+
result.children = await fetchNodesRecursive(node.nodeId);
|
|
340
|
+
}
|
|
341
|
+
results.push(result);
|
|
342
|
+
}
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
345
|
+
function registerListNodes(server2) {
|
|
346
|
+
server2.tool(
|
|
347
|
+
"dingtalk_list_nodes",
|
|
348
|
+
"\u5217\u51FA\u9489\u9489\u77E5\u8BC6\u5E93\u8282\u70B9\u4E0B\u7684\u5B50\u8282\u70B9\u3002nodeId \u53EF\u4ECE dingtalk_list_spaces \u83B7\u53D6\u7684 rootNodeId \u5F00\u59CB\u3002\u652F\u6301 recursive \u9012\u5F52\u5C55\u5F00\u6574\u4E2A\u76EE\u5F55\u6811\uFF08\u8282\u70B9\u6570\u91CF\u591A\u65F6\u8017\u65F6\u8F83\u957F\uFF09",
|
|
349
|
+
{
|
|
350
|
+
nodeId: z.string().describe("\u7236\u8282\u70B9 ID\uFF08dentryUuid\uFF09\uFF0C\u5982\u77E5\u8BC6\u5E93\u7684 rootNodeId"),
|
|
351
|
+
recursive: z.boolean().optional().default(false).describe("\u662F\u5426\u9012\u5F52\u5C55\u5F00\u6240\u6709\u5B50\u8282\u70B9\uFF0C\u9ED8\u8BA4 false")
|
|
352
|
+
},
|
|
353
|
+
async ({ nodeId, recursive }) => {
|
|
354
|
+
try {
|
|
355
|
+
let result;
|
|
356
|
+
if (recursive) {
|
|
357
|
+
result = await fetchNodesRecursive(nodeId);
|
|
358
|
+
} else {
|
|
359
|
+
const nodes = await fetchNodes(nodeId);
|
|
360
|
+
result = nodes.map((node) => ({
|
|
361
|
+
nodeId: node.nodeId,
|
|
362
|
+
name: node.name,
|
|
363
|
+
type: node.type,
|
|
364
|
+
category: node.category ?? null,
|
|
365
|
+
url: node.url ?? null,
|
|
366
|
+
hasChildren: node.hasChildren
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
371
|
+
};
|
|
372
|
+
} catch (err) {
|
|
373
|
+
return toToolError(err);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/tools/get-document.ts
|
|
380
|
+
import { z as z2 } from "zod";
|
|
381
|
+
|
|
382
|
+
// src/stream.ts
|
|
383
|
+
import { DWClient, EventAck } from "dingtalk-stream";
|
|
384
|
+
var DOC_CONTENT_EXPORT_EVENT = "doc_content_export_result";
|
|
385
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
386
|
+
var pending = /* @__PURE__ */ new Map();
|
|
387
|
+
var recentEvents = /* @__PURE__ */ new Map();
|
|
388
|
+
var RECENT_EVENT_TTL_MS = 6e4;
|
|
389
|
+
var streamClient = null;
|
|
390
|
+
var streamReady = false;
|
|
391
|
+
var docContentCache = /* @__PURE__ */ new Map();
|
|
392
|
+
function initDocStream(appKey, appSecret) {
|
|
393
|
+
if (streamClient) return;
|
|
394
|
+
streamClient = new DWClient({ clientId: appKey, clientSecret: appSecret });
|
|
395
|
+
streamClient.registerAllEventListener((msg) => {
|
|
396
|
+
logger.info(
|
|
397
|
+
`[Stream] \u6536\u5230\u4E8B\u4EF6 type=${msg.type} topic=${msg.headers.topic} eventType=${msg.headers.eventType ?? "(none)"}`
|
|
398
|
+
);
|
|
399
|
+
if (msg.headers.eventType === DOC_CONTENT_EXPORT_EVENT) {
|
|
400
|
+
try {
|
|
401
|
+
logger.info(`[Stream] \u4E8B\u4EF6\u539F\u59CB data: ${msg.data}`);
|
|
402
|
+
const data = JSON.parse(msg.data);
|
|
403
|
+
const taskId = Number(data.taskId);
|
|
404
|
+
const normalized = { ...data, taskId };
|
|
405
|
+
logger.info(`[Stream] \u6587\u6863\u5BFC\u51FA\u4E8B\u4EF6\u89E3\u6790\u5B8C\u6210\uFF0CtaskId=${taskId}\uFF08\u539F\u59CB: ${String(data.taskId)}\uFF09\uFF0Csuccess=${String(data.success)}\uFF0Cpending\u4E2D\u7684key\u5217\u8868: [${[...pending.keys()].join(", ")}]`);
|
|
406
|
+
if (normalized.success && normalized.content && normalized.dentryUuid) {
|
|
407
|
+
cacheDocContent(normalized.dentryUuid, normalized.content);
|
|
408
|
+
}
|
|
409
|
+
const resolver = pending.get(taskId);
|
|
410
|
+
if (resolver) {
|
|
411
|
+
pending.delete(taskId);
|
|
412
|
+
resolver(normalized);
|
|
413
|
+
} else if (normalized.success) {
|
|
414
|
+
logger.info(`[Stream] taskId ${taskId} \u6682\u5B58\u5230\u8FD1\u671F\u4E8B\u4EF6\u7F13\u5B58`);
|
|
415
|
+
recentEvents.set(taskId, normalized);
|
|
416
|
+
setTimeout(() => recentEvents.delete(taskId), RECENT_EVENT_TTL_MS);
|
|
417
|
+
} else {
|
|
418
|
+
logger.warn(`[Stream] taskId ${taskId} \u5BFC\u51FA\u5931\u8D25\u4E14\u65E0\u7B49\u5F85\u8BF7\u6C42\uFF0C\u4E22\u5F03`);
|
|
419
|
+
}
|
|
420
|
+
} catch (err) {
|
|
421
|
+
logger.error("\u89E3\u6790\u6587\u6863\u5BFC\u51FA\u4E8B\u4EF6\u5931\u8D25", err);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return { status: EventAck.SUCCESS };
|
|
425
|
+
});
|
|
426
|
+
streamClient.connect().then(() => {
|
|
427
|
+
streamReady = true;
|
|
428
|
+
logger.info("\u2713 \u9489\u9489 Stream \u8FDE\u63A5\u5DF2\u5EFA\u7ACB\uFF0C\u5F00\u59CB\u76D1\u542C\u6587\u6863\u5BFC\u51FA\u4E8B\u4EF6");
|
|
429
|
+
}).catch((err) => {
|
|
430
|
+
logger.error("\u9489\u9489 Stream \u8FDE\u63A5\u5931\u8D25\uFF0Cget_document \u5DE5\u5177\u5C06\u65E0\u6CD5\u83B7\u53D6\u6587\u6863\u5185\u5BB9", err);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function waitForDocExport(taskId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
434
|
+
if (!streamClient) {
|
|
435
|
+
return Promise.reject(
|
|
436
|
+
new Error(
|
|
437
|
+
"DingTalk Stream \u5BA2\u6237\u7AEF\u672A\u521D\u59CB\u5316\uFF0C\u65E0\u6CD5\u63A5\u6536\u6587\u6863\u5BFC\u51FA\u7ED3\u679C\u3002\u8BF7\u68C0\u67E5\u670D\u52A1\u542F\u52A8\u65E5\u5FD7\u3002"
|
|
438
|
+
)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const cached = recentEvents.get(taskId);
|
|
442
|
+
if (cached) {
|
|
443
|
+
logger.info(`[Stream] taskId ${taskId} \u547D\u4E2D\u8FD1\u671F\u4E8B\u4EF6\u7F13\u5B58\uFF0C\u76F4\u63A5\u8FD4\u56DE`);
|
|
444
|
+
recentEvents.delete(taskId);
|
|
445
|
+
return Promise.resolve(cached);
|
|
446
|
+
}
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
const timer = setTimeout(() => {
|
|
449
|
+
pending.delete(taskId);
|
|
450
|
+
reject(
|
|
451
|
+
new Error(
|
|
452
|
+
`\u6587\u6863\u5BFC\u51FA\u8D85\u65F6\uFF08taskId: ${taskId}\uFF0C\u7B49\u5F85 ${timeoutMs / 1e3}s\uFF09\u3002
|
|
453
|
+
\u53EF\u80FD\u539F\u56E0\uFF1AStream \u8FDE\u63A5\u65AD\u5F00\u3001\u5BFC\u51FA\u670D\u52A1\u7E41\u5FD9\uFF0C\u6216\u6587\u6863\u4E0D\u652F\u6301\u5BFC\u51FA\u3002`
|
|
454
|
+
)
|
|
455
|
+
);
|
|
456
|
+
}, timeoutMs);
|
|
457
|
+
pending.set(taskId, (result) => {
|
|
458
|
+
clearTimeout(timer);
|
|
459
|
+
resolve(result);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function isStreamReady() {
|
|
464
|
+
return streamReady;
|
|
465
|
+
}
|
|
466
|
+
function getCachedDocContent(nodeId) {
|
|
467
|
+
return docContentCache.get(nodeId);
|
|
468
|
+
}
|
|
469
|
+
function cacheDocContent(nodeId, content) {
|
|
470
|
+
docContentCache.set(nodeId, content);
|
|
471
|
+
logger.info(`[Cache] \u6587\u6863\u5185\u5BB9\u5DF2\u7F13\u5B58\uFF0CnodeId: ${nodeId}\uFF0C\u957F\u5EA6: ${content.length} \u5B57\u7B26`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/browser.ts
|
|
475
|
+
import { chromium } from "playwright";
|
|
476
|
+
import path from "path";
|
|
477
|
+
import os from "os";
|
|
478
|
+
import TurndownService from "turndown";
|
|
479
|
+
var USER_DATA_DIR = path.join(os.homedir(), ".insight-mcp", "dingdoc-browser");
|
|
480
|
+
var DOC_BASE_URL = "https://alidocs.dingtalk.com/i/nodes";
|
|
481
|
+
var CONTENT_SELECTORS = [
|
|
482
|
+
".ne-viewer-body",
|
|
483
|
+
".ne-doc-content",
|
|
484
|
+
".ne-engine-body",
|
|
485
|
+
".ne-doc",
|
|
486
|
+
"[class*='viewer-body']",
|
|
487
|
+
"[class*='docContent']",
|
|
488
|
+
"[class*='doc-content']",
|
|
489
|
+
"[class*='editor-body']",
|
|
490
|
+
"[class*='content-body']",
|
|
491
|
+
".wiki-doc-content",
|
|
492
|
+
"article"
|
|
493
|
+
];
|
|
494
|
+
function isLoginPage(url) {
|
|
495
|
+
return url.includes("/login") || url.includes("/sso") || url.includes("oauth") || url.includes("dingtalk.com/o/") || url.includes("account.dingtalk.com");
|
|
496
|
+
}
|
|
497
|
+
var browserContext = null;
|
|
498
|
+
var browserContextHeadless = true;
|
|
499
|
+
async function getContext(headless) {
|
|
500
|
+
if (browserContext && browserContextHeadless === headless) {
|
|
501
|
+
return browserContext;
|
|
502
|
+
}
|
|
503
|
+
if (browserContext) {
|
|
504
|
+
await browserContext.close();
|
|
505
|
+
browserContext = null;
|
|
506
|
+
}
|
|
507
|
+
logger.info(`[Browser] \u542F\u52A8 Chromium \u6301\u4E45\u5316\u4E0A\u4E0B\u6587\uFF08headless=${String(headless)}\uFF09...`);
|
|
508
|
+
logger.info(`[Browser] \u6D4F\u89C8\u5668\u6570\u636E\u76EE\u5F55\uFF1A${USER_DATA_DIR}`);
|
|
509
|
+
browserContext = await chromium.launchPersistentContext(USER_DATA_DIR, {
|
|
510
|
+
headless,
|
|
511
|
+
viewport: { width: 1280, height: 900 },
|
|
512
|
+
locale: "zh-CN"
|
|
513
|
+
});
|
|
514
|
+
browserContextHeadless = headless;
|
|
515
|
+
return browserContext;
|
|
516
|
+
}
|
|
517
|
+
async function fetchDocViaBrowser(nodeId) {
|
|
518
|
+
const url = `${DOC_BASE_URL}/${nodeId}`;
|
|
519
|
+
logger.info(`[Browser] \u6B63\u5728\u901A\u8FC7\u6D4F\u89C8\u5668\u8BBF\u95EE\u6587\u6863\uFF0CnodeId: ${nodeId}`);
|
|
520
|
+
let ctx = await getContext(true);
|
|
521
|
+
let page = await ctx.newPage();
|
|
522
|
+
try {
|
|
523
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 2e4 });
|
|
524
|
+
if (isLoginPage(page.url())) {
|
|
525
|
+
logger.warn(
|
|
526
|
+
"[Browser] \u68C0\u6D4B\u5230\u767B\u5F55\u9875\uFF0C\u5207\u6362\u5230\u6709\u754C\u9762\u6A21\u5F0F\uFF0C\u8BF7\u5728\u6D4F\u89C8\u5668\u7A97\u53E3\u4E2D\u5B8C\u6210\u9489\u9489\u767B\u5F55..."
|
|
527
|
+
);
|
|
528
|
+
await page.close();
|
|
529
|
+
ctx = await getContext(false);
|
|
530
|
+
page = await ctx.newPage();
|
|
531
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 2e4 });
|
|
532
|
+
if (isLoginPage(page.url())) {
|
|
533
|
+
logger.warn("[Browser] \u8BF7\u5728\u6253\u5F00\u7684\u6D4F\u89C8\u5668\u7A97\u53E3\u4E2D\u5B8C\u6210\u767B\u5F55\uFF08\u6700\u591A\u7B49\u5F85 3 \u5206\u949F\uFF09...");
|
|
534
|
+
await page.waitForURL((u) => u.href.startsWith(url), { timeout: 18e4 });
|
|
535
|
+
logger.info("[Browser] \u767B\u5F55\u6210\u529F\uFF0C\u7EE7\u7EED\u52A0\u8F7D\u6587\u6863...");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
await page.waitForLoadState("networkidle", { timeout: 2e4 }).catch(() => {
|
|
539
|
+
logger.warn("[Browser] \u7F51\u7EDC\u672A\u5B8C\u5168\u7A7A\u95F2\uFF0C\u7EE7\u7EED\u5C1D\u8BD5\u63D0\u53D6\u5185\u5BB9...");
|
|
540
|
+
});
|
|
541
|
+
const iframeCount = await page.locator("iframe").count();
|
|
542
|
+
logger.info(`[Browser] \u68C0\u6D4B\u5230 ${iframeCount} \u4E2A iframe`);
|
|
543
|
+
let iframeContentText = "";
|
|
544
|
+
if (iframeCount > 0) {
|
|
545
|
+
for (let i = 0; i < iframeCount; i++) {
|
|
546
|
+
try {
|
|
547
|
+
const frame = page.frameLocator("iframe").nth(i);
|
|
548
|
+
for (const sel of CONTENT_SELECTORS) {
|
|
549
|
+
const count = await frame.locator(sel).count();
|
|
550
|
+
if (count > 0) {
|
|
551
|
+
const text = await frame.locator(sel).first().innerText();
|
|
552
|
+
if (text.trim().length > 50) {
|
|
553
|
+
iframeContentText = text;
|
|
554
|
+
logger.info(`[Browser] \u5728 iframe[${i}] \u7684 "${sel}" \u627E\u5230\u5185\u5BB9\uFF08${text.length} \u5B57\u7B26\uFF09`);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (iframeContentText) break;
|
|
560
|
+
const neViewer = await frame.locator(".ne-viewer-body, .ne-doc, [class*='viewer']").first().innerText().catch(() => "");
|
|
561
|
+
if (neViewer.trim().length > 100) {
|
|
562
|
+
iframeContentText = neViewer;
|
|
563
|
+
logger.info(`[Browser] \u5728 iframe[${i}] ne-viewer \u627E\u5230\u5185\u5BB9\uFF08${neViewer.length} \u5B57\u7B26\uFF09`);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
const bodyText = await frame.locator("body").innerText().catch(() => "");
|
|
567
|
+
if (bodyText.trim().length > 100) {
|
|
568
|
+
iframeContentText = bodyText;
|
|
569
|
+
logger.info(`[Browser] \u5728 iframe[${i}] body \u627E\u5230\u5185\u5BB9\uFF08${bodyText.length} \u5B57\u7B26\uFF09`);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
let contentText = iframeContentText;
|
|
577
|
+
let usedSelector = "";
|
|
578
|
+
if (!contentText) {
|
|
579
|
+
for (const selector of CONTENT_SELECTORS) {
|
|
580
|
+
try {
|
|
581
|
+
const el = page.locator(selector).first();
|
|
582
|
+
const visible = await el.isVisible({ timeout: 2e3 });
|
|
583
|
+
if (visible) {
|
|
584
|
+
const text = await el.innerText();
|
|
585
|
+
if (text.trim().length > 50) {
|
|
586
|
+
contentText = text;
|
|
587
|
+
usedSelector = selector;
|
|
588
|
+
logger.info(`[Browser] \u4F7F\u7528\u9009\u62E9\u5668 "${selector}" \u63D0\u53D6\u5230\u5185\u5BB9\uFF08${text.length} \u5B57\u7B26\uFF09`);
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!contentText) {
|
|
597
|
+
logger.warn("[Browser] \u672A\u627E\u5230\u6587\u6863\u5185\u5BB9\u533A\uFF0C\u56DE\u9000\u5230 body \u5168\u6587\uFF08innerText\uFF09");
|
|
598
|
+
contentText = await page.locator("body").innerText().catch(() => "");
|
|
599
|
+
}
|
|
600
|
+
let result;
|
|
601
|
+
if (usedSelector) {
|
|
602
|
+
const html = await page.locator(usedSelector).first().innerHTML();
|
|
603
|
+
const td = new TurndownService({
|
|
604
|
+
headingStyle: "atx",
|
|
605
|
+
codeBlockStyle: "fenced",
|
|
606
|
+
bulletListMarker: "-"
|
|
607
|
+
});
|
|
608
|
+
td.addRule("removeNav", {
|
|
609
|
+
filter: ["nav", "header", "footer", "aside"],
|
|
610
|
+
replacement: () => ""
|
|
611
|
+
});
|
|
612
|
+
result = td.turndown(html).trim();
|
|
613
|
+
} else {
|
|
614
|
+
result = contentText.trim();
|
|
615
|
+
}
|
|
616
|
+
logger.info(`[Browser] \u6587\u6863\u63D0\u53D6\u6210\u529F\uFF0C\u957F\u5EA6: ${result.length} \u5B57\u7B26`);
|
|
617
|
+
return result;
|
|
618
|
+
} finally {
|
|
619
|
+
await page.close();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
process.on("exit", () => {
|
|
623
|
+
if (browserContext) {
|
|
624
|
+
void browserContext.close();
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
process.on("SIGINT", () => {
|
|
628
|
+
if (browserContext) {
|
|
629
|
+
void browserContext.close().then(() => process.exit(0));
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// src/tools/get-document.ts
|
|
634
|
+
var BROWSER_FALLBACK_CODES = /* @__PURE__ */ new Set(["permissionDenied", "documentNotExist"]);
|
|
635
|
+
var ALIDOC_URL_PATTERN = /alidocs\.dingtalk\.com\/i\/nodes\/([A-Za-z0-9_-]+)/;
|
|
636
|
+
function extractNodeId(url) {
|
|
637
|
+
return ALIDOC_URL_PATTERN.exec(url)?.[1] ?? null;
|
|
638
|
+
}
|
|
639
|
+
async function triggerAndWaitExport(nodeId, timeoutMs) {
|
|
640
|
+
logger.info(`\u6B63\u5728\u89E6\u53D1\u6587\u6863\u5BFC\u51FA\uFF0CnodeId: ${nodeId}`);
|
|
641
|
+
const resp = await dingtalkGet(
|
|
642
|
+
`/v2.0/doc/query/${nodeId}/contents`,
|
|
643
|
+
{ targetFormat: "markdown" }
|
|
644
|
+
);
|
|
645
|
+
const taskId = Number(resp.taskId);
|
|
646
|
+
logger.info(`\u6587\u6863\u5BFC\u51FA\u4EFB\u52A1\u5DF2\u89E6\u53D1\uFF0CtaskId: ${taskId}\uFF0C\u7B49\u5F85\u5BFC\u51FA\u7ED3\u679C\uFF08\u6700\u957F ${timeoutMs / 1e3}s\uFF09...`);
|
|
647
|
+
const result = await waitForDocExport(taskId, timeoutMs);
|
|
648
|
+
if (!result.success) {
|
|
649
|
+
throw new Error(`\u6587\u6863\u5BFC\u51FA\u5931\u8D25\uFF08taskId: ${taskId}\uFF09`);
|
|
650
|
+
}
|
|
651
|
+
const content = result.content ?? "";
|
|
652
|
+
logger.info(`\u6587\u6863\u5BFC\u51FA\u6210\u529F\uFF0CtaskId: ${taskId}\uFF0C\u5185\u5BB9\u957F\u5EA6: ${content.length} \u5B57\u7B26`);
|
|
653
|
+
return content;
|
|
654
|
+
}
|
|
655
|
+
async function fetchDocContent(nodeId) {
|
|
656
|
+
if (!isStreamReady()) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
"DingTalk Stream \u8FDE\u63A5\u5C1A\u672A\u5EFA\u7ACB\uFF0C\u65E0\u6CD5\u83B7\u53D6\u6587\u6863\u5185\u5BB9\u3002\n \u8BF7\u7B49\u5F85\u670D\u52A1\u542F\u52A8\u5B8C\u6210\u540E\u91CD\u8BD5\uFF0C\u6216\u68C0\u67E5\u542F\u52A8\u65E5\u5FD7\u4E2D\u662F\u5426\u6709 Stream \u8FDE\u63A5\u9519\u8BEF\u3002"
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const cached = getCachedDocContent(nodeId);
|
|
662
|
+
if (cached) {
|
|
663
|
+
logger.info(`[Cache] \u547D\u4E2D\u6587\u6863\u7F13\u5B58\uFF0CnodeId: ${nodeId}\uFF0C\u76F4\u63A5\u8FD4\u56DE`);
|
|
664
|
+
return cached;
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
return await triggerAndWaitExport(nodeId, 2e4);
|
|
668
|
+
} catch (err) {
|
|
669
|
+
if (err instanceof DingTalkApiError && BROWSER_FALLBACK_CODES.has(err.code)) {
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
673
|
+
logger.warn(`\u9996\u6B21\u5BFC\u51FA\u5931\u8D25\uFF0C\u81EA\u52A8\u91CD\u8BD5\uFF08nodeId: ${nodeId}\uFF0C\u539F\u56E0: ${reason}\uFF09...`);
|
|
674
|
+
return await triggerAndWaitExport(nodeId, 3e4);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function registerGetDocument(server2) {
|
|
678
|
+
server2.tool(
|
|
679
|
+
"dingtalk_get_document",
|
|
680
|
+
"\u8BFB\u53D6\u9489\u9489\u77E5\u8BC6\u5E93\u6587\u6863\u5185\u5BB9\uFF0C\u8FD4\u56DE Markdown \u683C\u5F0F\u3002\u652F\u6301\u901A\u8FC7 nodeId \u6216\u6587\u6863 URL\uFF08https://alidocs.dingtalk.com/i/nodes/...\uFF09\u4E24\u79CD\u65B9\u5F0F\u6307\u5B9A\u6587\u6863",
|
|
681
|
+
{
|
|
682
|
+
nodeId: z2.string().optional().describe("\u6587\u6863\u8282\u70B9 ID\uFF08\u4E0E url \u4E8C\u9009\u4E00\uFF09"),
|
|
683
|
+
url: z2.string().optional().describe("\u6587\u6863 URL\uFF0C\u683C\u5F0F\uFF1Ahttps://alidocs.dingtalk.com/i/nodes/{nodeId}\uFF08\u4E0E nodeId \u4E8C\u9009\u4E00\uFF09")
|
|
684
|
+
},
|
|
685
|
+
async ({ nodeId, url }) => {
|
|
686
|
+
let resolvedNodeId;
|
|
687
|
+
if (nodeId) {
|
|
688
|
+
resolvedNodeId = nodeId;
|
|
689
|
+
} else if (url) {
|
|
690
|
+
const extracted = extractNodeId(url);
|
|
691
|
+
if (!extracted) {
|
|
692
|
+
return toToolError(
|
|
693
|
+
new Error("\u65E0\u6CD5\u4ECE URL \u4E2D\u89E3\u6790 nodeId\uFF0C\u8BF7\u786E\u8BA4\u683C\u5F0F\u4E3A\uFF1Ahttps://alidocs.dingtalk.com/i/nodes/{nodeId}")
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
resolvedNodeId = extracted;
|
|
697
|
+
} else {
|
|
698
|
+
return toToolError(new Error("\u8BF7\u63D0\u4F9B nodeId \u6216 url \u53C2\u6570\uFF08\u4E8C\u9009\u4E00\uFF09"));
|
|
699
|
+
}
|
|
700
|
+
if (!isApiAvailable()) {
|
|
701
|
+
logger.info(`[Browser] API \u4E0D\u53EF\u7528\uFF0C\u4F7F\u7528\u6D4F\u89C8\u5668\u8BFB\u53D6\u6587\u6863\uFF08nodeId: ${resolvedNodeId}\uFF09`);
|
|
702
|
+
try {
|
|
703
|
+
const content = await fetchDocViaBrowser(resolvedNodeId);
|
|
704
|
+
return { content: [{ type: "text", text: content }] };
|
|
705
|
+
} catch (err) {
|
|
706
|
+
return toToolError(err);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const content = await fetchDocContent(resolvedNodeId);
|
|
711
|
+
return { content: [{ type: "text", text: content }] };
|
|
712
|
+
} catch (err) {
|
|
713
|
+
if (err instanceof DingTalkApiError && BROWSER_FALLBACK_CODES.has(err.code)) {
|
|
714
|
+
logger.warn(`[Fallback] API \u8FD4\u56DE ${err.code}\uFF0C\u5207\u6362\u6D4F\u89C8\u5668\u8BBF\u95EE\uFF08nodeId: ${resolvedNodeId}\uFF09`);
|
|
715
|
+
try {
|
|
716
|
+
const content = await fetchDocViaBrowser(resolvedNodeId);
|
|
717
|
+
return {
|
|
718
|
+
content: [{ type: "text", text: `> \u26A0\uFE0F API \u65E0\u6743\u8BBF\u95EE\u6B64\u6587\u6863\uFF0C\u4EE5\u4E0B\u5185\u5BB9\u901A\u8FC7\u6D4F\u89C8\u5668\u8BFB\u53D6\u3002
|
|
719
|
+
|
|
720
|
+
${content}` }]
|
|
721
|
+
};
|
|
722
|
+
} catch (browserErr) {
|
|
723
|
+
return toToolError(
|
|
724
|
+
new Error(
|
|
725
|
+
`API \u548C\u6D4F\u89C8\u5668\u5747\u65E0\u6CD5\u83B7\u53D6\u6587\u6863\u5185\u5BB9\uFF1A
|
|
726
|
+
API: ${err.message}
|
|
727
|
+
\u6D4F\u89C8\u5668: ${browserErr instanceof Error ? browserErr.message : String(browserErr)}`
|
|
728
|
+
)
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return toToolError(err);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/tools/index.ts
|
|
739
|
+
function registerDingdocTools(server2, apiAvailable2) {
|
|
740
|
+
if (apiAvailable2) {
|
|
741
|
+
registerListSpaces(server2);
|
|
742
|
+
registerListNodes(server2);
|
|
743
|
+
} else {
|
|
744
|
+
logger.info("\u4EC5\u6D4F\u89C8\u5668\u6A21\u5F0F\uFF1A\u4EC5\u6CE8\u518C dingtalk_get_document \u5DE5\u5177\uFF08list_spaces / list_nodes \u9700\u8981 API \u51ED\u8BC1\uFF09");
|
|
745
|
+
}
|
|
746
|
+
registerGetDocument(server2);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/index.ts
|
|
750
|
+
var toStderr = (...args) => process.stderr.write(args.map(String).join(" ") + "\n");
|
|
751
|
+
console.log = toStderr;
|
|
752
|
+
console.info = toStderr;
|
|
753
|
+
console.warn = toStderr;
|
|
754
|
+
console.error = toStderr;
|
|
755
|
+
console.debug = toStderr;
|
|
756
|
+
var apiAvailable = false;
|
|
757
|
+
if (isApiConfigured()) {
|
|
758
|
+
try {
|
|
759
|
+
logger.info("\u6B63\u5728\u9A8C\u8BC1\u9489\u9489\u51ED\u8BC1...");
|
|
760
|
+
await validateCredentials();
|
|
761
|
+
apiAvailable = true;
|
|
762
|
+
setApiAvailable(true);
|
|
763
|
+
const { appKey, appSecret } = getAppCredentials();
|
|
764
|
+
if (appKey && appSecret) {
|
|
765
|
+
logger.info(`\u6B63\u5728\u521D\u59CB\u5316 DingTalk Stream\uFF08appKey: ${appKey.slice(0, 4)}****\uFF09...`);
|
|
766
|
+
initDocStream(appKey, appSecret);
|
|
767
|
+
}
|
|
768
|
+
} catch (err) {
|
|
769
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
770
|
+
logger.warn(`\u9489\u9489\u51ED\u8BC1\u9A8C\u8BC1\u5931\u8D25\uFF0C\u5207\u6362\u5230\u4EC5\u6D4F\u89C8\u5668\u6A21\u5F0F\uFF1A
|
|
771
|
+
${msg}`);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
logger.info(
|
|
775
|
+
"\u672A\u68C0\u6D4B\u5230\u9489\u9489 API \u51ED\u8BC1\uFF08DINGTALK_APP_KEY / DINGTALK_APP_SECRET / DINGTALK_UNION_ID\uFF09\uFF0C\u4EE5\u4EC5\u6D4F\u89C8\u5668\u6A21\u5F0F\u542F\u52A8\u3002\n \u6587\u6863\u8BFB\u53D6\u5C06\u901A\u8FC7 Playwright \u6D4F\u89C8\u5668\u8FDB\u884C\uFF0C\u65E0\u9700 API \u6743\u9650\u3002\n \u5982\u9700 API \u6A21\u5F0F\uFF08\u652F\u6301\u77E5\u8BC6\u5E93\u6D4F\u89C8\uFF09\uFF0C\u8BF7\u53C2\u8003 README \u914D\u7F6E\u51ED\u8BC1\u3002"
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
var server = createServer({
|
|
779
|
+
name: "insight-dingdoc",
|
|
780
|
+
version: "0.1.0",
|
|
781
|
+
description: "\u9489\u9489\u77E5\u8BC6\u5E93 MCP \u670D\u52A1\u5668\uFF0C\u63D0\u4F9B\u77E5\u8BC6\u5E93\u7A7A\u95F4\u3001\u8282\u70B9\u6D4F\u89C8\u548C\u6587\u6863\u8BFB\u53D6\u80FD\u529B"
|
|
782
|
+
});
|
|
783
|
+
registerDingdocTools(server, apiAvailable);
|
|
784
|
+
logger.info("\u6B63\u5728\u542F\u52A8 insight-dingdoc \u670D\u52A1...");
|
|
785
|
+
launch(server).catch((err) => {
|
|
786
|
+
logger.error("Server failed to start", err);
|
|
787
|
+
process.exit(1);
|
|
788
|
+
});
|