@yixinkj/cli 1.0.8 → 1.0.10

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.
Files changed (3) hide show
  1. package/README.md +20 -70
  2. package/index.js +14 -336
  3. package/package.json +3 -6
package/README.md CHANGED
@@ -1,94 +1,44 @@
1
1
  # @yixinkj/cli
2
2
 
3
- 译心跨境本地 MCP stdio 服务器,用于在 MCP 客户端通过 `npx -y` 启动 MCP 服务并调用本地 Rust 能力。
3
+ 译心跨境原生 CLI 启动器。通过 `npx -y` 下载并缓存当前平台的原生二进制,然后把参数直接转发给 `alibaba-cli`。
4
4
 
5
- ## MCP 客户端配置
6
-
7
- 配置方式:`Stdio`
8
-
9
- 命令:
5
+ ## 使用
10
6
 
11
7
  ```bash
12
- npx -y @yixinkj/cli@latest
13
- ```
14
-
15
- 推荐环境变量:
16
-
17
- ```text
18
- YIXIN_PHONE=你的手机号
19
- YIXIN_AUTH_URL=http://47.237.81.135:3000/v1/token
8
+ npx -y @yixinkj/cli@latest --help
9
+ npx -y @yixinkj/cli@latest --ads --period last-week
20
10
  ```
21
11
 
22
- `YIXIN_AUTH_URL` 已内置默认值,普通用户只需要填写 `YIXIN_PHONE`。也可以通过 MCP tool `configure_auth` 写入本机配置;环境变量优先于本机配置。
23
-
24
- ## 工具
25
-
26
- ### configure_auth
27
-
28
- 参数:
29
-
30
- ```json
31
- {
32
- "phone": "客户授权手机号",
33
- "authUrl": "http://47.237.81.135:3000/v1/token",
34
- "skill": "ads"
35
- }
36
- ```
12
+ 也可以全局安装后使用 `yixin` 快捷命令:
37
13
 
38
- 执行流程:
39
-
40
- 1. 使用传入的 `phone`、`authUrl` 向授权服务换取一次短期 token。
41
- 2. 验证成功后写入 `~/.yixin/config.json`。
42
- 3. 文件权限尽量设置为 `0600`。
43
- 4. 环境变量仍然优先;环境变量缺失时才读取本机配置。
44
-
45
- ### check_auth
46
-
47
- 参数可省略:
48
-
49
- ```json
50
- {
51
- "skill": "ads"
52
- }
14
+ ```bash
15
+ npm i -g @yixinkj/cli
16
+ yixin --help
17
+ yixin --ads --period last-week
53
18
  ```
54
19
 
55
- 执行流程:
20
+ 首次运行会从公开 GitHub Release 下载二进制到 `~/.yixin/bin`。
56
21
 
57
- 1. 检查当前平台是否支持。
58
- 2. 如当前版本二进制尚未安装,则从公开 GitHub Release 下载对应平台包并解压到 `~/.yixin/bin`。
59
- 3. 从环境变量或 `~/.yixin/config.json` 读取授权手机号;授权服务地址默认使用 `http://47.237.81.135:3000/v1/token`,也可由配置覆盖。
60
- 4. 向授权服务提交 `{ phone, skill, version }`。
61
- 5. 返回自检结果;不会生成广告报告。
22
+ macOS 下下载完成后会自动处理每个二进制:
62
23
 
63
- ### run_ads_report
24
+ - 清除 `com.apple.quarantine`
25
+ - 设置可执行权限
64
26
 
65
- 参数:
27
+ 授权、手机号保存、token 缓存、广告后台登录等待、广告数据抓取、HTML 报告生成和报告自动打开均由原生 `alibaba-cli` 内置完成。报告生成后,CLI 会自动关闭本次抓取数据时打开的广告后台页面。
66
28
 
67
- ```json
68
- {
69
- "period": "last-week"
70
- }
71
- ```
72
-
73
- 执行流程:
74
-
75
- 1. 从环境变量或 `~/.yixin/config.json` 读取授权手机号;授权服务地址默认使用 `http://47.237.81.135:3000/v1/token`,也可由配置覆盖。
76
- 2. 向授权服务提交 `{ phone, skill: "ads", version }`。
77
- 3. 获取短期授权 token。
78
- 4. 如当前版本二进制尚未安装,则从公开 GitHub Release 下载对应平台包并解压到 `~/.yixin/bin`。
79
- 5. 使用以下参数运行 `~/.yixin/bin/alibaba-cli-{platform}`:
29
+ ## 环境变量
80
30
 
