@jackwener/opencli 1.7.3 → 1.7.5
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 +81 -59
- package/README.zh-CN.md +93 -67
- package/cli-manifest.json +5015 -2975
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/favorite.js +18 -13
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -3
- package/clis/chatgpt-app/ax.js +6 -3
- package/clis/deepseek/ask.js +74 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +208 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +250 -8
- package/clis/douban/utils.test.js +179 -4
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/comments.js +2 -2
- package/clis/xiaohongshu/comments.test.js +46 -25
- package/clis/xiaohongshu/download.js +6 -7
- package/clis/xiaohongshu/download.test.js +17 -5
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +3 -5
- package/clis/xiaohongshu/note.test.js +52 -25
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +53 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +1 -1
- package/dist/src/browser/base-page.js +25 -5
- package/dist/src/browser/bridge.d.ts +3 -0
- package/dist/src/browser/bridge.js +52 -15
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/daemon-client.d.ts +7 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.js +20 -3
- package/dist/src/browser/page.d.ts +18 -5
- package/dist/src/browser/page.js +96 -15
- package/dist/src/browser/page.test.js +158 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +272 -187
- package/dist/src/cli.test.js +167 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +10 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +32 -9
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -3
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +8 -9
- package/dist/src/plugin.js +24 -28
- package/dist/src/plugin.test.js +16 -60
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +15 -6
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
- /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
|
@@ -54,6 +54,20 @@ function jsonResponse(res, status, data) {
|
|
|
54
54
|
function sleep(ms) {
|
|
55
55
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
56
56
|
}
|
|
57
|
+
function parseTimeoutValue(val, label, fallback) {
|
|
58
|
+
if (val === undefined) {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
const parsed = typeof val === 'number' ? val : parseInt(String(val), 10);
|
|
62
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
63
|
+
console.error(`[serve] Invalid ${label}="${val}", using default ${fallback}s`);
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
function parseEnvTimeout(envVar, fallback) {
|
|
69
|
+
return parseTimeoutValue(process.env[envVar], envVar, fallback);
|
|
70
|
+
}
|
|
57
71
|
// ─── DOM helpers ─────────────────────────────────────────────────────
|
|
58
72
|
/**
|
|
59
73
|
* Click the 'New Conversation' button to reset context.
|
|
@@ -267,41 +281,65 @@ async function waitForReply(page, beforeText, opts = {}) {
|
|
|
267
281
|
let lastText = beforeText;
|
|
268
282
|
let stableCount = 0;
|
|
269
283
|
const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback
|
|
284
|
+
let reconnectCount = 0;
|
|
270
285
|
while (Date.now() < deadline) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
else {
|
|
279
|
-
if (hasStartedGenerating) {
|
|
280
|
-
// It actively generated and now it stopped -> DONE
|
|
281
|
-
// Provide a small buffer to let React render the final message fully
|
|
282
|
-
await sleep(500);
|
|
283
|
-
return;
|
|
286
|
+
try {
|
|
287
|
+
const generating = await isGenerating(page);
|
|
288
|
+
const currentText = await getConversationText(page);
|
|
289
|
+
const textChanged = currentText !== beforeText && currentText.length > 0;
|
|
290
|
+
if (generating) {
|
|
291
|
+
hasStartedGenerating = true;
|
|
292
|
+
stableCount = 0; // Reset stability while generating
|
|
284
293
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
294
|
+
else {
|
|
295
|
+
if (hasStartedGenerating) {
|
|
296
|
+
// It actively generated and now it stopped -> DONE
|
|
297
|
+
// Provide a small buffer to let React render the final message fully
|
|
298
|
+
await sleep(500);
|
|
299
|
+
return page;
|
|
300
|
+
}
|
|
301
|
+
// Fallback: If it never showed "Generating/Cancel", but text changed and is stable
|
|
302
|
+
if (textChanged) {
|
|
303
|
+
if (currentText === lastText) {
|
|
304
|
+
stableCount++;
|
|
305
|
+
if (stableCount >= stableThreshold) {
|
|
306
|
+
return page; // Text has been stable for 2 seconds -> DONE
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
stableCount = 0;
|
|
311
|
+
lastText = currentText;
|
|
291
312
|
}
|
|
292
313
|
}
|
|
293
|
-
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
const msg = err.message || String(err);
|
|
318
|
+
const isSessionLoss = /closed|lost|not open|websocket/i.test(msg);
|
|
319
|
+
if (opts.reconnect && isSessionLoss && reconnectCount < 2) {
|
|
320
|
+
reconnectCount++;
|
|
321
|
+
console.error(`[serve] CDP session loss detected (${msg}), attempting to reconnect (${reconnectCount}/2)...`);
|
|
322
|
+
try {
|
|
323
|
+
page = await opts.reconnect();
|
|
324
|
+
// Reset stability tracking after reconnect
|
|
294
325
|
stableCount = 0;
|
|
295
|
-
lastText =
|
|
326
|
+
lastText = beforeText;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
catch (reconnectErr) {
|
|
330
|
+
console.error(`[serve] Reconnection failed: ${reconnectErr.message}`);
|
|
331
|
+
throw err; // Throw original error if reconnection itself fails
|
|
296
332
|
}
|
|
297
333
|
}
|
|
334
|
+
throw err;
|
|
298
335
|
}
|
|
299
336
|
await sleep(pollInterval);
|
|
300
337
|
}
|
|
301
|
-
throw new Error(
|
|
338
|
+
throw new Error(`Timeout waiting for Antigravity reply after ${timeout / 1000}s`);
|
|
302
339
|
}
|
|
303
340
|
// ─── Request Handlers ────────────────────────────────────────────────
|
|
304
|
-
async function handleMessages(body, page,
|
|
341
|
+
async function handleMessages(body, page, opts = {}) {
|
|
342
|
+
const { bridge, timeout, reconnect } = opts;
|
|
305
343
|
// Extract the last user message
|
|
306
344
|
const userMessages = body.messages.filter(m => m.role === 'user');
|
|
307
345
|
if (userMessages.length === 0) {
|
|
@@ -328,7 +366,7 @@ async function handleMessages(body, page, bridge) {
|
|
|
328
366
|
await sendMessage(page, userText, bridge);
|
|
329
367
|
// Poll for reply (change detection)
|
|
330
368
|
console.error('[serve] Waiting for reply...');
|
|
331
|
-
await waitForReply(page, beforeText);
|
|
369
|
+
page = await waitForReply(page, beforeText, { timeout, reconnect });
|
|
332
370
|
// Extract the actual reply text precisely from the DOM
|
|
333
371
|
const replyText = await getLastAssistantReply(page, userText);
|
|
334
372
|
console.error(`[serve] Got reply: "${replyText.slice(0, 80)}${replyText.length > 80 ? '...' : ''}"`);
|
|
@@ -349,6 +387,10 @@ async function handleMessages(body, page, bridge) {
|
|
|
349
387
|
// ─── Server ──────────────────────────────────────────────────────────
|
|
350
388
|
export async function startServe(opts = {}) {
|
|
351
389
|
const port = opts.port ?? 8082;
|
|
390
|
+
const envTimeoutSeconds = parseEnvTimeout('OPENCLI_ANTIGRAVITY_TIMEOUT', 120);
|
|
391
|
+
const effectiveTimeoutSeconds = parseTimeoutValue(opts.timeout, '--timeout', envTimeoutSeconds);
|
|
392
|
+
const effectiveTimeout = effectiveTimeoutSeconds * 1000;
|
|
393
|
+
console.error(`[serve] Starting Antigravity API proxy on port ${port} (timeout: ${effectiveTimeout / 1000}s)`);
|
|
352
394
|
// Lazy CDP connection — connect when first request comes in
|
|
353
395
|
let cdp = null;
|
|
354
396
|
let page = null;
|
|
@@ -462,7 +504,11 @@ export async function startServe(opts = {}) {
|
|
|
462
504
|
}
|
|
463
505
|
// Lazy connect on first request
|
|
464
506
|
const activePage = await ensureConnected();
|
|
465
|
-
const response = await handleMessages(body, activePage,
|
|
507
|
+
const response = await handleMessages(body, activePage, {
|
|
508
|
+
bridge: cdp,
|
|
509
|
+
timeout: effectiveTimeout,
|
|
510
|
+
reconnect: ensureConnected,
|
|
511
|
+
});
|
|
466
512
|
jsonResponse(res, 200, response);
|
|
467
513
|
}
|
|
468
514
|
finally {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { clampInt, requireNonEmptyQuery } from '../_shared/common.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'baidu-scholar',
|
|
6
|
+
name: 'search',
|
|
7
|
+
description: '百度学术搜索',
|
|
8
|
+
domain: 'xueshu.baidu.com',
|
|
9
|
+
strategy: Strategy.PUBLIC,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'query', positional: true, required: true, help: '搜索关键词' },
|
|
13
|
+
{ name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['rank', 'title', 'authors', 'journal', 'year', 'cited', 'url'],
|
|
16
|
+
navigateBefore: false,
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
const limit = clampInt(kwargs.limit, 10, 1, 20);
|
|
19
|
+
const query = requireNonEmptyQuery(kwargs.query);
|
|
20
|
+
await page.goto(`https://xueshu.baidu.com/s?wd=${encodeURIComponent(query)}&pn=0&tn=SE_baiduxueshu_c1gjeupa`);
|
|
21
|
+
await page.wait(5);
|
|
22
|
+
const data = await page.evaluate(`
|
|
23
|
+
(async () => {
|
|
24
|
+
const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
|
|
25
|
+
for (let i = 0; i < 20; i++) {
|
|
26
|
+
if (document.querySelectorAll('.result').length > 0) break;
|
|
27
|
+
await new Promise(r => setTimeout(r, 500));
|
|
28
|
+
}
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const el of document.querySelectorAll('.result')) {
|
|
31
|
+
const titleEl = el.querySelector('h3 a, .paper-title a, .t a');
|
|
32
|
+
const title = normalize(titleEl?.textContent);
|
|
33
|
+
if (!title) continue;
|
|
34
|
+
|
|
35
|
+
let url = titleEl?.getAttribute('href') || '';
|
|
36
|
+
if (url && !url.startsWith('http')) url = 'https://xueshu.baidu.com' + url;
|
|
37
|
+
|
|
38
|
+
const infoEl = el.querySelector('.paper-info');
|
|
39
|
+
const infoText = normalize(infoEl?.textContent);
|
|
40
|
+
const spans = infoEl ? Array.from(infoEl.querySelectorAll('span')) : [];
|
|
41
|
+
|
|
42
|
+
let journal = '';
|
|
43
|
+
let year = '';
|
|
44
|
+
let cited = '0';
|
|
45
|
+
const authorParts = [];
|
|
46
|
+
|
|
47
|
+
for (const span of spans) {
|
|
48
|
+
const text = normalize(span.textContent);
|
|
49
|
+
if (!text || text === ',' || text === ',') continue;
|
|
50
|
+
if (text.startsWith('《') || text.startsWith('〈')) {
|
|
51
|
+
journal = text.replace(/[《》〈〉]/g, '');
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (/^被引量[::]/.test(text)) {
|
|
55
|
+
cited = text.match(/(\\d+)/)?.[1] || '0';
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (/^-\\s*(\\d{4})/.test(text) || /^\\d{4}年?$/.test(text)) {
|
|
59
|
+
year = text.match(/(\\d{4})/)?.[1] || '';
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!journal && !/^被引/.test(text) && !text.startsWith('-')) {
|
|
63
|
+
authorParts.push(text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!year) year = infoText.match(/(19|20)\\d{2}/)?.[0] || '';
|
|
68
|
+
if (!cited || cited === '0') cited = infoText.match(/被引量[::]\\s*(\\d+)/)?.[1] || '0';
|
|
69
|
+
|
|
70
|
+
results.push({
|
|
71
|
+
rank: results.length + 1,
|
|
72
|
+
title,
|
|
73
|
+
authors: authorParts.join(', ').slice(0, 80),
|
|
74
|
+
journal,
|
|
75
|
+
year,
|
|
76
|
+
cited,
|
|
77
|
+
url,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (results.length >= ${limit}) break;
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
})()
|
|
84
|
+
`);
|
|
85
|
+
return Array.isArray(data) ? data : [];
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import './search.js';
|
|
4
|
+
|
|
5
|
+
describe('baidu-scholar search command', () => {
|
|
6
|
+
const command = getRegistry().get('baidu-scholar/search');
|
|
7
|
+
|
|
8
|
+
it('registers as a public browser command', () => {
|
|
9
|
+
expect(command).toBeDefined();
|
|
10
|
+
expect(command.site).toBe('baidu-scholar');
|
|
11
|
+
expect(command.strategy).toBe('public');
|
|
12
|
+
expect(command.browser).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('rejects empty queries before browser navigation', async () => {
|
|
16
|
+
const page = { goto: vi.fn() };
|
|
17
|
+
await expect(command.func(page, { query: ' ' })).rejects.toMatchObject({
|
|
18
|
+
name: 'ArgumentError',
|
|
19
|
+
code: 'ARGUMENT',
|
|
20
|
+
});
|
|
21
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -3,27 +3,32 @@ import { apiGet, payloadData, getSelfUid } from './utils.js';
|
|
|
3
3
|
cli({
|
|
4
4
|
site: 'bilibili',
|
|
5
5
|
name: 'favorite',
|
|
6
|
-
description: '
|
|
6
|
+
description: '我的收藏夹',
|
|
7
7
|
domain: 'www.bilibili.com',
|
|
8
8
|
strategy: Strategy.COOKIE,
|
|
9
9
|
args: [
|
|
10
|
+
{ name: 'fid', type: 'int', required: false, help: 'Favorite folder ID (defaults to first folder)' },
|
|
10
11
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
11
12
|
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
12
13
|
],
|
|
13
14
|
columns: ['rank', 'title', 'author', 'plays', 'url'],
|
|
14
15
|
func: async (page, kwargs) => {
|
|
15
|
-
const { limit = 20, page: pageNum = 1 } = kwargs;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
const { fid: favoriteId, limit = 20, page: pageNum = 1 } = kwargs;
|
|
17
|
+
let fid;
|
|
18
|
+
if (favoriteId) {
|
|
19
|
+
fid = Number(favoriteId);
|
|
20
|
+
} else {
|
|
21
|
+
// Fall back to the default (first) favorite folder
|
|
22
|
+
const uid = await getSelfUid(page);
|
|
23
|
+
const foldersPayload = await apiGet(page, '/x/v3/fav/folder/created/list-all', {
|
|
24
|
+
params: { up_mid: uid },
|
|
25
|
+
signed: true,
|
|
26
|
+
});
|
|
27
|
+
const folders = payloadData(foldersPayload)?.list ?? [];
|
|
28
|
+
if (!folders.length)
|
|
29
|
+
return [];
|
|
30
|
+
fid = folders[0].id;
|
|
31
|
+
}
|
|
27
32
|
// Fetch favorite items
|
|
28
33
|
const payload = await apiGet(page, '/x/v3/fav/resource/list', {
|
|
29
34
|
params: { media_id: fid, pn: pageNum, ps: Math.min(Number(limit), 40) },
|
package/clis/binance/depth.js
CHANGED
|
@@ -3,7 +3,7 @@ import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
|
3
3
|
cli({
|
|
4
4
|
site: 'binance',
|
|
5
5
|
name: 'depth',
|
|
6
|
-
description: 'Order book bid prices for a trading pair',
|
|
6
|
+
description: 'Order book bid and ask prices for a trading pair',
|
|
7
7
|
domain: 'data-api.binance.vision',
|
|
8
8
|
strategy: Strategy.PUBLIC,
|
|
9
9
|
browser: false,
|
|
@@ -11,11 +11,10 @@ cli({
|
|
|
11
11
|
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
12
12
|
{ name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
|
|
13
13
|
],
|
|
14
|
-
columns: ['rank', 'bid_price', 'bid_qty'],
|
|
14
|
+
columns: ['rank', 'bid_price', 'bid_qty', 'ask_price', 'ask_qty'],
|
|
15
15
|
pipeline: [
|
|
16
16
|
{ fetch: { url: 'https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
|
|
17
|
-
{ select: 'bids' },
|
|
18
|
-
{ map: { rank: '${{ index + 1 }}', bid_price: '${{ item.0 }}', bid_qty: '${{ item.1 }}' } },
|
|
17
|
+
{ map: { select: 'bids', rank: '${{ index + 1 }}', bid_price: '${{ item[0] }}', bid_qty: '${{ item[1] }}', ask_price: '${{ root.asks[index]?.[0] ?? "" }}', ask_qty: '${{ root.asks[index]?.[1] ?? "" }}' } },
|
|
19
18
|
{ limit: '${{ args.limit }}' },
|
|
20
19
|
],
|
|
21
20
|
});
|
package/clis/boss/utils.js
CHANGED
|
@@ -214,11 +214,10 @@ export async function typeAndSendMessage(page, text) {
|
|
|
214
214
|
return true;
|
|
215
215
|
}
|
|
216
216
|
/**
|
|
217
|
-
* Verbose log helper — prints when OPENCLI_VERBOSE is set
|
|
218
|
-
* kept as a compatibility fallback.
|
|
217
|
+
* Verbose log helper — prints when OPENCLI_VERBOSE is set.
|
|
219
218
|
*/
|
|
220
219
|
export function verbose(msg) {
|
|
221
|
-
if (process.env.OPENCLI_VERBOSE
|
|
220
|
+
if (process.env.OPENCLI_VERBOSE) {
|
|
222
221
|
console.error(`[opencli:boss] ${msg}`);
|
|
223
222
|
}
|
|
224
223
|
}
|
package/clis/chatgpt-app/ax.js
CHANGED
|
@@ -121,11 +121,14 @@ let args = CommandLine.arguments
|
|
|
121
121
|
let target = args.count > 1 ? args[1] : ""
|
|
122
122
|
let needsLegacy = args.count > 2 && args[2] == "legacy"
|
|
123
123
|
|
|
124
|
-
// Step 1: Click the "Options" button to open the popover
|
|
125
|
-
|
|
124
|
+
// Step 1: Click the "Options" button to open the popover (support both English and Chinese UI)
|
|
125
|
+
var optionsBtn: AXUIElement? = nil
|
|
126
|
+
if let btn = findByDesc(win, "Options") { optionsBtn = btn }
|
|
127
|
+
else if let btn = findByDesc(win, "选项") { optionsBtn = btn }
|
|
128
|
+
guard let options = optionsBtn else {
|
|
126
129
|
fputs("Could not find Options button\\n", stderr); exit(1)
|
|
127
130
|
}
|
|
128
|
-
press(
|
|
131
|
+
press(options)
|
|
129
132
|
Thread.sleep(forTimeInterval: 0.8)
|
|
130
133
|
|
|
131
134
|
// Step 2: Find the popover that appeared, search ONLY within it
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import {
|
|
4
|
+
DEEPSEEK_DOMAIN, DEEPSEEK_URL, ensureOnDeepSeek, selectModel, setFeature,
|
|
5
|
+
sendMessage, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
|
|
6
|
+
} from './utils.js';
|
|
7
|
+
|
|
8
|
+
export const askCommand = cli({
|
|
9
|
+
site: 'deepseek',
|
|
10
|
+
name: 'ask',
|
|
11
|
+
description: 'Send a prompt to DeepSeek and get the response',
|
|
12
|
+
domain: DEEPSEEK_DOMAIN,
|
|
13
|
+
strategy: Strategy.COOKIE,
|
|
14
|
+
browser: true,
|
|
15
|
+
navigateBefore: false,
|
|
16
|
+
timeoutSeconds: 180,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
|
|
19
|
+
{ name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
|
|
20
|
+
{ name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
|
|
21
|
+
{ name: 'model', default: 'instant', choices: ['instant', 'expert'], help: 'Model to use: instant or expert' },
|
|
22
|
+
{ name: 'think', type: 'boolean', default: false, help: 'Enable DeepThink mode' },
|
|
23
|
+
{ name: 'search', type: 'boolean', default: false, help: 'Enable web search' },
|
|
24
|
+
],
|
|
25
|
+
columns: ['response'],
|
|
26
|
+
|
|
27
|
+
func: async (page, kwargs) => {
|
|
28
|
+
const prompt = kwargs.prompt;
|
|
29
|
+
const timeoutMs = (kwargs.timeout || 120) * 1000;
|
|
30
|
+
const wantThink = parseBoolFlag(kwargs.think);
|
|
31
|
+
const wantSearch = parseBoolFlag(kwargs.search);
|
|
32
|
+
|
|
33
|
+
if (parseBoolFlag(kwargs.new)) {
|
|
34
|
+
await page.goto(DEEPSEEK_URL);
|
|
35
|
+
await page.wait(3);
|
|
36
|
+
} else {
|
|
37
|
+
await ensureOnDeepSeek(page);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await page.wait(2);
|
|
41
|
+
|
|
42
|
+
const wantModel = kwargs.model || 'instant';
|
|
43
|
+
const modelResult = await withRetry(() => selectModel(page, wantModel));
|
|
44
|
+
if (!modelResult?.ok) {
|
|
45
|
+
throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
|
|
46
|
+
}
|
|
47
|
+
if (modelResult.toggled) await page.wait(0.5);
|
|
48
|
+
|
|
49
|
+
const thinkResult = await withRetry(() => setFeature(page, 'DeepThink', wantThink));
|
|
50
|
+
if (!thinkResult?.ok) {
|
|
51
|
+
throw new CommandExecutionError('Could not toggle DeepThink');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const searchResult = await withRetry(() => setFeature(page, 'Search', wantSearch));
|
|
55
|
+
if (!searchResult?.ok) {
|
|
56
|
+
throw new CommandExecutionError('Could not toggle Search');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (thinkResult.toggled || searchResult.toggled) await page.wait(0.5);
|
|
60
|
+
|
|
61
|
+
const baseline = await withRetry(() => getBubbleCount(page));
|
|
62
|
+
const sendResult = await withRetry(() => sendMessage(page, prompt));
|
|
63
|
+
if (!sendResult?.ok) {
|
|
64
|
+
throw new CommandExecutionError(sendResult?.reason || 'Failed to send message');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const response = await waitForResponse(page, baseline, prompt, timeoutMs);
|
|
68
|
+
if (!response) {
|
|
69
|
+
return [{ response: `[NO RESPONSE] No reply within ${kwargs.timeout}s.` }];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [{ response }];
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { DEEPSEEK_DOMAIN, getConversationList } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export const historyCommand = cli({
|
|
5
|
+
site: 'deepseek',
|
|
6
|
+
name: 'history',
|
|
7
|
+
description: 'List conversation history from DeepSeek sidebar',
|
|
8
|
+
domain: DEEPSEEK_DOMAIN,
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
navigateBefore: false,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max conversations to show' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['Index', 'Title', 'Url'],
|
|
16
|
+
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
const limit = Math.max(1, kwargs.limit || 20);
|
|
19
|
+
const conversations = await getConversationList(page);
|
|
20
|
+
if (conversations.length === 0) {
|
|
21
|
+
return [{ Index: 0, Title: 'No conversation history found.', Url: '' }];
|
|
22
|
+
}
|
|
23
|
+
return conversations.slice(0, limit);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { DEEPSEEK_DOMAIN, DEEPSEEK_URL } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export const newCommand = cli({
|
|
5
|
+
site: 'deepseek',
|
|
6
|
+
name: 'new',
|
|
7
|
+
description: 'Start a new conversation in DeepSeek',
|
|
8
|
+
domain: DEEPSEEK_DOMAIN,
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
navigateBefore: false,
|
|
12
|
+
args: [],
|
|
13
|
+
columns: ['Status'],
|
|
14
|
+
|
|
15
|
+
func: async (page) => {
|
|
16
|
+
await page.goto(DEEPSEEK_URL);
|
|
17
|
+
await page.wait(2);
|
|
18
|
+
return [{ Status: 'New chat started' }];
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { DEEPSEEK_DOMAIN, ensureOnDeepSeek, getVisibleMessages } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export const readCommand = cli({
|
|
5
|
+
site: 'deepseek',
|
|
6
|
+
name: 'read',
|
|
7
|
+
description: 'Read the current DeepSeek conversation',
|
|
8
|
+
domain: DEEPSEEK_DOMAIN,
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
navigateBefore: false,
|
|
12
|
+
args: [],
|
|
13
|
+
columns: ['Role', 'Text'],
|
|
14
|
+
|
|
15
|
+
func: async (page) => {
|
|
16
|
+
await ensureOnDeepSeek(page);
|
|
17
|
+
await page.wait(5);
|
|
18
|
+
const messages = await getVisibleMessages(page);
|
|
19
|
+
if (messages.length > 0) return messages;
|
|
20
|
+
return [{ Role: 'system', Text: 'No visible messages found.' }];
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { DEEPSEEK_DOMAIN, ensureOnDeepSeek, getPageState } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export const statusCommand = cli({
|
|
5
|
+
site: 'deepseek',
|
|
6
|
+
name: 'status',
|
|
7
|
+
description: 'Check DeepSeek page availability and login state',
|
|
8
|
+
domain: DEEPSEEK_DOMAIN,
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
navigateBefore: false,
|
|
12
|
+
args: [],
|
|
13
|
+
columns: ['Status', 'Login', 'Url'],
|
|
14
|
+
|
|
15
|
+
func: async (page) => {
|
|
16
|
+
await ensureOnDeepSeek(page);
|
|
17
|
+
const state = await getPageState(page);
|
|
18
|
+
return [{
|
|
19
|
+
Status: state.hasTextarea ? 'Connected' : 'Page not ready',
|
|
20
|
+
Login: state.isLoggedIn ? 'Yes' : 'No',
|
|
21
|
+
Url: state.url,
|
|
22
|
+
}];
|
|
23
|
+
},
|
|
24
|
+
});
|