Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 后台下载适配系统地区的正式版安装包。
Expand Down
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 26 additions & 5 deletions installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
} = {}) {
Expand All @@ -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)
))
Expand Down
3 changes: 2 additions & 1 deletion skills/tabbit-browser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 7 additions & 4 deletions skills/tabbit-browser/references/platform-invocation.md
Original file line number Diff line number Diff line change
@@ -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:

Expand Down
60 changes: 59 additions & 1 deletion tests/installer.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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') {
Expand All @@ -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 })
Expand Down
1 change: 1 addition & 0 deletions tests/plugin.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down