81
31
  ```text
82
- --ads --period <period>
32
+ YIXIN_PHONE 可选。授权手机号;未设置时 CLI 会读取 ~/.yixin/config.json,仍没有则交互式询问。
33
+ YIXIN_AUTH_URL 可选。授权服务地址,默认已内置。
34
+ YIXIN_RELEASE_REPO 可选。默认 yixinkj/cli-releases。
35
+ YIXIN_RELEASE_BASE_URL 可选。覆盖二进制下载基础 URL。
36
+ YIXIN_GITHUB_TOKEN 可选。下载私有 Release 时使用;正式公开分发通常不需要。
83
37
  ```
84
38
 
85
- 6. 将原生二进制的 stdout 返回给 MCP 客户端。
86
-
87
39
  ## 二进制发布包
88
40
 
89
41
  默认从 `yixinkj/cli-releases` 下载与 npm 包版本一致的 release asset:
90
42
 
91
43
  - `darwin-arm64` -> `yixin-mac-arm64.tar.gz`
92
44
  - `win32-x64` -> `yixin-win32-x64.zip`
93
-
94
- 可通过 `YIXIN_RELEASE_REPO` 或 `YIXIN_RELEASE_BASE_URL` 覆盖下载来源。
package/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import http from 'node:http';
4
3
  import https from 'node:https';
5
4
  import fs from 'node:fs';
6
5
  import os from 'node:os';
@@ -8,20 +7,14 @@ import path from 'node:path';
8
7
  import { spawnSync } from 'node:child_process';
9
8
  import { createRequire } from 'node:module';
10
9
 
11
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
- import { z } from 'zod';
14
-
15
10
  const require = createRequire(import.meta.url);
16
11
  const packageJson = require('./package.json');
17
12
 
18
13
  const VERSION = packageJson.version;
19
14
  const RELEASE_REPO = process.env.YIXIN_RELEASE_REPO || 'yixinkj/cli-releases';
20
15
  const CACHE_ROOT = path.join(os.homedir(), '.yixin');
21
- const BIN_DIR = path.join(os.homedir(), '.yixin', 'bin');
16
+ const BIN_DIR = path.join(CACHE_ROOT, 'bin');
22
17
  const VERSION_FILE = path.join(CACHE_ROOT, 'version');
23
- const CONFIG_FILE = path.join(CACHE_ROOT, 'config.json');
24
- const DEFAULT_AUTH_URL = process.env.YIXIN_DEFAULT_AUTH_URL || 'http://47.237.81.135:3000/v1/token';
25
18
 
