@sovovs/bycli 1.8.0 → 2.0.0
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/cli-manifest.json +0 -61
- package/dist/src/adapter-shadow.js +2 -2
- package/dist/src/browser/analyze.d.ts +10 -99
- package/dist/src/browser/analyze.js +10 -226
- package/dist/src/browser/cdp.js +64 -7
- package/dist/src/browser/navigation-guard.d.ts +78 -0
- package/dist/src/browser/navigation-guard.js +142 -0
- package/dist/src/browser/navigation-guard.test.d.ts +1 -0
- package/dist/src/browser/url-policy.d.ts +89 -0
- package/dist/src/browser/url-policy.js +311 -0
- package/dist/src/browser/url-policy.test.d.ts +1 -0
- package/dist/src/browser/verify-fixture.js +2 -2
- package/dist/src/cli.js +10 -13
- package/dist/src/config-paths.d.ts +19 -0
- package/dist/src/config-paths.js +31 -0
- package/dist/src/daemon.js +240 -0
- package/dist/src/discovery.d.ts +5 -3
- package/dist/src/discovery.js +8 -6
- package/dist/src/execution.js +8 -3
- package/dist/src/main.js +18 -2
- package/dist/src/plugin.js +11 -8
- package/dist/src/recorder/highlevel/analyze.d.ts +29 -0
- package/dist/src/recorder/highlevel/analyze.js +91 -0
- package/dist/src/recorder/highlevel/init.d.ts +125 -0
- package/dist/src/recorder/highlevel/init.js +279 -0
- package/dist/src/recorder/highlevel/init.test.d.ts +1 -0
- package/dist/src/recorder/highlevel/verify.d.ts +73 -0
- package/dist/src/recorder/highlevel/verify.js +74 -0
- package/dist/src/recorder/http/analyze-runner.d.ts +28 -0
- package/dist/src/recorder/http/analyze-runner.js +78 -0
- package/dist/src/recorder/http/analyze-runner.test.d.ts +1 -0
- package/dist/src/recorder/http/handlers.d.ts +51 -0
- package/dist/src/recorder/http/handlers.js +219 -0
- package/dist/src/recorder/http/wrapper-config.d.ts +20 -0
- package/dist/src/recorder/http/wrapper-config.js +82 -0
- package/dist/src/recorder/http/wrapper-envelope.d.ts +31 -0
- package/dist/src/recorder/http/wrapper-envelope.js +51 -0
- package/dist/src/recorder/http/wrapper-gates.d.ts +16 -0
- package/dist/src/recorder/http/wrapper-gates.js +28 -0
- package/dist/src/recorder/http/wrapper-metrics.d.ts +1 -0
- package/dist/src/recorder/http/wrapper-metrics.js +5 -0
- package/dist/src/recorder/http/wrapper-registry.d.ts +50 -0
- package/dist/src/recorder/http/wrapper-registry.js +98 -0
- package/dist/src/recorder/http/wrapper-server.d.ts +59 -0
- package/dist/src/recorder/http/wrapper-server.js +240 -0
- package/dist/src/recorder/http/wrapper.test.d.ts +1 -0
- package/dist/src/recorder/observability/logger.d.ts +5 -0
- package/dist/src/recorder/observability/logger.js +13 -0
- package/dist/src/recorder/runner/config.d.ts +34 -0
- package/dist/src/recorder/runner/config.js +84 -0
- package/dist/src/recorder/runner/config.test.d.ts +1 -0
- package/dist/src/recorder/runner/reap.d.ts +79 -0
- package/dist/src/recorder/runner/reap.js +230 -0
- package/dist/src/recorder/runner/reap.test.d.ts +1 -0
- package/dist/src/recorder/runner/runner-port.d.ts +95 -0
- package/dist/src/recorder/runner/runner-port.js +518 -0
- package/dist/src/recorder/runner/runner-port.test.d.ts +1 -0
- package/dist/src/recorder/runner/session-keys.d.ts +28 -0
- package/dist/src/recorder/runner/session-keys.js +72 -0
- package/dist/src/recorder/runner/session-keys.test.d.ts +1 -0
- package/dist/src/recorder/runner/verify-runner-main.d.ts +95 -0
- package/dist/src/recorder/runner/verify-runner-main.js +275 -0
- package/dist/src/recorder/runner/verify-runner-main.test.d.ts +1 -0
- package/package.json +8 -5
- package/scripts/m1-daemon-navigate-drive.mjs +138 -0
- package/scripts/m1-extension-spike.mjs +237 -0
- package/scripts/m1-live-spike.mjs +156 -0
- package/scripts/m6b-connect-back-spike.sh +31 -0
- package/scripts/recorder.sh +186 -0
- package/clis/12306/utils.test.js +0 -331
- package/clis/1688/assets.test.js +0 -39
- package/clis/1688/download.test.js +0 -31
- package/clis/1688/item.test.js +0 -67
- package/clis/1688/search.test.js +0 -75
- package/clis/1688/shared.test.js +0 -57
- package/clis/1688/store.test.js +0 -62
- package/clis/36kr/article.test.js +0 -46
- package/clis/36kr/hot.test.js +0 -15
- package/clis/36kr/news.test.js +0 -85
- package/clis/51job/utils.test.js +0 -69
- package/clis/_atlassian/shared.test.js +0 -170
- package/clis/aibase/news.test.js +0 -59
- package/clis/amazon/bestsellers.test.js +0 -29
- package/clis/amazon/discussion.test.js +0 -124
- package/clis/amazon/offer.test.js +0 -29
- package/clis/amazon/product.test.js +0 -24
- package/clis/amazon/rankings.test.js +0 -41
- package/clis/amazon/search.test.js +0 -22
- package/clis/amazon/shared.test.js +0 -44
- package/clis/apple-podcasts/commands.test.js +0 -119
- package/clis/apple-podcasts/utils.test.js +0 -57
- package/clis/arxiv/arxiv.test.js +0 -112
- package/clis/baidu-scholar/search.test.js +0 -23
- package/clis/barchart/greeks.test.js +0 -138
- package/clis/bilibili/comment.test.js +0 -153
- package/clis/bilibili/comments.test.js +0 -142
- package/clis/bilibili/dynamic.test.js +0 -68
- package/clis/bilibili/hot.test.js +0 -17
- package/clis/bilibili/subtitle.test.js +0 -196
- package/clis/bilibili/summary.test.js +0 -210
- package/clis/bilibili/utils.test.js +0 -61
- package/clis/bilibili/video.test.js +0 -132
- package/clis/binance/commands.test.js +0 -70
- package/clis/bloomberg/utils.test.js +0 -129
- package/clis/booking/booking.test.js +0 -356
- package/clis/boss/chatlist.test.js +0 -211
- package/clis/boss/chatmsg.test.js +0 -230
- package/clis/boss/search.test.js +0 -78
- package/clis/boss/utils.test.js +0 -34
- package/clis/brave/search.test.js +0 -76
- package/clis/chaoxing/utils.test.js +0 -45
- package/clis/chatgpt/commands.test.js +0 -45
- package/clis/chatgpt/envelope.test.js +0 -108
- package/clis/chatgpt/image.test.js +0 -188
- package/clis/chatgpt/utils.test.js +0 -330
- package/clis/chatgpt-app/ax.test.js +0 -35
- package/clis/chatwise/composer.test.js +0 -186
- package/clis/chess/analyze.test.js +0 -79
- package/clis/chess/game.test.js +0 -178
- package/clis/chess/games.test.js +0 -164
- package/clis/chess/stats.test.js +0 -79
- package/clis/chess/utils.test.js +0 -230
- package/clis/claude/ask.test.js +0 -338
- package/clis/claude/commands.test.js +0 -118
- package/clis/claude/utils.test.js +0 -148
- package/clis/cnki/search.test.js +0 -18
- package/clis/codex/sidebar.test.js +0 -329
- package/clis/coingecko/coingecko.test.js +0 -109
- package/clis/confluence/commands.test.js +0 -195
- package/clis/coupang/coupang.test.js +0 -159
- package/clis/coupang/utils.test.js +0 -62
- package/clis/ctrip/ctrip.test.js +0 -719
- package/clis/dblp/dblp.test.js +0 -397
- package/clis/deepseek/ask.test.js +0 -312
- package/clis/deepseek/detail.test.js +0 -81
- package/clis/deepseek/send.test.js +0 -107
- package/clis/deepseek/utils.test.js +0 -370
- package/clis/defillama/defillama.test.js +0 -99
- package/clis/devto/devto.test.js +0 -236
- package/clis/dianping/dianping.test.js +0 -578
- package/clis/douban/download.test.js +0 -170
- package/clis/douban/movie-hot.test.js +0 -14
- package/clis/douban/search.test.js +0 -11
- package/clis/douban/subject.test.js +0 -11
- package/clis/douban/utils.test.js +0 -438
- package/clis/doubao/detail.test.js +0 -42
- package/clis/doubao/history.test.js +0 -37
- package/clis/doubao/utils.test.js +0 -344
- package/clis/douyin/_shared/browser-fetch.test.js +0 -64
- package/clis/douyin/_shared/creation-id.test.js +0 -22
- package/clis/douyin/_shared/imagex-upload.test.js +0 -87
- package/clis/douyin/_shared/sts2.test.js +0 -27
- package/clis/douyin/_shared/text-extra.test.js +0 -37
- package/clis/douyin/_shared/timing.test.js +0 -28
- package/clis/douyin/_shared/tos-upload-short-read.test.js +0 -83
- package/clis/douyin/_shared/tos-upload.test.js +0 -229
- package/clis/douyin/_shared/transcode.test.js +0 -91
- package/clis/douyin/_shared/vod-upload.test.js +0 -38
- package/clis/douyin/activities.test.js +0 -55
- package/clis/douyin/collections.test.js +0 -45
- package/clis/douyin/delete.test.js +0 -100
- package/clis/douyin/draft.test.js +0 -335
- package/clis/douyin/drafts.test.js +0 -11
- package/clis/douyin/hashtag.test.js +0 -171
- package/clis/douyin/location.test.js +0 -23
- package/clis/douyin/profile.test.js +0 -46
- package/clis/douyin/publish-upload-id.test.js +0 -170
- package/clis/douyin/publish.test.js +0 -38
- package/clis/douyin/stats.test.js +0 -22
- package/clis/douyin/update.test.js +0 -11
- package/clis/douyin/user-videos.test.js +0 -151
- package/clis/douyin/videos.test.js +0 -54
- package/clis/duckduckgo/search.test.js +0 -128
- package/clis/duckduckgo/suggest.test.js +0 -66
- package/clis/eastmoney/hot-rank.test.js +0 -59
- package/clis/endoflife/endoflife.test.js +0 -51
- package/clis/facebook/feed.test.js +0 -169
- package/clis/facebook/marketplace.test.js +0 -91
- package/clis/facebook/notifications.test.js +0 -458
- package/clis/facebook/search.test.js +0 -55
- package/clis/flathub/flathub.test.js +0 -90
- package/clis/flomo/memos.test.js +0 -144
- package/clis/gemini/ask.test.js +0 -100
- package/clis/gemini/deep-research-result.test.js +0 -155
- package/clis/gemini/deep-research.test.js +0 -181
- package/clis/gemini/reply-state.test.js +0 -671
- package/clis/gemini/utils.test.js +0 -198
- package/clis/geogebra/geogebra.test.js +0 -175
- package/clis/gitee/search.test.js +0 -65
- package/clis/gitee/user.test.js +0 -63
- package/clis/google/utils.test.js +0 -75
- package/clis/google-scholar/cite.test.js +0 -47
- package/clis/google-scholar/profile.test.js +0 -49
- package/clis/google-scholar/search.test.js +0 -71
- package/clis/goproxy/goproxy.test.js +0 -103
- package/clis/gov-law/commands.test.js +0 -39
- package/clis/gov-policy/gov-policy.test.js +0 -224
- package/clis/grok/ask.test.js +0 -29
- package/clis/grok/utils.test.js +0 -103
- package/clis/hackernews/hackernews.test.js +0 -132
- package/clis/hf/hf.test.js +0 -16
- package/clis/hupu/hot.test.js +0 -224
- package/clis/imdb/utils.test.js +0 -88
- package/clis/indeed/indeed.test.js +0 -375
- package/clis/instagram/_shared/private-publish.test.js +0 -705
- package/clis/instagram/_shared/protocol-capture.test.js +0 -114
- package/clis/instagram/download.test.js +0 -118
- package/clis/instagram/note.test.js +0 -53
- package/clis/instagram/post.test.js +0 -1647
- package/clis/instagram/reel.test.js +0 -167
- package/clis/instagram/story.test.js +0 -134
- package/clis/jd/commands.test.js +0 -41
- package/clis/jd/item.test.js +0 -354
- package/clis/jianyu/search.test.js +0 -153
- package/clis/jianyu/shared/procurement-contract.test.js +0 -72
- package/clis/jianyu/shared/procurement-detail.test.js +0 -84
- package/clis/jike/read.test.js +0 -86
- package/clis/jira/commands.test.js +0 -287
- package/clis/juejin/search.js +0 -123
- package/clis/juejin/search.test.js +0 -98
- package/clis/lesswrong/frontpage.test.js +0 -37
- package/clis/lichess/lichess.test.js +0 -85
- package/clis/linkedin/connect.test.js +0 -213
- package/clis/linkedin/inbox.test.js +0 -152
- package/clis/linkedin/job-detail.test.js +0 -38
- package/clis/linkedin/jobs-preferences.test.js +0 -43
- package/clis/linkedin/people-search.test.js +0 -216
- package/clis/linkedin/post-analytics.test.js +0 -40
- package/clis/linkedin/posts.test.js +0 -40
- package/clis/linkedin/profile-analytics.test.js +0 -67
- package/clis/linkedin/profile-experience.test.js +0 -152
- package/clis/linkedin/profile-projects.test.js +0 -111
- package/clis/linkedin/profile-read.test.js +0 -77
- package/clis/linkedin/safe-send.test.js +0 -204
- package/clis/linkedin/salesnav-inbox.test.js +0 -113
- package/clis/linkedin/salesnav-message.test.js +0 -172
- package/clis/linkedin/salesnav-search.test.js +0 -76
- package/clis/linkedin/salesnav-thread.test.js +0 -79
- package/clis/linkedin/search.test.js +0 -222
- package/clis/linkedin/sent-invitations.test.js +0 -62
- package/clis/linkedin/services-read.test.js +0 -105
- package/clis/linkedin/thread-snapshot.test.js +0 -89
- package/clis/linkedin/timeline.test.js +0 -81
- package/clis/linkedin-learning/course.test.js +0 -114
- package/clis/linkedin-learning/search.test.js +0 -144
- package/clis/linkedin-learning/trending.test.js +0 -123
- package/clis/linux-do/feed.test.js +0 -153
- package/clis/linux-do/topic-content.test.js +0 -59
- package/clis/lobsters/lobsters.test.js +0 -169
- package/clis/notebooklm/add-source.test.js +0 -97
- package/clis/notebooklm/compat.test.js +0 -16
- package/clis/notebooklm/create.test.js +0 -58
- package/clis/notebooklm/generate-audio.test.js +0 -63
- package/clis/notebooklm/generate-slides.test.js +0 -75
- package/clis/notebooklm/history.test.js +0 -58
- package/clis/notebooklm/note-list.test.js +0 -56
- package/clis/notebooklm/notes-get.test.js +0 -72
- package/clis/notebooklm/open.test.js +0 -63
- package/clis/notebooklm/rpc.test.js +0 -131
- package/clis/notebooklm/source-fulltext.test.js +0 -106
- package/clis/notebooklm/source-get.test.js +0 -84
- package/clis/notebooklm/source-guide.test.js +0 -104
- package/clis/notebooklm/summary.test.js +0 -78
- package/clis/notebooklm/utils.test.js +0 -449
- package/clis/notebooklm/write-note.test.js +0 -70
- package/clis/nuget/nuget.test.js +0 -111
- package/clis/oeis/oeis.test.js +0 -88
- package/clis/ones/task-helpers.test.js +0 -12
- package/clis/ones/worklog.test.js +0 -20
- package/clis/openfda/openfda.test.js +0 -114
- package/clis/openreview/openreview.test.js +0 -427
- package/clis/osv/osv.test.js +0 -97
- package/clis/paperreview/commands.test.js +0 -243
- package/clis/paperreview/utils.test.js +0 -49
- package/clis/pixiv/download.test.js +0 -87
- package/clis/pixiv/illusts.test.js +0 -99
- package/clis/pixiv/search.test.js +0 -83
- package/clis/powerchina/search.test.js +0 -93
- package/clis/producthunt/utils.test.js +0 -64
- package/clis/pubmed/pubmed.test.js +0 -276
- package/clis/quark/utils.test.js +0 -58
- package/clis/qwen/utils.test.js +0 -45
- package/clis/reddit/extract-media.test.js +0 -149
- package/clis/reddit/frontpage.test.js +0 -34
- package/clis/reddit/home.test.js +0 -170
- package/clis/reddit/hot.test.js +0 -32
- package/clis/reddit/popular.test.js +0 -26
- package/clis/reddit/read.test.js +0 -335
- package/clis/reddit/reply.test.js +0 -89
- package/clis/reddit/search.test.js +0 -26
- package/clis/reddit/subreddit-info.test.js +0 -163
- package/clis/reddit/subreddit.test.js +0 -31
- package/clis/reddit/subscribed.test.js +0 -168
- package/clis/reddit/whoami.test.js +0 -105
- package/clis/rednote/navigation.test.js +0 -26
- package/clis/rednote/rednote.test.js +0 -222
- package/clis/rest-countries/rest-countries.test.js +0 -83
- package/clis/reuters/reuters.test.js +0 -299
- package/clis/rfc/rfc.test.js +0 -74
- package/clis/sinafinance/stock.test.js +0 -59
- package/clis/spotify/utils.test.js +0 -67
- package/clis/stackoverflow/stackoverflow.test.js +0 -404
- package/clis/steam/steam.test.js +0 -46
- package/clis/substack/utils.test.js +0 -44
- package/clis/suno/commands.test.js +0 -188
- package/clis/suno/download.test.js +0 -151
- package/clis/suno/generate.test.js +0 -252
- package/clis/suno/utils.test.js +0 -329
- package/clis/taobao/commands.test.js +0 -41
- package/clis/tdx/hot-rank.test.js +0 -59
- package/clis/ths/hot-rank.test.js +0 -64
- package/clis/tieba/commands.test.js +0 -79
- package/clis/tieba/utils.test.js +0 -290
- package/clis/tiktok/creator-videos.test.js +0 -113
- package/clis/tiktok/refactor.test.js +0 -389
- package/clis/tiktok/write-refactor.test.js +0 -370
- package/clis/toutiao/toutiao.test.js +0 -378
- package/clis/tvmaze/tvmaze.test.js +0 -93
- package/clis/twitter/bookmark-folder.test.js +0 -395
- package/clis/twitter/bookmark-folders.test.js +0 -152
- package/clis/twitter/bookmark.test.js +0 -74
- package/clis/twitter/bookmarks.test.js +0 -205
- package/clis/twitter/delete.test.js +0 -85
- package/clis/twitter/device-follow.test.js +0 -287
- package/clis/twitter/download.test.js +0 -457
- package/clis/twitter/followers.test.js +0 -62
- package/clis/twitter/following.test.js +0 -338
- package/clis/twitter/hide-reply.test.js +0 -76
- package/clis/twitter/like.test.js +0 -73
- package/clis/twitter/likes.test.js +0 -220
- package/clis/twitter/list-add.test.js +0 -133
- package/clis/twitter/list-create.test.js +0 -169
- package/clis/twitter/list-remove.test.js +0 -110
- package/clis/twitter/list-tweets.test.js +0 -196
- package/clis/twitter/lists.test.js +0 -253
- package/clis/twitter/post.test.js +0 -258
- package/clis/twitter/profile.test.js +0 -39
- package/clis/twitter/quote.test.js +0 -194
- package/clis/twitter/reply.test.js +0 -237
- package/clis/twitter/retweet.test.js +0 -73
- package/clis/twitter/search.test.js +0 -349
- package/clis/twitter/shared.test.js +0 -794
- package/clis/twitter/thread.test.js +0 -58
- package/clis/twitter/timeline.test.js +0 -104
- package/clis/twitter/trending.test.js +0 -15
- package/clis/twitter/tweets.test.js +0 -362
- package/clis/twitter/unbookmark.test.js +0 -73
- package/clis/twitter/unlike.test.js +0 -75
- package/clis/twitter/unretweet.test.js +0 -73
- package/clis/twitter/utils.test.js +0 -169
- package/clis/uisdc/news.test.js +0 -66
- package/clis/uiverse/_shared.test.js +0 -62
- package/clis/uiverse/navigation.test.js +0 -12
- package/clis/upwork/upwork.test.js +0 -566
- package/clis/wanfang/search.test.js +0 -23
- package/clis/web/read.test.js +0 -392
- package/clis/weibo/delete.test.js +0 -94
- package/clis/weibo/envelope.test.js +0 -85
- package/clis/weibo/favorites.test.js +0 -114
- package/clis/weibo/publish.test.js +0 -192
- package/clis/weibo/user-posts.test.js +0 -92
- package/clis/weibo/utils.test.js +0 -36
- package/clis/weixin/drafts.test.js +0 -69
- package/clis/weixin/search.test.js +0 -227
- package/clis/weread/ai-outline.test.js +0 -83
- package/clis/weread/book-search.test.js +0 -242
- package/clis/weread/commands.test.js +0 -398
- package/clis/weread/private-api-regression.test.js +0 -369
- package/clis/weread/search-regression.test.js +0 -494
- package/clis/weread/utils.test.js +0 -128
- package/clis/weread-official/commands.test.js +0 -385
- package/clis/weread-official/utils.test.js +0 -242
- package/clis/wikidata/wikidata.test.js +0 -83
- package/clis/wikipedia/trending.test.js +0 -57
- package/clis/wttr/wttr.test.js +0 -84
- package/clis/xianyu/chat.test.js +0 -84
- package/clis/xianyu/im.test.js +0 -253
- package/clis/xianyu/item.test.js +0 -56
- package/clis/xianyu/publish.test.js +0 -220
- package/clis/xianyu/search.test.js +0 -17
- package/clis/xiaoe/xiaoe.test.js +0 -486
- package/clis/xiaohongshu/comments.test.js +0 -258
- package/clis/xiaohongshu/creator-note-detail.test.js +0 -451
- package/clis/xiaohongshu/creator-notes-summary.test.js +0 -94
- package/clis/xiaohongshu/creator-notes.test.js +0 -281
- package/clis/xiaohongshu/creator-stats.test.js +0 -24
- package/clis/xiaohongshu/delete-note.test.js +0 -172
- package/clis/xiaohongshu/download.test.js +0 -316
- package/clis/xiaohongshu/navigation.test.js +0 -34
- package/clis/xiaohongshu/note.test.js +0 -249
- package/clis/xiaohongshu/publish.test.js +0 -608
- package/clis/xiaohongshu/search.test.js +0 -325
- package/clis/xiaohongshu/user-helpers.test.js +0 -160
- package/clis/xiaoyuzhou/auth.test.js +0 -124
- package/clis/xiaoyuzhou/download.test.js +0 -135
- package/clis/xiaoyuzhou/podcast-episodes.test.js +0 -78
- package/clis/xiaoyuzhou/transcript.test.js +0 -195
- package/clis/xiaoyuzhou/utils.test.js +0 -39
- package/clis/xueqiu/comments.test.js +0 -696
- package/clis/xueqiu/danjuan-utils.test.js +0 -41
- package/clis/xueqiu/utils.test.js +0 -26
- package/clis/yahoo/search.test.js +0 -94
- package/clis/youdao/note.test.js +0 -99
- package/clis/youtube/channel.test.js +0 -59
- package/clis/youtube/feed.test.js +0 -131
- package/clis/youtube/transcript-group.test.js +0 -99
- package/clis/youtube/transcript.test.js +0 -296
- package/clis/youtube/utils.test.js +0 -84
- package/clis/yuanbao/ask.test.js +0 -124
- package/clis/yuanbao/new.test.js +0 -30
- package/clis/yuanbao/shared.test.js +0 -80
- package/clis/zhihu/answer-comments.test.js +0 -287
- package/clis/zhihu/answer-detail.test.js +0 -338
- package/clis/zhihu/answer.test.js +0 -54
- package/clis/zhihu/collection.test.js +0 -337
- package/clis/zhihu/collections.test.js +0 -182
- package/clis/zhihu/comment.test.js +0 -50
- package/clis/zhihu/download.test.js +0 -12
- package/clis/zhihu/favorite.test.js +0 -59
- package/clis/zhihu/follow.test.js +0 -42
- package/clis/zhihu/like.test.js +0 -41
- package/clis/zhihu/question.test.js +0 -251
- package/clis/zhihu/recommend.test.js +0 -143
- package/clis/zhihu/search.test.js +0 -198
- package/clis/zhihu/target.test.js +0 -77
- package/clis/zhihu/text.test.js +0 -24
- package/clis/zhihu/write-shared.test.js +0 -176
- package/clis/zlibrary/commands.test.js +0 -65
- package/clis/zsxq/search.test.js +0 -24
- package/clis/zsxq/topic.test.js +0 -30
- package/clis/zsxq/topics.test.js +0 -24
- package/clis/zsxq/utils.test.js +0 -31
- package/dist/src/adapter-shadow.test.js +0 -49
- package/dist/src/adapter-source.test.js +0 -29
- package/dist/src/browser/analyze.test.js +0 -166
- package/dist/src/browser/article-extract.e2e.test.js +0 -105
- package/dist/src/browser/article-extract.test.js +0 -94
- package/dist/src/browser/ax-snapshot.test.js +0 -91
- package/dist/src/browser/base-page.test.js +0 -758
- package/dist/src/browser/cdp-click-fixture.test.js +0 -87
- package/dist/src/browser/cdp.test.js +0 -79
- package/dist/src/browser/compound.test.js +0 -175
- package/dist/src/browser/daemon-client.test.js +0 -199
- package/dist/src/browser/dom-helpers.test.js +0 -106
- package/dist/src/browser/dom-snapshot.test.js +0 -313
- package/dist/src/browser/errors.test.js +0 -60
- package/dist/src/browser/extract.test.js +0 -129
- package/dist/src/browser/find.test.js +0 -180
- package/dist/src/browser/html-tree.test.js +0 -181
- package/dist/src/browser/network-cache.test.js +0 -58
- package/dist/src/browser/network-key.test.js +0 -49
- package/dist/src/browser/page.test.js +0 -378
- package/dist/src/browser/shape-filter.test.js +0 -101
- package/dist/src/browser/shape.test.js +0 -82
- package/dist/src/browser/stealth.test.js +0 -134
- package/dist/src/browser/target-errors.test.js +0 -94
- package/dist/src/browser/target-resolver.test.js +0 -118
- package/dist/src/browser/utils.test.js +0 -29
- package/dist/src/browser/verify-fixture.test.js +0 -219
- package/dist/src/browser.test.js +0 -232
- package/dist/src/build-manifest.test.js +0 -341
- package/dist/src/capabilityRouting.test.js +0 -63
- package/dist/src/cli-argv-preprocess.test.js +0 -209
- package/dist/src/cli.test.js +0 -2803
- package/dist/src/commanderAdapter.test.js +0 -352
- package/dist/src/commands/daemon.test.js +0 -219
- package/dist/src/completion.test.js +0 -24
- package/dist/src/convention-audit.test.js +0 -247
- package/dist/src/daemon.test.js +0 -60
- package/dist/src/doctor.test.js +0 -263
- package/dist/src/download/article-download.test.js +0 -235
- package/dist/src/download/index.test.js +0 -140
- package/dist/src/download/media-download.test.js +0 -112
- package/dist/src/download/progress.test.js +0 -36
- package/dist/src/electron-apps.test.js +0 -49
- package/dist/src/engine.test.js +0 -312
- package/dist/src/errors.test.js +0 -109
- package/dist/src/execution.test.js +0 -623
- package/dist/src/extension-manifest-regression.test.js +0 -12
- package/dist/src/external.test.js +0 -146
- package/dist/src/help.test.js +0 -58
- package/dist/src/hooks.test.js +0 -92
- package/dist/src/interceptor.test.js +0 -81
- package/dist/src/launcher.test.js +0 -138
- package/dist/src/node-network.test.js +0 -55
- package/dist/src/observation/artifact.test.js +0 -121
- package/dist/src/observation/manager.test.js +0 -13
- package/dist/src/observation/redaction.test.js +0 -32
- package/dist/src/observation/retention.test.js +0 -118
- package/dist/src/observation/ring-buffer.test.js +0 -22
- package/dist/src/output.test.js +0 -53
- package/dist/src/package-exports.test.js +0 -112
- package/dist/src/pipeline/executor.test.js +0 -182
- package/dist/src/pipeline/steps/download.test.js +0 -102
- package/dist/src/pipeline/steps/fetch.test.js +0 -117
- package/dist/src/pipeline/template.test.js +0 -175
- package/dist/src/pipeline/transform.test.js +0 -133
- package/dist/src/plugin-manifest.test.js +0 -179
- package/dist/src/plugin-scaffold.test.js +0 -83
- package/dist/src/plugin.test.js +0 -1324
- package/dist/src/registry.test.js +0 -209
- package/dist/src/runtime-detect.test.js +0 -39
- package/dist/src/serialization.test.js +0 -78
- package/dist/src/snapshotFormatter.test.js +0 -521
- package/dist/src/tui.test.js +0 -19
- package/dist/src/update-check.test.js +0 -116
- package/dist/src/utils.test.js +0 -155
- package/dist/src/validate.test.js +0 -90
- package/dist/src/weixin-download.test.js +0 -45
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const TERMINAL = ['succeeded', 'failed', 'timeout', 'cancelled'];
|
|
2
|
+
function project(rec) {
|
|
3
|
+
const { runnerId: _runnerId, ...status } = rec;
|
|
4
|
+
return status;
|
|
5
|
+
}
|
|
6
|
+
export class WrapperRegistry {
|
|
7
|
+
opts;
|
|
8
|
+
requests = new Map();
|
|
9
|
+
now;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.opts = opts;
|
|
12
|
+
this.now = opts.now ?? Date.now;
|
|
13
|
+
}
|
|
14
|
+
/** 铸一条 in-flight 记录。init 同步跑完会立刻 finalize;verify/analyze 留 running 待轮询/后台收尾。 */
|
|
15
|
+
createRequest(input) {
|
|
16
|
+
const t = this.now();
|
|
17
|
+
const rec = {
|
|
18
|
+
requestId: input.requestId,
|
|
19
|
+
type: input.type,
|
|
20
|
+
status: input.status ?? 'running',
|
|
21
|
+
startedAt: t,
|
|
22
|
+
updatedAt: t,
|
|
23
|
+
expiresAt: null,
|
|
24
|
+
pollAfterMs: this.opts.pollAfterMs,
|
|
25
|
+
queueReason: null,
|
|
26
|
+
progress: null,
|
|
27
|
+
result: null,
|
|
28
|
+
error: null,
|
|
29
|
+
runnerId: input.runnerId ?? input.requestId,
|
|
30
|
+
};
|
|
31
|
+
this.requests.set(input.requestId, rec);
|
|
32
|
+
return project(rec);
|
|
33
|
+
}
|
|
34
|
+
/** 合并一条非终态 status/result/error/queueReason 更新;终态记录不可变。 */
|
|
35
|
+
updateRequest(requestId, patch) {
|
|
36
|
+
const rec = this.requests.get(requestId);
|
|
37
|
+
if (!rec)
|
|
38
|
+
return;
|
|
39
|
+
if (TERMINAL.includes(rec.status))
|
|
40
|
+
return;
|
|
41
|
+
if (patch.status)
|
|
42
|
+
rec.status = patch.status;
|
|
43
|
+
if (patch.result !== undefined)
|
|
44
|
+
rec.result = patch.result;
|
|
45
|
+
if (patch.error !== undefined)
|
|
46
|
+
rec.error = patch.error;
|
|
47
|
+
if (patch.queueReason !== undefined)
|
|
48
|
+
rec.queueReason = patch.queueReason;
|
|
49
|
+
rec.updatedAt = this.now();
|
|
50
|
+
}
|
|
51
|
+
/** 置终态 + 武装 TTL(终态过期 → getRequest 返 undefined → 404)。已终态则幂等 no-op。 */
|
|
52
|
+
finalizeRequest(requestId, patch) {
|
|
53
|
+
const rec = this.requests.get(requestId);
|
|
54
|
+
if (!rec)
|
|
55
|
+
return;
|
|
56
|
+
if (TERMINAL.includes(rec.status))
|
|
57
|
+
return;
|
|
58
|
+
rec.status = patch.status;
|
|
59
|
+
if (patch.result !== undefined)
|
|
60
|
+
rec.result = patch.result;
|
|
61
|
+
if (patch.error !== undefined)
|
|
62
|
+
rec.error = patch.error;
|
|
63
|
+
const t = this.now();
|
|
64
|
+
rec.updatedAt = t;
|
|
65
|
+
rec.expiresAt = t + this.opts.terminalTtlMs;
|
|
66
|
+
}
|
|
67
|
+
/** 面向客户端的 RequestStatus(TTL 校验,wrapper-only 字段投影掉)。 */
|
|
68
|
+
getRequest(id) {
|
|
69
|
+
const rec = this.getRecord(id);
|
|
70
|
+
return rec ? project(rec) : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** 内部记录(含 runnerId;handler 用,不返客户端)。TTL 过期即删并返 undefined。 */
|
|
73
|
+
getRecord(id) {
|
|
74
|
+
const r = this.requests.get(id);
|
|
75
|
+
if (!r)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (r.expiresAt !== null && this.now() > r.expiresAt) {
|
|
78
|
+
this.requests.delete(id);
|
|
79
|
+
return undefined; // TTL 过期 → request_not_found
|
|
80
|
+
}
|
|
81
|
+
return r;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 周期清理:删除所有已过 TTL 的终态记录(getRecord 只在"被访问"时删,abandoned 的终态记录否则常驻
|
|
85
|
+
* 内存)。non-terminal 记录 expiresAt=null,绝不被清。返回清理条数。
|
|
86
|
+
*/
|
|
87
|
+
sweepExpired() {
|
|
88
|
+
const t = this.now();
|
|
89
|
+
let removed = 0;
|
|
90
|
+
for (const [id, rec] of this.requests) {
|
|
91
|
+
if (rec.expiresAt !== null && t > rec.expiresAt) {
|
|
92
|
+
this.requests.delete(id);
|
|
93
|
+
removed += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { type WrapperConfig } from './wrapper-config.js';
|
|
3
|
+
import { WrapperRegistry } from './wrapper-registry.js';
|
|
4
|
+
import { type Metrics } from './wrapper-metrics.js';
|
|
5
|
+
import { type AnalyzeRunner } from './analyze-runner.js';
|
|
6
|
+
import { createAdapterDraft } from '../highlevel/init.js';
|
|
7
|
+
import { type RunnerPortWithLifecycle } from '../runner/runner-port.js';
|
|
8
|
+
/** 结构化日志允许字段(09;forbidden=token/body/seed 类型上就进不来)。 */
|
|
9
|
+
interface LogFields {
|
|
10
|
+
status: string;
|
|
11
|
+
errorCode?: string;
|
|
12
|
+
durationMs?: number;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface WrapperDeps {
|
|
16
|
+
/** 注入时钟(测试 TTL)。 */
|
|
17
|
+
now?: () => number;
|
|
18
|
+
/** 注入 init 实现(测试无需写盘)。默认真实 createAdapterDraft。 */
|
|
19
|
+
createDraft?: typeof createAdapterDraft;
|
|
20
|
+
/** 结构化日志 sink(默认 stderr 单行 JSON)。 */
|
|
21
|
+
logSink?: (line: string) => void;
|
|
22
|
+
/** 注入 metrics(测试断言)。默认 createMetrics()。 */
|
|
23
|
+
metrics?: Metrics;
|
|
24
|
+
/** 注入 verify runner(测试 fake;production = defaultRunnerPort,连回 daemon 拿 Page 跑 browser adapter)。 */
|
|
25
|
+
runner?: RunnerPortWithLifecycle;
|
|
26
|
+
/** 注入 sessionId→HMAC key(测试)。默认进程级 defaultSessionKeyRegistry。 */
|
|
27
|
+
sessionKeyFor?: (sessionId?: string) => string;
|
|
28
|
+
/** 注入 analyze runner(测试 fake;production = daemon-backed Page)。 */
|
|
29
|
+
analyzeRunner?: AnalyzeRunner;
|
|
30
|
+
}
|
|
31
|
+
export interface WrapperCtx {
|
|
32
|
+
cfg: WrapperConfig;
|
|
33
|
+
registry: WrapperRegistry;
|
|
34
|
+
createDraft: typeof createAdapterDraft;
|
|
35
|
+
runner: RunnerPortWithLifecycle;
|
|
36
|
+
sessionKeyFor?: (sessionId?: string) => string;
|
|
37
|
+
analyzeRunner: AnalyzeRunner;
|
|
38
|
+
/** 在飞后台作业(analyze in-process Page);shutdown drain 用(Codex M9 Med)。 */
|
|
39
|
+
inflight: Set<Promise<unknown>>;
|
|
40
|
+
log: (operation: string, fields: LogFields) => void;
|
|
41
|
+
metrics: Metrics;
|
|
42
|
+
}
|
|
43
|
+
/** Shutdown 时等在飞作业跑完(其 finally 释放 Page lease),最多 graceMs;超时则放弃(lease 靠 daemon
|
|
44
|
+
* idle-expire 兜底)。返回时 inflight 要么空、要么已超 grace。 */
|
|
45
|
+
export declare function drainInflight(inflight: Set<Promise<unknown>>, graceMs: number): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* 装配 wrapper app(工厂 + 可注入 deps 供测试)。返回 { server, ctx };调用方负责 server.listen。
|
|
48
|
+
*/
|
|
49
|
+
export declare function createWrapperApp(cfg: WrapperConfig, deps?: WrapperDeps): {
|
|
50
|
+
server: ReturnType<typeof createServer>;
|
|
51
|
+
ctx: WrapperCtx;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* `bycli internal highlevel-http` 入口:load config → 装配 → bind 127.0.0.1。默认关闭(opt-in)。
|
|
55
|
+
* 绝不打印 token(只打印 listening 行)。**在 server 存活期间不 resolve**(等 'close'),这样 main.ts
|
|
56
|
+
* 的 `await runWrapperCli()` 会一直挂起、绝不 fall-through 到完整 CLI discovery。
|
|
57
|
+
*/
|
|
58
|
+
export declare function runWrapperCli(): Promise<void>;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// M9 High-Level HTTP wrapper —— 独立 loopback HTTP server(07 · Optional HTTP Wrapper)。
|
|
2
|
+
// 形态镜像 dashboard-be/src/server.ts 的 createApp/createServer/finish-logger,但**主仓侧**(直调
|
|
3
|
+
// high-level 模块,不经 daemon bridge,不 import be)。与 daemon 的 /v1/*(daemon-high-level.v1)是
|
|
4
|
+
// 独立 server / 独立路由族(ADR-0007,严禁混淆)。默认关闭,经 `bycli internal highlevel-http` 启动。
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import { loadWrapperConfig } from './wrapper-config.js';
|
|
7
|
+
import { WrapperRegistry } from './wrapper-registry.js';
|
|
8
|
+
import { checkWrapperGates } from './wrapper-gates.js';
|
|
9
|
+
import { errorBody } from './wrapper-envelope.js';
|
|
10
|
+
import { handleHealth, handleInit, handleVerify, handleAnalyze, handleRequestStatus, } from './handlers.js';
|
|
11
|
+
import { createMetrics } from './wrapper-metrics.js';
|
|
12
|
+
import { createAnalyzeRunner } from './analyze-runner.js';
|
|
13
|
+
import { createAdapterDraft } from '../highlevel/init.js';
|
|
14
|
+
import { defaultRunnerPort, setDefaultRunnerDaemonPort, } from '../runner/runner-port.js';
|
|
15
|
+
/** Shutdown 时等在飞作业跑完(其 finally 释放 Page lease),最多 graceMs;超时则放弃(lease 靠 daemon
|
|
16
|
+
* idle-expire 兜底)。返回时 inflight 要么空、要么已超 grace。 */
|
|
17
|
+
export async function drainInflight(inflight, graceMs) {
|
|
18
|
+
if (inflight.size === 0)
|
|
19
|
+
return;
|
|
20
|
+
let timer;
|
|
21
|
+
const deadline = new Promise((r) => { timer = setTimeout(r, graceMs); });
|
|
22
|
+
try {
|
|
23
|
+
await Promise.race([Promise.allSettled([...inflight]).then(() => undefined), deadline]);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (timer)
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const REQUESTS_PREFIX = '/v1/requests/';
|
|
31
|
+
/** Shutdown 时等在飞 analyze 释放 Page lease 的最长宽限(Codex M9 Med)。 */
|
|
32
|
+
const SHUTDOWN_GRACE_MS = 5000;
|
|
33
|
+
function sendOutcome(res, outcome) {
|
|
34
|
+
if (outcome.errorCode)
|
|
35
|
+
res.__errorCode = outcome.errorCode;
|
|
36
|
+
const payload = JSON.stringify(outcome.body);
|
|
37
|
+
// 04 章:不回 CORS 头(no-CORS);nosniff。
|
|
38
|
+
res.writeHead(outcome.status, { 'Content-Type': 'application/json', 'X-Content-Type-Options': 'nosniff' });
|
|
39
|
+
res.end(payload);
|
|
40
|
+
}
|
|
41
|
+
async function readJson(req, maxBytes) {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let size = 0;
|
|
44
|
+
for await (const c of req) {
|
|
45
|
+
size += c.length;
|
|
46
|
+
if (size > maxBytes)
|
|
47
|
+
throw new Error('payload too large');
|
|
48
|
+
chunks.push(c);
|
|
49
|
+
}
|
|
50
|
+
if (!chunks.length)
|
|
51
|
+
return {};
|
|
52
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
53
|
+
}
|
|
54
|
+
function clampWaitMs(raw, max) {
|
|
55
|
+
if (!raw)
|
|
56
|
+
return 0;
|
|
57
|
+
const n = Number(raw);
|
|
58
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
59
|
+
return 0;
|
|
60
|
+
return Math.min(Math.floor(n), max);
|
|
61
|
+
}
|
|
62
|
+
/** 路由分发:gate(全端点)→ 已注册端点;未匹配 404。 */
|
|
63
|
+
async function route(ctx, req, res) {
|
|
64
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
65
|
+
const pathname = url.pathname;
|
|
66
|
+
const method = req.method ?? 'GET';
|
|
67
|
+
// 门禁:所有端点(含 /health)统一过 X-byCLI + Origin + token。
|
|
68
|
+
const gate = checkWrapperGates(req, { allowedOrigins: ctx.cfg.ALLOWED_ORIGINS, token: ctx.cfg.TOKEN });
|
|
69
|
+
if (!gate.ok) {
|
|
70
|
+
sendOutcome(res, {
|
|
71
|
+
status: 403,
|
|
72
|
+
body: { error: errorBody(gate.code ?? 'auth_failed', gate.message ?? 'forbidden') },
|
|
73
|
+
errorCode: gate.code ?? 'auth_failed',
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (method === 'GET' && pathname === '/health') {
|
|
78
|
+
sendOutcome(res, handleHealth());
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// GET /metrics —— loopback metrics scrape(09 · M8 复审遗留,gated 同其它端点)。返回 snapshot JSON
|
|
82
|
+
// (counters/histograms,标签只非敏感 enum)。不入 finish-logger 计数(避免 scrape 自计/噪声)。
|
|
83
|
+
if (method === 'GET' && pathname === '/metrics') {
|
|
84
|
+
sendOutcome(res, { status: 200, body: ctx.metrics.snapshot() });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (method === 'POST' && pathname === '/v1/adapters/init') {
|
|
88
|
+
let body;
|
|
89
|
+
try {
|
|
90
|
+
body = await readJson(req, ctx.cfg.BODY_LIMIT_BYTES);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
sendOutcome(res, {
|
|
94
|
+
status: 400,
|
|
95
|
+
body: { error: errorBody('validation_failed', 'request body too large or invalid JSON') },
|
|
96
|
+
errorCode: 'validation_failed',
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
sendOutcome(res, handleInit(ctx, body));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (method === 'POST' && pathname === '/v1/adapters/verify') {
|
|
104
|
+
let body;
|
|
105
|
+
try {
|
|
106
|
+
body = await readJson(req, ctx.cfg.BODY_LIMIT_BYTES);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
sendOutcome(res, {
|
|
110
|
+
status: 400,
|
|
111
|
+
body: { error: errorBody('validation_failed', 'request body too large or invalid JSON') },
|
|
112
|
+
errorCode: 'validation_failed',
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
sendOutcome(res, await handleVerify(ctx, body));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (method === 'POST' && pathname === '/v1/browser/analyze') {
|
|
120
|
+
let body;
|
|
121
|
+
try {
|
|
122
|
+
body = await readJson(req, ctx.cfg.BODY_LIMIT_BYTES);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
sendOutcome(res, {
|
|
126
|
+
status: 400,
|
|
127
|
+
body: { error: errorBody('validation_failed', 'request body too large or invalid JSON') },
|
|
128
|
+
errorCode: 'validation_failed',
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
sendOutcome(res, handleAnalyze(ctx, body));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (method === 'GET' && pathname.startsWith(REQUESTS_PREFIX)) {
|
|
136
|
+
const requestId = pathname.slice(REQUESTS_PREFIX.length);
|
|
137
|
+
const waitMs = clampWaitMs(url.searchParams.get('waitMs'), ctx.cfg.REQUEST_WAIT_MS_MAX);
|
|
138
|
+
sendOutcome(res, await handleRequestStatus(ctx, requestId, waitMs));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// 未注册路由(含 M9b/c 待接的 verify/analyze)→ 404。
|
|
142
|
+
sendOutcome(res, { status: 404, body: { error: { code: 'request_not_found', message: 'not found' } } });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 装配 wrapper app(工厂 + 可注入 deps 供测试)。返回 { server, ctx };调用方负责 server.listen。
|
|
146
|
+
*/
|
|
147
|
+
export function createWrapperApp(cfg, deps = {}) {
|
|
148
|
+
const logSink = deps.logSink ?? ((line) => process.stderr.write(line + '\n'));
|
|
149
|
+
// production runner = defaultRunnerPort();browser-verify 子进程经 BYCLI_DAEMON_PORT 连回 daemon 拿 Page,
|
|
150
|
+
// 故必须在首次 defaultRunnerPort() 前注入 daemon 端口(同 daemon.ts:49)。测试注入 deps.runner 绕开全局单例。
|
|
151
|
+
if (!deps.runner)
|
|
152
|
+
setDefaultRunnerDaemonPort(cfg.DAEMON_PORT);
|
|
153
|
+
const ctx = {
|
|
154
|
+
cfg,
|
|
155
|
+
registry: new WrapperRegistry({
|
|
156
|
+
terminalTtlMs: cfg.REQUEST_TERMINAL_STATUS_TTL_MS,
|
|
157
|
+
pollAfterMs: cfg.REQUEST_POLL_AFTER_MS,
|
|
158
|
+
now: deps.now,
|
|
159
|
+
}),
|
|
160
|
+
createDraft: deps.createDraft ?? createAdapterDraft,
|
|
161
|
+
runner: deps.runner ?? defaultRunnerPort(),
|
|
162
|
+
sessionKeyFor: deps.sessionKeyFor,
|
|
163
|
+
analyzeRunner: deps.analyzeRunner ?? createAnalyzeRunner({ timeoutMs: cfg.ANALYZE_TIMEOUT_MS }),
|
|
164
|
+
inflight: new Set(),
|
|
165
|
+
log: (operation, fields) => {
|
|
166
|
+
if (cfg.LOG_LEVEL === 'error' && fields.status === 'ok')
|
|
167
|
+
return; // 静音成功日志(error level)
|
|
168
|
+
logSink(JSON.stringify({ time: Date.now(), level: 'info', operation, ...fields }));
|
|
169
|
+
},
|
|
170
|
+
metrics: deps.metrics ?? createMetrics(),
|
|
171
|
+
};
|
|
172
|
+
const server = createServer((req, res) => {
|
|
173
|
+
// 单点 finish-logger + metrics(09 Structured Logging/Metrics):operation + outcome + latency。只
|
|
174
|
+
// path/method/status/errorCode/durationMs —— 永不含 header/token/body/seed。
|
|
175
|
+
const started = Date.now();
|
|
176
|
+
res.on('finish', () => {
|
|
177
|
+
const path = (req.url ?? '/').split('?')[0] ?? '/';
|
|
178
|
+
if (path === '/metrics')
|
|
179
|
+
return; // 不给 /metrics scrape 自计/记日志
|
|
180
|
+
// 把唯一动态段(GET /v1/requests/{id})collapse 成稳定模板,operation 基数有界。
|
|
181
|
+
const operation = path.startsWith(REQUESTS_PREFIX)
|
|
182
|
+
? 'highlevel.requests'
|
|
183
|
+
: `highlevel.${path.replace(/^\/+/, '').replace(/\//g, '.') || 'root'}`;
|
|
184
|
+
const status = res.statusCode < 400 ? 'ok' : 'failed';
|
|
185
|
+
const errorCode = res.__errorCode;
|
|
186
|
+
const durationMs = Date.now() - started;
|
|
187
|
+
ctx.log(operation, { status, errorCode, durationMs });
|
|
188
|
+
ctx.metrics.inc('highlevel_requests_total', { operation, status, errorCode });
|
|
189
|
+
ctx.metrics.observe('highlevel_request_duration_ms', durationMs);
|
|
190
|
+
});
|
|
191
|
+
route(ctx, req, res).catch(() => {
|
|
192
|
+
if (!res.headersSent) {
|
|
193
|
+
sendOutcome(res, {
|
|
194
|
+
status: 500,
|
|
195
|
+
body: { error: errorBody('network_error', 'internal error') },
|
|
196
|
+
errorCode: 'network_error',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
// 周期 sweep:回收已过 TTL 的终态记录(getRecord 只在访问时删,abandoned 终态否则常驻内存)。
|
|
202
|
+
// 间隔绑 TTL(Codex M9 Low:固定 5min 对 1min TTL 会多滞留 ~4min)——取 min(5min, max(30s, TTL/2))。
|
|
203
|
+
// unref → 不吊住进程;server 'close' 清掉(测试关 server 即停 timer,不泄漏)。
|
|
204
|
+
const sweepMs = Math.min(300_000, Math.max(30_000, Math.floor(cfg.REQUEST_TERMINAL_STATUS_TTL_MS / 2)));
|
|
205
|
+
const sweepTimer = setInterval(() => ctx.registry.sweepExpired(), sweepMs);
|
|
206
|
+
sweepTimer.unref?.();
|
|
207
|
+
server.on('close', () => clearInterval(sweepTimer));
|
|
208
|
+
return { server, ctx };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* `bycli internal highlevel-http` 入口:load config → 装配 → bind 127.0.0.1。默认关闭(opt-in)。
|
|
212
|
+
* 绝不打印 token(只打印 listening 行)。**在 server 存活期间不 resolve**(等 'close'),这样 main.ts
|
|
213
|
+
* 的 `await runWrapperCli()` 会一直挂起、绝不 fall-through 到完整 CLI discovery。
|
|
214
|
+
*/
|
|
215
|
+
export async function runWrapperCli() {
|
|
216
|
+
const cfg = loadWrapperConfig();
|
|
217
|
+
// analyze 的 daemon-backed Page 经 daemon-client(模块加载时读 process.env.BYCLI_DAEMON_PORT)连回 daemon;
|
|
218
|
+
// 显式钉到 cfg.DAEMON_PORT,保证 Page 连的是 wrapper 配置的同一 daemon(Page 在首次 analyze 才懒加载)。
|
|
219
|
+
process.env.BYCLI_DAEMON_PORT = String(cfg.DAEMON_PORT);
|
|
220
|
+
const { server, ctx } = createWrapperApp(cfg);
|
|
221
|
+
// 在 server 存活期间不 resolve(阻止 main.ts fall-through);仅在 SIGTERM/SIGINT 优雅关闭后 resolve。
|
|
222
|
+
await new Promise((resolve, reject) => {
|
|
223
|
+
let closing = false;
|
|
224
|
+
const shutdown = () => {
|
|
225
|
+
if (closing)
|
|
226
|
+
return;
|
|
227
|
+
closing = true;
|
|
228
|
+
server.close(); // 停止接新连接
|
|
229
|
+
// 先 drain 在飞 analyze(其 finally closeWindow 释放 daemon Page lease),最多 SHUTDOWN_GRACE_MS,
|
|
230
|
+
// 再 resolve → main.ts process.exit;不再被硬切(Codex M9 Med)。超 grace 则放弃,靠 lease idle-expire。
|
|
231
|
+
void drainInflight(ctx.inflight, SHUTDOWN_GRACE_MS).finally(() => resolve());
|
|
232
|
+
};
|
|
233
|
+
server.once('error', reject);
|
|
234
|
+
process.once('SIGTERM', shutdown);
|
|
235
|
+
process.once('SIGINT', shutdown);
|
|
236
|
+
server.listen(cfg.PORT, cfg.HOST, () => {
|
|
237
|
+
process.stderr.write(JSON.stringify({ time: Date.now(), level: 'info', operation: 'highlevel.listening', status: 'ok', port: cfg.PORT }) + '\n');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Logger, type LogLevel } from '@sovovs/bycli-recorder-core';
|
|
2
|
+
export type { Logger, LogLevel } from '@sovovs/bycli-recorder-core';
|
|
3
|
+
export type { LogFields } from '@sovovs/bycli-recorder-core';
|
|
4
|
+
/** daemon/runner structured logger: single-line JSON to stderr. Inject `sink` in tests. */
|
|
5
|
+
export declare function createRecorderLogger(level: LogLevel, sink?: (line: string) => void): Logger;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Structured logger for the daemon/runner recording path (#1a · 09 Structured Logging).
|
|
2
|
+
//
|
|
3
|
+
// The main repo keeps TWO loggers on purpose: `src/logger.ts` (human-facing emoji singleton,
|
|
4
|
+
// 17 CLI consumers, unchanged) and THIS one — single-line JSON for the daemon/runner SERVICE path,
|
|
5
|
+
// whose output goes to a detached process's stderr captured to daemon logs, never to an interactive
|
|
6
|
+
// user. The formatter + by-construction redaction (LogFields = only 09 allowed fields) live in
|
|
7
|
+
// recorder-core; this thin factory supplies the stderr sink that core deliberately omits to stay
|
|
8
|
+
// IO-free. Reuses the exact same engine as dashboard-be's logger so daemon/be log shapes match.
|
|
9
|
+
import { createLogger as createCoreLogger } from '@sovovs/bycli-recorder-core';
|
|
10
|
+
/** daemon/runner structured logger: single-line JSON to stderr. Inject `sink` in tests. */
|
|
11
|
+
export function createRecorderLogger(level, sink = (line) => process.stderr.write(line + '\n')) {
|
|
12
|
+
return createCoreLogger(level, { sink });
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify-runner config — read VERIFY_RUNNER_* from the environment (09 HighLevelConfig).
|
|
3
|
+
*
|
|
4
|
+
* The static range validation is the pure `validateRunnerConfig` in recorder-core (no
|
|
5
|
+
* process.env access, unit-tested there). This thin wrapper reads the env keys and applies
|
|
6
|
+
* the one env-side rule the pure layer can't: clamp `maxConcurrency` to the CPU count
|
|
7
|
+
* (09: "int, 1-CPU count"). A range error is surfaced as a thrown ConfigError (fail closed,
|
|
8
|
+
* 08: unsupported config fails before launch).
|
|
9
|
+
*/
|
|
10
|
+
import { type RunnerConfig, type TempCapacity } from '@sovovs/bycli-recorder-core';
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the verify-runner config from `env` (defaults to process.env). Missing/empty →
|
|
13
|
+
* default; out-of-range/non-integer → ConfigError. `maxConcurrency` is additionally clamped
|
|
14
|
+
* down to the CPU count (never below 1).
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveRunnerConfig(env?: NodeJS.ProcessEnv): RunnerConfig;
|
|
17
|
+
/**
|
|
18
|
+
* Temp-store reap policy (M7b · 09:27-29 RecorderConfig). Read on the daemon side (the process
|
|
19
|
+
* that owns verify temp dirs) to drive startup reap + the periodic TTL sweep.
|
|
20
|
+
*/
|
|
21
|
+
export interface TempPolicy {
|
|
22
|
+
/** Periodic sweep threshold: a temp dir older than this is a leak (09: default 1h). */
|
|
23
|
+
tempTtlMs: number;
|
|
24
|
+
/** Startup reap age backstop: clean dirs older than this regardless of owner (09: default 24h). */
|
|
25
|
+
startupReapMaxAgeMs: number;
|
|
26
|
+
/** SIGTERM→SIGKILL grace when terminating a reaped child (09: default 1500ms). */
|
|
27
|
+
orphanKillGraceMs: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function resolveTempCapacity(env?: NodeJS.ProcessEnv): TempCapacity;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the temp-store reap policy from `env` (defaults to process.env). Missing/empty → default;
|
|
32
|
+
* out-of-range / non-integer → ConfigError (fail closed, consistent with resolveRunnerConfig).
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveTempPolicy(env?: NodeJS.ProcessEnv): TempPolicy;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify-runner config — read VERIFY_RUNNER_* from the environment (09 HighLevelConfig).
|
|
3
|
+
*
|
|
4
|
+
* The static range validation is the pure `validateRunnerConfig` in recorder-core (no
|
|
5
|
+
* process.env access, unit-tested there). This thin wrapper reads the env keys and applies
|
|
6
|
+
* the one env-side rule the pure layer can't: clamp `maxConcurrency` to the CPU count
|
|
7
|
+
* (09: "int, 1-CPU count"). A range error is surfaced as a thrown ConfigError (fail closed,
|
|
8
|
+
* 08: unsupported config fails before launch).
|
|
9
|
+
*/
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import { validateRunnerConfig, validateTempCapacity } from '@sovovs/bycli-recorder-core';
|
|
12
|
+
import { ConfigError } from '../../errors.js';
|
|
13
|
+
const ENV_KEYS = {
|
|
14
|
+
maxConcurrency: 'VERIFY_RUNNER_MAX_CONCURRENCY',
|
|
15
|
+
queueLimit: 'HIGH_LEVEL_QUEUE_LIMIT',
|
|
16
|
+
stdoutLimitBytes: 'VERIFY_RUNNER_STDOUT_LIMIT_BYTES',
|
|
17
|
+
stderrLimitBytes: 'VERIFY_RUNNER_STDERR_LIMIT_BYTES',
|
|
18
|
+
jsonlLineLimit: 'VERIFY_RUNNER_JSONL_LINE_LIMIT',
|
|
19
|
+
timeoutMs: 'VERIFY_RUNNER_TIMEOUT_MS',
|
|
20
|
+
killGraceMs: 'VERIFY_RUNNER_KILL_GRACE_MS',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the verify-runner config from `env` (defaults to process.env). Missing/empty →
|
|
24
|
+
* default; out-of-range/non-integer → ConfigError. `maxConcurrency` is additionally clamped
|
|
25
|
+
* down to the CPU count (never below 1).
|
|
26
|
+
*/
|
|
27
|
+
export function resolveRunnerConfig(env = process.env) {
|
|
28
|
+
const raw = {};
|
|
29
|
+
for (const key of Object.keys(ENV_KEYS)) {
|
|
30
|
+
raw[key] = env[ENV_KEYS[key]];
|
|
31
|
+
}
|
|
32
|
+
const r = validateRunnerConfig(raw);
|
|
33
|
+
if (!r.ok)
|
|
34
|
+
throw new ConfigError(r.reason, 'Fix the VERIFY_RUNNER_* environment variable and retry.');
|
|
35
|
+
const cpus = Math.max(1, os.cpus().length || 1);
|
|
36
|
+
return { ...r.config, maxConcurrency: Math.min(r.config.maxConcurrency, cpus) };
|
|
37
|
+
}
|
|
38
|
+
const TEMP_POLICY_FIELDS = {
|
|
39
|
+
tempTtlMs: { key: 'RECORDER_TEMP_TTL_MS', def: 3_600_000, min: 60_000, max: 86_400_000 },
|
|
40
|
+
startupReapMaxAgeMs: { key: 'RECORDER_STARTUP_REAP_MAX_AGE_MS', def: 86_400_000, min: 60_000, max: 86_400_000 },
|
|
41
|
+
orphanKillGraceMs: { key: 'RECORDER_ORPHAN_KILL_GRACE_MS', def: 1_500, min: 100, max: 30_000 },
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Temp-store capacity env keys (#1d · 09:33-36). The pure range/band-order validation is
|
|
45
|
+
* `validateTempCapacity` in recorder-core; this maps env key names → field names (mirroring
|
|
46
|
+
* resolveRunnerConfig) and fail-closes via ConfigError. The temp ROOT is restart-only (09:182):
|
|
47
|
+
* these are read at daemon startup, not hot-reloaded.
|
|
48
|
+
*/
|
|
49
|
+
const TEMP_CAPACITY_ENV_KEYS = {
|
|
50
|
+
maxBytes: 'RECORDER_TEMP_MAX_BYTES',
|
|
51
|
+
highWatermarkRatio: 'RECORDER_TEMP_HIGH_WATERMARK_RATIO',
|
|
52
|
+
lowWatermarkRatio: 'RECORDER_TEMP_LOW_WATERMARK_RATIO',
|
|
53
|
+
};
|
|
54
|
+
export function resolveTempCapacity(env = process.env) {
|
|
55
|
+
const raw = {};
|
|
56
|
+
for (const k of Object.keys(TEMP_CAPACITY_ENV_KEYS)) {
|
|
57
|
+
raw[k] = env[TEMP_CAPACITY_ENV_KEYS[k]];
|
|
58
|
+
}
|
|
59
|
+
const r = validateTempCapacity(raw);
|
|
60
|
+
if (!r.ok)
|
|
61
|
+
throw new ConfigError(r.reason, 'Fix the RECORDER_TEMP_* environment variable and retry.');
|
|
62
|
+
return r.capacity;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the temp-store reap policy from `env` (defaults to process.env). Missing/empty → default;
|
|
66
|
+
* out-of-range / non-integer → ConfigError (fail closed, consistent with resolveRunnerConfig).
|
|
67
|
+
*/
|
|
68
|
+
export function resolveTempPolicy(env = process.env) {
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const k of Object.keys(TEMP_POLICY_FIELDS)) {
|
|
71
|
+
const f = TEMP_POLICY_FIELDS[k];
|
|
72
|
+
const raw = env[f.key];
|
|
73
|
+
if (raw === undefined || raw === '') {
|
|
74
|
+
out[k] = f.def;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const n = Number(raw);
|
|
78
|
+
if (!Number.isInteger(n) || n < f.min || n > f.max) {
|
|
79
|
+
throw new ConfigError(`${f.key}=${raw} is out of range [${f.min}, ${f.max}]`, `Fix the ${f.key} environment variable and retry.`);
|
|
80
|
+
}
|
|
81
|
+
out[k] = n;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface VerifyMarker {
|
|
2
|
+
requestId: string;
|
|
3
|
+
/** The runner child pid (backfilled into marker.json after spawn). */
|
|
4
|
+
pid: number | null;
|
|
5
|
+
/** The process that spawned the runner (daemon/CLI); stands in for 08's localServiceRunId. */
|
|
6
|
+
ownerPid: number | null;
|
|
7
|
+
startedAt: number;
|
|
8
|
+
tempRoot: string;
|
|
9
|
+
operation: 'verify';
|
|
10
|
+
}
|
|
11
|
+
/** Parse + validate marker.json. Returns null unless it is a well-formed verify marker. */
|
|
12
|
+
export declare function parseVerifyMarker(raw: string): VerifyMarker | null;
|
|
13
|
+
/** Does a live process's command line belong to THIS marker's runner? (pid-reuse guard) */
|
|
14
|
+
export declare function cmdlineMatchesMarker(cmdline: string, marker: VerifyMarker): boolean;
|
|
15
|
+
/** Containment: the dir must sit directly under tmpRoot and carry our prefix. */
|
|
16
|
+
export declare function isReapableTempDir(dir: string, tmpRoot: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* True orphan iff the spawning owner is gone. An unknown ownerPid (legacy marker without the
|
|
19
|
+
* field) is treated as live → skipped, so we never kill a run we cannot attribute to a dead owner.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isOrphan(marker: VerifyMarker, ownerAlive: boolean): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Aged-out leak (M7b · RECORDER_TEMP_TTL_MS / RECORDER_STARTUP_REAP_MAX_AGE_MS, 09:27-28):
|
|
24
|
+
* a temp dir older than `maxAgeMs` is reaped regardless of owner liveness. CALLERS MUST pass the
|
|
25
|
+
* floored threshold from `effectiveLeakThresholdMs` (never the raw config) — `maxAgeMs` here is
|
|
26
|
+
* assumed to already exceed the run's hard deadline, so a dir older than it is a definite leak,
|
|
27
|
+
* never a live run. An undatable legacy marker (startedAt<=0) is never aged-out (we cannot
|
|
28
|
+
* attribute its age). maxAgeMs<=0 disables age-based reaping (orphan-only, the pre-M7b behavior).
|
|
29
|
+
*/
|
|
30
|
+
export declare function isAgedOut(marker: VerifyMarker, now: number, maxAgeMs: number): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Effective age-reap threshold: the configured `maxAgeMs` floored at the run's hard deadline
|
|
33
|
+
* (`minLeakAgeMs` = timeoutMs + killGraceMs). A threshold below the max legitimate run lifetime would
|
|
34
|
+
* let the sweep kill a still-running verify — a live run can outlive a short TTL but never its own
|
|
35
|
+
* kill deadline, so flooring closes that hole. A 0/negative configured value keeps age-reaping
|
|
36
|
+
* disabled (orphan-only).
|
|
37
|
+
*/
|
|
38
|
+
export declare function effectiveLeakThresholdMs(maxAgeMs: number, minLeakAgeMs: number): number;
|
|
39
|
+
export interface ProcProbe {
|
|
40
|
+
isAlive(pid: number): boolean;
|
|
41
|
+
cmdline(pid: number): string | null;
|
|
42
|
+
/** Process start time (epoch ms) for the live pid, or null if undeterminable (win32 / ps failure).
|
|
43
|
+
* Used for the owner pid-reuse guard (#6); optional so injected test probes need not implement it. */
|
|
44
|
+
startTime?(pid: number): number | null;
|
|
45
|
+
kill(pid: number, signal: NodeJS.Signals): void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Owner pid-reuse guard (#6): the spawning owner necessarily started BEFORE it created this run, so a
|
|
49
|
+
* process now occupying `ownerPid` whose start time is meaningfully AFTER the run was created cannot be
|
|
50
|
+
* that owner — the real owner died and the pid was recycled, making this a true orphan. Undeterminable
|
|
51
|
+
* start time (win32 / ps failure → null) → NOT reused: we never escalate to a kill on a guess, so a
|
|
52
|
+
* live owner is never misclassified (its start time is ≤ runStartedAt, far below the threshold).
|
|
53
|
+
*/
|
|
54
|
+
export declare function isOwnerPidReused(liveStartTime: number | null, runStartedAt: number, toleranceMs: number): boolean;
|
|
55
|
+
export interface ReapResult {
|
|
56
|
+
scanned: number;
|
|
57
|
+
orphans: number;
|
|
58
|
+
agedOut: number;
|
|
59
|
+
killed: number;
|
|
60
|
+
deleted: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Scan tmpRoot for orphaned / aged-out verify temp dirs and clean them up. Synchronous (runs at
|
|
64
|
+
* daemon startup and on a periodic sweep); the SIGKILL backstop is scheduled async + unref'd so it
|
|
65
|
+
* never blocks. `maxAgeMs` (M7b) enables age-based reaping; 0/undefined keeps orphan-only behavior.
|
|
66
|
+
*/
|
|
67
|
+
export declare function reapOrphanedVerifyRuns(opts?: {
|
|
68
|
+
tmpRoot?: string;
|
|
69
|
+
log?: (msg: string) => void;
|
|
70
|
+
graceMs?: number;
|
|
71
|
+
/** Age-based leak threshold (M7b). 0/undefined → orphan-only (pre-M7b behavior). */
|
|
72
|
+
maxAgeMs?: number;
|
|
73
|
+
/** Run hard-deadline floor (timeoutMs + killGraceMs); the age threshold is never lower than this
|
|
74
|
+
* so a still-running verify is never age-reaped (08 / M7b safety floor). */
|
|
75
|
+
minLeakAgeMs?: number;
|
|
76
|
+
/** Injectable clock (tests). */
|
|
77
|
+
now?: number;
|
|
78
|
+
probe?: ProcProbe;
|
|
79
|
+
}): ReapResult;
|