@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/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# dingdoc MCP Server
|
|
2
|
+
|
|
3
|
+
通过 MCP 协议读取钉钉知识库文档内容,支持两种工作模式:**API 模式**(完整功能)和**仅浏览器模式**(无需凭证)。
|
|
4
|
+
|
|
5
|
+
## 运行模式
|
|
6
|
+
|
|
7
|
+
| 模式 | 触发条件 | 可用工具 |
|
|
8
|
+
|------|---------|---------|
|
|
9
|
+
| **API 模式** | 提供了 AppKey / AppSecret / 用户身份,且验证通过 | 全部 3 个工具 |
|
|
10
|
+
| **仅浏览器模式** | 未提供凭证,或凭证验证失败 | 仅 `dingtalk_get_document` |
|
|
11
|
+
|
|
12
|
+
- 服务启动时自动检测,无需手动切换
|
|
13
|
+
- 仅浏览器模式首次使用时会打开登录窗口,完成钉钉扫码登录后 session 持久保存(`~/.insight-mcp/dingdoc-browser/`)
|
|
14
|
+
|
|
15
|
+
## 在 MCP 客户端中配置
|
|
16
|
+
|
|
17
|
+
### 仅浏览器模式(最简配置)
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"dingdoc": {
|
|
23
|
+
"command": "node",
|
|
24
|
+
"args": ["/path/to/Insight-MCP/servers/dingdoc/dist/index.js"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### API 模式(完整功能)
|
|
31
|
+
|
|
32
|
+
直接写入凭证:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"dingdoc": {
|
|
38
|
+
"command": "node",
|
|
39
|
+
"args": ["/path/to/Insight-MCP/servers/dingdoc/dist/index.js"],
|
|
40
|
+
"env": {
|
|
41
|
+
"DINGTALK_APP_KEY": "your_app_key",
|
|
42
|
+
"DINGTALK_APP_SECRET": "your_app_secret",
|
|
43
|
+
"DINGTALK_UNION_ID": "your_union_id"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
引用已有的环境变量名(凭证不写入配置文件):
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"dingdoc": {
|
|
56
|
+
"command": "node",
|
|
57
|
+
"args": [
|
|
58
|
+
"/path/to/Insight-MCP/servers/dingdoc/dist/index.js",
|
|
59
|
+
"--app-key-env=MY_APP_KEY",
|
|
60
|
+
"--app-secret-env=MY_APP_SECRET",
|
|
61
|
+
"--union-id-env=MY_UNION_ID"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 凭证说明
|
|
69
|
+
|
|
70
|
+
服务支持三种方式提供凭证,按优先级从高到低:
|
|
71
|
+
|
|
72
|
+
| 方式 | 示例 |
|
|
73
|
+
|------|------|
|
|
74
|
+
| CLI 直传明文 | `--app-key=xxx --app-secret=xxx` |
|
|
75
|
+
| CLI 指定环境变量名 | `--app-key-env=MY_VAR --app-secret-env=MY_VAR` |
|
|
76
|
+
| 默认环境变量 | `DINGTALK_APP_KEY` / `DINGTALK_APP_SECRET` |
|
|
77
|
+
|
|
78
|
+
操作人身份(三种方式均支持):
|
|
79
|
+
|
|
80
|
+
| 凭证 | 默认环境变量 | CLI 明文 | CLI 指定 env 名 |
|
|
81
|
+
|------|------------|---------|----------------|
|
|
82
|
+
| unionId(推荐) | `DINGTALK_UNION_ID` | `--union-id=xxx` | `--union-id-env=MY_VAR` |
|
|
83
|
+
| userId(自动解析为 unionId)| `DINGTALK_USER_ID` | `--user-id=xxx` | `--user-id-env=MY_VAR` |
|
|
84
|
+
|
|
85
|
+
**unionId 与 userId 的区别:**
|
|
86
|
+
|
|
87
|
+
- **unionId**:钉钉跨企业唯一用户标识,格式为 Base64-like 字符串(如 `HOqmx7FOhIBxUBjJ580XJgiEiE`)。API 调用时作为操作人身份,直接使用无需额外查询,推荐优先使用。
|
|
88
|
+
- **userId**:企业内部的员工工号(如 `STAFF-001`),仅在本企业内唯一。服务会在启动时自动调用 `Contact.User.Read` 接口将其解析为 `unionId`,解析成功后打印结果供日后直接使用。
|
|
89
|
+
|
|
90
|
+
> **如何获取 unionId?** 配置 `DINGTALK_USER_ID`(工号)启动一次服务,日志中会打印对应的 `unionId`,复制后改用 `DINGTALK_UNION_ID` 配置即可,后续启动不再需要查询接口。
|
|
91
|
+
|
|
92
|
+
API 权限要求:
|
|
93
|
+
- `Wiki.Read`(知识库读取)
|
|
94
|
+
- `Contact.User.Read`(用户信息查询,仅使用 userId 时需要)
|
|
95
|
+
|
|
96
|
+
## 可用工具
|
|
97
|
+
|
|
98
|
+
### `dingtalk_get_document` _(API 模式 + 仅浏览器模式均可用)_
|
|
99
|
+
|
|
100
|
+
读取文档内容,返回 Markdown 格式全文。
|
|
101
|
+
|
|
102
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
103
|
+
|------|------|------|------|
|
|
104
|
+
| `nodeId` | string | 二选一 | 文档节点 ID(dentryUuid) |
|
|
105
|
+
| `url` | string | 二选一 | 文档 URL,格式:`https://alidocs.dingtalk.com/i/nodes/{nodeId}` |
|
|
106
|
+
|
|
107
|
+
API 模式优先通过 DingTalk Stream 异步导出获取;若 API 无权限,自动 fallback 到浏览器读取。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `dingtalk_list_spaces` _(仅 API 模式)_
|
|
112
|
+
|
|
113
|
+
列出当前用户有权限的所有钉钉知识库,返回 `spaceId`、名称、`rootNodeId` 和访问链接。
|
|
114
|
+
|
|
115
|
+
无需参数,直接调用即可作为浏览目录的起点。
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### `dingtalk_list_nodes` _(仅 API 模式)_
|
|
120
|
+
|
|
121
|
+
列出指定节点下的子节点(文件和文件夹)。
|
|
122
|
+
|
|
123
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
124
|
+
|------|------|------|------|
|
|
125
|
+
| `nodeId` | string | 是 | 父节点 ID,从 `dingtalk_list_spaces` 的 `rootNodeId` 开始 |
|
|
126
|
+
| `recursive` | boolean | 否 | 是否递归展开整个目录树,默认 `false` |
|
|
127
|
+
|
|
128
|
+
## 工作原理
|
|
129
|
+
|
|
130
|
+
### API 模式文档读取
|
|
131
|
+
|
|
132
|
+
钉钉文档导出是异步的,`dingtalk_get_document` 采用以下机制同步返回结果:
|
|
133
|
+
|
|
134
|
+
1. 服务启动时建立 **DingTalk Stream WebSocket 长连接**,监听 `doc_content_export_result` 事件
|
|
135
|
+
2. 调用工具时,通过 REST API 触发导出任务,获取 `taskId`
|
|
136
|
+
3. 等待 Stream 推送的导出完成事件,事件中包含 Markdown 全文
|
|
137
|
+
4. 若钉钉因服务端缓存未推送新事件,自动重试一次(总等待上限约 50s)
|
|
138
|
+
5. 导出结果在进程内缓存,同一文档再次请求直接返回
|
|
139
|
+
|
|
140
|
+
### 仅浏览器模式文档读取
|
|
141
|
+
|
|
142
|
+
使用 Playwright 驱动 Chromium 无头浏览器:
|
|
143
|
+
|
|
144
|
+
1. 导航到文档页面(`https://alidocs.dingtalk.com/i/nodes/{nodeId}`)
|
|
145
|
+
2. 等待 SPA 渲染完成,在 iframe 及主页面中依次查找内容区元素
|
|
146
|
+
3. 提取 HTML 内容并转换为 Markdown
|
|
147
|
+
|
|
148
|
+
## 从 npm 安装
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npx @insightsuen/dingdoc
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
在 MCP 客户端配置中使用(无需本地安装):
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"mcpServers": {
|
|
159
|
+
"dingdoc": {
|
|
160
|
+
"command": "npx",
|
|
161
|
+
"args": ["-y", "@insightsuen/dingdoc@latest"],
|
|
162
|
+
"env": {
|
|
163
|
+
"DINGTALK_APP_KEY": "xxx",
|
|
164
|
+
"DINGTALK_APP_SECRET": "xxx",
|
|
165
|
+
"DINGTALK_UNION_ID": "xxx"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> **注意**:首次使用浏览器模式时,`npx` 方式无法弹出登录窗口(headless 环境)。建议先在本地完成一次登录,session 保存后再用 `npx` 方式运行。
|
|
173
|
+
|
|
174
|
+
## 从源码构建
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# 克隆仓库后在项目根目录执行
|
|
178
|
+
pnpm install
|
|
179
|
+
pnpm --filter @insightsuen/dingdoc build
|
|
180
|
+
|
|
181
|
+
# 安装 Playwright 浏览器(仅浏览器模式需要)
|
|
182
|
+
pnpm --filter @insightsuen/dingdoc exec playwright install chromium
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 手动启动 / 调试
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# 使用环境变量
|
|
189
|
+
DINGTALK_APP_KEY=xxx DINGTALK_APP_SECRET=xxx DINGTALK_UNION_ID=xxx \
|
|
190
|
+
node servers/dingdoc/dist/index.js
|
|
191
|
+
|
|
192
|
+
# 使用 CLI 参数
|
|
193
|
+
node servers/dingdoc/dist/index.js \
|
|
194
|
+
--app-key=xxx \
|
|
195
|
+
--app-secret=xxx \
|
|
196
|
+
--union-id=xxx
|
|
197
|
+
|
|
198
|
+
# MCP Inspector
|
|
199
|
+
npx @modelcontextprotocol/inspector node servers/dingdoc/dist/index.js
|
|
200
|
+
```
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function getAccessToken(): Promise<string>;
|
|
2
|
+
export declare function getOperatorId(): Promise<string>;
|
|
3
|
+
/** 返回应用凭证(供 stream 客户端使用) */
|
|
4
|
+
export declare function getAppCredentials(): {
|
|
5
|
+
appKey: string | undefined;
|
|
6
|
+
appSecret: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* 判断 API 凭证是否已配置(不抛错)。
|
|
10
|
+
* 凭证缺失时服务器以"仅浏览器模式"运行,所有文档读取走 Playwright。
|
|
11
|
+
*/
|
|
12
|
+
export declare function isApiConfigured(): boolean;
|
|
13
|
+
/** 标记 API 已通过验证,可正常使用 */
|
|
14
|
+
export declare function setApiAvailable(val: boolean): void;
|
|
15
|
+
/** 返回 API 是否已通过验证并可用(凭证存在 + 启动时验证通过) */
|
|
16
|
+
export declare function isApiAvailable(): boolean;
|
|
17
|
+
/** 启动时验证凭证有效性,失败则抛出错误 */
|
|
18
|
+
export declare function validateCredentials(): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAsEA,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAqCtD;AAyBD,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAwCrD;AAED,6BAA6B;AAC7B,wBAAgB,iBAAiB,IAAI;IAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAKjG;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAOzC;AAMD,yBAAyB;AACzB,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAElD;AAED,wCAAwC;AACxC,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAID,yBAAyB;AACzB,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAUzD"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { logger } from "@insight-mcp/shared";
|
|
2
|
+
const OAUTH_URL = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
|
|
3
|
+
const USER_GET_URL = "https://oapi.dingtalk.com/topapi/v2/user/get";
|
|
4
|
+
const LEAD_TIME_MS = 10 * 60 * 1000; // 提前 10 分钟刷新
|
|
5
|
+
let cachedToken = null;
|
|
6
|
+
let tokenExpiresAt = 0;
|
|
7
|
+
let cachedOperatorId = null;
|
|
8
|
+
// ─── CLI 参数解析 ─────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* 解析命令行参数,支持:
|
|
11
|
+
* --app-key=xxx 直接传值
|
|
12
|
+
* --app-key-env=MY_VAR 指定读取的环境变量名
|
|
13
|
+
*/
|
|
14
|
+
function parseCliArgs() {
|
|
15
|
+
const map = new Map();
|
|
16
|
+
for (const arg of process.argv.slice(2)) {
|
|
17
|
+
if (arg.startsWith("--")) {
|
|
18
|
+
const eqIdx = arg.indexOf("=");
|
|
19
|
+
if (eqIdx > 2) {
|
|
20
|
+
const key = arg.slice(2, eqIdx);
|
|
21
|
+
const val = arg.slice(eqIdx + 1);
|
|
22
|
+
map.set(key, val);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
const CLI_ARGS = parseCliArgs();
|
|
29
|
+
/**
|
|
30
|
+
* 凭证解析优先级:
|
|
31
|
+
* 1. CLI 直传: --{argKey}=VALUE
|
|
32
|
+
* 2. 自定义 env 名: --{argKey}-env=MY_VAR → process.env.MY_VAR
|
|
33
|
+
* 3. 默认 env 名: process.env[defaultEnvKey]
|
|
34
|
+
*/
|
|
35
|
+
function resolveCredential(argKey, defaultEnvKey) {
|
|
36
|
+
// 1. CLI 直传
|
|
37
|
+
const direct = CLI_ARGS.get(argKey);
|
|
38
|
+
if (direct)
|
|
39
|
+
return direct;
|
|
40
|
+
// 2. 自定义 env var 名
|
|
41
|
+
const customEnvName = CLI_ARGS.get(`${argKey}-env`);
|
|
42
|
+
if (customEnvName) {
|
|
43
|
+
const val = process.env[customEnvName];
|
|
44
|
+
if (val)
|
|
45
|
+
return val;
|
|
46
|
+
logger.warn(`--${argKey}-env 指定了 ${customEnvName},但该环境变量未设置`);
|
|
47
|
+
}
|
|
48
|
+
// 3. 默认 env var
|
|
49
|
+
return process.env[defaultEnvKey];
|
|
50
|
+
}
|
|
51
|
+
// ─── Access Token ─────────────────────────────────────────────────────────────
|
|
52
|
+
export async function getAccessToken() {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
if (cachedToken && now < tokenExpiresAt) {
|
|
55
|
+
return cachedToken;
|
|
56
|
+
}
|
|
57
|
+
const appKey = resolveCredential("app-key", "DINGTALK_APP_KEY");
|
|
58
|
+
const appSecret = resolveCredential("app-secret", "DINGTALK_APP_SECRET");
|
|
59
|
+
if (!appKey || !appSecret) {
|
|
60
|
+
throw new Error([
|
|
61
|
+
"缺少钉钉应用凭证,请通过以下任一方式提供:",
|
|
62
|
+
" • 环境变量:DINGTALK_APP_KEY / DINGTALK_APP_SECRET",
|
|
63
|
+
" • CLI 参数:--app-key=xxx --app-secret=xxx",
|
|
64
|
+
" • 自定义 env 名:--app-key-env=MY_VAR --app-secret-env=MY_VAR",
|
|
65
|
+
" AppKey/AppSecret 可在钉钉开发者后台「凭证与基础信息」页面获取。",
|
|
66
|
+
].join("\n"));
|
|
67
|
+
}
|
|
68
|
+
logger.debug("Refreshing DingTalk access token...");
|
|
69
|
+
const resp = await fetch(OAUTH_URL, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ appKey, appSecret, grantType: "client_credentials" }),
|
|
73
|
+
});
|
|
74
|
+
if (!resp.ok) {
|
|
75
|
+
throw new Error(`获取 access token 失败:HTTP ${resp.status}`);
|
|
76
|
+
}
|
|
77
|
+
const data = (await resp.json());
|
|
78
|
+
cachedToken = data.accessToken;
|
|
79
|
+
tokenExpiresAt = now + data.expireIn * 1000 - LEAD_TIME_MS;
|
|
80
|
+
logger.debug("DingTalk access token refreshed");
|
|
81
|
+
return cachedToken;
|
|
82
|
+
}
|
|
83
|
+
// ─── Operator ID(unionId)───────────────────────────────────────────────────
|
|
84
|
+
/** 通过 userId 查询 unionId */
|
|
85
|
+
async function fetchUnionId(userId) {
|
|
86
|
+
const token = await getAccessToken();
|
|
87
|
+
const resp = await fetch(`${USER_GET_URL}?access_token=${token}`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "Content-Type": "application/json" },
|
|
90
|
+
body: JSON.stringify({ userid: userId }),
|
|
91
|
+
});
|
|
92
|
+
if (!resp.ok) {
|
|
93
|
+
throw new Error(`获取 unionId 失败:HTTP ${resp.status}`);
|
|
94
|
+
}
|
|
95
|
+
const data = (await resp.json());
|
|
96
|
+
if (data.errcode !== 0 || !data.result?.unionid) {
|
|
97
|
+
throw new Error(`获取 unionId 失败:${data.errmsg}(errcode: ${data.errcode})`);
|
|
98
|
+
}
|
|
99
|
+
return data.result.unionid;
|
|
100
|
+
}
|
|
101
|
+
export async function getOperatorId() {
|
|
102
|
+
if (cachedOperatorId)
|
|
103
|
+
return cachedOperatorId;
|
|
104
|
+
// 优先使用 unionId
|
|
105
|
+
const unionId = resolveCredential("union-id", "DINGTALK_UNION_ID");
|
|
106
|
+
if (unionId) {
|
|
107
|
+
cachedOperatorId = unionId;
|
|
108
|
+
return cachedOperatorId;
|
|
109
|
+
}
|
|
110
|
+
// 回退:通过 userId 换取 unionId
|
|
111
|
+
const userId = resolveCredential("user-id", "DINGTALK_USER_ID");
|
|
112
|
+
if (userId) {
|
|
113
|
+
logger.info([
|
|
114
|
+
`检测到 DINGTALK_USER_ID,正在查询对应的 unionId...`,
|
|
115
|
+
` 提示:你也可以直接设置 DINGTALK_UNION_ID 跳过此步骤,`,
|
|
116
|
+
` unionId 可从钉钉开发者后台「用户管理」或首次运行日志中获取。`,
|
|
117
|
+
].join("\n"));
|
|
118
|
+
const resolved = await fetchUnionId(userId);
|
|
119
|
+
logger.info(` ✓ unionId 解析成功:${resolved}`);
|
|
120
|
+
logger.info(` 建议将 DINGTALK_UNION_ID=${resolved} 写入环境变量以加速后续启动。`);
|
|
121
|
+
cachedOperatorId = resolved;
|
|
122
|
+
return cachedOperatorId;
|
|
123
|
+
}
|
|
124
|
+
throw new Error([
|
|
125
|
+
"缺少操作人身份(unionId),请通过以下任一方式提供:",
|
|
126
|
+
" • 环境变量(推荐):DINGTALK_UNION_ID=xxx",
|
|
127
|
+
" • 环境变量(自动解析):DINGTALK_USER_ID=xxx (将自动查询 unionId)",
|
|
128
|
+
" • CLI 参数:--union-id=xxx 或 --user-id=xxx",
|
|
129
|
+
" • 自定义 env 名:--union-id-env=MY_VAR",
|
|
130
|
+
"",
|
|
131
|
+
" unionId 获取方式:",
|
|
132
|
+
" 1. 钉钉开发者后台 → 用户管理 → 点击用户 → 查看 unionId",
|
|
133
|
+
" 2. 或先设置 DINGTALK_USER_ID(如工号 BG012345),服务启动时会自动显示 unionId",
|
|
134
|
+
].join("\n"));
|
|
135
|
+
}
|
|
136
|
+
/** 返回应用凭证(供 stream 客户端使用) */
|
|
137
|
+
export function getAppCredentials() {
|
|
138
|
+
return {
|
|
139
|
+
appKey: resolveCredential("app-key", "DINGTALK_APP_KEY"),
|
|
140
|
+
appSecret: resolveCredential("app-secret", "DINGTALK_APP_SECRET"),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 判断 API 凭证是否已配置(不抛错)。
|
|
145
|
+
* 凭证缺失时服务器以"仅浏览器模式"运行,所有文档读取走 Playwright。
|
|
146
|
+
*/
|
|
147
|
+
export function isApiConfigured() {
|
|
148
|
+
const appKey = resolveCredential("app-key", "DINGTALK_APP_KEY");
|
|
149
|
+
const appSecret = resolveCredential("app-secret", "DINGTALK_APP_SECRET");
|
|
150
|
+
const hasUser = !!resolveCredential("union-id", "DINGTALK_UNION_ID") ||
|
|
151
|
+
!!resolveCredential("user-id", "DINGTALK_USER_ID");
|
|
152
|
+
return !!(appKey && appSecret && hasUser);
|
|
153
|
+
}
|
|
154
|
+
// ─── 运行时 API 可用状态 ──────────────────────────────────────────────────────
|
|
155
|
+
let _apiAvailable = false;
|
|
156
|
+
/** 标记 API 已通过验证,可正常使用 */
|
|
157
|
+
export function setApiAvailable(val) {
|
|
158
|
+
_apiAvailable = val;
|
|
159
|
+
}
|
|
160
|
+
/** 返回 API 是否已通过验证并可用(凭证存在 + 启动时验证通过) */
|
|
161
|
+
export function isApiAvailable() {
|
|
162
|
+
return _apiAvailable;
|
|
163
|
+
}
|
|
164
|
+
// ─── 启动验证 ─────────────────────────────────────────────────────────────────
|
|
165
|
+
/** 启动时验证凭证有效性,失败则抛出错误 */
|
|
166
|
+
export async function validateCredentials() {
|
|
167
|
+
logger.info("正在验证钉钉凭证...");
|
|
168
|
+
try {
|
|
169
|
+
await getAccessToken();
|
|
170
|
+
await getOperatorId();
|
|
171
|
+
logger.info("✓ 钉钉凭证验证成功");
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
throw new Error(`钉钉凭证验证失败:\n${msg}`, { cause: err });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通过浏览器访问钉钉文档并提取内容为 Markdown。
|
|
3
|
+
*
|
|
4
|
+
* 首次使用需要登录:
|
|
5
|
+
* - 若检测到登录页,自动切换到有界面模式供用户完成登录
|
|
6
|
+
* - 登录后 session 保存在 ~/.insight-mcp/dingdoc-browser/
|
|
7
|
+
* - 后续调用直接复用登录状态(无需重新登录)
|
|
8
|
+
*/
|
|
9
|
+
export declare function fetchDocViaBrowser(nodeId: string): Promise<string>;
|
|
10
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAgEA;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA+HxE"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import TurndownService from "turndown";
|
|
5
|
+
import { logger } from "@insight-mcp/shared";
|
|
6
|
+
const USER_DATA_DIR = path.join(os.homedir(), ".insight-mcp", "dingdoc-browser");
|
|
7
|
+
const DOC_BASE_URL = "https://alidocs.dingtalk.com/i/nodes";
|
|
8
|
+
/**
|
|
9
|
+
* 钉钉文档内容区的候选选择器,按优先级排列。
|
|
10
|
+
* 钉钉编辑器(ne-engine / alidoc)的 DOM 结构可能随版本变化,保留多个备选。
|
|
11
|
+
*/
|
|
12
|
+
const CONTENT_SELECTORS = [
|
|
13
|
+
".ne-viewer-body",
|
|
14
|
+
".ne-doc-content",
|
|
15
|
+
".ne-engine-body",
|
|
16
|
+
".ne-doc",
|
|
17
|
+
"[class*='viewer-body']",
|
|
18
|
+
"[class*='docContent']",
|
|
19
|
+
"[class*='doc-content']",
|
|
20
|
+
"[class*='editor-body']",
|
|
21
|
+
"[class*='content-body']",
|
|
22
|
+
".wiki-doc-content",
|
|
23
|
+
"article",
|
|
24
|
+
];
|
|
25
|
+
/** 判断当前页面是否需要登录 */
|
|
26
|
+
function isLoginPage(url) {
|
|
27
|
+
return (url.includes("/login") ||
|
|
28
|
+
url.includes("/sso") ||
|
|
29
|
+
url.includes("oauth") ||
|
|
30
|
+
url.includes("dingtalk.com/o/") ||
|
|
31
|
+
url.includes("account.dingtalk.com"));
|
|
32
|
+
}
|
|
33
|
+
let browserContext = null;
|
|
34
|
+
let browserContextHeadless = true;
|
|
35
|
+
/** 获取或创建浏览器上下文(持久化,保留登录 session) */
|
|
36
|
+
async function getContext(headless) {
|
|
37
|
+
if (browserContext && browserContextHeadless === headless) {
|
|
38
|
+
return browserContext;
|
|
39
|
+
}
|
|
40
|
+
if (browserContext) {
|
|
41
|
+
await browserContext.close();
|
|
42
|
+
browserContext = null;
|
|
43
|
+
}
|
|
44
|
+
logger.info(`[Browser] 启动 Chromium 持久化上下文(headless=${String(headless)})...`);
|
|
45
|
+
logger.info(`[Browser] 浏览器数据目录:${USER_DATA_DIR}`);
|
|
46
|
+
browserContext = await chromium.launchPersistentContext(USER_DATA_DIR, {
|
|
47
|
+
headless,
|
|
48
|
+
viewport: { width: 1280, height: 900 },
|
|
49
|
+
locale: "zh-CN",
|
|
50
|
+
});
|
|
51
|
+
browserContextHeadless = headless;
|
|
52
|
+
return browserContext;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 通过浏览器访问钉钉文档并提取内容为 Markdown。
|
|
56
|
+
*
|
|
57
|
+
* 首次使用需要登录:
|
|
58
|
+
* - 若检测到登录页,自动切换到有界面模式供用户完成登录
|
|
59
|
+
* - 登录后 session 保存在 ~/.insight-mcp/dingdoc-browser/
|
|
60
|
+
* - 后续调用直接复用登录状态(无需重新登录)
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchDocViaBrowser(nodeId) {
|
|
63
|
+
const url = `${DOC_BASE_URL}/${nodeId}`;
|
|
64
|
+
logger.info(`[Browser] 正在通过浏览器访问文档,nodeId: ${nodeId}`);
|
|
65
|
+
let ctx = await getContext(true); // 先尝试 headless
|
|
66
|
+
let page = await ctx.newPage();
|
|
67
|
+
try {
|
|
68
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
69
|
+
// 检测是否跳转到登录页
|
|
70
|
+
if (isLoginPage(page.url())) {
|
|
71
|
+
logger.warn("[Browser] 检测到登录页,切换到有界面模式,请在浏览器窗口中完成钉钉登录...");
|
|
72
|
+
await page.close();
|
|
73
|
+
// 切换到有界面模式
|
|
74
|
+
ctx = await getContext(false);
|
|
75
|
+
page = await ctx.newPage();
|
|
76
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
77
|
+
// 等待用户登录并跳转回文档页(最多等 3 分钟)
|
|
78
|
+
if (isLoginPage(page.url())) {
|
|
79
|
+
logger.warn("[Browser] 请在打开的浏览器窗口中完成登录(最多等待 3 分钟)...");
|
|
80
|
+
await page.waitForURL((u) => u.href.startsWith(url), { timeout: 180_000 });
|
|
81
|
+
logger.info("[Browser] 登录成功,继续加载文档...");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 等待网络空闲,让 SPA 完成渲染
|
|
85
|
+
await page.waitForLoadState("networkidle", { timeout: 20_000 }).catch(() => {
|
|
86
|
+
logger.warn("[Browser] 网络未完全空闲,继续尝试提取内容...");
|
|
87
|
+
});
|
|
88
|
+
// 优先在 iframe 内搜索内容(钉钉文档编辑区通常嵌入在 iframe 中)
|
|
89
|
+
const iframeCount = await page.locator("iframe").count();
|
|
90
|
+
logger.info(`[Browser] 检测到 ${iframeCount} 个 iframe`);
|
|
91
|
+
let iframeContentText = "";
|
|
92
|
+
if (iframeCount > 0) {
|
|
93
|
+
for (let i = 0; i < iframeCount; i++) {
|
|
94
|
+
try {
|
|
95
|
+
const frame = page.frameLocator("iframe").nth(i);
|
|
96
|
+
for (const sel of CONTENT_SELECTORS) {
|
|
97
|
+
const count = await frame.locator(sel).count();
|
|
98
|
+
if (count > 0) {
|
|
99
|
+
const text = await frame.locator(sel).first().innerText();
|
|
100
|
+
if (text.trim().length > 50) {
|
|
101
|
+
iframeContentText = text;
|
|
102
|
+
logger.info(`[Browser] 在 iframe[${i}] 的 "${sel}" 找到内容(${text.length} 字符)`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (iframeContentText)
|
|
108
|
+
break;
|
|
109
|
+
// 尝试 iframe 的 .ne-viewer-body 或 body
|
|
110
|
+
const neViewer = await frame.locator(".ne-viewer-body, .ne-doc, [class*='viewer']").first().innerText().catch(() => "");
|
|
111
|
+
if (neViewer.trim().length > 100) {
|
|
112
|
+
iframeContentText = neViewer;
|
|
113
|
+
logger.info(`[Browser] 在 iframe[${i}] ne-viewer 找到内容(${neViewer.length} 字符)`);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const bodyText = await frame.locator("body").innerText().catch(() => "");
|
|
117
|
+
if (bodyText.trim().length > 100) {
|
|
118
|
+
iframeContentText = bodyText;
|
|
119
|
+
logger.info(`[Browser] 在 iframe[${i}] body 找到内容(${bodyText.length} 字符)`);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// iframe 跨域或其他错误,跳过
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 若 iframe 里没找到,再尝试主页面内容选择器
|
|
129
|
+
let contentText = iframeContentText;
|
|
130
|
+
let usedSelector = "";
|
|
131
|
+
if (!contentText) {
|
|
132
|
+
for (const selector of CONTENT_SELECTORS) {
|
|
133
|
+
try {
|
|
134
|
+
const el = page.locator(selector).first();
|
|
135
|
+
const visible = await el.isVisible({ timeout: 2_000 });
|
|
136
|
+
if (visible) {
|
|
137
|
+
const text = await el.innerText();
|
|
138
|
+
if (text.trim().length > 50) {
|
|
139
|
+
contentText = text;
|
|
140
|
+
usedSelector = selector;
|
|
141
|
+
logger.info(`[Browser] 使用选择器 "${selector}" 提取到内容(${text.length} 字符)`);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// 继续尝试下一个
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!contentText) {
|
|
152
|
+
// 回退:获取 body 全文(Playwright 的 innerText 会自动跳过不可见元素)
|
|
153
|
+
logger.warn("[Browser] 未找到文档内容区,回退到 body 全文(innerText)");
|
|
154
|
+
contentText = await page.locator("body").innerText().catch(() => "");
|
|
155
|
+
}
|
|
156
|
+
// 若找到了 HTML 结构的选择器则转 Markdown,否则直接返回纯文本
|
|
157
|
+
let result;
|
|
158
|
+
if (usedSelector) {
|
|
159
|
+
const html = await page.locator(usedSelector).first().innerHTML();
|
|
160
|
+
const td = new TurndownService({
|
|
161
|
+
headingStyle: "atx",
|
|
162
|
+
codeBlockStyle: "fenced",
|
|
163
|
+
bulletListMarker: "-",
|
|
164
|
+
});
|
|
165
|
+
td.addRule("removeNav", {
|
|
166
|
+
filter: ["nav", "header", "footer", "aside"],
|
|
167
|
+
replacement: () => "",
|
|
168
|
+
});
|
|
169
|
+
result = td.turndown(html).trim();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
result = contentText.trim();
|
|
173
|
+
}
|
|
174
|
+
logger.info(`[Browser] 文档提取成功,长度: ${result.length} 字符`);
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await page.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// 进程退出时关闭浏览器
|
|
182
|
+
process.on("exit", () => {
|
|
183
|
+
if (browserContext) {
|
|
184
|
+
void browserContext.close();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
process.on("SIGINT", () => {
|
|
188
|
+
if (browserContext) {
|
|
189
|
+
void browserContext.close().then(() => process.exit(0));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class DingTalkApiError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(status: number, code: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
/** GET 请求,自动附加认证头和 operatorId */
|
|
7
|
+
export declare function dingtalkGet<T>(path: string, params?: Record<string, string | number | boolean>): Promise<T>;
|
|
8
|
+
/** POST 请求,自动附加认证头 */
|
|
9
|
+
export declare function dingtalkPost<T>(path: string, body?: Record<string, unknown>): Promise<T>;
|
|
10
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,qBAAa,gBAAiB,SAAQ,KAAK;aAEvB,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;CAKlB;AAOD,iCAAiC;AACjC,wBAAsB,WAAW,CAAC,CAAC,EACjC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAM,GACrD,OAAO,CAAC,CAAC,CAAC,CA6BZ;AAED,sBAAsB;AACtB,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACjC,OAAO,CAAC,CAAC,CAAC,CA2BZ"}
|