dingtalk-ask-mcp-server 0.1.0 → 0.1.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 +5 -1
- package/dist/daemon.js +9 -0
- package/dist/dingtalk/client.js +8 -0
- package/dist/greeting.js +60 -0
- package/dist/index.js +20 -1
- package/dist/installCli.js +90 -22
- package/dist/version.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,10 +140,14 @@ Codex 若 `approval_policy="never"` 本就不弹命令审批,故只提供 `ask_h
|
|
|
140
140
|
dingtalk-ask install … # 一键接入(见上)
|
|
141
141
|
dingtalk-ask daemon status|stop # 查看/停止共享守护进程
|
|
142
142
|
dingtalk-ask away on|off|status # 本地切换离开模式
|
|
143
|
-
dingtalk-ask
|
|
143
|
+
dingtalk-ask clean # 清空运行时状态(守护进程/锁/flag/就绪标记/日志),不动配置文件
|
|
144
|
+
dingtalk-ask -v | -h # 版本 / 帮助
|
|
145
|
+
dingtalk-ask-mcp-server --client-id … … # 服务本体(一般由宿主拉起,极少手动跑;支持 -v/-h)
|
|
144
146
|
dingtalk-ask-install-hook [--global|<path>] # 只装 CC 命令审批钩子
|
|
145
147
|
```
|
|
146
148
|
|
|
149
|
+
首次连上钉钉时,会给你单聊发一条**「已连接就绪(附 robotCode)」**确认,便于核对连的是哪个机器人;每个机器人只发一次。
|
|
150
|
+
|
|
147
151
|
守护进程自动拉起、闲置自动退出;`DINGTALK_DAEMON_IDLE_MS` 可调(`0` 禁用)。诊断:守护进程日志在系统临时目录 `dingtalk-daemon.log`;设 `DINGTALK_DEBUG=1` 打印钉钉每帧。
|
|
148
152
|
|
|
149
153
|
---
|
package/dist/daemon.js
CHANGED
|
@@ -15,6 +15,8 @@ import { DingtalkClient } from './dingtalk/client.js';
|
|
|
15
15
|
import { DecisionRegistry } from './decision/registry.js';
|
|
16
16
|
import { askWithTimeout } from './decision/coordinator.js';
|
|
17
17
|
import { startEndpoint } from './daemon/endpoint.js';
|
|
18
|
+
import { buildTextMessage } from './dingtalk/cardPayload.js';
|
|
19
|
+
import { hasGreeted, markGreeted, buildGreeting } from './greeting.js';
|
|
18
20
|
import { daemonLockPath, writeDaemonLockExclusive, readDaemonLock, removeDaemonLock, } from './daemon/lock.js';
|
|
19
21
|
import { probeHealth } from './daemonClient.js';
|
|
20
22
|
/** 选举重试次数(应对陈旧锁的回收竞争)。 */
|
|
@@ -136,6 +138,13 @@ async function main() {
|
|
|
136
138
|
registry = new DecisionRegistry(client);
|
|
137
139
|
ready = true;
|
|
138
140
|
registerCleanup(endpoint, client, lockPath);
|
|
141
|
+
// 首次连接成功时给钉钉发一条就绪提示(每个 robotCode 只发一次),便于确认连的是哪个机器人。
|
|
142
|
+
if (client.isConnected() && !hasGreeted(config.robotCode)) {
|
|
143
|
+
client
|
|
144
|
+
.sendOto(buildTextMessage(buildGreeting(config.robotCode)))
|
|
145
|
+
.then(() => markGreeted(config.robotCode))
|
|
146
|
+
.catch((error) => console.error(`就绪提示发送失败:${error instanceof Error ? error.message : String(error)}`));
|
|
147
|
+
}
|
|
139
148
|
// 空闲自关:无待答且超过阈值无 /ask 活动则退出删锁(下次 MCP server 再拉起)。
|
|
140
149
|
const idleMs = resolveIdleMs(process.env.DINGTALK_DAEMON_IDLE_MS);
|
|
141
150
|
if (idleMs > 0) {
|
package/dist/dingtalk/client.js
CHANGED
|
@@ -209,6 +209,14 @@ export class DingtalkClient {
|
|
|
209
209
|
throw new Error(`获取钉钉 accessToken 失败:${describeAxiosError(error)}`);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* 当前是否已成功建立 Stream 长连(用于首次连接就绪提示等)。
|
|
214
|
+
*
|
|
215
|
+
* @returns 已连上 → true;未建连或握手未成功 → false。
|
|
216
|
+
*/
|
|
217
|
+
isConnected() {
|
|
218
|
+
return this.dwClient?.connected === true;
|
|
219
|
+
}
|
|
212
220
|
/**
|
|
213
221
|
* 断开 Stream 长连(若已建连)。进程退出前调用,释放 socket。
|
|
214
222
|
*
|
package/dist/greeting.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 首次连接就绪提示:守护进程成功连上钉钉后,给用户单聊发一条"已连接(哪个机器人)"确认,
|
|
3
|
+
* 便于确认凭证对不对、连的是哪个 bot、接收链路通不通。
|
|
4
|
+
*
|
|
5
|
+
* 每个 robotCode 只发一次(tmpdir 标记文件),避免每次开 CC / 重启守护进程都刷屏。
|
|
6
|
+
* 纯逻辑(路径/文案)与极薄 IO 分离,便于单测。
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
/**
|
|
12
|
+
* 就绪提示标记文件路径(按 robotCode 区分;非法字符替换为下划线)。
|
|
13
|
+
*
|
|
14
|
+
* @param robotCode 机器人 robotCode。
|
|
15
|
+
* @returns tmpdir 下的标记文件绝对路径。
|
|
16
|
+
*/
|
|
17
|
+
export function greetingMarkerPath(robotCode) {
|
|
18
|
+
const safe = robotCode.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
19
|
+
return join(tmpdir(), `dingtalk-greeted-${safe}.flag`);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 是否已对该 robotCode 发过就绪提示。
|
|
23
|
+
*
|
|
24
|
+
* @param robotCode 机器人 robotCode。
|
|
25
|
+
* @param path 标记路径;缺省用 greetingMarkerPath(测试注入隔离路径)。
|
|
26
|
+
* @returns 已发过 → true。
|
|
27
|
+
*/
|
|
28
|
+
export function hasGreeted(robotCode, path = greetingMarkerPath(robotCode)) {
|
|
29
|
+
try {
|
|
30
|
+
return existsSync(path);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 记录已发过就绪提示(写标记文件,内容为 ISO 时间戳)。
|
|
38
|
+
*
|
|
39
|
+
* @param robotCode 机器人 robotCode。
|
|
40
|
+
* @param path 标记路径;缺省用 greetingMarkerPath(测试注入隔离路径)。
|
|
41
|
+
*/
|
|
42
|
+
export function markGreeted(robotCode, path = greetingMarkerPath(robotCode)) {
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(path, new Date().toISOString(), 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* 写标记失败不影响主流程(最多下次重复发一次)。 */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 构造就绪提示文案(纯函数,可测)。
|
|
52
|
+
*
|
|
53
|
+
* @param robotCode 机器人 robotCode(帮用户确认连的是哪个 bot)。
|
|
54
|
+
* @returns 提示正文。
|
|
55
|
+
*/
|
|
56
|
+
export function buildGreeting(robotCode) {
|
|
57
|
+
return (`✅ dingtalk-ask 已连接就绪,可以在这里接收决策与命令审批。\n` +
|
|
58
|
+
`机器人 robotCode:${robotCode}\n` +
|
|
59
|
+
`离开电脑时发 /away 开启推送,回来发 /back 关闭。`);
|
|
60
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,10 +17,29 @@ import { resolveDingtalkConfig } from "./dingtalk/config.js";
|
|
|
17
17
|
import { isAway, setAway } from "./awayState.js";
|
|
18
18
|
import { resolveAgentLabel } from "./agentLabel.js";
|
|
19
19
|
import { mergeArgsIntoEnv } from "./cliArgs.js";
|
|
20
|
+
import { packageVersion } from "./version.js";
|
|
20
21
|
import { ensureDaemon, postAskToDaemon } from "./daemonClient.js";
|
|
21
22
|
import { daemonLockPath } from "./daemon/lock.js";
|
|
23
|
+
// -v/--version、-h/--help:在读配置前拦截,免凭证也能用。
|
|
24
|
+
const rawArgs = process.argv.slice(2);
|
|
25
|
+
if (rawArgs.includes("-v") || rawArgs.includes("--version")) {
|
|
26
|
+
console.log(packageVersion());
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
if (rawArgs.includes("-h") || rawArgs.includes("--help")) {
|
|
30
|
+
console.log(`dingtalk-ask-mcp-server v${packageVersion()} —— 钉钉 human-in-the-loop MCP server`);
|
|
31
|
+
console.log("");
|
|
32
|
+
console.log("一般由宿主(Claude Code / Codex)按 MCP 配置拉起,也可手动启动:");
|
|
33
|
+
console.log(" dingtalk-ask-mcp-server --client-id <AppKey> --client-secret <AppSecret> \\");
|
|
34
|
+
console.log(" --robot-code <robotCode> --user-id <userId> \\");
|
|
35
|
+
console.log(" [--timeout <ms>] [--agent-label <名字>]");
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log("一键接入 / 管理请用 `dingtalk-ask`(install / daemon / away / clean)。");
|
|
38
|
+
console.log("凭证也可用 DINGTALK_CLIENT_ID 等环境变量提供。");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
22
41
|
/** 合并 CLI 参数与环境变量(参数优先):允许用 --client-id 等命令行参数传钉钉配置。 */
|
|
23
|
-
const env = mergeArgsIntoEnv(process.env,
|
|
42
|
+
const env = mergeArgsIntoEnv(process.env, rawArgs);
|
|
24
43
|
/**
|
|
25
44
|
* 解析钉钉配置;缺凭证时打印缺失项到 stderr 并 exit(1)(AC5:不产生半可用状态)。
|
|
26
45
|
*
|
package/dist/installCli.js
CHANGED
|
@@ -10,16 +10,17 @@
|
|
|
10
10
|
* 写入均为宿主本地配置(.mcp.json / .claude/settings.json / ~/.codex/config.toml,皆不入库),
|
|
11
11
|
* 合并保留既有内容、幂等。纯逻辑在 installConfig.ts / installHookCore.ts。
|
|
12
12
|
*/
|
|
13
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from 'node:fs';
|
|
14
|
-
import { homedir } from 'node:os';
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { homedir, tmpdir } from 'node:os';
|
|
15
15
|
import { join, dirname } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { resolveDingtalkConfig } from './dingtalk/config.js';
|
|
18
18
|
import { mergeArgsIntoEnv } from './cliArgs.js';
|
|
19
|
+
import { packageVersion } from './version.js';
|
|
19
20
|
import { mergeHook, permissionHookPath, resolveSettingsPath } from './installHookCore.js';
|
|
20
21
|
import { daemonLockPath, readDaemonLock, removeDaemonLock } from './daemon/lock.js';
|
|
21
22
|
import { probeHealth } from './daemonClient.js';
|
|
22
|
-
import { isAway, setAway } from './awayState.js';
|
|
23
|
+
import { isAway, setAway, awayFlagPath } from './awayState.js';
|
|
23
24
|
import { buildMcpEntry, mergeMcpServers, ensureMcpToolTimeout, codexHasDingtalk, buildCodexBlock, MCP_SERVER_NAME, } from './installConfig.js';
|
|
24
25
|
/** 解析目标宿主。默认 CC;Codex 见 resolveCodexTarget。 */
|
|
25
26
|
function parseHost(argv) {
|
|
@@ -93,15 +94,100 @@ function installCodex(config, nodePath, indexPath) {
|
|
|
93
94
|
console.log(`✅ [Codex] 追加 [mcp_servers.dingtalk-ask] 到 ${cfgPath}${content !== '' ? '(已备份 .bak)' : ''}`);
|
|
94
95
|
}
|
|
95
96
|
function usage() {
|
|
96
|
-
console.log(
|
|
97
|
+
console.log(`dingtalk-ask v${packageVersion()} —— 钉钉 human-in-the-loop MCP 工具`);
|
|
97
98
|
console.log('');
|
|
98
99
|
console.log('用法:');
|
|
99
100
|
console.log(' dingtalk-ask install --client-id … --client-secret … --robot-code … --user-id … \\');
|
|
100
101
|
console.log(' [--timeout <ms>] [--host cc|codex|both] # 一键装 MCP+钩子');
|
|
101
102
|
console.log(' dingtalk-ask daemon <status|stop> # 查看/停止共享守护进程');
|
|
102
103
|
console.log(' dingtalk-ask away <on|off|status> # 本地切换离开模式');
|
|
104
|
+
console.log(' dingtalk-ask clean # 清空运行时状态(守护进程/锁/flag/标记/日志)');
|
|
105
|
+
console.log(' dingtalk-ask -v | --version # 版本');
|
|
106
|
+
console.log(' dingtalk-ask -h | --help # 帮助');
|
|
103
107
|
process.exit(1);
|
|
104
108
|
}
|
|
109
|
+
/** clean 子命令:清空运行时临时状态(不动配置文件)。 */
|
|
110
|
+
function runClean() {
|
|
111
|
+
const tmp = tmpdir();
|
|
112
|
+
// 1. 停守护进程 + 删锁。
|
|
113
|
+
const lock = readDaemonLock(daemonLockPath());
|
|
114
|
+
if (lock !== undefined && lock.pid > 0) {
|
|
115
|
+
try {
|
|
116
|
+
process.kill(lock.pid, 'SIGTERM');
|
|
117
|
+
console.log(`· 已停守护进程 pid=${lock.pid}`);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* 可能已退出 */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
removeDaemonLock(daemonLockPath());
|
|
124
|
+
// 2. 离开模式 flag。
|
|
125
|
+
try {
|
|
126
|
+
unlinkSync(awayFlagPath());
|
|
127
|
+
console.log('· 已删离开模式 flag');
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* 不存在忽略 */
|
|
131
|
+
}
|
|
132
|
+
// 3. 就绪提示标记(各 robotCode)。
|
|
133
|
+
let markers = 0;
|
|
134
|
+
try {
|
|
135
|
+
for (const f of readdirSync(tmp)) {
|
|
136
|
+
if (f.startsWith('dingtalk-greeted-')) {
|
|
137
|
+
try {
|
|
138
|
+
unlinkSync(join(tmp, f));
|
|
139
|
+
markers += 1;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* 忽略 */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* 忽略 */
|
|
149
|
+
}
|
|
150
|
+
if (markers > 0) {
|
|
151
|
+
console.log(`· 已删 ${markers} 个就绪提示标记`);
|
|
152
|
+
}
|
|
153
|
+
// 4. 守护进程日志。
|
|
154
|
+
try {
|
|
155
|
+
unlinkSync(join(tmp, 'dingtalk-daemon.log'));
|
|
156
|
+
console.log('· 已删守护进程日志');
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
/* 忽略 */
|
|
160
|
+
}
|
|
161
|
+
console.log('✅ 已清空运行时状态。配置文件(.mcp.json / settings.json / config.toml)未改动。');
|
|
162
|
+
}
|
|
163
|
+
function main() {
|
|
164
|
+
const args = process.argv.slice(2);
|
|
165
|
+
const cmd = args[0];
|
|
166
|
+
const rest = args.slice(1);
|
|
167
|
+
if (cmd === '-v' || cmd === '--version') {
|
|
168
|
+
console.log(packageVersion());
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (cmd === '-h' || cmd === '--help') {
|
|
172
|
+
usage();
|
|
173
|
+
}
|
|
174
|
+
if (cmd === 'install') {
|
|
175
|
+
runInstall(rest);
|
|
176
|
+
}
|
|
177
|
+
else if (cmd === 'daemon') {
|
|
178
|
+
void runDaemon(rest);
|
|
179
|
+
}
|
|
180
|
+
else if (cmd === 'away') {
|
|
181
|
+
runAway(rest);
|
|
182
|
+
}
|
|
183
|
+
else if (cmd === 'clean') {
|
|
184
|
+
runClean();
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
usage();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
main();
|
|
105
191
|
/** install 子命令:装 MCP + 钩子。 */
|
|
106
192
|
function runInstall(rest) {
|
|
107
193
|
let config;
|
|
@@ -187,21 +273,3 @@ function runAway(rest) {
|
|
|
187
273
|
}
|
|
188
274
|
usage();
|
|
189
275
|
}
|
|
190
|
-
function main() {
|
|
191
|
-
const args = process.argv.slice(2);
|
|
192
|
-
const cmd = args[0];
|
|
193
|
-
const rest = args.slice(1);
|
|
194
|
-
if (cmd === 'install') {
|
|
195
|
-
runInstall(rest);
|
|
196
|
-
}
|
|
197
|
-
else if (cmd === 'daemon') {
|
|
198
|
-
void runDaemon(rest);
|
|
199
|
-
}
|
|
200
|
-
else if (cmd === 'away') {
|
|
201
|
-
runAway(rest);
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
usage();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
main();
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读取本包版本号(从 dist 上层的 package.json 动态读,随发版自动更新)。
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
/**
|
|
7
|
+
* 读取 package.json 的 version。
|
|
8
|
+
*
|
|
9
|
+
* @returns 版本号;读取失败时返回 'unknown'。
|
|
10
|
+
*/
|
|
11
|
+
export function packageVersion() {
|
|
12
|
+
try {
|
|
13
|
+
const p = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
14
|
+
const pkg = JSON.parse(readFileSync(p, 'utf8'));
|
|
15
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return 'unknown';
|
|
19
|
+
}
|
|
20
|
+
}
|