@sovovs/bycli 2.1.37 → 2.1.38

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/README.md CHANGED
@@ -30,6 +30,28 @@ bycli 12306 stations 北京 # public command, no login required
30
30
  bycli juejin search bycli # search Juejin
31
31
  ```
32
32
 
33
+ ### Search adapters
34
+
35
+ The following public search commands are available:
36
+
37
+ ```bash
38
+ bycli baidu search "open cli" --limit 10 --page 1 --site github.com
39
+ bycli bing search "open cli" --freshness week --market en-US
40
+ bycli yandex search "open cli" --lr 213 --sort date
41
+ bycli so search "open cli" --type news
42
+ bycli sogou search "open cli" --time week --sort date
43
+ bycli gitlab search runner --scope issues --order-by updated_at --sort desc
44
+ bycli csdn search "node cli" --content-type blog --sort latest
45
+ bycli threads search "open cli" --author alice
46
+ bycli 52pojie search "open cli" --sort latest
47
+ ```
48
+
49
+ All search commands accept `--limit` and pagination where supported, and return
50
+ structured rows including rank, title, URL, snippet, result type, author,
51
+ publication time, score, and platform-specific `extra` data. These adapters use
52
+ public pages or the active browser session; login prompts, CAPTCHA, and anti-bot
53
+ pages are reported as typed errors instead of empty successful results.
54
+
33
55
  ## License
34
56
 
35
57
  Licensed under the [Apache License 2.0](./LICENSE).
package/README.zh-CN.md CHANGED
@@ -29,6 +29,27 @@ bycli 12306 stations 北京 # 公开命令,无需登录
29
29
  bycli juejin search bycli # 掘金搜索
30
30
  ```
31
31
 
32
+ ### 搜索适配器
33
+
34
+ 当前可用的搜索命令包括:
35
+
36
+ ```bash
37
+ bycli baidu search "open cli" --limit 10 --page 1 --site github.com
38
+ bycli bing search "open cli" --freshness week --market zh-CN
39
+ bycli yandex search "open cli" --lr 213 --sort date
40
+ bycli so search "open cli" --type news
41
+ bycli sogou search "open cli" --time week --sort date
42
+ bycli gitlab search runner --scope issues --order-by updated_at --sort desc
43
+ bycli csdn search "node cli" --content-type blog --sort latest
44
+ bycli threads search "open cli" --author alice
45
+ bycli 52pojie search "open cli" --sort latest
46
+ ```
47
+
48
+ 这些命令会尽量提供 `--limit`、分页、排序、时间、区域、类型等参数,
49
+ 并返回包含排名、标题、URL、摘要、结果类型、作者、发布时间、评分和平台专属
50
+ `extra` 字段的结构化结果。适配器使用公开页面或当前浏览器会话;遇到登录、验证码
51
+ 或反爬页面时会返回明确的类型化错误,而不是伪造空结果。
52
+
32
53
  ## 许可证
33
54
 
34
55
  基于 [Apache License 2.0](./LICENSE) 开源。
package/cli-manifest.json CHANGED
@@ -1177,6 +1177,66 @@
1177
1177
  "sourceFile": "51job/search.js",
1178
1178
  "navigateBefore": false
1179
1179
  },
1180
+ {
1181
+ "site": "52pojie",
1182
+ "name": "search",
1183
+ "description": "Search 52pojie",
1184
+ "access": "read",
1185
+ "domain": "www.52pojie.cn",
1186
+ "strategy": "public",
1187
+ "browser": true,
1188
+ "args": [
1189
+ {
1190
+ "name": "keyword",
1191
+ "type": "str",
1192
+ "required": true,
1193
+ "positional": true,
1194
+ "help": "Search query"
1195
+ },
1196
+ {
1197
+ "name": "limit",
1198
+ "type": "int",
1199
+ "default": 10,
1200
+ "required": false,
1201
+ "help": "Number of threads (1-50)"
1202
+ },
1203
+ {
1204
+ "name": "page",
1205
+ "type": "int",
1206
+ "default": 1,
1207
+ "required": false,
1208
+ "help": "Result page number"
1209
+ },
1210
+ {
1211
+ "name": "section",
1212
+ "type": "str",
1213
+ "required": false,
1214
+ "help": "Forum section identifier"
1215
+ },
1216
+ {
1217
+ "name": "sort",
1218
+ "type": "str",
1219
+ "required": false,
1220
+ "help": "Sort order: relevance, latest, replies, views"
1221
+ }
1222
+ ],
1223
+ "columns": [
1224
+ "rank",
1225
+ "title",
1226
+ "url",
1227
+ "snippet",
1228
+ "displayUrl",
1229
+ "source",
1230
+ "resultType",
1231
+ "author",
1232
+ "publishedAt",
1233
+ "score",
1234
+ "extra"
1235
+ ],
1236
+ "type": "js",
1237
+ "modulePath": "52pojie/search.js",
1238
+ "sourceFile": "52pojie/search.js"
1239
+ },
1180
1240
  {
1181
1241
  "site": "aibase",
1182
1242
  "name": "news",
@@ -1855,6 +1915,78 @@
1855
1915
  "modulePath": "arxiv/search.js",
1856
1916
  "sourceFile": "arxiv/search.js"
1857
1917
  },
1918
+ {
1919
+ "site": "baidu",
1920
+ "name": "search",
1921
+ "description": "Search Baidu",
1922
+ "access": "read",
1923
+ "domain": "www.baidu.com",
1924
+ "strategy": "public",
1925
+ "browser": true,
1926
+ "args": [
1927
+ {
1928
+ "name": "keyword",
1929
+ "type": "str",
1930
+ "required": true,
1931
+ "positional": true,
1932
+ "help": "Search query"
1933
+ },
1934
+ {
1935
+ "name": "limit",
1936
+ "type": "int",
1937
+ "default": 10,
1938
+ "required": false,
1939
+ "help": "Number of results (1-50)"
1940
+ },
1941
+ {
1942
+ "name": "page",
1943
+ "type": "int",
1944
+ "default": 1,
1945
+ "required": false,
1946
+ "help": "Result page number"
1947
+ },
1948
+ {
1949
+ "name": "site",
1950
+ "type": "str",
1951
+ "required": false,
1952
+ "help": "Restrict results to a domain"
1953
+ },
1954
+ {
1955
+ "name": "filetype",
1956
+ "type": "str",
1957
+ "required": false,
1958
+ "help": "File type: pdf, doc, xls, ppt, rtf, all"
1959
+ },
1960
+ {
1961
+ "name": "platform",
1962
+ "type": "str",
1963
+ "required": false,
1964
+ "help": "Client platform: pc or mobile"
1965
+ },
1966
+ {
1967
+ "name": "time",
1968
+ "type": "str",
1969
+ "required": false,
1970
+ "help": "Recent time filter understood by Baidu"
1971
+ }
1972
+ ],
1973
+ "columns": [
1974
+ "rank",
1975
+ "title",
1976
+ "url",
1977
+ "snippet",
1978
+ "displayUrl",
1979
+ "source",
1980
+ "resultType",
1981
+ "author",
1982
+ "publishedAt",
1983
+ "score",
1984
+ "extra"
1985
+ ],
1986
+ "type": "js",
1987
+ "modulePath": "baidu/search.js",
1988
+ "sourceFile": "baidu/search.js"
1989
+ },
1858
1990
  {
1859
1991
  "site": "baidu-scholar",
1860
1992
  "name": "search",
@@ -3265,6 +3397,78 @@
3265
3397
  "modulePath": "binance/trades.js",
3266
3398
  "sourceFile": "binance/trades.js"
3267
3399
  },
