Files
ebook-skill/bin/novel.ps1
T
cheney 221b1f8add feat: 引入 so-novel submodule 并提供非交互下载工具
- vendor/so-novel: 上游 v1.12.0 (规则驱动的书源抓取/下载引擎)
- tools/src: ToolMain 封装 search/download/toc/sources,stdout 只输出 JSON
- tools/build.ps1: 自动定位/下载 JDK21,按平台显式激活 javet 原生库 profile
- bin/novel.ps1|.cmd: 命令行入口(人读输出或 -Json)
- SKILL.md/README.md: 使用说明与下载原理分析
2026-11-04 15:12:10 +08:00

230 lines
9.1 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ebook-skill :: so-novel command line
#
# novel search <keyword> [-Limit N] [-Source ID] [-Timeout SEC] [-Log] [-Json]
# novel download <detail-url> [-Ext txt|epub|html|pdf] [-Out DIR] [-Log] [-Json]
# novel toc <detail-url> [-Json]
# novel sources [-Json]
#
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '..\tools\lib.ps1')
$paths = Get-ToolPaths
function Show-Help {
@"
ebook-skill / so-novel
novel search <关键词> [-Limit N] [-Source ID] [-Timeout SEC] [-Log] [-Json]
novel download <详情页链接> [-Ext txt|epub|html|pdf] [-Out 目录] [-Log] [-Json]
novel toc <详情页链接> [-Json] 只看目录,不下载
novel sources [-Json] 列出内置书源
说明
-Json 输出原始 JSON(默认输出人读表格)
-Log 附带 so-novel 运行日志尾部,便于排查源站问题
-Limit 每个书源最多取多少条搜索结果(默认 30)
下载落盘默认在 <仓库>\books,文件名形如 书名(作者).txt
"@ | Write-Host
}
function Ensure-Built {
if ((Test-Path $paths.AppJar) -and (Test-Path $paths.Classes)) { return }
Write-Host '[setup] first run: building so-novel engine + wrapper ...' -ForegroundColor Yellow
& (Join-Path $paths.Tools 'build.ps1')
if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" }
}
function Invoke-Tool {
param([string[]]$JavaArgs)
Ensure-Built
$java = Get-JavaCommand
$cp = $paths.AppJar + ';' + $paths.Classes
Push-Location $paths.Runtime
try {
$raw = (& $java '-Dfile.encoding=UTF-8' "-Dconfig.file=$($paths.Config)" '-cp' $cp 'skills.novel.ToolMain' @JavaArgs 2>&1 | Out-String)
$code = $LASTEXITCODE
} finally {
Pop-Location
}
return [pscustomobject]@{ Raw = $raw.Trim(); Code = $code }
}
function Get-Option {
param([string[]]$Tokens, [string]$Name, $Default = $null)
for ($i = 0; $i -lt $Tokens.Count; $i++) {
$t = $Tokens[$i]
if ($t -ieq $Name -and ($i + 1) -lt $Tokens.Count) { return $Tokens[$i + 1] }
if ($t -like "$Name=*") { return $t.Substring($Name.Length + 1) }
}
return $Default
}
function Test-Flag {
param([string[]]$Tokens, [string]$Name)
foreach ($t in $Tokens) { if ($t -ieq $Name) { return $true } }
return $false
}
function Get-Positionals {
param([string[]]$Tokens, [string[]]$ValueOptions)
$values = @()
for ($i = 0; $i -lt $Tokens.Count; $i++) {
$t = $Tokens[$i]
if ($t.StartsWith('-')) {
if ($ValueOptions -contains $t -and ($i + 1) -lt $Tokens.Count) { $i++ }
continue
}
$values += $t
}
return $values
}
function Show-SearchResult {
param($Result)
if (-not $Result.ok) {
Write-Host ("搜索失败: " + $Result.error) -ForegroundColor Red
return
}
$failedCount = 0
if ($Result.sources.failed) { $failedCount = @($Result.sources.failed).Count }
Write-Host ("关键词「{0}」:命中 {1} 条,书源 {2} 个(空 {3},异常 {4}),耗时 {5} ms" -f `
$Result.keyword, $Result.count, $Result.sources.total, $Result.sources.empty, $failedCount, $Result.tookMs) -ForegroundColor Cyan
if ($Result.count -eq 0) {
Write-Host '没有命中。试试更完整的书名/作者,或 -Limit 调大。' -ForegroundColor Yellow
return
}
$rows = @()
foreach ($r in $Result.results) {
$latest = [string]$r.latestChapter
if ($latest.Length -gt 24) { $latest = $latest.Substring(0, 24) + '...' }
$author = [string]$r.author
if (-not $author) { $author = '?' }
Write-Host ("[{0}] {1} —— {2}" -f $r.index, $r.bookName, $author) -ForegroundColor White
Write-Host (" 最新章节: {0} 更新: {1} 书源: {2}" -f $latest, $r.lastUpdateTime, $r.sourceName) -ForegroundColor DarkGray
Write-Host (" {0}" -f $r.url) -ForegroundColor DarkCyan
}
Write-Host '下载示例: novel download <链接> -Ext txt' -ForegroundColor DarkGray
}
function Show-DownloadResult {
param($Result)
if (-not $Result.ok) {
Write-Host ("下载失败: " + $Result.error) -ForegroundColor Red
if ($Result.bookName) { Write-Host (" 书籍: {0} / {1}" -f $Result.bookName, $Result.author) }
return
}
Write-Host ("完成《{0}》({1}) {2} 章 {3} s" -f $Result.bookName, $Result.author, $Result.chapters, $Result.seconds) -ForegroundColor Green
Write-Host (" 文件: {0} ({1:N0} KB)" -f $Result.file, ($Result.bytes / 1KB))
Write-Host (" 书源: #{0} {1}" -f $Result.sourceId, $Result.sourceName) -ForegroundColor DarkGray
}
function Show-TocResult {
param($Result)
if (-not $Result.ok) {
Write-Host ("目录解析失败: " + $Result.error) -ForegroundColor Red
return
}
Write-Host ("《{0}》 书源 #{1} {2}" -f $Result.url, $Result.sourceId, $Result.sourceName) -ForegroundColor Cyan
Write-Host (" 章节数: {0}" -f $Result.chapters)
Write-Host (" 开头: " + (@($Result.firstChapters) -join ' / '))
Write-Host (" 结尾: " + (@($Result.lastChapters) -join ' / '))
}
function Show-Sources {
param($Result)
Write-Host ("激活规则: {0} 共 {1} 个书源" -f $Result.activeRules, $Result.count) -ForegroundColor Cyan
foreach ($s in $Result.sources) {
$searchable = '-'
if ($s.searchable) { $searchable = '支持搜索' }
Write-Host ("{0,2}. {1,-14} {2,-8} {3}" -f $s.id, $s.name, $searchable, $s.url)
}
}
# ---------------------------------------------------------------- dispatch
function Invoke-SearchCommand {
param([string[]]$Tokens)
$valueOptions = @('-Limit', '-Source', '-Timeout')
$keyword = (@(Get-Positionals -Tokens $Tokens -ValueOptions $valueOptions) -join ' ').Trim()
if (-not $keyword) { Write-Host '用法: novel search <关键词>' -ForegroundColor Yellow; exit 2 }
$javaArgs = @('search', $keyword,
'--limit', [string](Get-Option $Tokens '-Limit' 30),
'--source', [string](Get-Option $Tokens '-Source' '0'),
'--timeout', [string](Get-Option $Tokens '-Timeout' '180'))
if (Test-Flag $Tokens '-Log') { $javaArgs += '--log' }
$result = Invoke-Tool -JavaArgs $javaArgs
if (Test-Flag $Tokens '-Json') { $result.Raw | Write-Output }
else { Show-SearchResult ($result.Raw | ConvertFrom-Json) }
exit $result.Code
}
function Invoke-DownloadCommand {
param([string[]]$Tokens)
$valueOptions = @('-Ext', '-Out', '-Url')
$url = Get-Option $Tokens '-Url' $null
if (-not $url) {
$positionals = @(Get-Positionals -Tokens $Tokens -ValueOptions $valueOptions)
if ($positionals.Count -gt 0) { $url = $positionals[0] }
}
if (-not $url) { Write-Host '用法: novel download <详情页链接> [-Ext txt] [-Out 目录]' -ForegroundColor Yellow; exit 2 }
$out = Get-Option $Tokens '-Out' $null
if ($out) { $out = (New-Item -ItemType Directory -Force -Path $out).FullName }
$javaArgs = @('download', $url, '--ext', [string](Get-Option $Tokens '-Ext' 'txt'))
if ($out) { $javaArgs += @('--out', $out) }
if (Test-Flag $Tokens '-Log') { $javaArgs += '--log' }
$result = Invoke-Tool -JavaArgs $javaArgs
if (Test-Flag $Tokens '-Json') { $result.Raw | Write-Output }
else { Show-DownloadResult ($result.Raw | ConvertFrom-Json) }
exit $result.Code
}
function Invoke-TocCommand {
param([string[]]$Tokens)
$url = Get-Option $Tokens '-Url' $null
if (-not $url) {
$positionals = @(Get-Positionals -Tokens $Tokens -ValueOptions @('-Url'))
if ($positionals.Count -gt 0) { $url = $positionals[0] }
}
if (-not $url) { Write-Host '用法: novel toc <详情页链接>' -ForegroundColor Yellow; exit 2 }
$javaArgs = @('toc', $url)
if (Test-Flag $Tokens '-Log') { $javaArgs += '--log' }
$result = Invoke-Tool -JavaArgs $javaArgs
if (Test-Flag $Tokens '-Json') { $result.Raw | Write-Output }
else { Show-TocResult ($result.Raw | ConvertFrom-Json) }
exit $result.Code
}
$argv = @($args)
if ($argv.Count -eq 0) { Show-Help; exit 0 }
$command = ([string]$argv[0]).ToLowerInvariant()
$rest = @()
if ($argv.Count -gt 1) { $rest = $argv[1..($argv.Count - 1)] }
switch ($command) {
'search' { Invoke-SearchCommand -Tokens $rest }
'download' { Invoke-DownloadCommand -Tokens $rest }
'dl' { Invoke-DownloadCommand -Tokens $rest }
'toc' { Invoke-TocCommand -Tokens $rest }
'chapters' { Invoke-TocCommand -Tokens $rest }
'sources' {
$result = Invoke-Tool -JavaArgs @('sources')
if (Test-Flag $rest '-Json') { $result.Raw | Write-Output }
else { Show-Sources ($result.Raw | ConvertFrom-Json) }
exit $result.Code
}
'build' {
& (Join-Path $paths.Tools 'build.ps1') @rest
exit $LASTEXITCODE
}
default {
if ($command -in @('-h', '--help', 'help')) { Show-Help; exit 0 }
Write-Host "未知命令: $command" -ForegroundColor Red
Show-Help
exit 2
}
}