dingtalk-ask-mcp-server 0.1.3 → 0.1.5

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 CHANGED
@@ -60,6 +60,8 @@ dingtalk-ask install --client-id <AppKey> --client-secret <AppSecret> \
60
60
  [--timeout 1440000] [--host cc|codex|both]
61
61
  ```
62
62
 
63
+ > **不想记参数?** 在终端直接跑 `dingtalk-ask install`(不带凭证)会进入**交互式向导**,一步步问你 AppKey / AppSecret / robotCode / userId / 宿主(有默认值回车即用)。
64
+
63
65
  它会自动改这些(合并保留既有内容、幂等):
64
66
 
65
67
  | 宿主 | 改动的文件 | 写入内容 |
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { request } from 'node:http';
8
8
  import { spawn } from 'node:child_process';
9
- import { openSync, existsSync } from 'node:fs';
9
+ import { openSync, closeSync, existsSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
@@ -152,11 +152,22 @@ function resolveDaemonScript() {
152
152
  */
153
153
  function spawnDaemon(config) {
154
154
  const daemonScript = resolveDaemonScript();
155
- const logFd = openSync(daemonLogPath(), 'a');
155
+ // 日志重定向是可选的调试输出:文件被占用/无权限(EPERM)时退回不记日志,绝不因此阻断守护进程拉起。
156
+ let stdio = ['ignore', 'ignore', 'ignore'];
157
+ let logFd;
158
+ let logNote = '无日志(文件不可写)';
159
+ try {
160
+ logFd = openSync(daemonLogPath(), 'a');
161
+ stdio = ['ignore', logFd, logFd];
162
+ logNote = `日志 ${daemonLogPath()}`;
163
+ }
164
+ catch {
165
+ /* 日志打不开:退回 ignore,仍拉起守护进程 */
166
+ }
156
167
  const child = spawn(process.execPath, [daemonScript], {
157
168
  detached: true,
158
169
  windowsHide: true,
159
- stdio: ['ignore', logFd, logFd],
170
+ stdio,
160
171
  env: {
161
172
  ...process.env,
162
173
  // 注入解析后的凭证,覆盖可能缺失/过期的 env(支持 CLI 参数来源)。
@@ -168,7 +179,16 @@ function spawnDaemon(config) {
168
179
  },
169
180
  });
170
181
  child.unref();
171
- console.error(`已拉起钉钉守护进程(pid=${child.pid ?? '?'},脚本 ${daemonScript},日志 ${daemonLogPath()})。`);
182
+ // 关闭父进程持有的日志 fd 副本:子进程已继承自己的句柄;不关会泄漏、长期锁住日志文件。
183
+ if (logFd !== undefined) {
184
+ try {
185
+ closeSync(logFd);
186
+ }
187
+ catch {
188
+ /* 忽略 */
189
+ }
190
+ }
191
+ console.error(`已拉起钉钉守护进程(pid=${child.pid ?? '?'},脚本 ${daemonScript},${logNote})。`);
172
192
  }
173
193
  /**
174
194
  * 确保守护进程在跑并就绪,返回其锁(port + token)。
@@ -14,6 +14,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, unlin
14
14
  import { homedir, tmpdir } from 'node:os';
15
15
  import { join, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { createInterface } from 'node:readline/promises';
17
18
  import { resolveDingtalkConfig } from './dingtalk/config.js';
18
19
  import { mergeArgsIntoEnv } from './cliArgs.js';
19
20
  import { packageVersion } from './version.js';
@@ -200,7 +201,7 @@ function main() {
200
201
  usage();
201
202
  }
202
203
  if (cmd === 'install') {
203
- runInstall(rest);
204
+ void runInstall(rest);
204
205
  }
205
206
  else if (cmd === 'uninstall') {
206
207
  runUninstall(rest);
@@ -328,23 +329,75 @@ function runUninstall(rest) {
328
329
  cleanRuntime();
329
330
  console.log('\n✅ 已卸载。重启 Claude Code / 重开 Codex 会话后彻底生效。');
330
331
  }
331
- /** install 子命令:装 MCP + 钩子。 */
332
- function runInstall(rest) {
332
+ /** 交互式逐步收集凭证(缺配置时的向导)。返回解析后的 config 与可选 host。 */
333
+ async function promptConfig(env, cwd) {
334
+ const existing = inspectMcpJson(tolerantJson(join(cwd, '.mcp.json')));
335
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
336
+ try {
337
+ console.log('未检测到完整凭证,进入交互式配置(直接回车用【中括号】内默认值):\n');
338
+ const ask = async (label, def) => {
339
+ const a = (await rl.question(` ${label}${def ? ` [${def}]` : ''}: `)).trim();
340
+ return a || def || '';
341
+ };
342
+ const askSecret = async (label, realDef) => {
343
+ const a = (await rl.question(` ${label}${realDef ? ` [${mask(realDef)}]` : ''}: `)).trim();
344
+ return a || realDef || '';
345
+ };
346
+ const clientId = await ask('AppKey (client-id)', env.DINGTALK_CLIENT_ID);
347
+ const clientSecret = await askSecret('AppSecret (client-secret)', env.DINGTALK_CLIENT_SECRET);
348
+ const robotCode = await ask('机器人 robotCode(一般同 AppKey)', env.DINGTALK_ROBOT_CODE ?? clientId);
349
+ const userId = await ask('你的钉钉 userId', env.DINGTALK_USER_ID ?? existing.userId);
350
+ const defaultHost = existsSync(join(homedir(), '.codex')) ? 'both' : 'cc';
351
+ const hostRaw = await ask('装到哪个宿主 cc/codex/both', defaultHost);
352
+ const host = hostRaw === 'cc' || hostRaw === 'codex' || hostRaw === 'both' ? hostRaw : undefined;
353
+ const merged = {
354
+ ...env,
355
+ DINGTALK_CLIENT_ID: clientId,
356
+ DINGTALK_CLIENT_SECRET: clientSecret,
357
+ DINGTALK_ROBOT_CODE: robotCode || clientId,
358
+ DINGTALK_USER_ID: userId,
359
+ };
360
+ let config;
361
+ try {
362
+ config = resolveDingtalkConfig(merged);
363
+ }
364
+ catch (error) {
365
+ console.error(`\n凭证不完整:${error instanceof Error ? error.message : String(error)}`);
366
+ process.exit(1);
367
+ }
368
+ return { config, host };
369
+ }
370
+ finally {
371
+ rl.close();
372
+ }
373
+ }
374
+ /** install 子命令:装 MCP + 钩子(缺凭证时进交互式向导)。 */
375
+ async function runInstall(rest) {
376
+ const cwd = process.cwd();
377
+ const env = mergeArgsIntoEnv(process.env, rest);
333
378
  let config;
379
+ let interactiveHost;
334
380
  try {
335
- config = resolveDingtalkConfig(mergeArgsIntoEnv(process.env, rest));
381
+ config = resolveDingtalkConfig(env);
336
382
  }
337
- catch (error) {
338
- console.error(`配置错误:${error instanceof Error ? error.message : String(error)}`);
339
- console.error('请用 --client-id / --client-secret / --robot-code / --user-id 提供凭证。');
340
- process.exit(1);
383
+ catch {
384
+ // 缺凭证:交互式终端 逐步引导输入;非交互(管道/CI)→ 报错提示用参数。
385
+ if (process.stdin.isTTY !== true) {
386
+ console.error('缺少钉钉凭证。请用 --client-id / --client-secret / --robot-code / --user-id 提供,');
387
+ console.error('或在交互式终端直接运行 `dingtalk-ask install` 逐步输入。');
388
+ process.exit(1);
389
+ }
390
+ const r = await promptConfig(env, cwd);
391
+ config = r.config;
392
+ interactiveHost = r.host;
341
393
  }
342
394
  const nodePath = process.execPath;
343
395
  const indexPath = fileURLToPath(new URL('./index.js', import.meta.url));
344
- // 目标宿主:显式 --host 优先;否则 CC 必装,Codex 存在则一并装。
345
- const host = parseHost(rest);
396
+ // 目标宿主:显式 --host 优先,其次交互式选择;否则 CC 必装、Codex 存在则一并装。
397
+ const host = parseHost(rest) ?? interactiveHost;
346
398
  const doCc = host === undefined || host === 'cc' || host === 'both';
347
399
  const doCodex = host === 'codex' || host === 'both' || (host === undefined && existsSync(join(homedir(), '.codex')));
400
+ console.log('');
348
401
  if (doCc) {
349
402
  installClaudeCode(config, rest, nodePath, indexPath);
350
403
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dingtalk-ask-mcp-server",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Human-in-the-loop MCP server:把 code agent(Claude Code / Codex)的决策与命令审批推送到钉钉,阻塞等你在手机回复。离开电脑也能远程拍板。",
5
5
  "keywords": [
6
6
  "mcp",