3400
+ {
3401
+ "site": "bing",
3402
+ "name": "search",
3403
+ "description": "Search Bing",
3404
+ "access": "read",
3405
+ "domain": "www.bing.com",
3406
+ "strategy": "public",
3407
+ "browser": true,
3408
+ "args": [
3409
+ {
3410
+ "name": "keyword",
3411
+ "type": "str",
3412
+ "required": true,
3413
+ "positional": true,
3414
+ "help": "Search query"
3415
+ },
3416
+ {
3417
+ "name": "limit",
3418
+ "type": "int",
3419
+ "default": 10,
3420
+ "required": false,
3421
+ "help": "Number of results (1-50)"
3422
+ },
3423
+ {
3424
+ "name": "page",
3425
+ "type": "int",
3426
+ "default": 1,
3427
+ "required": false,
3428
+ "help": "Result page number"
3429
+ },
3430
+ {
3431
+ "name": "freshness",
3432
+ "type": "str",
3433
+ "required": false,
3434
+ "help": "Time range: day, week, month, year"
3435
+ },
3436
+ {
3437
+ "name": "market",
3438
+ "type": "str",
3439
+ "required": false,
3440
+ "help": "Market code such as en-US or zh-CN"
3441
+ },
3442
+ {
3443
+ "name": "answer",
3444
+ "type": "str",
3445
+ "required": false,
3446
+ "help": "Result scope: web, news, video, images"
3447
+ },
3448
+ {
3449
+ "name": "safe",
3450
+ "type": "str",
3451
+ "required": false,
3452
+ "help": "Safe search: off, moderate, strict"
3453
+ }
3454
+ ],
3455
+ "columns": [
3456
+ "rank",
3457
+ "title",
3458
+ "url",
3459
+ "snippet",
3460
+ "displayUrl",
3461
+ "source",
3462
+ "resultType",
3463
+ "author",
3464
+ "publishedAt",
3465
+ "score",
3466
+ "extra"
3467
+ ],
3468
+ "type": "js",
3469
+ "modulePath": "bing/search.js",
3470
+ "sourceFile": "bing/search.js"
3471
+ },
3268
3472
  {
3269
3473
  "site": "bloomberg",
3270
3474
  "name": "businessweek",
@@ -6722,6 +6926,72 @@
6722
6926
  "modulePath": "crates/search.js",
6723
6927
  "sourceFile": "crates/search.js"
6724
6928
  },
6929
+ {
6930
+ "site": "csdn",
6931
+ "name": "search",
6932
+ "description": "Search CSDN",
6933
+ "access": "read",
6934
+ "domain": "so.csdn.net",
6935
+ "strategy": "public",
6936
+ "browser": true,
6937
+ "args": [
6938
+ {
6939
+ "name": "keyword",
6940
+ "type": "str",
6941
+ "required": true,
6942
+ "positional": true,
6943
+ "help": "Search query"
6944
+ },
6945
+ {
6946
+ "name": "limit",
6947
+ "type": "int",
6948
+ "default": 10,
6949
+ "required": false,
6950
+ "help": "Number of results (1-50)"
6951
+ },
6952
+ {
6953
+ "name": "page",
6954
+ "type": "int",
6955
+ "default": 1,
6956
+ "required": false,
6957
+ "help": "Result page number"
6958
+ },
6959
+ {
6960
+ "name": "content-type",
6961
+ "type": "str",
6962
+ "required": false,
6963
+ "help": "Content type: all, blog, download, course"
6964
+ },
6965
+ {
6966
+ "name": "sort",
6967
+ "type": "str",
6968
+ "required": false,
6969
+ "help": "Sort order: relevance, latest, hot"
6970
+ },
6971
+ {
6972
+ "name": "time",
6973
+ "type": "str",
6974
+ "required": false,
6975
+ "help": "Time range: day, week, month, year"
6976
+ }
6977
+ ],
6978
+ "columns": [
6979
+ "rank",
6980
+ "title",
6981
+ "url",
6982
+ "snippet",
6983
+ "displayUrl",
6984
+ "source",
6985
+ "resultType",
6986
+ "author",
6987
+ "publishedAt",
6988
+ "score",
6989
+ "extra"
6990
+ ],
6991
+ "type": "js",
6992
+ "modulePath": "csdn/search.js",
6993
+ "sourceFile": "csdn/search.js"
6994
+ },
6725
6995
  {
6726
6996
  "site": "ctrip",
6727
6997
  "name": "flight",
@@ -11535,6 +11805,72 @@
11535
11805
  "modulePath": "github/search.js",
11536
11806
  "sourceFile": "github/search.js"
11537
11807
  },
