diff --git a/README.md b/README.md index bc18ffa..5094e29 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ dsh web - 检测国际正式版 `Tabbit` 和国内正式版 `Tabbit Browser`。 - 要求任一正式版版本不低于 `1.9.0`。 - 检查 `tabbit-cli` 常驻运行时;未运行时提醒用户重启一次 Tabbit。 +- 优先发现 Windows 上 Browser 自带的 + `%LOCALAPPDATA%\Tabbit\LocalAgent\bin\tabbit-cli.exe`,工具结果返回 `cliPath` + 供模型按确切路径调用。 - 多个 Tabbit 实例同时运行时,仍判定 Runtime 可用;模型需根据 CLI 提示设置 `TABBIT_PLAYWRIGHT_INSTANCE`,不会把实例选择歧义误报为 Runtime 未运行。 - 在两者均未安装或所有正式版版本过低时,通过 `ctx.jobs` 后台下载适配系统地区的正式版安装包。 diff --git a/index.js b/index.js index 677d5e8..581b311 100644 --- a/index.js +++ b/index.js @@ -81,6 +81,7 @@ function registerInstallerTool(ctx) { message: { type: 'string' }, jobId: { type: 'string' }, cliReady: { type: 'boolean' }, + cliPath: { type: 'string' }, minimumVersion: { type: 'string' }, playwrightProcessRunning: { type: 'boolean' }, playwrightInstanceCount: { type: 'integer' }, @@ -140,6 +141,7 @@ function registerInstallerTool(ctx) { status: 'ready', message: `Environment check passed. Tabbit is ready${versions ? ` (${versions})` : ''}.${instanceNote}`, cliReady: detected.cliReady, + cliPath: detected.cliPath, minimumVersion: detected.minimumVersion, playwrightProcessRunning: detected.playwrightProcessRunning, playwrightInstanceCount: detected.playwrightInstanceCount, @@ -155,6 +157,7 @@ function registerInstallerTool(ctx) { status: 'restart-required', message: `Environment check failed: ${versions} meets the minimum version ${detected.minimumVersion}, but the tabbit-cli Runtime is not running. Please restart Tabbit Browser once before using browser automation.`, cliReady: detected.cliReady, + cliPath: detected.cliPath, minimumVersion: detected.minimumVersion, playwrightProcessRunning: false, playwrightInstanceCount: detected.playwrightInstanceCount, @@ -185,6 +188,7 @@ function registerInstallerTool(ctx) { message: `Environment check failed: ${downloadReason} Started the region-appropriate Tabbit installer download as ${jobId}. DSH will report progress and notify you when the installer is ready.`, jobId: String(jobId), cliReady: detected.cliReady, + cliPath: detected.cliPath, minimumVersion: detected.minimumVersion, playwrightProcessRunning: false, playwrightInstanceCount: detected.playwrightInstanceCount, diff --git a/installer.js b/installer.js index eb85d22..947c26d 100644 --- a/installer.js +++ b/installer.js @@ -270,9 +270,29 @@ async function findBundledCli(root) { return found } -async function detectCli(userHome = homedir(), platform = process.platform, installations = []) { +const WINDOWS_LOCAL_AGENT_CLI_DIRECTORY = ['Tabbit', 'LocalAgent', 'bin'] + +function localAgentCliCandidates(localAppData) { + if (!localAppData) return [] + const directory = join(localAppData, ...WINDOWS_LOCAL_AGENT_CLI_DIRECTORY) + return [ + join(directory, 'tabbit-cli.exe'), + join(directory, 'tabbit-cli.cmd'), + join(directory, 'tabbit-cli'), + ] +} + +async function detectCli({ + userHome = homedir(), + platform = process.platform, + env = process.env, + installations = [], +} = {}) { const candidates = platform === 'win32' ? [ + // Browser-owned LocalAgent launcher first: it stays current across + // app updates and is the same launcher the browser itself uses. + ...localAgentCliCandidates(env.LOCALAPPDATA), join(userHome, '.local', 'bin', 'tabbit-cli.cmd'), join(userHome, '.local', 'bin', 'tabbit-cli.exe'), join(userHome, '.local', 'bin', 'tabbit-cli'), @@ -311,9 +331,9 @@ export function isVersionAtLeast(version, minimum = MINIMUM_TABBIT_VERSION) { function isTabbitRuntimeProcess(name, command) { const value = `${name ?? ''} ${command ?? ''}` - return /(?:^|[\\/\s])tabbit-cli(?:\.cmd|\.exe)?(?:\s|$)/i.test(value) - || /(?:^|[\\/\s])nodejs-playwright-runtime\.mjs(?:\s|$)/i.test(value) - || /(?:^|[\\/\s])browser-runtime-service\.mjs(?:\s|$)/i.test(value) + return /(?:^|[\\/\s"])tabbit-cli(?:\.cmd|\.exe)?(?:\s|"|$)/i.test(value) + || /(?:^|[\\/\s"])nodejs-playwright-runtime\.mjs(?:\s|"|$)/i.test(value) + || /(?:^|[\\/\s"])browser-runtime-service\.mjs(?:\s|"|$)/i.test(value) } export function parseUnixProcessList(output) { @@ -376,6 +396,7 @@ export function summarizeTabbitRuntime(playwrightProcesses) { export async function detectTabbit({ platform = process.platform, userHome = homedir(), + env = process.env, run = spawnSync, minimumVersion = MINIMUM_TABBIT_VERSION, } = {}) { @@ -384,7 +405,7 @@ export async function detectTabbit({ : platform === 'win32' ? detectWindowsInstallations({ run }) : [] - const cli = await detectCli(userHome, platform, installations) + const cli = await detectCli({ userHome, platform, env, installations }) const supportedInstallations = installations.filter(item => ( isVersionAtLeast(item.version, minimumVersion) )) diff --git a/skills/tabbit-browser/SKILL.md b/skills/tabbit-browser/SKILL.md index e47ad09..d4db8c3 100644 --- a/skills/tabbit-browser/SKILL.md +++ b/skills/tabbit-browser/SKILL.md @@ -15,7 +15,8 @@ plugin. Before the first CLI command, read [`references/platform-invocation.md`](references/platform-invocation.md). Use -the exact launcher path documented there. The launcher must be the first command +the exact launcher path documented there, preferring the `cliPath` reported by +`tabbit_browser_install` when present. The launcher must be the first command token on every invocation; do not wrap it with `env`, `time`, or `sh -lc`. Browser owns the Runtime Service. diff --git a/skills/tabbit-browser/references/platform-invocation.md b/skills/tabbit-browser/references/platform-invocation.md index ed1c9e9..18c11c0 100644 --- a/skills/tabbit-browser/references/platform-invocation.md +++ b/skills/tabbit-browser/references/platform-invocation.md @@ -1,10 +1,13 @@ # Platform invocation -Use the stable Tabbit CLI installed by Tabbit Browser: +Use the stable `tabbit-cli` launcher installed by Tabbit Browser. When +`tabbit_browser_install` returned a `cliPath`, use that exact path. -```text -~/.local/bin/tabbit-cli -``` +Otherwise use the platform default: + +- Windows PowerShell: `$env:LOCALAPPDATA\Tabbit\LocalAgent\bin\tabbit-cli.exe` +- Windows Git Bash: `"$LOCALAPPDATA/Tabbit/LocalAgent/bin/tabbit-cli.exe"` +- macOS/Linux: `~/.local/bin/tabbit-cli` Invoke it as the first command token. For example: diff --git a/tests/installer.test.mjs b/tests/installer.test.mjs index 5c61ef2..da2ad44 100644 --- a/tests/installer.test.mjs +++ b/tests/installer.test.mjs @@ -171,8 +171,17 @@ test('recognizes the persistent tabbit-cli runtime process', () => { const windows = parseWindowsProcessList(JSON.stringify([ { ProcessId: 201, Name: 'tabbit-cli.exe', CommandLine: String.raw`C:\Users\User\.local\bin\tabbit-cli.exe nodejs` }, { ProcessId: 202, Name: 'Tabbit.exe', CommandLine: String.raw`C:\Tabbit\Tabbit.exe` }, + { ProcessId: 203, Name: 'node.exe', CommandLine: String.raw`"E:\Tabbit\Application\1.9.22.0\TabbitDance\node.exe" "E:\Tabbit\Application\1.9.22.0\TabbitDance\runtime\src\browser-runtime-service.mjs" --browser-transport="{}"` }, + { ProcessId: 204, Name: 'tabbit-cli.exe', CommandLine: String.raw`"C:\Users\User\.local\bin\tabbit-cli.exe" nodejs --task x` }, + { ProcessId: 205, Name: 'node.exe', CommandLine: String.raw`"C:\Tabbit\TabbitDance\node.exe" "C:\Tabbit\TabbitDance\runtime\src\nodejs-playwright-runtime.mjs"` }, + { ProcessId: 206, Name: 'Tabbit.exe', CommandLine: String.raw`"C:\Tabbit\Tabbit.exe"` }, ])) - assert.deepEqual(windows, [{ pid: 201, name: 'tabbit-cli.exe' }]) + assert.deepEqual(windows, [ + { pid: 201, name: 'tabbit-cli.exe' }, + { pid: 203, name: 'node.exe' }, + { pid: 204, name: 'tabbit-cli.exe' }, + { pid: 205, name: 'node.exe' }, + ]) }) test('treats multiple runtime processes as running but ambiguous', () => { @@ -299,6 +308,7 @@ HKEY_CURRENT_USER\\Software\\TabbitBrowser const detected = await detectTabbit({ platform: 'win32', userHome: join(root, 'home'), + env: {}, run(command) { if (command === 'reg.exe') return { status: 0, stdout: registry } if (command === 'powershell.exe') { @@ -315,6 +325,54 @@ HKEY_CURRENT_USER\\Software\\TabbitBrowser }, }) assert.equal(detected.cliReady, true) + assert.equal(detected.cliPath, cliPath) + assert.equal(detected.recommendation, 'ready') + } finally { + await rm(root, { recursive: true, force: true }) + } +}) + +test('prefers the Browser-owned Windows LocalAgent CLI over bundled helpers', async () => { + const root = await mkdtemp(join(tmpdir(), 'tabbit-localagent-cli-')) + const userHome = join(root, 'home') + const localAppData = join(root, 'LocalAppData') + const installationPath = join(root, 'Tabbit') + const localAgentCli = join(localAppData, 'Tabbit', 'LocalAgent', 'bin', 'tabbit-cli.exe') + const bundledCli = join(installationPath, 'TabbitDance', 'tabbit-playwright-cli.exe') + await mkdir(join(localAgentCli, '..'), { recursive: true }) + await writeFile(localAgentCli, 'launcher') + await mkdir(join(bundledCli, '..'), { recursive: true }) + await writeFile(bundledCli, 'bundled') + + const registry = ` +HKEY_CURRENT_USER\\Software\\Tabbit + DisplayName REG_SZ Tabbit + DisplayVersion REG_SZ 1.9.22.0 + InstallLocation REG_SZ ${installationPath} +` + try { + const detected = await detectTabbit({ + platform: 'win32', + userHome, + env: { LOCALAPPDATA: localAppData }, + run(command) { + if (command === 'reg.exe') return { status: 0, stdout: registry } + if (command === 'powershell.exe') { + return { + status: 0, + stdout: JSON.stringify({ + ProcessId: 42, + Name: 'node.exe', + CommandLine: String.raw`"C:\Tabbit\TabbitDance\node.exe" "C:\Tabbit\TabbitDance\runtime\src\browser-runtime-service.mjs"`, + }), + } + } + return { status: 1, stdout: '' } + }, + }) + assert.equal(detected.cliReady, true) + assert.equal(detected.cliPath, localAgentCli) + assert.equal(detected.playwrightProcessRunning, true) assert.equal(detected.recommendation, 'ready') } finally { await rm(root, { recursive: true, force: true }) diff --git a/tests/plugin.test.mjs b/tests/plugin.test.mjs index ae5c999..410b07f 100644 --- a/tests/plugin.test.mjs +++ b/tests/plugin.test.mjs @@ -31,6 +31,7 @@ test('registers one bundled tabbit-browser skill', async () => { tool.output.schema.properties.status.enum, ['ready', 'restart-required', 'background'], ) + assert.equal(tool.output.schema.properties.cliPath.type, 'string') const provider = factory({}) const candidates = await provider.list()