NotebookLM CLI 로그인 버튼 — nlm 실행 파일 자동 탐색
- 사이드바 도구 메뉴에 'NotebookLM 로그인' 버튼 추가, 파이썬 Scripts 폴더에서 nlm(.exe) 자동 탐색(버전 하드코딩 없음) 후 login 인자로 실행 - g1nation.notebookLmCliPath 설정으로 수동 경로 지정도 가능
This commit is contained in:
@@ -23,6 +23,7 @@ import { execShell } from '../../lib/execUtil';
|
||||
*/
|
||||
|
||||
const TERMINAL_NAME = 'NotebookLM Bridge';
|
||||
const LOGIN_TERMINAL_NAME = 'NotebookLM Login';
|
||||
const BRIDGE_PORT = 3002;
|
||||
/** 자동 탐색 시 찾는 프로젝트 폴더 이름 후보 (맥/윈도우 클론 이름이 다를 수 있음). */
|
||||
const PROJECT_DIR_NAMES = ['Datacollector_MAC', 'Datacollector', 'Datacollect'];
|
||||
@@ -55,6 +56,71 @@ export function buildPortKillCommand(platform: NodeJS.Platform, port: number): s
|
||||
return `lsof -ti :${port} | xargs kill 2>/dev/null`;
|
||||
}
|
||||
|
||||
/**
|
||||
* `nlm` 실행 파일 후보 경로를 우선순위대로 나열한다 (존재 검증 안 함 — 순수 함수).
|
||||
*
|
||||
* nlm 은 pip 로 설치되는 콘솔 스크립트라 PATH 에 없는 경우가 흔하다(실측: 이 PC 도 PATH 에
|
||||
* 없고 %APPDATA%\Python\Python314\Scripts 에만 있었다). 파이썬 버전이 올라가면 경로가
|
||||
* 바뀌므로 특정 버전을 하드코딩하지 않고 Scripts 디렉터리들을 훑는다.
|
||||
* 마지막 후보로 맨 이름('nlm')을 두어 PATH 에 있는 환경에서는 그대로 동작하게 한다.
|
||||
*/
|
||||
export function candidateNlmPaths(
|
||||
platform: NodeJS.Platform,
|
||||
env: Record<string, string | undefined>,
|
||||
pythonScriptDirs: string[] = [],
|
||||
cfgPath = '',
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
const push = (p: string) => { if (p && !out.includes(p)) out.push(p); };
|
||||
if (cfgPath && cfgPath.trim()) push(cfgPath.trim());
|
||||
|
||||
if (platform === 'win32') {
|
||||
for (const d of pythonScriptDirs) push(path.join(d, 'nlm.exe'));
|
||||
const local = env.LOCALAPPDATA;
|
||||
if (local) push(path.join(local, 'Programs', 'Python', 'Scripts', 'nlm.exe'));
|
||||
push('nlm.exe');
|
||||
} else {
|
||||
for (const d of pythonScriptDirs) push(path.join(d, 'nlm'));
|
||||
const home = env.HOME;
|
||||
if (home) {
|
||||
push(path.join(home, '.local', 'bin', 'nlm'));
|
||||
push(path.join(home, 'Library', 'Python', 'bin', 'nlm'));
|
||||
}
|
||||
push('/usr/local/bin/nlm');
|
||||
}
|
||||
push('nlm'); // PATH 폴백
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 파이썬 사용자 설치 경로 아래의 Scripts(win) / bin(mac) 디렉터리들을 나열한다. */
|
||||
function pythonScriptDirs(): string[] {
|
||||
const dirs: string[] = [];
|
||||
const roots = process.platform === 'win32'
|
||||
? [process.env.APPDATA ? path.join(process.env.APPDATA, 'Python') : '']
|
||||
: [process.env.HOME ? path.join(process.env.HOME, 'Library', 'Python') : ''];
|
||||
for (const root of roots.filter(Boolean)) {
|
||||
try {
|
||||
for (const name of fs.readdirSync(root)) {
|
||||
const s = path.join(root, name, 'Scripts');
|
||||
const b = path.join(root, name, 'bin');
|
||||
if (fs.existsSync(s)) dirs.push(s);
|
||||
if (fs.existsSync(b)) dirs.push(b);
|
||||
}
|
||||
} catch { /* 접근 불가 — skip */ }
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
/** 실제 존재하는 nlm 실행 파일 경로. 못 찾으면 null (PATH 폴백은 호출측이 판단). */
|
||||
function findNlmExecutable(): string | null {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation').get<string>('notebookLmCliPath', '') || '';
|
||||
for (const cand of candidateNlmPaths(process.platform, process.env, pythonScriptDirs(), cfg)) {
|
||||
if (!path.isAbsolute(cand)) continue; // PATH 이름은 존재 검증 대상이 아님
|
||||
try { if (fs.existsSync(cand)) return cand; } catch { /* skip */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findBridgeProject(): string | null {
|
||||
const cfgPath = vscode.workspace.getConfiguration('g1nation').get<string>('datacollectLocalProjectPath', '') || '';
|
||||
const wsDirs = (vscode.workspace.workspaceFolders || [])
|
||||
@@ -141,6 +207,48 @@ export async function stopNotebookLmBridge(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotebookLM 재로그인(구글 인증) 원클릭.
|
||||
*
|
||||
* 배경: NotebookLM 토큰은 ~90분에 만료되고, 전용 프로필의 구글 **세션 자체**가 끊기면
|
||||
* 자동 갱신(Datacollect 의 스케줄 작업)이 매번 실패하면서 로그인 창만 반복해서 띄운다.
|
||||
* 복구는 `nlm login` 을 한 번 사람이 실행해 브라우저에서 인증하는 것뿐인데, 그러려면
|
||||
* 터미널과 실행 파일 경로를 알아야 했다 — 그 한 단계를 버튼으로 없앤다.
|
||||
*
|
||||
* 구현: 셸을 거치지 않고 실행 파일을 터미널 프로세스로 직접 띄운다(shellPath/shellArgs).
|
||||
* 경로에 공백이 있어도 따옴표 처리가 필요 없고, PowerShell/cmd/zsh 차이도 사라진다.
|
||||
* 인증 결과(성공/실패)는 터미널에 그대로 보이므로 별도 폴링을 하지 않는다.
|
||||
*/
|
||||
export async function loginNotebookLm(): Promise<void> {
|
||||
let exe = findNlmExecutable();
|
||||
if (!exe) {
|
||||
const pick = await vscode.window.showErrorMessage(
|
||||
'NotebookLM CLI(nlm) 실행 파일을 찾지 못했습니다. 파일을 직접 선택하면 설정에 저장되어 다음부터는 버튼만 누르면 됩니다.',
|
||||
'파일 선택'
|
||||
);
|
||||
if (pick !== '파일 선택') return;
|
||||
const chosen = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true, canSelectFolders: false, canSelectMany: false,
|
||||
openLabel: 'nlm 실행 파일 선택',
|
||||
});
|
||||
const f = chosen?.[0]?.fsPath;
|
||||
if (!f) return;
|
||||
await vscode.workspace.getConfiguration('g1nation')
|
||||
.update('notebookLmCliPath', f, vscode.ConfigurationTarget.Global);
|
||||
exe = f;
|
||||
}
|
||||
|
||||
vscode.window.terminals.find(t => t.name === LOGIN_TERMINAL_NAME)?.dispose();
|
||||
const terminal = vscode.window.createTerminal({
|
||||
name: LOGIN_TERMINAL_NAME, shellPath: exe, shellArgs: ['login'],
|
||||
});
|
||||
terminal.show(true);
|
||||
vscode.window.showInformationMessage(
|
||||
'NotebookLM 로그인을 시작했습니다 — 브라우저에서 구글 인증을 완료하세요. ' +
|
||||
'결과는 "NotebookLM Login" 터미널에 표시됩니다.'
|
||||
);
|
||||
}
|
||||
|
||||
/** 사이드바 웹뷰 메시지 핸들러 — sidebarProvider 의 핸들러 체인에 등록된다. */
|
||||
export async function handleBridgeLauncherMessage(_provider: unknown, data: any): Promise<boolean> {
|
||||
switch (data?.type) {
|
||||
@@ -150,6 +258,9 @@ export async function handleBridgeLauncherMessage(_provider: unknown, data: any)
|
||||
case 'stopNotebookLmBridge':
|
||||
await stopNotebookLmBridge();
|
||||
return true;
|
||||
case 'loginNotebookLm':
|
||||
await loginNotebookLm();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user