11808
+ {
11809
+ "site": "gitlab",
11810
+ "name": "search",
11811
+ "description": "Search GitLab",
11812
+ "access": "read",
11813
+ "domain": "gitlab.com",
11814
+ "strategy": "public",
11815
+ "browser": true,
11816
+ "args": [
11817
+ {
11818
+ "name": "keyword",
11819
+ "type": "str",
11820
+ "required": true,
11821
+ "positional": true,
11822
+ "help": "Search query"
11823
+ },
11824
+ {
11825
+ "name": "limit",
11826
+ "type": "int",
11827
+ "default": 10,
11828
+ "required": false,
11829
+ "help": "Number of results (1-50)"
11830
+ },
11831
+ {
11832
+ "name": "page",
11833
+ "type": "int",
11834
+ "default": 1,
11835
+ "required": false,
11836
+ "help": "Result page number"
11837
+ },
11838
+ {
11839
+ "name": "scope",
11840
+ "type": "str",
11841
+ "required": false,
11842
+ "help": "Search scope: projects, issues, merge_requests, commits, blobs, users"
11843
+ },
11844
+ {
11845
+ "name": "order-by",
11846
+ "type": "str",
11847
+ "required": false,
11848
+ "help": "Order by: created_at, updated_at, latest_activity_at"
11849
+ },
11850
+ {
11851
+ "name": "sort",
11852
+ "type": "str",
11853
+ "required": false,
11854
+ "help": "Sort direction: asc or desc"
11855
+ }
11856
+ ],
11857
+ "columns": [
11858
+ "rank",
11859
+ "title",
11860
+ "url",
11861
+ "snippet",
11862
+ "displayUrl",
11863
+ "source",
11864
+ "resultType",
11865
+ "author",
11866
+ "publishedAt",
11867
+ "score",
11868
+ "extra"
11869
+ ],
11870
+ "type": "js",
11871
+ "modulePath": "gitlab/search.js",
11872
+ "sourceFile": "gitlab/search.js"
11873
+ },
11538
11874
  {
11539
11875
  "site": "google",
11540
11876
  "name": "news",
@@ -23468,6 +23804,132 @@
23468
23804
  "sourceFile": "smzdm/search.js",
23469
23805
  "navigateBefore": "https://www.smzdm.com"
23470
23806
  },
23807
+ {
23808
+ "site": "so",
23809
+ "name": "search",
23810
+ "description": "Search 360 Search",
23811
+ "access": "read",
23812
+ "domain": "so.com",
23813
+ "strategy": "public",
23814
+ "browser": true,
23815
+ "args": [
23816
+ {
23817
+ "name": "keyword",
23818
+ "type": "str",
23819
+ "required": true,
23820
+ "positional": true,
23821
+ "help": "Search query"
23822
+ },
23823
+ {
23824
+ "name": "limit",
23825
+ "type": "int",
23826
+ "default": 10,
23827
+ "required": false,
23828
+ "help": "Number of results (1-50)"
23829
+ },
23830
+ {
23831
+ "name": "page",
23832
+ "type": "int",
23833
+ "default": 1,
23834
+ "required": false,
23835
+ "help": "Result page number"
23836
+ },
23837
+ {
23838
+ "name": "type",
23839
+ "type": "str",
23840
+ "required": false,
23841
+ "help": "Result type: web, news, video, image"
23842
+ },
23843
+ {
23844
+ "name": "safe",
23845
+ "type": "str",
23846
+ "required": false,
23847
+ "help": "Safe search: off or on"
23848
+ }
23849
+ ],
23850
+ "columns": [
23851
+ "rank",
23852
+ "title",
23853
+ "url",
23854
+ "snippet",
23855
+ "displayUrl",
23856
+ "source",
23857
+ "resultType",
23858
+ "author",
23859
+ "publishedAt",
23860
+ "score",
23861
+ "extra"
23862
+ ],
23863
+ "type": "js",
23864
+ "modulePath": "so/search.js",
23865
+ "sourceFile": "so/search.js"
23866
+ },
23867
+ {
23868
+ "site": "sogou",
23869
+ "name": "search",
23870
+ "description": "Search Sogou",
23871
+ "access": "read",
23872
+ "domain": "sogou.com",
23873
+ "strategy": "public",
23874
+ "browser": true,
23875
+ "args": [
23876
+ {
23877
+ "name": "keyword",
23878
+ "type": "str",
23879
+ "required": true,
23880
+ "positional": true,
23881
+ "help": "Search query"
23882
+ },
23883
+ {
23884
+ "name": "limit",
23885
+ "type": "int",
23886
+ "default": 10,
23887
+ "required": false,
23888
+ "help": "Number of results (1-50)"
23889
+ },
23890
+ {
23891
+ "name": "page",
23892
+ "type": "int",
23893
+ "default": 1,
23894
+ "required": false,
23895
+ "help": "Result page number"
23896
+ },
23897
+ {
23898
+ "name": "type",
23899
+ "type": "str",
23900
+ "required": false,
23901
+ "help": "Result type: web, news, video, image"
23902
+ },
23903
+ {
23904
+ "name": "time",
23905
+ "type": "str",
23906
+ "required": false,
23907
+ "help": "Time range: day, week, month, year"
23908
+ },
23909
+ {
23910
+ "name": "sort",
23911
+ "type": "str",
23912
+ "required": false,
23913
+ "help": "Sort order: relevance or date"
23914
+ }
23915
+ ],
23916
+ "columns": [
23917
+ "rank",
23918
+ "title",
23919
+ "url",
23920
+ "snippet",
23921
+ "displayUrl",
23922
+ "source",
23923
+ "resultType",
23924
+ "author",
23925
+ "publishedAt",
23926
+ "score",
23927
+ "extra"
23928
+ ],
23929
+ "type": "js",
23930
+ "modulePath": "sogou/search.js",
23931
+ "sourceFile": "sogou/search.js"
23932
+ },
23471
23933
  {
23472
23934
  "site": "spotify",
23473
23935
  "name": "auth",
@@ -24718,6 +25180,72 @@
24718
25180
  "sourceFile": "tdx/hot-rank.js",
24719
25181
  "navigateBefore": true
24720
25182
  },
