@bigbrain-work/mcp-connect 1.3.1 → 1.3.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/package.json +1 -1
- package/src/cli.js +47 -14
- package/src/constants.js +6 -1
- package/src/device-auth-client.js +75 -0
- package/src/token-store.js +1 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -22,6 +22,8 @@ import { detectInstalledAgents } from "./detection.js";
|
|
|
22
22
|
import {
|
|
23
23
|
DeviceAuthClient,
|
|
24
24
|
DeviceAuthError,
|
|
25
|
+
createLoginQrCode,
|
|
26
|
+
removeLoginQrCode,
|
|
25
27
|
renderQrCode,
|
|
26
28
|
waitForDeviceAuthorization,
|
|
27
29
|
} from "./device-auth-client.js";
|
|
@@ -110,7 +112,7 @@ async function legacyLogin({ home, dryRun, url }) {
|
|
|
110
112
|
);
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
function buildPendingLogin(start, authUrl, allowLocalhost) {
|
|
115
|
+
function buildPendingLogin(start, authUrl, allowLocalhost, qrCodePath) {
|
|
114
116
|
return {
|
|
115
117
|
sessionId: normalizeDeviceUserCode(start.user_code),
|
|
116
118
|
deviceCode: start.device_code,
|
|
@@ -118,10 +120,14 @@ function buildPendingLogin(start, authUrl, allowLocalhost) {
|
|
|
118
120
|
interval: Number(start.interval) || 5,
|
|
119
121
|
authUrl,
|
|
120
122
|
allowLocalhost,
|
|
123
|
+
qrCodePath,
|
|
121
124
|
};
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
export function buildLoginInstructions(
|
|
127
|
+
export function buildLoginInstructions(
|
|
128
|
+
start,
|
|
129
|
+
{ allowLocalhost = false, qrCodePath } = {},
|
|
130
|
+
) {
|
|
125
131
|
const sessionId = normalizeDeviceUserCode(start.user_code);
|
|
126
132
|
const localhostOption = allowLocalhost ? " --allow-localhost" : "";
|
|
127
133
|
return {
|
|
@@ -132,10 +138,12 @@ export function buildLoginInstructions(start, { allowLocalhost = false } = {}) {
|
|
|
132
138
|
start.verification_uri_complete ||
|
|
133
139
|
start.qr_code_uri ||
|
|
134
140
|
start.verification_uri,
|
|
141
|
+
qr_code_path: qrCodePath,
|
|
142
|
+
qr_code_mime_type: "image/png",
|
|
135
143
|
expires_in: Number(start.expires_in),
|
|
136
144
|
poll_command: `shiliu login poll --session ${sessionId} --wait --json${localhostOption}`,
|
|
137
145
|
next_action_hint:
|
|
138
|
-
"
|
|
146
|
+
"二维码已由石榴 CLI 生成。请直接向用户展示 qr_code_path 指向的图片,让用户使用微信扫码授权;不要自行生成二维码,也不要把 verification_uri 当作普通网页打开。用户确认后运行 poll_command。",
|
|
139
147
|
};
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -151,22 +159,41 @@ async function startPendingDeviceLogin({
|
|
|
151
159
|
}) {
|
|
152
160
|
const client = new DeviceAuthClient({ baseUrl: authUrl });
|
|
153
161
|
const start = await client.start();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
const sessionId = normalizeDeviceUserCode(start.user_code);
|
|
163
|
+
const qrCodeValue =
|
|
164
|
+
start.qr_code_uri ||
|
|
165
|
+
start.verification_uri_complete ||
|
|
166
|
+
start.verification_uri;
|
|
167
|
+
const qrCodePath = await createLoginQrCode(qrCodeValue, sessionId);
|
|
168
|
+
try {
|
|
169
|
+
await pendingLoginStore.save(
|
|
170
|
+
buildPendingLogin(start, authUrl, allowLocalhost, qrCodePath),
|
|
171
|
+
);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
await removeLoginQrCode(qrCodePath);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const instructions = buildLoginInstructions(start, {
|
|
177
|
+
allowLocalhost,
|
|
178
|
+
qrCodePath,
|
|
179
|
+
});
|
|
158
180
|
if (json) {
|
|
159
181
|
printJson(instructions);
|
|
160
182
|
return;
|
|
161
183
|
}
|
|
162
184
|
console.log(`请使用微信扫描二维码完成登录(验证码 ${start.user_code}):`);
|
|
163
|
-
console.log(await renderQrCode(
|
|
164
|
-
console.log(
|
|
185
|
+
console.log(await renderQrCode(qrCodeValue));
|
|
186
|
+
console.log(`二维码图片:${instructions.qr_code_path}`);
|
|
165
187
|
console.log(
|
|
166
188
|
`完成后运行:${instructions.poll_command.replace(" --json", "")}`,
|
|
167
189
|
);
|
|
168
190
|
}
|
|
169
191
|
|
|
192
|
+
async function clearPendingLogin(pendingLoginStore, pending) {
|
|
193
|
+
await pendingLoginStore.clear();
|
|
194
|
+
await removeLoginQrCode(pending?.qrCodePath);
|
|
195
|
+
}
|
|
196
|
+
|
|
170
197
|
async function pollPendingDeviceLogin({
|
|
171
198
|
session,
|
|
172
199
|
wait,
|
|
@@ -181,7 +208,7 @@ async function pollPendingDeviceLogin({
|
|
|
181
208
|
throw new Error("未找到对应的待处理登录会话,请重新运行 shiliu login");
|
|
182
209
|
}
|
|
183
210
|
if (pending.expiresAt <= Date.now()) {
|
|
184
|
-
await pendingLoginStore
|
|
211
|
+
await clearPendingLogin(pendingLoginStore, pending);
|
|
185
212
|
throw new Error("登录会话已过期,请重新运行 shiliu login");
|
|
186
213
|
}
|
|
187
214
|
|
|
@@ -222,8 +249,13 @@ async function pollPendingDeviceLogin({
|
|
|
222
249
|
else console.log("登录尚未完成,请授权后重新运行并加上 --wait。");
|
|
223
250
|
return;
|
|
224
251
|
}
|
|
225
|
-
if (
|
|
226
|
-
|
|
252
|
+
if (
|
|
253
|
+
error instanceof DeviceAuthError &&
|
|
254
|
+
["expired_token", "access_denied", "authorization_declined"].includes(
|
|
255
|
+
error.code,
|
|
256
|
+
)
|
|
257
|
+
) {
|
|
258
|
+
await clearPendingLogin(pendingLoginStore, pending);
|
|
227
259
|
}
|
|
228
260
|
throw error;
|
|
229
261
|
}
|
|
@@ -234,7 +266,7 @@ async function pollPendingDeviceLogin({
|
|
|
234
266
|
tokenStore,
|
|
235
267
|
url,
|
|
236
268
|
});
|
|
237
|
-
await pendingLoginStore
|
|
269
|
+
await clearPendingLogin(pendingLoginStore, pending);
|
|
238
270
|
const result = {
|
|
239
271
|
status: "success",
|
|
240
272
|
message: "登录成功",
|
|
@@ -341,7 +373,8 @@ export async function logout({
|
|
|
341
373
|
}
|
|
342
374
|
|
|
343
375
|
await tokenStore.clear();
|
|
344
|
-
await pendingLoginStore.
|
|
376
|
+
const pending = await pendingLoginStore.load();
|
|
377
|
+
await clearPendingLogin(pendingLoginStore, pending);
|
|
345
378
|
await clearLegacyApiKey({ home, dryRun: false });
|
|
346
379
|
console.log("已退出登录,并从系统凭据库清除令牌和旧版兼容凭据。");
|
|
347
380
|
}
|
package/src/constants.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const packageMetadata = require("../package.json");
|
|
5
|
+
|
|
1
6
|
export const PACKAGE_NAME = "@bigbrain-work/mcp-connect";
|
|
2
|
-
export const PACKAGE_VERSION =
|
|
7
|
+
export const PACKAGE_VERSION = packageMetadata.version;
|
|
3
8
|
export const SERVER_NAME = "shiliu_mcp";
|
|
4
9
|
export const MCP_URL = "https://api.bigbrain.work/shiliu/mcp";
|
|
5
10
|
export const AUTH_URL = "https://api.bigbrain.work/shiliu/auth/device";
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { chmod, mkdir, readdir, stat, unlink } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
1
5
|
import QRCode from "qrcode";
|
|
2
6
|
|
|
3
7
|
import { AUTH_URL } from "./constants.js";
|
|
@@ -97,6 +101,77 @@ export async function renderQrCode(value) {
|
|
|
97
101
|
});
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
const QR_CODE_DIRECTORY = "shiliu-ai";
|
|
105
|
+
const QR_CODE_PREFIX = "shiliu-login-";
|
|
106
|
+
const QR_CODE_MAX_AGE_MS = 15 * 60 * 1000;
|
|
107
|
+
|
|
108
|
+
function qrCodeDirectory(temporaryDirectory = os.tmpdir()) {
|
|
109
|
+
return path.resolve(temporaryDirectory, QR_CODE_DIRECTORY);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isManagedQrCodePath(filePath, temporaryDirectory = os.tmpdir()) {
|
|
113
|
+
if (typeof filePath !== "string" || filePath.length === 0) return false;
|
|
114
|
+
const resolvedPath = path.resolve(filePath);
|
|
115
|
+
return (
|
|
116
|
+
path.dirname(resolvedPath) === qrCodeDirectory(temporaryDirectory) &&
|
|
117
|
+
path.basename(resolvedPath).startsWith(QR_CODE_PREFIX) &&
|
|
118
|
+
path.extname(resolvedPath).toLowerCase() === ".png"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function removeStaleQrCodes(directory, now) {
|
|
123
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
124
|
+
await Promise.all(
|
|
125
|
+
entries
|
|
126
|
+
.filter(
|
|
127
|
+
(entry) =>
|
|
128
|
+
entry.isFile() &&
|
|
129
|
+
entry.name.startsWith(QR_CODE_PREFIX) &&
|
|
130
|
+
entry.name.endsWith(".png"),
|
|
131
|
+
)
|
|
132
|
+
.map(async (entry) => {
|
|
133
|
+
const filePath = path.join(directory, entry.name);
|
|
134
|
+
const metadata = await stat(filePath);
|
|
135
|
+
if (now() - metadata.mtimeMs > QR_CODE_MAX_AGE_MS) {
|
|
136
|
+
await unlink(filePath).catch(() => {});
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function createLoginQrCode(
|
|
143
|
+
value,
|
|
144
|
+
sessionId,
|
|
145
|
+
{ temporaryDirectory = os.tmpdir(), now = Date.now } = {},
|
|
146
|
+
) {
|
|
147
|
+
if (!/^[A-Z0-9-]{4,64}$/u.test(sessionId)) {
|
|
148
|
+
throw new Error("登录会话编号格式无效");
|
|
149
|
+
}
|
|
150
|
+
const directory = qrCodeDirectory(temporaryDirectory);
|
|
151
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
152
|
+
await removeStaleQrCodes(directory, now);
|
|
153
|
+
const filePath = path.join(directory, `${QR_CODE_PREFIX}${sessionId}.png`);
|
|
154
|
+
await QRCode.toFile(filePath, value, {
|
|
155
|
+
type: "png",
|
|
156
|
+
errorCorrectionLevel: "M",
|
|
157
|
+
margin: 2,
|
|
158
|
+
width: 480,
|
|
159
|
+
});
|
|
160
|
+
await chmod(filePath, 0o600).catch(() => {});
|
|
161
|
+
return path.resolve(filePath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function removeLoginQrCode(
|
|
165
|
+
filePath,
|
|
166
|
+
{ temporaryDirectory = os.tmpdir() } = {},
|
|
167
|
+
) {
|
|
168
|
+
if (!isManagedQrCodePath(filePath, temporaryDirectory)) return false;
|
|
169
|
+
await unlink(filePath).catch((error) => {
|
|
170
|
+
if (error?.code !== "ENOENT") throw error;
|
|
171
|
+
});
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
100
175
|
function defaultSleep(milliseconds) {
|
|
101
176
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
102
177
|
}
|
package/src/token-store.js
CHANGED
|
@@ -84,6 +84,7 @@ function validatePendingLogin(value) {
|
|
|
84
84
|
typeof value.expiresAt !== "number" ||
|
|
85
85
|
typeof value.interval !== "number" ||
|
|
86
86
|
typeof value.authUrl !== "string" ||
|
|
87
|
+
(value.qrCodePath !== undefined && typeof value.qrCodePath !== "string") ||
|
|
87
88
|
(value.allowLocalhost !== undefined &&
|
|
88
89
|
typeof value.allowLocalhost !== "boolean")
|
|
89
90
|
) {
|