@mkterswingman/5mghost-yonder 0.0.29 → 0.0.32
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/dist/cli/index.js +0 -0
- package/dist/cli/setupCookies.js +12 -1
- package/dist/download/downloader.js +16 -5
- package/dist/tools/downloads.js +11 -1
- package/dist/utils/cookieRefresh.d.ts +12 -10
- package/dist/utils/cookieRefresh.js +84 -19
- package/package.json +1 -1
- package/skills/use-yt-mcp/SKILL.md +3 -1
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/cli/setupCookies.js
CHANGED
|
@@ -251,7 +251,18 @@ export async function readImportedBrowserCookies(candidate, chromium, deps) {
|
|
|
251
251
|
"--headless=new",
|
|
252
252
|
"--no-first-run",
|
|
253
253
|
"--no-default-browser-check",
|
|
254
|
-
|
|
254
|
+
// Why: these flags prevent Chrome's background services from contacting Google
|
|
255
|
+
// servers, which would trigger Account Reconcilor / token rotation and invalidate
|
|
256
|
+
// the user's real Chrome YouTube session.
|
|
257
|
+
"--disable-sync",
|
|
258
|
+
"--disable-background-networking",
|
|
259
|
+
"--disable-features=AccountReconcilor,IdentityManager",
|
|
260
|
+
"--disable-client-side-phishing-detection",
|
|
261
|
+
"--disable-component-update",
|
|
262
|
+
"--disable-extensions",
|
|
263
|
+
"--disable-breakpad",
|
|
264
|
+
"--metrics-recording-only",
|
|
265
|
+
"about:blank",
|
|
255
266
|
], {
|
|
256
267
|
detached: true,
|
|
257
268
|
stdio: "ignore",
|
|
@@ -261,14 +261,25 @@ function assertManagerInput(manager, jobId) {
|
|
|
261
261
|
throw new Error("jobManager and jobId must be provided together.");
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
+
const VIDEO_FORMAT_BY_QUALITY = {
|
|
265
|
+
"2160p60": "bv*[height<=2160][fps<=60]+ba/b[height<=2160]/b",
|
|
266
|
+
"1440p60": "bv*[height<=1440][fps<=60]+ba/b[height<=1440]/b",
|
|
267
|
+
"1080p60": "bv*[height<=1080][fps<=60]+ba/b[height<=1080]/b",
|
|
268
|
+
"1080p": "bv*[height<=1080]+ba/b[height<=1080]/b",
|
|
269
|
+
"720p60": "bv*[height<=720][fps<=60]+ba/b[height<=720]/b",
|
|
270
|
+
"480p": "bv*[height<=480]+ba/b[height<=480]/b",
|
|
271
|
+
"360p": "bv*[height<=360]+ba/b[height<=360]/b",
|
|
272
|
+
"240p": "bv*[height<=240]+ba/b[height<=240]/b",
|
|
273
|
+
max: "bv*+ba/b",
|
|
274
|
+
};
|
|
264
275
|
function normalizeVideoQuality(videoQuality) {
|
|
265
|
-
|
|
276
|
+
if (videoQuality && Object.hasOwn(VIDEO_FORMAT_BY_QUALITY, videoQuality)) {
|
|
277
|
+
return videoQuality;
|
|
278
|
+
}
|
|
279
|
+
return "1080p";
|
|
266
280
|
}
|
|
267
281
|
function resolveVideoFormat(videoQuality) {
|
|
268
|
-
|
|
269
|
-
return "bv*+ba/b";
|
|
270
|
-
}
|
|
271
|
-
return "bv*[height<=1080]+ba/b[height<=1080]/b";
|
|
282
|
+
return VIDEO_FORMAT_BY_QUALITY[videoQuality] ?? VIDEO_FORMAT_BY_QUALITY["1080p"];
|
|
272
283
|
}
|
|
273
284
|
function needsVideo(mode) {
|
|
274
285
|
return mode === "video" || mode === "both";
|
package/dist/tools/downloads.js
CHANGED
|
@@ -5,7 +5,17 @@ import { buildMediaOutputPaths } from "../utils/mediaPaths.js";
|
|
|
5
5
|
import { resolveVideoInput } from "../utils/videoInput.js";
|
|
6
6
|
const AUTH_REQUIRED_MSG = "❌ 未认证。请先登录:\n• OAuth: npx @mkterswingman/5mghost-yonder setup\n• PAT: 设置环境变量 YT_MCP_TOKEN 或在 https://mkterswingman.com/pat/login 生成 token";
|
|
7
7
|
const DOWNLOAD_MODES = ["video", "subtitles", "both"];
|
|
8
|
-
const VIDEO_QUALITIES = [
|
|
8
|
+
const VIDEO_QUALITIES = [
|
|
9
|
+
"2160p60",
|
|
10
|
+
"1440p60",
|
|
11
|
+
"1080p60",
|
|
12
|
+
"1080p",
|
|
13
|
+
"720p60",
|
|
14
|
+
"480p",
|
|
15
|
+
"360p",
|
|
16
|
+
"240p",
|
|
17
|
+
"max",
|
|
18
|
+
];
|
|
9
19
|
const SUBTITLE_FORMATS = ["vtt", "srt", "ttml", "srv3", "csv"];
|
|
10
20
|
const MAX_DOWNLOAD_VIDEOS = 5;
|
|
11
21
|
class DownloadToolError extends Error {
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cookie auto-refresh with three-tier fallback.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* extracted and saved automatically — no user interaction needed.
|
|
4
|
+
* Tier 1: Headless Playwright refresh using existing browser-profile.
|
|
5
|
+
* If Google session is still valid, cookies are extracted automatically.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Tier 2: Re-import from system Chrome/Edge (headless, about:blank).
|
|
8
|
+
* Copies the real browser profile to a temp dir and reads cookies via CDP
|
|
9
|
+
* without making any network requests (avoids Google token rotation).
|
|
10
|
+
*
|
|
11
|
+
* Tier 3: Open a headed browser for manual user login.
|
|
12
|
+
* Blocks the MCP tool call for up to ~2 minutes while the user logs in.
|
|
13
|
+
* Creates browser-profile so Tier 1 works next time.
|
|
10
14
|
*/
|
|
11
15
|
/**
|
|
12
|
-
* Attempt to refresh YouTube cookies
|
|
16
|
+
* Attempt to refresh YouTube cookies through a three-tier fallback chain.
|
|
13
17
|
*
|
|
14
|
-
* @returns true if cookies were refreshed, false if
|
|
15
|
-
* (needs interactive login).
|
|
16
|
-
* @throws if Playwright is not installed or browser can't launch.
|
|
18
|
+
* @returns true if cookies were refreshed, false if all tiers failed.
|
|
17
19
|
*/
|
|
18
20
|
export declare function tryHeadlessRefresh(): Promise<boolean>;
|
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cookie auto-refresh with three-tier fallback.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* extracted and saved automatically — no user interaction needed.
|
|
4
|
+
* Tier 1: Headless Playwright refresh using existing browser-profile.
|
|
5
|
+
* If Google session is still valid, cookies are extracted automatically.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Tier 2: Re-import from system Chrome/Edge (headless, about:blank).
|
|
8
|
+
* Copies the real browser profile to a temp dir and reads cookies via CDP
|
|
9
|
+
* without making any network requests (avoids Google token rotation).
|
|
10
|
+
*
|
|
11
|
+
* Tier 3: Open a headed browser for manual user login.
|
|
12
|
+
* Blocks the MCP tool call for up to ~2 minutes while the user logs in.
|
|
13
|
+
* Creates browser-profile so Tier 1 works next time.
|
|
10
14
|
*/
|
|
11
15
|
import { existsSync } from "node:fs";
|
|
12
16
|
import { PATHS, ensureConfigDir } from "./config.js";
|
|
13
17
|
import { detectBrowserChannel, hasYouTubeSession, saveCookiesToDisk, } from "../cli/setupCookies.js";
|
|
18
|
+
import { hasSIDCookies } from "./cookies.js";
|
|
14
19
|
const HEADLESS_TIMEOUT_MS = 30_000;
|
|
15
20
|
/**
|
|
16
|
-
* Attempt to refresh YouTube cookies
|
|
21
|
+
* Attempt to refresh YouTube cookies through a three-tier fallback chain.
|
|
17
22
|
*
|
|
18
|
-
* @returns true if cookies were refreshed, false if
|
|
19
|
-
* (needs interactive login).
|
|
20
|
-
* @throws if Playwright is not installed or browser can't launch.
|
|
23
|
+
* @returns true if cookies were refreshed, false if all tiers failed.
|
|
21
24
|
*/
|
|
22
25
|
export async function tryHeadlessRefresh() {
|
|
23
|
-
// No browser profile = never logged in = can't refresh
|
|
24
|
-
if (!existsSync(PATHS.browserProfile)) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
26
|
ensureConfigDir();
|
|
28
|
-
//
|
|
27
|
+
// Tier 1: Playwright persistent context (requires browser-profile from prior manual login)
|
|
28
|
+
if (existsSync(PATHS.browserProfile)) {
|
|
29
|
+
const tier1 = await tryPlaywrightRefresh();
|
|
30
|
+
if (tier1)
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// Tier 2: Re-import from system browser (headless, no network requests)
|
|
34
|
+
const tier2 = await tryReimportFromSystemBrowser();
|
|
35
|
+
if (tier2)
|
|
36
|
+
return true;
|
|
37
|
+
// Tier 3: Open headed browser for manual login
|
|
38
|
+
const tier3 = await tryHeadedManualLogin();
|
|
39
|
+
return tier3;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Tier 1: Use existing browser-profile with Playwright to refresh cookies.
|
|
43
|
+
* This is the original tryHeadlessRefresh logic.
|
|
44
|
+
*/
|
|
45
|
+
async function tryPlaywrightRefresh() {
|
|
29
46
|
let chromium;
|
|
30
47
|
try {
|
|
31
48
|
const pw = await import("playwright");
|
|
32
49
|
chromium = pw.chromium;
|
|
33
50
|
}
|
|
34
51
|
catch {
|
|
35
|
-
return false;
|
|
52
|
+
return false;
|
|
36
53
|
}
|
|
37
54
|
const channel = await detectBrowserChannel(chromium);
|
|
38
55
|
let context;
|
|
@@ -44,7 +61,7 @@ export async function tryHeadlessRefresh() {
|
|
|
44
61
|
});
|
|
45
62
|
}
|
|
46
63
|
catch {
|
|
47
|
-
return false;
|
|
64
|
+
return false;
|
|
48
65
|
}
|
|
49
66
|
try {
|
|
50
67
|
const page = context.pages()[0] ?? (await context.newPage());
|
|
@@ -52,11 +69,9 @@ export async function tryHeadlessRefresh() {
|
|
|
52
69
|
await page.waitForLoadState("domcontentloaded");
|
|
53
70
|
const cookies = await context.cookies("https://www.youtube.com");
|
|
54
71
|
if (!hasYouTubeSession(cookies)) {
|
|
55
|
-
// Google session expired — can't auto-refresh
|
|
56
72
|
await context.close().catch(() => { });
|
|
57
73
|
return false;
|
|
58
74
|
}
|
|
59
|
-
// Wait a moment for all cookies to settle
|
|
60
75
|
await new Promise((r) => setTimeout(r, 1500));
|
|
61
76
|
const finalCookies = await context.cookies("https://www.youtube.com");
|
|
62
77
|
saveCookiesToDisk(finalCookies);
|
|
@@ -68,3 +83,53 @@ export async function tryHeadlessRefresh() {
|
|
|
68
83
|
return false;
|
|
69
84
|
}
|
|
70
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Tier 2: Re-import cookies from the user's system Chrome/Edge.
|
|
88
|
+
* Uses headless Chrome with about:blank to avoid triggering Google token rotation.
|
|
89
|
+
*/
|
|
90
|
+
async function tryReimportFromSystemBrowser() {
|
|
91
|
+
try {
|
|
92
|
+
const pw = await import("playwright");
|
|
93
|
+
const { tryImportBrowserCookies, saveCookiesAndClose, readImportedBrowserCookies, } = await import("../cli/setupCookies.js");
|
|
94
|
+
const { findImportableBrowserProfileCandidates, prepareImportedBrowserWorkspace, cleanupImportedBrowserWorkspace, } = await import("./browserProfileImport.js");
|
|
95
|
+
const deps = {
|
|
96
|
+
ensureConfigDir,
|
|
97
|
+
loadChromium: async () => pw.chromium,
|
|
98
|
+
detectBrowserChannel,
|
|
99
|
+
hasYouTubeSession,
|
|
100
|
+
saveCookiesAndClose,
|
|
101
|
+
// Why: waitForLogin is unused in import path but required by SetupCookiesDeps type
|
|
102
|
+
waitForLogin: async () => null,
|
|
103
|
+
findImportableBrowserProfileCandidates,
|
|
104
|
+
prepareImportedBrowserWorkspace,
|
|
105
|
+
cleanupImportedBrowserWorkspace,
|
|
106
|
+
readImportedBrowserCookies,
|
|
107
|
+
tryImportBrowserCookies,
|
|
108
|
+
// Why: runManualCookieSetup is unused in import path but required by type
|
|
109
|
+
runManualCookieSetup: async () => { },
|
|
110
|
+
// Why: silent logging during auto-refresh — user doesn't see MCP server stdout
|
|
111
|
+
log: () => { },
|
|
112
|
+
};
|
|
113
|
+
return await tryImportBrowserCookies(pw.chromium, deps);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Tier 3: Open a headed browser for the user to manually log in.
|
|
121
|
+
* Blocks the MCP tool call for up to ~2 minutes.
|
|
122
|
+
* Creates browser-profile so Tier 1 works for future refreshes.
|
|
123
|
+
*/
|
|
124
|
+
async function tryHeadedManualLogin() {
|
|
125
|
+
try {
|
|
126
|
+
// Why: runSetupCookies with { headed: true } skips import and goes straight
|
|
127
|
+
// to manual Playwright login, which creates browser-profile for future Tier 1 use.
|
|
128
|
+
const { runSetupCookies } = await import("../cli/setupCookies.js");
|
|
129
|
+
await runSetupCookies({}, { headed: true });
|
|
130
|
+
return hasSIDCookies(PATHS.cookiesTxt);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: use-yt-mcp
|
|
3
3
|
preamble-tier: 3
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
description: |
|
|
6
6
|
Use when the user wants YouTube analysis through yt-mcp: channel performance,
|
|
7
7
|
video stats, subtitles, comments, trending, or batch YouTube data work.
|
|
@@ -54,6 +54,8 @@ All tools are prefixed `mcp__yt-mcp__` in actual calls.
|
|
|
54
54
|
- If the user provides a fuzzy channel name (for example only a brand/person name), call `search_channels` first and present 3-5 candidates before doing channel analysis.
|
|
55
55
|
- After `search_channels` returns candidates, explicitly ask the user which channel they mean; do not auto-select candidate #1.
|
|
56
56
|
- If the user already provides a channel URL, `@handle`, or explicit `channel_id`, skip candidate search and go directly to `get_channel_stats` / `list_channel_uploads`.
|
|
57
|
+
- For local media download requests where the user says to download/save a video but does not explicitly choose mode, call `start_download_job` with `mode: "both"` so subtitles are downloaded by default.
|
|
58
|
+
- If the user explicitly says no subtitles / video-only (for example "不要字幕", "只下视频"), then call `start_download_job` with `mode: "video"`.
|
|
57
59
|
- `get_video_stats` accepts up to 1000 items. If the response returns `mode: "file"`, then inline `preview_items` is only a preview sample from a larger dataset and must not be used as the full basis of analysis. Tell the user the complete result is in `output_file` or `download_url`.
|
|
58
60
|
- For large video/comment analyses where completeness matters, prefer async jobs: `start_video_stats_job` -> `poll_video_stats_job`, or `start_comments_job` -> `poll_comments_job`.
|
|
59
61
|
- When using async jobs, poll until status is `completed`, and tell the user if the result is still partial or in progress.
|