@yixinkj/cli 1.0.1 → 1.0.3
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 +10 -6
- package/index.js +173 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,16 +36,20 @@ YIXIN_AUTH_URL=https://你的授权服务域名/v1/token
|
|
|
36
36
|
1. 从环境变量读取 `YIXIN_KEY`。
|
|
37
37
|
2. 向 `YIXIN_AUTH_URL` 提交 `{ key, skill: "ads", version }`。
|
|
38
38
|
3. 获取短期授权 token。
|
|
39
|
-
4.
|
|
39
|
+
4. 如当前版本二进制尚未安装,则从公开 GitHub Release 下载对应平台包并解压到 `~/.yixin/bin`。
|
|
40
|
+
5. 使用以下参数运行 `~/.yixin/bin/alibaba-cli-{platform}`:
|
|
40
41
|
|
|
41
42
|
```text
|
|
42
43
|
--ads --period <period>
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
6. 将原生二进制的 stdout 返回给 MCP 客户端。
|
|
46
47
|
|
|
47
|
-
##
|
|
48
|
+
## 二进制发布包
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- `
|
|
50
|
+
默认从 `yixinkj/cli-releases` 下载与 npm 包版本一致的 release asset:
|
|
51
|
+
|
|
52
|
+
- `darwin-arm64` -> `yixin-mac-arm64.tar.gz`
|
|
53
|
+
- `win32-x64` -> `yixin-win-x64.zip`
|
|
54
|
+
|
|
55
|
+
可通过 `YIXIN_RELEASE_REPO` 或 `YIXIN_RELEASE_BASE_URL` 覆盖下载来源。
|
package/index.js
CHANGED
|
@@ -16,12 +16,24 @@ const require = createRequire(import.meta.url);
|
|
|
16
16
|
const packageJson = require('./package.json');
|
|
17
17
|
|
|
18
18
|
const VERSION = packageJson.version;
|
|
19
|
+
const RELEASE_REPO = process.env.YIXIN_RELEASE_REPO || 'yixinkj/cli-releases';
|
|
20
|
+
const CACHE_ROOT = path.join(os.homedir(), '.yixin');
|
|
19
21
|
const BIN_DIR = path.join(os.homedir(), '.yixin', 'bin');
|
|
22
|
+
const VERSION_FILE = path.join(CACHE_ROOT, 'version');
|
|
20
23
|
|
|
21
|
-
const
|
|
22
|
-
'darwin-arm64':
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const PLATFORM_TARGETS = {
|
|
25
|
+
'darwin-arm64': {
|
|
26
|
+
id: 'mac-arm64',
|
|
27
|
+
archive: 'yixin-mac-arm64.tar.gz',
|
|
28
|
+
mainBinary: 'alibaba-cli-mac-arm64',
|
|
29
|
+
binaries: ['alibaba-cli-mac-arm64', 'report-engine-mac-arm64', 'browser-bridge-mac-arm64']
|
|
30
|
+
},
|
|
31
|
+
'win32-x64': {
|
|
32
|
+
id: 'win-x64',
|
|
33
|
+
archive: 'yixin-win-x64.zip',
|
|
34
|
+
mainBinary: 'alibaba-cli-win-x64.exe',
|
|
35
|
+
binaries: ['alibaba-cli-win-x64.exe', 'report-engine-win-x64.exe']
|
|
36
|
+
}
|
|
25
37
|
};
|
|
26
38
|
|
|
27
39
|
const PERIOD_ALIASES = {
|
|
@@ -47,25 +59,161 @@ const PERIOD_ALIASES = {
|
|
|
47
59
|
'last-30-days': 'last-30-days'
|
|
48
60
|
};
|
|
49
61
|
|
|
50
|
-
function
|
|
62
|
+
function detectTarget() {
|
|
51
63
|
const platformKey = `${process.platform}-${process.arch}`;
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
54
|
-
throw new Error(`暂不支持当前平台:${process.platform}/${process.arch}。已支持:mac-arm64、
|
|
64
|
+
const target = PLATFORM_TARGETS[platformKey];
|
|
65
|
+
if (!target) {
|
|
66
|
+
throw new Error(`暂不支持当前平台:${process.platform}/${process.arch}。已支持:mac-arm64、win-x64。`);
|
|
67
|
+
}
|
|
68
|
+
return target;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function binaryPath(target) {
|
|
72
|
+
return path.join(BIN_DIR, target.mainBinary);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isInstalled(target) {
|
|
76
|
+
if (!fs.existsSync(VERSION_FILE)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (fs.readFileSync(VERSION_FILE, 'utf8').trim() !== VERSION) {
|
|
80
|
+
return false;
|
|
55
81
|
}
|
|
56
|
-
return path.join(BIN_DIR,
|
|
82
|
+
return target.binaries.every((name) => fs.existsSync(path.join(BIN_DIR, name)));
|
|
57
83
|
}
|
|
58
84
|
|
|
59
|
-
function
|
|
60
|
-
if (
|
|
61
|
-
|
|
85
|
+
function releaseUrl(target) {
|
|
86
|
+
if (process.env.YIXIN_RELEASE_BASE_URL) {
|
|
87
|
+
return `${process.env.YIXIN_RELEASE_BASE_URL.replace(/\/$/, '')}/${target.archive}`;
|
|
62
88
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
return `https://github.com/${RELEASE_REPO}/releases/download/v${VERSION}/${target.archive}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function downloadFile(url, outputPath, redirectsLeft = 5) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const requestUrl = new URL(url);
|
|
95
|
+
const headers = {
|
|
96
|
+
'User-Agent': '@yixinkj/cli',
|
|
97
|
+
Accept: 'application/octet-stream'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (process.env.YIXIN_GITHUB_TOKEN && requestUrl.hostname.endsWith('github.com')) {
|
|
101
|
+
headers.Authorization = `Bearer ${process.env.YIXIN_GITHUB_TOKEN}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
https.get(url, { headers }, (response) => {
|
|
105
|
+
if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
|
|
106
|
+
response.resume();
|
|
107
|
+
if (redirectsLeft <= 0) {
|
|
108
|
+
reject(new Error(`下载时重定向次数过多:${url}`));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const location = response.headers.location;
|
|
112
|
+
if (!location) {
|
|
113
|
+
reject(new Error(`重定向响应缺少 Location:${url}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const nextUrl = new URL(location, url).toString();
|
|
117
|
+
downloadFile(nextUrl, outputPath, redirectsLeft - 1).then(resolve, reject);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (response.statusCode !== 200) {
|
|
122
|
+
response.resume();
|
|
123
|
+
reject(new Error(`下载失败:HTTP ${response.statusCode} ${url}`));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const file = fs.createWriteStream(outputPath);
|
|
128
|
+
response.pipe(file);
|
|
129
|
+
file.on('finish', () => {
|
|
130
|
+
file.close(resolve);
|
|
131
|
+
});
|
|
132
|
+
file.on('error', reject);
|
|
133
|
+
}).on('error', reject);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function runCommand(command, args) {
|
|
138
|
+
const result = spawnSync(command, args, {
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
141
|
+
});
|
|
142
|
+
if (result.error) {
|
|
143
|
+
throw result.error;
|
|
144
|
+
}
|
|
145
|
+
if (result.status !== 0) {
|
|
146
|
+
const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
147
|
+
throw new Error(detail || `${command} 退出码为 ${result.status}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractArchive(archivePath, target) {
|
|
152
|
+
fs.rmSync(BIN_DIR, { recursive: true, force: true });
|
|
153
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
154
|
+
|
|
155
|
+
if (target.archive.endsWith('.tar.gz')) {
|
|
156
|
+
runCommand('tar', ['-xzf', archivePath, '-C', BIN_DIR]);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (target.archive.endsWith('.zip')) {
|
|
161
|
+
if (process.platform === 'win32') {
|
|
162
|
+
runCommand('powershell.exe', [
|
|
163
|
+
'-NoProfile',
|
|
164
|
+
'-ExecutionPolicy',
|
|
165
|
+
'Bypass',
|
|
166
|
+
'-Command',
|
|
167
|
+
`Expand-Archive -LiteralPath ${JSON.stringify(archivePath)} -DestinationPath ${JSON.stringify(BIN_DIR)} -Force`
|
|
168
|
+
]);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
runCommand('unzip', ['-q', archivePath, '-d', BIN_DIR]);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw new Error(`暂不支持的压缩包类型:${target.archive}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function chmodExecutables(target) {
|
|
179
|
+
if (process.platform === 'win32') {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const name of target.binaries) {
|
|
183
|
+
const filePath = path.join(BIN_DIR, name);
|
|
184
|
+
if (fs.existsSync(filePath)) {
|
|
185
|
+
fs.chmodSync(filePath, 0o755);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function ensureInstalled(target) {
|
|
191
|
+
if (isInstalled(target)) {
|
|
192
|
+
chmodExecutables(target);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'yixin-cli-'));
|
|
197
|
+
const archivePath = path.join(tempDir, target.archive);
|
|
198
|
+
const url = releaseUrl(target);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
console.error(`[yixin] 正在安装 ${target.id} 二进制,版本 ${VERSION}`);
|
|
202
|
+
console.error(`[yixin] 正在下载 ${url}`);
|
|
203
|
+
await downloadFile(url, archivePath);
|
|
204
|
+
extractArchive(archivePath, target);
|
|
205
|
+
chmodExecutables(target);
|
|
206
|
+
|
|
207
|
+
for (const name of target.binaries) {
|
|
208
|
+
const filePath = path.join(BIN_DIR, name);
|
|
209
|
+
if (!fs.existsSync(filePath)) {
|
|
210
|
+
throw new Error(`压缩包中缺少 ${name}`);
|
|
211
|
+
}
|
|
68
212
|
}
|
|
213
|
+
fs.mkdirSync(CACHE_ROOT, { recursive: true });
|
|
214
|
+
fs.writeFileSync(VERSION_FILE, `${VERSION}\n`);
|
|
215
|
+
} finally {
|
|
216
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
69
217
|
}
|
|
70
218
|
}
|
|
71
219
|
|
|
@@ -132,9 +280,10 @@ async function authorize(skill) {
|
|
|
132
280
|
return auth.token;
|
|
133
281
|
}
|
|
134
282
|
|
|
135
|
-
function runNative({
|
|
136
|
-
|
|
137
|
-
const
|
|
283
|
+
async function runNative({ target, args, token }) {
|
|
284
|
+
await ensureInstalled(target);
|
|
285
|
+
const executablePath = binaryPath(target);
|
|
286
|
+
const result = spawnSync(executablePath, args, {
|
|
138
287
|
encoding: 'utf8',
|
|
139
288
|
env: {
|
|
140
289
|
...process.env,
|
|
@@ -147,7 +296,7 @@ function runNative({ binaryPath, args, token }) {
|
|
|
147
296
|
}
|
|
148
297
|
if (result.status !== 0) {
|
|
149
298
|
const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
150
|
-
throw new Error(detail || `${
|
|
299
|
+
throw new Error(detail || `${executablePath} exited with code ${result.status}`);
|
|
151
300
|
}
|
|
152
301
|
return result.stdout || '';
|
|
153
302
|
}
|
|
@@ -159,8 +308,9 @@ async function runAdsReport({ period }) {
|
|
|
159
308
|
}
|
|
160
309
|
|
|
161
310
|
const token = await authorize('ads');
|
|
162
|
-
const
|
|
163
|
-
|
|
311
|
+
const target = detectTarget();
|
|
312
|
+
const stdout = await runNative({
|
|
313
|
+
target,
|
|
164
314
|
args: ['--ads', '--period', normalizedPeriod],
|
|
165
315
|
token
|
|
166
316
|
});
|