25183
+ {
25184
+ "site": "threads",
25185
+ "name": "search",
25186
+ "description": "Search Threads",
25187
+ "access": "read",
25188
+ "domain": "www.threads.com",
25189
+ "strategy": "public",
25190
+ "browser": true,
25191
+ "args": [
25192
+ {
25193
+ "name": "keyword",
25194
+ "type": "str",
25195
+ "required": true,
25196
+ "positional": true,
25197
+ "help": "Search query"
25198
+ },
25199
+ {
25200
+ "name": "limit",
25201
+ "type": "int",
25202
+ "default": 10,
25203
+ "required": false,
25204
+ "help": "Number of posts (1-50)"
25205
+ },
25206
+ {
25207
+ "name": "page",
25208
+ "type": "int",
25209
+ "default": 1,
25210
+ "required": false,
25211
+ "help": "Result page number"
25212
+ },
25213
+ {
25214
+ "name": "author",
25215
+ "type": "str",
25216
+ "required": false,
25217
+ "help": "Filter by author handle"
25218
+ },
25219
+ {
25220
+ "name": "since",
25221
+ "type": "str",
25222
+ "required": false,
25223
+ "help": "Only posts on or after YYYY-MM-DD"
25224
+ },
25225
+ {
25226
+ "name": "until",
25227
+ "type": "str",
25228
+ "required": false,
25229
+ "help": "Only posts on or before YYYY-MM-DD"
25230
+ }
25231
+ ],
25232
+ "columns": [
25233
+ "rank",
25234
+ "title",
25235
+ "url",
25236
+ "snippet",
25237
+ "displayUrl",
25238
+ "source",
25239
+ "resultType",
25240
+ "author",
25241
+ "publishedAt",
25242
+ "score",
25243
+ "extra"
25244
+ ],
25245
+ "type": "js",
25246
+ "modulePath": "threads/search.js",
25247
+ "sourceFile": "threads/search.js"
25248
+ },
24721
25249
  {
24722
25250
  "site": "ths",
24723
25251
  "name": "hot-rank",
@@ -31443,6 +31971,72 @@
31443
31971
  "sourceFile": "yahoo-finance/quote.js",
31444
31972
  "navigateBefore": "https://finance.yahoo.com"
31445
31973
  },