26
19
  const PLATFORM_TARGETS = {
27
20
  'darwin-arm64': {
@@ -38,28 +31,10 @@ const PLATFORM_TARGETS = {
38
31
  }
39
32
  };
40
33
 
41
- const PERIOD_ALIASES = {
42
- '上周': 'last-week',
43
- 'last_week': 'last-week',
44
- 'last-week': 'last-week',
45
- '上月': 'last-month',
46
- 'last_month': 'last-month',
47
- 'last-month': 'last-month',
48
- '昨日': 'yesterday',
49
- '昨天': 'yesterday',
50
- 'yesterday': 'yesterday',
51
- '今日': 'today',
52
- '今天': 'today',
53
- 'today': 'today',
54
- '近7天': 'last-7-days',
55
- '最近7天': 'last-7-days',
56
- 'last_7_days': 'last-7-days',
57
- 'last-7-days': 'last-7-days',
58
- '近30天': 'last-30-days',
59
- '最近30天': 'last-30-days',
60
- 'last_30_days': 'last-30-days',
61
- 'last-30-days': 'last-30-days'
62
- };
34
+ function fail(message) {
35
+ console.error(`[yixin] ${message}`);
36
+ process.exit(1);
37
+ }
63
38
 
64
39
  function detectTarget() {
65
40
  const platformKey = `${process.platform}-${process.arch}`;
@@ -91,51 +66,6 @@ function releaseUrl(target) {
91
66
  return `https://github.com/${RELEASE_REPO}/releases/download/v${VERSION}/${target.archive}`;
92
67
  }
93
68
 
94
- function readLocalConfig() {
95
- if (!fs.existsSync(CONFIG_FILE)) {
96
- return {};
97
- }
98
- try {
99
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
100
- } catch (error) {
101
- throw new Error(`本地授权配置文件无效:${CONFIG_FILE},${error.message}`);
102
- }
103
- }
104
-
105
- function writeLocalConfig(config) {
106
- fs.mkdirSync(CACHE_ROOT, { recursive: true });
107
- const tempPath = `${CONFIG_FILE}.${process.pid}.tmp`;
108
- fs.writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
109
- fs.renameSync(tempPath, CONFIG_FILE);
110
- try {
111
- fs.chmodSync(CONFIG_FILE, 0o600);
112
- } catch (_) {
113
- // Windows 可能会忽略 POSIX 权限位。
114
- }
115
- }
116
-
117
- function getAuthSettings() {
118
- const localConfig = readLocalConfig();
119
- const envPhone = String(process.env.YIXIN_PHONE || '').trim();
120
- const envLegacyKey = String(process.env.YIXIN_KEY || '').trim();
121
- const envAuthUrl = String(process.env.YIXIN_AUTH_URL || '').trim();
122
- const localPhone = String(localConfig.yixinPhone || localConfig.yixinKey || '').trim();
123
- const localAuthUrl = String(localConfig.yixinAuthUrl || '').trim();
124
- const phone = envPhone || envLegacyKey || localPhone;
125
- const authUrl = envAuthUrl || localAuthUrl || DEFAULT_AUTH_URL;
126
-
127
- return {
128
- phone,
129
- authUrl,
130
- phoneSource: envPhone
131
- ? 'env:YIXIN_PHONE'
132
- : (envLegacyKey ? 'env:YIXIN_KEY' : (localPhone ? CONFIG_FILE : 'missing')),
133
- authUrlSource: envAuthUrl
134
- ? 'env:YIXIN_AUTH_URL'
135
- : (localAuthUrl ? CONFIG_FILE : 'default')
136
- };
137
- }
138
-
139
69
  function downloadFile(url, outputPath, redirectsLeft = 5) {
140
70
  return new Promise((resolve, reject) => {
141
71
  const requestUrl = new URL(url);
@@ -173,9 +103,7 @@ function downloadFile(url, outputPath, redirectsLeft = 5) {
173
103
 
174
104
  const file = fs.createWriteStream(outputPath);
175
105
  response.pipe(file);
176
- file.on('finish', () => {
177
- file.close(resolve);
178
- });
106
+ file.on('finish', () => file.close(resolve));
179
107
  file.on('error', reject);
180
108
  }).on('error', reject);
181
109
  });
@@ -283,271 +211,21 @@ async function ensureInstalled(target) {
283
211
  }
284
212
  }
285
213
 
286
- async function requestToken({ phone, skill, version, authUrl }) {
287
- const normalizedAuthUrl = String(authUrl || '').trim();
288
- if (!normalizedAuthUrl) {
289
- throw new Error('未配置授权服务器地址,请添加 YIXIN_AUTH_URL');
290
- }
291
-
292
- const url = new URL(normalizedAuthUrl);
293
- const body = JSON.stringify({ phone, skill, version });
294
- const client = url.protocol === 'https:' ? https : http;
295
-
296
- return new Promise((resolve, reject) => {
297
- const request = client.request({
298
- method: 'POST',
299
- protocol: url.protocol,
300
- hostname: url.hostname,
301
- port: url.port || undefined,
302
- path: `${url.pathname}${url.search}`,
303
- headers: {
304
- 'Content-Type': 'application/json',
305
- 'Content-Length': Buffer.byteLength(body),
306
- 'User-Agent': '@yixinkj/cli'
307
- }
308
- }, (response) => {
309
- let data = '';
310
- response.setEncoding('utf8');
311
- response.on('data', (chunk) => {
312
- data += chunk;
313
- });
314
- response.on('end', () => {
315
- if (response.statusCode < 200 || response.statusCode >= 300) {
316
- reject(new Error(`授权服务器请求失败:HTTP ${response.statusCode}`));
317
- return;
318
- }
319
- try {
320
- resolve(JSON.parse(data));
321
- } catch (error) {
322
- reject(new Error(`授权服务器返回了无效 JSON:${error.message}`));
323
- }
324
- });
325
- });
326
-
327
- request.setTimeout(15000, () => {
328
- request.destroy(new Error('授权服务器请求超时'));
329
- });
330
- request.on('error', reject);
331
- request.write(body);
332
- request.end();
333
- });
334
- }
335
-
336
- async function authorize(skill) {
337
- const settings = getAuthSettings();
338
- const phone = settings.phone;
339
- if (!phone) {
340
- throw new Error('未配置授权手机号,请在 MCP 客户端环境变量里添加 YIXIN_PHONE=手机号,或调用 configure_auth 写入本机配置');
341
- }
342
-
343
- const auth = await requestToken({ phone, skill, version: VERSION, authUrl: settings.authUrl });
344
- if (!auth || auth.valid !== true || !auth.token) {
345
- throw new Error(auth && auth.reason ? auth.reason : '授权失败');
346
- }
347
- return auth.token;
348
- }
349
-
350
- async function runNative({ target, args, token }) {
351
- await ensureInstalled(target);
352
- const executablePath = binaryPath(target);
353
- const result = spawnSync(executablePath, args, {
354
- encoding: 'utf8',
355
- env: {
356
- ...process.env,
357
- YIXIN_TOKEN: token
358
- }
359
- });
360
-
361
- if (result.error) {
362
- throw result.error;
363
- }
364
- if (result.status !== 0) {
365
- const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
366
- throw new Error(detail || `${executablePath} exited with code ${result.status}`);
367
- }
368
- return result.stdout || '';
369
- }
370
-
371
- async function runAdsReport({ period }) {
372
- const normalizedPeriod = normalizePeriod(period);
373
- if (!normalizedPeriod) {
374
- throw new Error('period 不能为空');
375
- }
376
-
377
- const token = await authorize('ads');
378
- const target = detectTarget();
379
- const stdout = await runNative({
380
- target,
381
- args: ['--ads', '--period', normalizedPeriod],
382
- token
383
- });
384
- return stdout;
385
- }
386
-
387
- async function checkAuth({ skill }) {
388
- const normalizedSkill = String(skill || 'ads').trim() || 'ads';
214
+ async function main() {
389
215
  const target = detectTarget();
390
216
  await ensureInstalled(target);
391
- const settings = getAuthSettings();
392
- await authorize(normalizedSkill);
393
-
394
- return [
395
- 'Yixin MCP 自检通过',
396
- `version: ${VERSION}`,
397
- `skill: ${normalizedSkill}`,
398
- `platform: ${target.id}`,
399
- `binary: ${binaryPath(target)}`,
400
- `phone_source: ${settings.phoneSource}`,
401
- `auth_url_source: ${settings.authUrlSource}`,
402
- `config_file: ${CONFIG_FILE}`
403
- ].join('\n');
404
- }
405
-
406
- async function configureAuth({ phone, authUrl, skill }) {
407
- const normalizedPhone = String(phone || '').trim();
408
- const hasCustomAuthUrl = authUrl !== undefined && authUrl !== null && String(authUrl).trim() !== '';
409
- const normalizedAuthUrl = hasCustomAuthUrl ? String(authUrl).trim() : DEFAULT_AUTH_URL;
410
- const normalizedSkill = String(skill || 'ads').trim() || 'ads';
411
217
 
412
- if (!normalizedPhone) {
413
- throw new Error('phone 不能为空');
414
- }
415
- if (!normalizedAuthUrl) {
416
- throw new Error('authUrl 不能为空');
417
- }
418
- try {
419
- new URL(normalizedAuthUrl);
420
- } catch (error) {
421
- throw new Error(`authUrl 不是有效 URL:${error.message}`);
422
- }
423
-
424
- const auth = await requestToken({
425
- phone: normalizedPhone,
426
- skill: normalizedSkill,
427
- version: VERSION,
428
- authUrl: normalizedAuthUrl
218
+ const result = spawnSync(binaryPath(target), process.argv.slice(2), {
219
+ stdio: 'inherit',
220
+ env: process.env
429
221
  });
430
- if (!auth || auth.valid !== true || !auth.token) {
431
- throw new Error(auth && auth.reason ? auth.reason : '授权验证失败,未写入本地配置');
432
- }
433
222
 
434
- const existingConfig = readLocalConfig();
435
- const nextConfig = {
436
- ...existingConfig,
437
- yixinPhone: normalizedPhone,
438
- updatedAt: new Date().toISOString()
439
- };
440
- delete nextConfig.yixinKey;
441
- if (hasCustomAuthUrl) {
442
- nextConfig.yixinAuthUrl = normalizedAuthUrl;
443
- } else {
444
- delete nextConfig.yixinAuthUrl;
223
+ if (result.error) {
224
+ throw result.error;
445
225
  }
446
- writeLocalConfig(nextConfig);
447
-
448
- return [
449
- 'Yixin 授权配置已写入本机',
450
- `skill: ${normalizedSkill}`,
451
- `auth_url: ${hasCustomAuthUrl ? normalizedAuthUrl : `${DEFAULT_AUTH_URL} (default)`}`,
452
- `config_file: ${CONFIG_FILE}`,
453
- '后续 check_auth/run_ads_report 会优先使用环境变量;环境变量缺失时使用本机配置'
454
- ].join('\n');
455
- }
456
-
457
- function normalizePeriod(period) {
458
- const value = String(period || '').trim();
459
- return PERIOD_ALIASES[value] || PERIOD_ALIASES[value.toLowerCase()] || value;
460
- }
461
-
462
- function registerTools(server) {
463
- server.registerTool(
464
- 'configure_auth',
465
- {
466
- title: 'Configure Auth',
467
- description: '验证并保存译心授权手机号到本机配置;授权服务地址默认内置,也可用 authUrl 或 YIXIN_AUTH_URL 覆盖。',
468
- inputSchema: {
469
- phone: z
470
- .string()
471
- .min(1)
472
- .describe('客户授权手机号。'),
473
- authUrl: z
474
- .string()
475
- .optional()
476
- .describe(`授权服务地址,默认 ${DEFAULT_AUTH_URL}。`),
477
- skill: z
478
- .string()
479
- .optional()
480
- .describe('授权能力名,默认 ads。')
481
- }
482
- },
483
- async ({ phone, authUrl, skill }) => {
484
- const stdout = await configureAuth({ phone, authUrl, skill });
485
- return {
486
- content: [{ type: 'text', text: stdout }]
487
- };
488
- }
489
- );
490
-
491
- server.registerTool(
492
- 'check_auth',
493
- {
494
- title: 'Check Auth',
495
- description: '检查译心 MCP 环境、二进制安装和授权手机号是否可用;不生成广告报告。',
496
- inputSchema: {
497
- skill: z
498
- .string()
499
- .optional()
500
- .describe('授权能力名,默认 ads。')
501
- }
502
- },
503
- async ({ skill }) => {
504
- const stdout = await checkAuth({ skill });
505
- return {
506
- content: [{ type: 'text', text: stdout }]
507
- };
508
- }
509
- );
510
-
511
- server.registerTool(
512
- 'run_ads_report',
513
- {
514
- title: 'Run Ads Report',
515
- description: '运行本地译心广告报告二进制。period 可用 last-week(上周)、last-month(上月)、yesterday(昨日)、today(今日)、last-7-days(近7天)、last-30-days(近30天),也支持中文别名和下划线别名。',
516
- inputSchema: {
517
- period: z
518
- .string()
519
- .min(1)
520
- .describe('相对时间范围。推荐值:last-week、last-month、yesterday、today、last-7-days、last-30-days。')
521
- }
522
- },
523
- async ({ period }) => {
524
- const stdout = await runAdsReport({ period });
525
- return {
526
- content: [{ type: 'text', text: stdout }]
527
- };
528
- }
529
- );
530
-
531
- // 后续新增能力时,在这里继续注册 server.registerTool(...)。
532
- }
533
-
534
- async function startMcpServer() {
535
- const server = new McpServer({
536
- name: 'yixin-cli',
537
- version: VERSION
538
- });
539
- registerTools(server);
540
-
541
- const transport = new StdioServerTransport();
542
- await server.connect(transport);
543
- console.error(`Yixin MCP Server v${VERSION} running on stdio`);
544
- }
545
-
546
- async function main() {
547
- await startMcpServer();
226
+ process.exit(result.status === null ? 1 : result.status);
548
227
  }
549
228
 
550
229
  main().catch((error) => {
551
- console.error(`[yixin] ${error.message}`);
552
- process.exit(1);
230
+ fail(error.message);
553
231
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yixinkj/cli",
3
- "version": "1.0.8",
4
- "description": "Yixin local MCP server and CLI launcher for MCP clients.",
3
+ "version": "1.0.10",
4
+ "description": "Yixin native CLI launcher.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "yixin": "index.js"
@@ -10,10 +10,7 @@
10
10
  "index.js",
11
11
  "README.md"
12
12
  ],
13
- "dependencies": {
14
- "@modelcontextprotocol/sdk": "^1.25.2",
15
- "zod": "^3.25.76"
16
- },
13
+ "dependencies": {},
17
14
  "engines": {
18
15
  "node": ">=18"
19
16
  },