31974
+ {
31975
+ "site": "yandex",
31976
+ "name": "search",
31977
+ "description": "Search Yandex",
31978
+ "access": "read",
31979
+ "domain": "yandex.com",
31980
+ "strategy": "public",
31981
+ "browser": true,
31982
+ "args": [
31983
+ {
31984
+ "name": "keyword",
31985
+ "type": "str",
31986
+ "required": true,
31987
+ "positional": true,
31988
+ "help": "Search query"
31989
+ },
31990
+ {
31991
+ "name": "limit",
31992
+ "type": "int",
31993
+ "default": 10,
31994
+ "required": false,
31995
+ "help": "Number of results (1-50)"
31996
+ },
31997
+ {
31998
+ "name": "page",
31999
+ "type": "int",
32000
+ "default": 1,
32001
+ "required": false,
32002
+ "help": "Result page number"
32003
+ },
32004
+ {
32005
+ "name": "lr",
32006
+ "type": "str",
32007
+ "required": false,
32008
+ "help": "Yandex region code"
32009
+ },
32010
+ {
32011
+ "name": "lang",
32012
+ "type": "str",
32013
+ "required": false,
32014
+ "help": "Language code"
32015
+ },
32016
+ {
32017
+ "name": "sort",
32018
+ "type": "str",
32019
+ "required": false,
32020
+ "help": "Sort order: relevance or date"
32021
+ }
32022
+ ],
32023
+ "columns": [
32024
+ "rank",
32025
+ "title",
32026
+ "url",
32027
+ "snippet",
32028
+ "displayUrl",
32029
+ "source",
32030
+ "resultType",
32031
+ "author",
32032
+ "publishedAt",
32033
+ "score",
32034
+ "extra"
32035
+ ],
32036
+ "type": "js",
32037
+ "modulePath": "yandex/search.js",
32038
+ "sourceFile": "yandex/search.js"
32039
+ },
31446
32040
  {
31447
32041
  "site": "yollomi",
31448
32042
  "name": "background",
@@ -0,0 +1,32 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
4
+
5
+ const SORTS = ['relevance', 'latest', 'replies', 'views'];
6
+
7
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, section, sort }) {
8
+ const url = new URL('https://www.52pojie.cn/search.php'); url.searchParams.set('srchtxt', keyword); url.searchParams.set('page', String(page)); url.searchParams.set('perpage', String(limit));
9
+ if (section) url.searchParams.set('section', section); if (sort) url.searchParams.set('sort', sort); return url.toString();
10
+ }
11
+
12
+ function extractItems() {
13
+ const body = document.body?.textContent || ''; const blocked = /验证码|访问频繁|captcha|access denied/i.test(body); const items = []; const seen = new Set(); const numberFrom = (card, selector) => { const value = (card.querySelector(selector)?.textContent || '').replace(/[^\d.]/g, ''); return value ? Number(value) : null; };
14
+ for (const card of document.querySelectorAll('.forum-item, .search-list li, .sltm')) {
15
+ const anchor = card.querySelector('.thread-title[href], h3 a[href], h2 a[href]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue; seen.add(anchor.href); const time = card.querySelector('time');
16
+ items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.summary, .desc, p')?.textContent || '').trim(), displayUrl: new URL(anchor.href).hostname + new URL(anchor.href).pathname, source: '52pojie', resultType: 'thread', author: (card.querySelector('.author, .username')?.textContent || '').trim() || null, publishedAt: time?.getAttribute('datetime') || null, score: null, extra: { replies: numberFrom(card, '.replies, .reply'), views: numberFrom(card, '.views, .view'), section: (card.querySelector('.section, .forum')?.textContent || '').trim() || null } });
17
+ }
18
+ return { blocked, items };
19
+ }
20
+
21
+ export const command = cli({
22
+ site: '52pojie', name: 'search', access: 'read', description: 'Search 52pojie', domain: 'www.52pojie.cn', strategy: Strategy.PUBLIC, browser: true,
23
+ args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of threads (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'section', help: 'Forum section identifier' }, { name: 'sort', help: 'Sort order: relevance, latest, replies, views' }],
24
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
25
+ func: async (page, kwargs) => {
26
+ const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page'); if (kwargs.sort && !SORTS.includes(String(kwargs.sort))) throw new ArgumentError(`--sort must be one of: ${SORTS.join(', ')}`);
27
+ await runBrowserStep('52pojie search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, section: kwargs.section, sort: kwargs.sort }))); await page.wait({ selector: '.forum-item, .search-list, .sltm', timeout: 8 }).catch(() => page.wait(2).catch(() => {})); const data = await runBrowserStep('52pojie search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
28
+ if (data?.blocked) throw new CommandExecutionError('52pojie search was blocked by a verification page', 'Complete the verification in the browser and retry.'); if (!data?.items?.length) throw emptySearchResults('52pojie', keyword); return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item }));
29
+ },
30
+ });
31
+
32
+ export const __test__ = { command, buildSearchUrl };
@@ -0,0 +1,68 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireNonNegativeInteger, requireSearchQuery, runBrowserStep, toHttpsUrl } from '../_shared/search-adapter.js';
4
+
5
+ const TYPES = ['web', 'news', 'image', 'video'];
6
+ const FILETYPES = ['pdf', 'doc', 'xls', 'ppt', 'rtf', 'all'];
7
+
8
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, site, filetype, platform, time }) {
9
+ const url = new URL('https://www.baidu.com/s');
10
+ url.searchParams.set('wd', keyword);
11
+ url.searchParams.set('pn', String((page - 1) * limit));
12
+ if (site) url.searchParams.set('si', site);
13
+ if (filetype) url.searchParams.set('ft', filetype);
14
+ if (platform) url.searchParams.set('ie', platform === 'mobile' ? 'utf-8' : 'utf-8');
15
+ if (time) url.searchParams.set('gpc', `stf=${time}`);
16
+ return url.toString();
17
+ }
18
+
19
+ function validateChoice(value, choices, flag) {
20
+ if (value !== undefined && !choices.includes(String(value))) throw new ArgumentError(`--${flag} must be one of: ${choices.join(', ')}`);
21
+ return value === undefined ? undefined : String(value);
22
+ }
23
+
24
+ function extractItems() {
25
+ const blocked = /验证码|安全验证|百度安全验证|访问过于频繁|请完成验证/.test(document.body?.textContent || '');
26
+ const items = [];
27
+ const seen = new Set();
28
+ for (const card of document.querySelectorAll('.result, .c-container')) {
29
+ const anchor = card.querySelector('h3 a, h3 a[href]');
30
+ if (!anchor) continue;
31
+ const url = anchor.href || '';
32
+ if (!/^https?:/i.test(url) || seen.has(url)) continue;
33
+ seen.add(url);
34
+ items.push({ title: (anchor.textContent || '').trim(), url, snippet: (card.querySelector('.c-abstract, .content-right_8Zs40, .c-span-last')?.textContent || '').trim(), displayUrl: (card.querySelector('.c-showurl, .c-color-gray2')?.textContent || '').trim(), resultType: 'web', extra: {} });
35
+ }
36
+ return { blocked, items };
37
+ }
38
+
39
+ export const command = cli({
40
+ site: 'baidu', name: 'search', access: 'read', description: 'Search Baidu', domain: 'www.baidu.com', strategy: Strategy.PUBLIC, browser: true,
41
+ args: [
42
+ { name: 'keyword', positional: true, required: true, help: 'Search query' },
43
+ { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' },
44
+ { name: 'page', type: 'int', default: 1, help: 'Result page number' },
45
+ { name: 'site', help: 'Restrict results to a domain' },
46
+ { name: 'filetype', help: 'File type: pdf, doc, xls, ppt, rtf, all' },
47
+ { name: 'platform', help: 'Client platform: pc or mobile' },
48
+ { name: 'time', help: 'Recent time filter understood by Baidu' },
49
+ ],
50
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
51
+ func: async (page, kwargs) => {
52
+ const keyword = requireSearchQuery(kwargs.keyword);
53
+ const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit');
54
+ const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page');
55
+ const filetype = validateChoice(kwargs.filetype, FILETYPES, 'filetype');
56
+ const platform = validateChoice(kwargs.platform, ['pc', 'mobile'], 'platform');
57
+ const raw = await runBrowserStep('Baidu search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, site: kwargs.site, filetype, platform, time: kwargs.time })));
58
+ void raw;
59
+ await page.wait({ selector: '.result, .c-container', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
60
+ const data = await runBrowserStep('Baidu search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
61
+ if (data?.blocked) throw new CommandExecutionError('Baidu search was blocked by a verification page', 'Complete the verification in the browser and retry.');
62
+ const items = data?.items || [];
63
+ if (!items.length) throw emptySearchResults('Baidu', keyword);
64
+ return items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item, source: 'baidu', author: null, publishedAt: null, score: null }));
65
+ },
66
+ });
67
+
68
+ export const __test__ = { command, buildSearchUrl };
@@ -0,0 +1,58 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
4
+
5
+ const FRESHNESS = ['day', 'week', 'month', 'year'];
6
+ const ANSWERS = ['web', 'news', 'video', 'images'];
7
+ const SAFE = ['off', 'moderate', 'strict'];
8
+
9
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, freshness, market, answer, safe }) {
10
+ const url = new URL('https://www.bing.com/search');
11
+ url.searchParams.set('q', keyword);
12
+ url.searchParams.set('count', String(limit));
13
+ url.searchParams.set('first', String((page - 1) * limit + 1));
14
+ if (freshness) url.searchParams.set('freshness', freshness);
15
+ if (market) url.searchParams.set('cc', String(market).split('-').pop().toUpperCase());
16
+ if (answer && answer !== 'web') url.searchParams.set('scope', answer);
17
+ if (safe) url.searchParams.set('safesearch', safe);
18
+ return url.toString();
19
+ }
20
+
21
+ function validate(value, choices, flag) {
22
+ if (value !== undefined && !choices.includes(String(value))) throw new ArgumentError(`--${flag} must be one of: ${choices.join(', ')}`);
23
+ return value === undefined ? undefined : String(value);
24
+ }
25
+
26
+ function extractItems() {
27
+ const blocked = /unusual traffic|verify you are human|captcha|access denied/i.test(document.body?.textContent || '');
28
+ const items = [];
29
+ const seen = new Set();
30
+ for (const card of document.querySelectorAll('li.b_algo')) {
31
+ const anchor = card.querySelector('h2 a[href]');
32
+ if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
33
+ seen.add(anchor.href);
34
+ items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.b_caption p')?.textContent || '').trim(), displayUrl: (card.querySelector('cite')?.textContent || '').trim(), resultType: 'web', extra: {} });
35
+ }
36
+ return { blocked, items };
37
+ }
38
+
39
+ export const command = cli({
40
+ site: 'bing', name: 'search', access: 'read', description: 'Search Bing', domain: 'www.bing.com', strategy: Strategy.PUBLIC, browser: true,
41
+ args: [
42
+ { name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' },
43
+ { name: 'freshness', help: 'Time range: day, week, month, year' }, { name: 'market', help: 'Market code such as en-US or zh-CN' }, { name: 'answer', help: 'Result scope: web, news, video, images' }, { name: 'safe', help: 'Safe search: off, moderate, strict' },
44
+ ],
45
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
46
+ func: async (page, kwargs) => {
47
+ const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page');
48
+ const freshness = validate(kwargs.freshness, FRESHNESS, 'freshness'); const answer = validate(kwargs.answer, ANSWERS, 'answer'); const safe = validate(kwargs.safe, SAFE, 'safe');
49
+ await runBrowserStep('Bing search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, freshness, market: kwargs.market, answer, safe })));
50
+ await page.wait({ selector: 'li.b_algo', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
51
+ const data = await runBrowserStep('Bing search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
52
+ if (data?.blocked) throw new CommandExecutionError('Bing search was blocked by a verification page', 'Complete the verification in the browser and retry.');
53
+ if (!data?.items?.length) throw emptySearchResults('Bing', keyword);
54
+ return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item, source: 'bing', author: null, publishedAt: null, score: null }));
55
+ },
56
+ });
57
+
58
+ export const __test__ = { command, buildSearchUrl };
@@ -0,0 +1,34 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
4
+
5
+ const TYPES = ['all', 'blog', 'download', 'course']; const SORTS = ['relevance', 'latest', 'hot']; const TIMES = ['day', 'week', 'month', 'year'];
6
+
7
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, contentType = 'all', sort, time }) {
8
+ const url = new URL('https://so.csdn.net/so/search'); url.searchParams.set('q', keyword); url.searchParams.set('p', String(page)); url.searchParams.set('t', contentType); url.searchParams.set('size', String(limit));
9
+ if (sort) url.searchParams.set('sort', sort); if (time) url.searchParams.set('time', time); return url.toString();
10
+ }
11
+
12
+ function extractItems() {
13
+ const blocked = /登录|验证码|访问异常|captcha|sign in/i.test(document.body?.textContent || '') && !document.querySelector('.search-result-info, .search-result'); const items = []; const seen = new Set();
14
+ for (const card of document.querySelectorAll('.search-result-info, .search-result')) {
15
+ const anchor = card.querySelector('h3 a[href], h4 a[href]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
16
+ seen.add(anchor.href); const time = card.querySelector('time'); items.push({ rank: items.length + 1, title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.search-result-desc, p')?.textContent || '').trim(), displayUrl: new URL(anchor.href).hostname + new URL(anchor.href).pathname, source: 'csdn', resultType: 'article', author: (card.querySelector('.author, .user-name')?.textContent || '').trim() || null, publishedAt: time?.getAttribute('datetime') || null, score: null, extra: {} });
17
+ }
18
+ return { blocked, items };
19
+ }
20
+
21
+ export const command = cli({
22
+ site: 'csdn', name: 'search', access: 'read', description: 'Search CSDN', domain: 'so.csdn.net', strategy: Strategy.PUBLIC, browser: true,
23
+ args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'content-type', help: 'Content type: all, blog, download, course' }, { name: 'sort', help: 'Sort order: relevance, latest, hot' }, { name: 'time', help: 'Time range: day, week, month, year' }],
24
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
25
+ func: async (page, kwargs) => {
26
+ const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page'); const contentType = kwargs['content-type'] ?? kwargs.contentType ?? 'all';
27
+ if (!TYPES.includes(String(contentType))) throw new ArgumentError(`--content-type must be one of: ${TYPES.join(', ')}`); if (kwargs.sort && !SORTS.includes(String(kwargs.sort))) throw new ArgumentError(`--sort must be one of: ${SORTS.join(', ')}`); if (kwargs.time && !TIMES.includes(String(kwargs.time))) throw new ArgumentError(`--time must be one of: ${TIMES.join(', ')}`);
28
+ await runBrowserStep('CSDN search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, contentType, sort: kwargs.sort, time: kwargs.time }))); await page.wait({ selector: '.search-result-info, .search-result', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
29
+ const data = await runBrowserStep('CSDN search extraction', () => page.evaluate(`(${extractItems.toString()})()`)); if (data?.blocked) throw new CommandExecutionError('CSDN search requires login or was blocked', 'Open CSDN in the browser, complete any prompt, and retry.'); if (!data?.items?.length) throw emptySearchResults('CSDN', keyword);
30
+ return data.items.slice(0, limit).map((item, index) => ({ ...item, rank: (pageNumber - 1) * limit + index + 1 }));
31
+ },
32
+ });
33
+
34
+ export const __test__ = { command, buildSearchUrl };
@@ -0,0 +1,36 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
4
+
5
+ const SCOPES = ['projects', 'issues', 'merge_requests', 'commits', 'blobs', 'users'];
6
+ const ORDER = ['created_at', 'updated_at', 'latest_activity_at']; const SORT = ['asc', 'desc'];
7
+
8
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, scope = 'projects', orderBy, sort }) {
9
+ const url = new URL('https://gitlab.com/search'); url.searchParams.set('search', keyword); url.searchParams.set('page', String(page)); url.searchParams.set('per_page', String(limit));
10
+ if (scope) url.searchParams.set('scope', scope); if (orderBy) url.searchParams.set('order_by', orderBy); if (sort) url.searchParams.set('sort', sort); return url.toString();
11
+ }
12
+
13
+ function extractItems(scope) {
14
+ const blocked = /sign in|login|access denied|captcha/i.test(document.body?.textContent || '') && !document.querySelector('.search-result-row, .search-results'); const items = []; const seen = new Set();
15
+ for (const card of document.querySelectorAll('.search-result-row, .search-result-item, .search-results li')) {
16
+ const anchor = card.querySelector('a.gl-link[href], a[href*="gitlab.com/"]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
17
+ seen.add(anchor.href); const time = card.querySelector('time'); const type = scope === 'issues' ? 'issue' : scope === 'merge_requests' ? 'merge_request' : scope === 'projects' ? 'project' : scope || 'result';
18
+ items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.description, .description p, p')?.textContent || '').trim(), displayUrl: new URL(anchor.href).hostname + new URL(anchor.href).pathname, source: 'gitlab', resultType: type, author: (card.querySelector('.author, [data-testid="author"]')?.textContent || '').trim() || null, publishedAt: time?.getAttribute('datetime') || null, score: null, extra: {} });
19
+ }
20
+ return { blocked, items };
21
+ }
22
+
23
+ export const command = cli({
24
+ site: 'gitlab', name: 'search', access: 'read', description: 'Search GitLab', domain: 'gitlab.com', strategy: Strategy.PUBLIC, browser: true,
25
+ args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'scope', help: 'Search scope: projects, issues, merge_requests, commits, blobs, users' }, { name: 'order-by', help: 'Order by: created_at, updated_at, latest_activity_at' }, { name: 'sort', help: 'Sort direction: asc or desc' }],
26
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
27
+ func: async (page, kwargs) => {
28
+ const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page'); const scope = kwargs.scope || 'projects'; const orderBy = kwargs['order-by'] ?? kwargs.orderBy; const sort = kwargs.sort;
29
+ if (!SCOPES.includes(String(scope))) throw new ArgumentError(`--scope must be one of: ${SCOPES.join(', ')}`); if (orderBy && !ORDER.includes(String(orderBy))) throw new ArgumentError(`--order-by must be one of: ${ORDER.join(', ')}`); if (sort && !SORT.includes(String(sort))) throw new ArgumentError('--sort must be one of: asc, desc');
30
+ await runBrowserStep('GitLab search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, scope, orderBy, sort }))); await page.wait({ selector: '.search-result-row, .search-results', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
31
+ const data = await runBrowserStep('GitLab search extraction', () => page.evaluate(`(${extractItems.toString()})(${JSON.stringify(scope)})`)); if (data?.blocked) throw new CommandExecutionError('GitLab search requires sign-in or was blocked', 'Open GitLab in the browser, sign in if needed, and retry.'); if (!data?.items?.length) throw emptySearchResults('GitLab', keyword);
32
+ return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item }));
33
+ },
34
+ });
35
+
36
+ export const __test__ = { command, buildSearchUrl };
@@ -0,0 +1,36 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
4
+
5
+ const TYPES = ['web', 'news', 'video', 'image']; const SAFE = ['off', 'on'];
6
+
7
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, type, safe }) {
8
+ const url = new URL('https://so.com/s'); url.searchParams.set('q', keyword); url.searchParams.set('pn', String(page)); url.searchParams.set('num', String(limit));
9
+ if (type && type !== 'web') url.searchParams.set('type', type); if (safe) url.searchParams.set('safe', safe); return url.toString();
10
+ }
11
+
12
+ function extractItems() {
13
+ const blocked = /验证码|安全验证|访问异常|captcha|access denied/i.test(document.body?.textContent || ''); const items = []; const seen = new Set();
14
+ for (const card of document.querySelectorAll('.result, .res-list')) {
15
+ if (/result-ad|ad-result|推广/.test(card.className || '') || /推广/.test(card.textContent || '')) continue;
16
+ const anchor = card.querySelector('h3 a[href], h2 a[href]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
17
+ seen.add(anchor.href); items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.res-desc, .res-rich, p')?.textContent || '').trim(), displayUrl: (card.querySelector('cite, .res-link')?.textContent || '').trim(), resultType: 'web', extra: {} });
18
+ }
19
+ return { blocked, items };
20
+ }
21
+
22
+ export const command = cli({
23
+ site: 'so', name: 'search', access: 'read', description: 'Search 360 Search', domain: 'so.com', strategy: Strategy.PUBLIC, browser: true,
24
+ args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'type', help: 'Result type: web, news, video, image' }, { name: 'safe', help: 'Safe search: off or on' }],
25
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
26
+ func: async (page, kwargs) => {
27
+ const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page');
28
+ if (kwargs.type !== undefined && !TYPES.includes(String(kwargs.type))) throw new ArgumentError(`--type must be one of: ${TYPES.join(', ')}`); if (kwargs.safe !== undefined && !SAFE.includes(String(kwargs.safe))) throw new ArgumentError('--safe must be one of: off, on');
29
+ await runBrowserStep('360 search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, type: kwargs.type, safe: kwargs.safe })));
30
+ await page.wait({ selector: '.result, .res-list', timeout: 8 }).catch(() => page.wait(2).catch(() => {})); const data = await runBrowserStep('360 search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
31
+ if (data?.blocked) throw new CommandExecutionError('360 Search was blocked by a verification page', 'Complete the verification in the browser and retry.'); if (!data?.items?.length) throw emptySearchResults('360 Search', keyword);
32
+ return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item, source: 'so', author: null, publishedAt: null, score: null }));
33
+ },
34
+ });
35
+
36
+ export const __test__ = { command, buildSearchUrl };
@@ -0,0 +1,35 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
4
+
5
+ const TYPES = ['web', 'news', 'video', 'image']; const TIMES = ['day', 'week', 'month', 'year']; const SORTS = ['relevance', 'date'];
6
+
7
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, type, time, sort }) {
8
+ const url = new URL('https://sogou.com/web'); url.searchParams.set('query', keyword); url.searchParams.set('num', String(limit)); url.searchParams.set('page', String(page));
9
+ if (type && type !== 'web') url.searchParams.set('type', type); if (time) url.searchParams.set('tsn', time); if (sort === 'date') url.searchParams.set('sort', 'time'); return url.toString();
10
+ }
11
+
12
+ function extractItems() {
13
+ const blocked = /验证码|安全验证|访问异常|请完成验证|captcha/i.test(document.body?.textContent || ''); const items = []; const seen = new Set();
14
+ for (const card of document.querySelectorAll('.vrwrap, .rb')) {
15
+ const anchor = card.querySelector('h3 a[href], h4 a[href]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
16
+ if (/推广|广告/.test(card.textContent || '')) continue; seen.add(anchor.href); items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.str_info, .ft, .text-layout')?.textContent || '').trim(), displayUrl: (card.querySelector('cite, .citeurl')?.textContent || '').trim(), resultType: 'web', extra: {} });
17
+ }
18
+ return { blocked, items };
19
+ }
20
+
21
+ export const command = cli({
22
+ site: 'sogou', name: 'search', access: 'read', description: 'Search Sogou', domain: 'sogou.com', strategy: Strategy.PUBLIC, browser: true,
23
+ args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'type', help: 'Result type: web, news, video, image' }, { name: 'time', help: 'Time range: day, week, month, year' }, { name: 'sort', help: 'Sort order: relevance or date' }],
24
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
25
+ func: async (page, kwargs) => {
26
+ const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page');
27
+ for (const [value, choices, flag] of [[kwargs.type, TYPES, 'type'], [kwargs.time, TIMES, 'time'], [kwargs.sort, SORTS, 'sort']]) if (value !== undefined && !choices.includes(String(value))) throw new ArgumentError(`--${flag} must be one of: ${choices.join(', ')}`);
28
+ await runBrowserStep('Sogou search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, type: kwargs.type, time: kwargs.time, sort: kwargs.sort })));
29
+ await page.wait({ selector: '.vrwrap, .rb', timeout: 8 }).catch(() => page.wait(2).catch(() => {})); const data = await runBrowserStep('Sogou search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
30
+ if (data?.blocked) throw new CommandExecutionError('Sogou search was blocked by a verification page', 'Complete the verification in the browser and retry.'); if (!data?.items?.length) throw emptySearchResults('Sogou', keyword);
31
+ return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item, source: 'sogou', author: null, publishedAt: null, score: null }));
32
+ },
33
+ });
34
+
35
+ export const __test__ = { command, buildSearchUrl };
@@ -0,0 +1,33 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
4
+
5
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, author, since, until }) {
6
+ const url = new URL('https://www.threads.com/search'); url.searchParams.set('q', keyword); url.searchParams.set('limit', String(limit)); url.searchParams.set('page', String(page));
7
+ if (author) url.searchParams.set('author', String(author).replace(/^@+/, '')); if (since) url.searchParams.set('since', since); if (until) url.searchParams.set('until', until); return url.toString();
8
+ }
9
+
10
+ function extractItems() {
11
+ const body = document.body?.textContent || ''; const authRequired = /log in|sign up|登录|注册/i.test(body) && !document.querySelector('article[data-pressable-container], article'); const blocked = /captcha|unusual activity|try again later|暂时无法/i.test(body); const items = []; const seen = new Set();
12
+ for (const card of document.querySelectorAll('article[data-pressable-container], article')) {
13
+ const anchor = card.querySelector('a[href*="/post/"]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue; seen.add(anchor.href);
14
+ const authorAnchor = card.querySelector('a[href^="/@"], a[href*="/@"]'); const author = (authorAnchor?.textContent || authorAnchor?.getAttribute('href') || '').trim().replace(/^@+/, '').replace(/^\//, '').split('/')[0] || null; const time = card.querySelector('time');
15
+ items.push({ title: (card.querySelector('.text, [data-pressable-container] div')?.textContent || card.textContent || '').trim().slice(0, 500), url: anchor.href, snippet: (card.textContent || '').trim().slice(0, 500), displayUrl: new URL(anchor.href).hostname + new URL(anchor.href).pathname, source: 'threads', resultType: 'post', author, publishedAt: time?.getAttribute('datetime') || null, score: null, extra: {} });
16
+ }
17
+ return { authRequired, blocked, items };
18
+ }
19
+
20
+ export const command = cli({
21
+ site: 'threads', name: 'search', access: 'read', description: 'Search Threads', domain: 'www.threads.com', strategy: Strategy.PUBLIC, browser: true,
22
+ args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of posts (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'author', help: 'Filter by author handle' }, { name: 'since', help: 'Only posts on or after YYYY-MM-DD' }, { name: 'until', help: 'Only posts on or before YYYY-MM-DD' }],
23
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
24
+ func: async (page, kwargs) => {
25
+ const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page');
26
+ for (const [value, flag] of [[kwargs.since, 'since'], [kwargs.until, 'until']]) if (value !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(String(value))) throw new ArgumentError(`--${flag} must use YYYY-MM-DD format`);
27
+ await runBrowserStep('Threads search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, author: kwargs.author, since: kwargs.since, until: kwargs.until }))); await page.wait({ selector: 'article', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
28
+ const data = await runBrowserStep('Threads search extraction', () => page.evaluate(`(${extractItems.toString()})()`)); if (data?.authRequired) throw new AuthRequiredError('Threads search requires login', 'Open Threads in the browser and sign in before retrying.'); if (data?.blocked) throw new CommandExecutionError('Threads search was blocked by an anti-bot page', 'Complete the browser prompt and retry.'); if (!data?.items?.length) throw emptySearchResults('Threads', keyword);
29
+ return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item }));
30
+ },
31
+ });
32
+
33
+ export const __test__ = { command, buildSearchUrl };
@@ -0,0 +1,43 @@
1
+ import { cli, Strategy } from '@sovovs/bycli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
3
+ import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
4
+
5
+ const SORTS = ['relevance', 'date'];
6
+
7
+ export function buildSearchUrl({ keyword, limit = 10, page = 1, lr, lang, sort }) {
8
+ const url = new URL('https://yandex.com/search/');
9
+ url.searchParams.set('text', keyword); url.searchParams.set('numdoc', String(limit)); url.searchParams.set('p', String(page - 1));
10
+ if (lr) url.searchParams.set('lr', lr); if (lang) url.searchParams.set('lang', lang); if (sort === 'date') url.searchParams.set('how', 'tm');
11
+ return url.toString();
12
+ }
13
+
14
+ function extractItems() {
15
+ const blocked = /captcha|verify|access denied|consent/i.test(document.body?.textContent || '') && !document.querySelector('.serp-item');
16
+ const items = []; const seen = new Set();
17
+ for (const card of document.querySelectorAll('.serp-item')) {
18
+ const anchor = card.querySelector('h2 a[href], .OrganicTitle a[href]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
19
+ seen.add(anchor.href); items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.OrganicText, .TextContainer')?.textContent || '').trim(), displayUrl: (card.querySelector('.Path, .OrganicUrl')?.textContent || '').trim(), resultType: 'web', extra: {} });
20
+ }
21
+ return { blocked, items };
22
+ }
23
+
24
+ export const command = cli({
25
+ site: 'yandex', name: 'search', access: 'read', description: 'Search Yandex', domain: 'yandex.com', strategy: Strategy.PUBLIC, browser: true,
26
+ args: [
27
+ { name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' },
28
+ { name: 'lr', help: 'Yandex region code' }, { name: 'lang', help: 'Language code' }, { name: 'sort', help: 'Sort order: relevance or date' },
29
+ ],
30
+ columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
31
+ func: async (page, kwargs) => {
32
+ const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page');
33
+ if (kwargs.sort !== undefined && !SORTS.includes(String(kwargs.sort))) throw new ArgumentError('--sort must be one of: relevance, date');
34
+ await runBrowserStep('Yandex search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, lr: kwargs.lr, lang: kwargs.lang, sort: kwargs.sort })));
35
+ await page.wait({ selector: '.serp-item', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
36
+ const data = await runBrowserStep('Yandex search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
37
+ if (data?.blocked) throw new CommandExecutionError('Yandex search was blocked by a consent or verification page', 'Complete the page prompt in the browser and retry.');
38
+ if (!data?.items?.length) throw emptySearchResults('Yandex', keyword);
39
+ return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item, source: 'yandex', author: null, publishedAt: null, score: null }));
40
+ },
41
+ });
42
+
43
+ export const __test__ = { command, buildSearchUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovovs/bycli",
3
- "version": "2.1.37",
3
+ "version": "2.1.38",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },