@jackwener/opencli 1.7.8 → 1.7.10
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 +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +646 -30
- package/clis/36kr/news.js +1 -1
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +1 -1
- package/clis/chatgpt-app/ax.js +4 -2
- package/clis/chatgpt-app/ax.test.js +12 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +1 -1
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +17 -4
- package/clis/deepseek/ask.test.js +46 -0
- package/clis/deepseek/utils.js +55 -16
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/instagram/collection-create.js +57 -0
- package/clis/instagram/saved.js +21 -7
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +1 -1
- package/clis/sinafinance/stock.test.js +2 -2
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +5 -6
- package/clis/toutiao/articles.test.js +22 -15
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -2
- package/dist/src/browser/bridge.js +40 -41
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
- package/dist/src/browser/daemon-lifecycle.js +67 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +477 -35
- package/dist/src/cli.test.js +303 -2
- package/dist/src/commanderAdapter.js +17 -9
- package/dist/src/commanderAdapter.test.js +67 -2
- package/dist/src/commands/daemon.d.ts +2 -0
- package/dist/src/commands/daemon.js +42 -1
- package/dist/src/commands/daemon.test.js +103 -2
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +5 -6
- package/dist/src/doctor.js +77 -19
- package/dist/src/doctor.test.js +117 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
package/clis/cursor/composer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
export const composerCommand = cli({
|
|
4
4
|
site: 'cursor',
|
|
5
5
|
name: 'composer',
|
|
@@ -27,7 +27,7 @@ export const composerCommand = cli({
|
|
|
27
27
|
return true;
|
|
28
28
|
})(${JSON.stringify(textToInsert)})`);
|
|
29
29
|
if (!typed) {
|
|
30
|
-
throw
|
|
30
|
+
throw selectorError('Cursor Composer input element', 'Could not find Cursor Composer input element after pressing Cmd+I.');
|
|
31
31
|
}
|
|
32
32
|
await page.wait(0.5);
|
|
33
33
|
await page.pressKey('Enter');
|
package/clis/cursor/send.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
export const sendCommand = cli({
|
|
4
4
|
site: 'cursor',
|
|
5
5
|
name: 'send',
|
|
@@ -24,7 +24,7 @@ export const sendCommand = cli({
|
|
|
24
24
|
return true;
|
|
25
25
|
})(${JSON.stringify(textToInsert)})`);
|
|
26
26
|
if (!injected) {
|
|
27
|
-
throw
|
|
27
|
+
throw selectorError('Cursor Composer input element');
|
|
28
28
|
}
|
|
29
29
|
// Submit the command. In Cursor, Enter usually submits the chat.
|
|
30
30
|
await page.wait(0.5);
|
package/clis/deepseek/ask.js
CHANGED
|
@@ -18,7 +18,7 @@ export const askCommand = cli({
|
|
|
18
18
|
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
|
|
19
19
|
{ name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
|
|
20
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
|
|
21
|
+
{ name: 'model', default: 'instant', choices: ['instant', 'expert', 'vision'], help: 'Model to use: instant, expert, or vision' },
|
|
22
22
|
{ name: 'think', type: 'boolean', default: false, help: 'Enable DeepThink mode' },
|
|
23
23
|
{ name: 'search', type: 'boolean', default: false, help: 'Enable web search' },
|
|
24
24
|
{ name: 'file', help: 'Attach a file (PDF, image, text) with the prompt' },
|
|
@@ -78,9 +78,22 @@ export const askCommand = cli({
|
|
|
78
78
|
throw new CommandExecutionError('Could not enable DeepThink');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
if (wantModel === 'vision' && wantSearch) {
|
|
82
|
+
throw new CliError(
|
|
83
|
+
'ARGUMENT',
|
|
84
|
+
'DeepSeek vision mode does not support --search.',
|
|
85
|
+
'Run without --search, or use --model instant/expert for web search.',
|
|
86
|
+
EXIT_CODES.USAGE_ERROR,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Vision mode does not have the search toggle.
|
|
91
|
+
let searchResult;
|
|
92
|
+
if (wantModel !== 'vision') {
|
|
93
|
+
searchResult = await withRetry(() => setFeature(page, 'Search', wantSearch));
|
|
94
|
+
if (!searchResult?.ok && wantSearch) {
|
|
95
|
+
throw new CommandExecutionError('Could not enable Search');
|
|
96
|
+
}
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
if (thinkResult?.toggled || searchResult?.toggled) await page.wait(0.5);
|
|
@@ -263,4 +263,50 @@ describe('deepseek ask conversation resume', () => {
|
|
|
263
263
|
expect(rows).toEqual([{ response: 'follow-up reply' }]);
|
|
264
264
|
expect(mockSelectModel).toHaveBeenCalled();
|
|
265
265
|
});
|
|
266
|
+
|
|
267
|
+
it('skips search toggle in vision mode when search is not requested', async () => {
|
|
268
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
269
|
+
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
270
|
+
mockSetFeature.mockResolvedValue({ ok: true, toggled: false });
|
|
271
|
+
mockSendMessage.mockResolvedValue({ ok: true });
|
|
272
|
+
mockGetBubbleCount.mockResolvedValue(0);
|
|
273
|
+
mockWaitForResponse.mockResolvedValue('vision reply');
|
|
274
|
+
page.evaluate.mockResolvedValue('https://chat.deepseek.com/');
|
|
275
|
+
|
|
276
|
+
const rows = await askCommand.func(page, {
|
|
277
|
+
prompt: 'describe',
|
|
278
|
+
timeout: 120,
|
|
279
|
+
new: false,
|
|
280
|
+
model: 'vision',
|
|
281
|
+
think: false,
|
|
282
|
+
search: false,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(rows).toEqual([{ response: 'vision reply' }]);
|
|
286
|
+
expect(mockSetFeature).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(mockSetFeature).toHaveBeenCalledWith(expect.anything(), 'DeepThink', false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('fails fast instead of silently ignoring --search in vision mode', async () => {
|
|
291
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
292
|
+
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
293
|
+
page.evaluate.mockResolvedValue('https://chat.deepseek.com/');
|
|
294
|
+
|
|
295
|
+
await expect(askCommand.func(page, {
|
|
296
|
+
prompt: 'describe',
|
|
297
|
+
timeout: 120,
|
|
298
|
+
new: false,
|
|
299
|
+
model: 'vision',
|
|
300
|
+
think: false,
|
|
301
|
+
search: true,
|
|
302
|
+
})).rejects.toMatchObject(new CliError(
|
|
303
|
+
'ARGUMENT',
|
|
304
|
+
'DeepSeek vision mode does not support --search.',
|
|
305
|
+
'Run without --search, or use --model instant/expert for web search.',
|
|
306
|
+
EXIT_CODES.USAGE_ERROR,
|
|
307
|
+
));
|
|
308
|
+
|
|
309
|
+
expect(mockSendMessage).not.toHaveBeenCalled();
|
|
310
|
+
expect(mockSendWithFile).not.toHaveBeenCalled();
|
|
311
|
+
});
|
|
266
312
|
});
|
package/clis/deepseek/utils.js
CHANGED
|
@@ -40,9 +40,10 @@ export async function selectModel(page, modelName) {
|
|
|
40
40
|
return page.evaluate(`(() => {
|
|
41
41
|
var radios = document.querySelectorAll('div[role="radio"]');
|
|
42
42
|
if (radios.length === 0) return { ok: false };
|
|
43
|
-
var
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
var name = '${modelName}'.toLowerCase();
|
|
44
|
+
var index = name === 'instant' ? 0 : name === 'expert' ? 1 : name === 'vision' ? 2 : -1;
|
|
45
|
+
if (index < 0 || index >= radios.length) return { ok: false };
|
|
46
|
+
var target = radios[index];
|
|
46
47
|
var alreadySelected = target.getAttribute('aria-checked') === 'true';
|
|
47
48
|
if (!alreadySelected) target.click();
|
|
48
49
|
return { ok: true, toggled: !alreadySelected };
|
|
@@ -74,14 +75,18 @@ export async function sendMessage(page, prompt) {
|
|
|
74
75
|
document.execCommand('insertText', false, ${promptJson});
|
|
75
76
|
await new Promise(r => setTimeout(r, 800));
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
// Find the send button: last non-toggle button in the textarea's container
|
|
79
|
+
var container = box.parentElement;
|
|
80
|
+
while (container && !container.querySelector('div[role="button"]')) {
|
|
81
|
+
container = container.parentElement;
|
|
82
|
+
}
|
|
83
|
+
if (container) {
|
|
84
|
+
var btns = container.querySelectorAll('div[role="button"]:not(.ds-toggle-button)');
|
|
85
|
+
var sendBtn = btns[btns.length - 1];
|
|
86
|
+
if (sendBtn && sendBtn.getAttribute('aria-disabled') === 'false'
|
|
87
|
+
&& sendBtn.querySelectorAll('svg').length > 0) {
|
|
88
|
+
sendBtn.click();
|
|
89
|
+
return { ok: true };
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
@@ -273,9 +278,18 @@ async function waitForFilePreview(page, fileName) {
|
|
|
273
278
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
274
279
|
await page.wait(2);
|
|
275
280
|
const ready = await page.evaluate(`(() => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
.some((el)
|
|
281
|
+
var name = ${JSON.stringify(fileName)};
|
|
282
|
+
var hasFileName = Array.from(document.querySelectorAll('div'))
|
|
283
|
+
.some(function(el) { return el.children.length === 0 && (el.textContent || '').trim() === name; });
|
|
284
|
+
if (hasFileName) return true;
|
|
285
|
+
// Vision mode shows an image thumbnail, not filename text. Require
|
|
286
|
+
// a preview-like node here; send-button readiness is checked later.
|
|
287
|
+
var box = document.querySelector('${TEXTAREA_SELECTOR}');
|
|
288
|
+
if (!box) return false;
|
|
289
|
+
var c = box.parentElement;
|
|
290
|
+
while (c && !c.querySelector('div[role="button"]')) c = c.parentElement;
|
|
291
|
+
if (!c) return false;
|
|
292
|
+
return !!c.querySelector('img[src], canvas, video, [style*="background-image"], [class*="preview"], [class*="upload"]');
|
|
279
293
|
})()`);
|
|
280
294
|
if (ready) return true;
|
|
281
295
|
}
|
|
@@ -314,7 +328,7 @@ export async function sendWithFile(page, filePath, prompt) {
|
|
|
314
328
|
uploaded = true;
|
|
315
329
|
} catch (err) {
|
|
316
330
|
const msg = String(err?.message || err);
|
|
317
|
-
if (!msg.includes('Unknown action') && !msg.includes('not supported')) {
|
|
331
|
+
if (!msg.includes('Unknown action') && !msg.includes('not supported') && !msg.includes('Not allowed')) {
|
|
318
332
|
throw err;
|
|
319
333
|
}
|
|
320
334
|
}
|
|
@@ -341,7 +355,8 @@ export async function sendWithFile(page, filePath, prompt) {
|
|
|
341
355
|
}
|
|
342
356
|
|
|
343
357
|
inp.files = dt.files;
|
|
344
|
-
inp
|
|
358
|
+
// Use inp.files, not dt.files; assignment transfers ownership
|
|
359
|
+
inp[propsKey].onChange({ target: { files: inp.files } });
|
|
345
360
|
return { ok: true };
|
|
346
361
|
})()`);
|
|
347
362
|
if (fallbackResult && !fallbackResult.ok) return fallbackResult;
|
|
@@ -350,6 +365,30 @@ export async function sendWithFile(page, filePath, prompt) {
|
|
|
350
365
|
const ready = await waitForFilePreview(page, fileName);
|
|
351
366
|
if (!ready) return { ok: false, reason: 'file preview did not appear' };
|
|
352
367
|
|
|
368
|
+
// File preview appears immediately but send button stays disabled until
|
|
369
|
+
// the server upload finishes. Wait for it.
|
|
370
|
+
let sendEnabled = false;
|
|
371
|
+
for (let tick = 0; tick < 15; tick++) {
|
|
372
|
+
const enabled = await page.evaluate(`(() => {
|
|
373
|
+
var box = document.querySelector('${TEXTAREA_SELECTOR}');
|
|
374
|
+
if (!box) return false;
|
|
375
|
+
var c = box.parentElement;
|
|
376
|
+
while (c && !c.querySelector('div[role="button"]')) c = c.parentElement;
|
|
377
|
+
if (!c) return false;
|
|
378
|
+
var btns = c.querySelectorAll('div[role="button"]:not(.ds-toggle-button)');
|
|
379
|
+
var last = btns[btns.length - 1];
|
|
380
|
+
return !!(last && last.getAttribute('aria-disabled') === 'false');
|
|
381
|
+
})()`);
|
|
382
|
+
if (enabled) {
|
|
383
|
+
sendEnabled = true;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
await page.wait(1);
|
|
387
|
+
}
|
|
388
|
+
if (!sendEnabled) {
|
|
389
|
+
return { ok: false, reason: 'send button did not enable after upload' };
|
|
390
|
+
}
|
|
391
|
+
|
|
353
392
|
return sendMessage(page, prompt);
|
|
354
393
|
}
|
|
355
394
|
|
|
@@ -107,15 +107,36 @@ describe('deepseek sendWithFile', () => {
|
|
|
107
107
|
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
108
108
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
109
109
|
evaluate: vi.fn()
|
|
110
|
-
.mockResolvedValueOnce(undefined)
|
|
111
|
-
.mockResolvedValueOnce(true)
|
|
112
|
-
.mockResolvedValueOnce(
|
|
110
|
+
.mockResolvedValueOnce(undefined) // sidebar collapse
|
|
111
|
+
.mockResolvedValueOnce(true) // waitForFilePreview
|
|
112
|
+
.mockResolvedValueOnce(true) // send button enabled check
|
|
113
|
+
.mockResolvedValueOnce({ ok: true }), // sendMessage
|
|
113
114
|
};
|
|
114
115
|
|
|
115
116
|
const result = await sendWithFile(page, filePath, 'summarize this');
|
|
116
117
|
|
|
117
|
-
expect(result).toEqual({ ok: true });
|
|
118
|
-
|
|
118
|
+
expect(result).toEqual({ ok: true }); expect(page.setFileInput).toHaveBeenCalledWith([filePath], 'input[type="file"]');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('fails closed when upload preview appears but send button never enables', async () => {
|
|
122
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
|
|
123
|
+
tempDirs.push(dir);
|
|
124
|
+
const filePath = path.join(dir, 'report.txt');
|
|
125
|
+
fs.writeFileSync(filePath, 'hello');
|
|
126
|
+
|
|
127
|
+
const page = {
|
|
128
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
129
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
130
|
+
evaluate: vi.fn()
|
|
131
|
+
.mockResolvedValueOnce(undefined) // sidebar collapse
|
|
132
|
+
.mockResolvedValueOnce(true) // waitForFilePreview
|
|
133
|
+
.mockResolvedValue(false), // send button never enables
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const result = await sendWithFile(page, filePath, 'summarize this');
|
|
137
|
+
|
|
138
|
+
expect(result).toEqual({ ok: false, reason: 'send button did not enable after upload' });
|
|
139
|
+
expect(page.evaluate).toHaveBeenCalledTimes(17);
|
|
119
140
|
});
|
|
120
141
|
});
|
|
121
142
|
|
|
@@ -142,4 +163,102 @@ describe('deepseek selectModel', () => {
|
|
|
142
163
|
expect(result).toEqual({ ok: false });
|
|
143
164
|
expect(instantRadio.click).not.toHaveBeenCalled();
|
|
144
165
|
});
|
|
166
|
+
|
|
167
|
+
it('selects the correct radio for each model', async () => {
|
|
168
|
+
const radios = [0, 1, 2].map(() => ({
|
|
169
|
+
getAttribute: vi.fn(() => 'false'),
|
|
170
|
+
click: vi.fn(),
|
|
171
|
+
}));
|
|
172
|
+
global.document = {
|
|
173
|
+
querySelectorAll: vi.fn(() => radios),
|
|
174
|
+
};
|
|
175
|
+
const page = {
|
|
176
|
+
evaluate: vi.fn(async (script) => eval(script)),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
await selectModel(page, 'instant');
|
|
180
|
+
expect(radios[0].click).toHaveBeenCalled();
|
|
181
|
+
expect(radios[1].click).not.toHaveBeenCalled();
|
|
182
|
+
expect(radios[2].click).not.toHaveBeenCalled();
|
|
183
|
+
|
|
184
|
+
radios.forEach(r => r.click.mockClear());
|
|
185
|
+
await selectModel(page, 'expert');
|
|
186
|
+
expect(radios[1].click).toHaveBeenCalled();
|
|
187
|
+
|
|
188
|
+
radios.forEach(r => r.click.mockClear());
|
|
189
|
+
await selectModel(page, 'vision');
|
|
190
|
+
expect(radios[2].click).toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('rejects unknown model names', async () => {
|
|
194
|
+
const radios = [0, 1, 2].map(() => ({
|
|
195
|
+
getAttribute: vi.fn(() => 'false'),
|
|
196
|
+
click: vi.fn(),
|
|
197
|
+
}));
|
|
198
|
+
global.document = {
|
|
199
|
+
querySelectorAll: vi.fn(() => radios),
|
|
200
|
+
};
|
|
201
|
+
const page = {
|
|
202
|
+
evaluate: vi.fn(async (script) => eval(script)),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const result = await selectModel(page, 'turbo');
|
|
206
|
+
expect(result).toEqual({ ok: false });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('deepseek sendWithFile Not allowed fallback', () => {
|
|
211
|
+
const tempDirs = [];
|
|
212
|
+
|
|
213
|
+
afterEach(() => {
|
|
214
|
+
vi.restoreAllMocks();
|
|
215
|
+
while (tempDirs.length) {
|
|
216
|
+
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('falls back to DataTransfer when setFileInput throws Not allowed', async () => {
|
|
221
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
|
|
222
|
+
tempDirs.push(dir);
|
|
223
|
+
const filePath = path.join(dir, 'image.png');
|
|
224
|
+
fs.writeFileSync(filePath, 'fake-png');
|
|
225
|
+
|
|
226
|
+
const page = {
|
|
227
|
+
setFileInput: vi.fn().mockRejectedValue(new Error('Not allowed')),
|
|
228
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
229
|
+
evaluate: vi.fn()
|
|
230
|
+
.mockResolvedValueOnce(undefined) // sidebar collapse
|
|
231
|
+
.mockResolvedValueOnce({ ok: true }) // DataTransfer fallback
|
|
232
|
+
.mockResolvedValueOnce(true) // waitForFilePreview
|
|
233
|
+
.mockResolvedValueOnce(true) // send button enabled
|
|
234
|
+
.mockResolvedValueOnce({ ok: true }),// sendMessage
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = await sendWithFile(page, filePath, 'describe');
|
|
238
|
+
|
|
239
|
+
expect(page.setFileInput).toHaveBeenCalled();
|
|
240
|
+
expect(page.evaluate).toHaveBeenCalledTimes(5);
|
|
241
|
+
expect(result).toEqual({ ok: true });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not treat send-button enablement alone as image upload proof', async () => {
|
|
245
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
|
|
246
|
+
tempDirs.push(dir);
|
|
247
|
+
const filePath = path.join(dir, 'image.png');
|
|
248
|
+
fs.writeFileSync(filePath, 'fake-png');
|
|
249
|
+
|
|
250
|
+
const page = {
|
|
251
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
252
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
253
|
+
evaluate: vi.fn()
|
|
254
|
+
.mockResolvedValueOnce(undefined) // sidebar collapse
|
|
255
|
+
.mockResolvedValue(false), // no filename / thumbnail preview
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const result = await sendWithFile(page, filePath, 'describe');
|
|
259
|
+
|
|
260
|
+
expect(result).toEqual({ ok: false, reason: 'file preview did not appear' });
|
|
261
|
+
expect(page.evaluate.mock.calls[1][0]).toContain('img[src], canvas, video');
|
|
262
|
+
expect(page.evaluate.mock.calls[1][0]).not.toContain("aria-disabled') === 'false'");
|
|
263
|
+
});
|
|
145
264
|
});
|
package/clis/doubao/utils.js
CHANGED
|
@@ -63,7 +63,7 @@ function getTranscriptLinesScript() {
|
|
|
63
63
|
const stopLines = new Set([
|
|
64
64
|
'豆包',
|
|
65
65
|
'新对话',
|
|
66
|
-
'内容由豆包 AI
|
|
66
|
+
'内容由豆包 AI 生成,请仔细甄别',
|
|
67
67
|
'AI 创作',
|
|
68
68
|
'云盘',
|
|
69
69
|
'更多',
|
|
@@ -75,6 +75,8 @@ function getTranscriptLinesScript() {
|
|
|
75
75
|
'PPT 生成',
|
|
76
76
|
'图像生成',
|
|
77
77
|
'帮我写作',
|
|
78
|
+
'请仔细甄别',
|
|
79
|
+
'下载电脑版',
|
|
78
80
|
]);
|
|
79
81
|
|
|
80
82
|
const noisyPatterns = [
|
|
@@ -88,7 +90,7 @@ function getTranscriptLinesScript() {
|
|
|
88
90
|
|
|
89
91
|
const transcriptText = clean(root.innerText || root.textContent || '')
|
|
90
92
|
.replace(/新对话/g, '\\n')
|
|
91
|
-
.replace(/内容由豆包 AI
|
|
93
|
+
.replace(/内容由豆包 AI 生成,请仔细甄别/g, '\\n')
|
|
92
94
|
.replace(/在此处拖放文件/g, '\\n')
|
|
93
95
|
.replace(/文件数量:[^\\n]*/g, '')
|
|
94
96
|
.replace(/文件类型:[^\\n]*/g, '');
|
|
@@ -144,12 +146,20 @@ function getTurnsScript() {
|
|
|
144
146
|
if (
|
|
145
147
|
root.matches('[data-testid="send_message"], [class*="send-message"]')
|
|
146
148
|
|| root.querySelector('[data-testid="send_message"], [class*="send-message"]')
|
|
149
|
+
|| root.matches('[class*="bg-g-send-msg-bubble"]')
|
|
150
|
+
||
|
|
151
|
+
root.querySelector('[class*="bg-g-send-msg-bubble"]')
|
|
152
|
+
|| root.querySelector('[data-foundation-type="send-message-action-bar"]')
|
|
147
153
|
) {
|
|
148
154
|
return 'User';
|
|
149
155
|
}
|
|
150
156
|
if (
|
|
151
157
|
root.matches('[data-testid="receive_message"], [data-testid*="receive_message"], [class*="receive-message"]')
|
|
152
158
|
|| root.querySelector('[data-testid="receive_message"], [data-testid*="receive_message"], [class*="receive-message"]')
|
|
159
|
+
|| root.matches('[class*="bg-g-receive-msg-bubble"]')
|
|
160
|
+
||
|
|
161
|
+
root.querySelector('[class*="bg-g-receive-msg-bubble"]')
|
|
162
|
+
|| root.querySelector('[data-foundation-type="receive-message-action-bar"]')
|
|
153
163
|
) {
|
|
154
164
|
return 'Assistant';
|
|
155
165
|
}
|
|
@@ -163,6 +173,10 @@ function getTurnsScript() {
|
|
|
163
173
|
'[data-testid*="message_content"]',
|
|
164
174
|
'[class*="message-text"]',
|
|
165
175
|
'[class*="message-content"]',
|
|
176
|
+
'[class*="bg-g-send-msg-bubble"]',
|
|
177
|
+
'[class*="bg-g-receive-msg-bubble"]',
|
|
178
|
+
'.flow-markdown-body',
|
|
179
|
+
'[class*="bubble"]',
|
|
166
180
|
];
|
|
167
181
|
const messageImageSelector = messageTextSelectors.map((s) => s + ' img').join(', ');
|
|
168
182
|
|
|
@@ -205,14 +219,30 @@ function getTurnsScript() {
|
|
|
205
219
|
return text ? text + '\\n' + imageLines.join('\\n') : imageLines.join('\\n');
|
|
206
220
|
};
|
|
207
221
|
|
|
208
|
-
const messageList = document.querySelector('[data-testid="message-list"]');
|
|
222
|
+
const messageList = document.querySelector('[class*="message-list-S2Fv2S"], .container-PvPoAn, .scroll-view-OEiNXD, [data-testid="message-list"]');
|
|
209
223
|
if (!messageList) return [];
|
|
210
224
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
const itemSelectors = [
|
|
226
|
+
'[class*="item-kDun2N"]',
|
|
227
|
+
'[data-testid="union_message"]',
|
|
228
|
+
'[data-testid="message-block-container"]',
|
|
229
|
+
'[data-message-id]',
|
|
230
|
+
'[class*="bg-g-send-msg-bubble"]',
|
|
231
|
+
'[class*="bg-g-receive-msg-bubble"]',
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const allRoots = [];
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
for (const sel of itemSelectors) {
|
|
237
|
+
messageList.querySelectorAll(sel).forEach((el) => {
|
|
238
|
+
if (!seen.has(el)) {
|
|
239
|
+
seen.add(el);
|
|
240
|
+
allRoots.push(el);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const roots = allRoots
|
|
245
|
+
.filter((el) => isVisible(el) && !el.closest('script, style, noscript'))
|
|
216
246
|
.filter((el, index, items) => !items.some((other, otherIndex) => otherIndex !== index && other.contains(el)));
|
|
217
247
|
|
|
218
248
|
const turns = roots
|
|
@@ -230,11 +260,11 @@ function getTurnsScript() {
|
|
|
230
260
|
});
|
|
231
261
|
|
|
232
262
|
const deduped = [];
|
|
233
|
-
const
|
|
263
|
+
const dedupedSeen = new Set();
|
|
234
264
|
for (const turn of turns) {
|
|
235
265
|
const key = turn.role + '::' + turn.text;
|
|
236
|
-
if (
|
|
237
|
-
|
|
266
|
+
if (dedupedSeen.has(key)) continue;
|
|
267
|
+
dedupedSeen.add(key);
|
|
238
268
|
deduped.push({ Role: turn.role, Text: turn.text });
|
|
239
269
|
}
|
|
240
270
|
|
|
@@ -421,6 +451,16 @@ function clickSendButtonScript() {
|
|
|
421
451
|
return `
|
|
422
452
|
(() => {
|
|
423
453
|
${buildDoubaoComposerLocatorScript()}
|
|
454
|
+
const directSendButton = document.querySelector('button#flow-end-msg-send');
|
|
455
|
+
if (directSendButton instanceof HTMLElement && isVisible(directSendButton)) {
|
|
456
|
+
const disabled = directSendButton.getAttribute('disabled') !== null
|
|
457
|
+
|| directSendButton.getAttribute('aria-disabled') === 'true';
|
|
458
|
+
if (!disabled) {
|
|
459
|
+
directSendButton.click();
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
424
464
|
const composer = findComposer();
|
|
425
465
|
if (!(composer instanceof HTMLElement)) return false;
|
|
426
466
|
|
|
@@ -1076,6 +1116,8 @@ export const __test__ = {
|
|
|
1076
1116
|
clickSendButtonScript,
|
|
1077
1117
|
composerStateScript,
|
|
1078
1118
|
detectDoubaoVerificationScript,
|
|
1119
|
+
getTurnsScript,
|
|
1120
|
+
getTranscriptLinesScript,
|
|
1079
1121
|
};
|
|
1080
1122
|
export async function startNewDoubaoChat(page) {
|
|
1081
1123
|
await ensureDoubaoChatPage(page);
|
|
@@ -144,6 +144,25 @@ describe('doubao send strategy', () => {
|
|
|
144
144
|
await expect(sendDoubaoMessage(page, '你好')).rejects.toBeInstanceOf(CommandExecutionError);
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
+
describe('doubao receive strategy', () => {
|
|
148
|
+
it('keeps both the new skin selectors and the older structural fallbacks in the turns script', () => {
|
|
149
|
+
const turnsScript = __test__.getTurnsScript();
|
|
150
|
+
expect(turnsScript).toContain('[class*="message-list-S2Fv2S"]');
|
|
151
|
+
expect(turnsScript).toContain('.container-PvPoAn');
|
|
152
|
+
expect(turnsScript).toContain('[data-testid="message-list"]');
|
|
153
|
+
expect(turnsScript).toContain('[class*="bg-g-receive-msg-bubble"]');
|
|
154
|
+
expect(turnsScript).toContain('[data-testid="receive_message"]');
|
|
155
|
+
expect(turnsScript).toContain('[data-foundation-type="receive-message-action-bar"]');
|
|
156
|
+
expect(turnsScript).toContain('[data-testid="union_message"]');
|
|
157
|
+
expect(turnsScript).toContain('[data-testid="message-block-container"]');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('extends transcript-noise cleanup for the current zh-CN chrome copy', () => {
|
|
161
|
+
const transcriptScript = __test__.getTranscriptLinesScript();
|
|
162
|
+
expect(transcriptScript).toContain('请仔细甄别');
|
|
163
|
+
expect(transcriptScript).toContain('下载电脑版');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
147
166
|
describe('collectDoubaoTranscriptAdditions', () => {
|
|
148
167
|
it('ignores landing-page capability chips that are not assistant content', () => {
|
|
149
168
|
const before = ['older'];
|
|
@@ -202,9 +221,10 @@ describe('collectDoubaoTranscriptAdditions', () => {
|
|
|
202
221
|
expect(collectDoubaoTranscriptAdditions(before, current, '测试一下,只回复OK', (value) => value.replace('测试一下,只回复OK', '').trim())).toBe('');
|
|
203
222
|
});
|
|
204
223
|
it('treats only the exact landing-page chip string as UI noise', () => {
|
|
205
|
-
expect(__test__.clickSendButtonScript()).
|
|
224
|
+
expect(__test__.clickSendButtonScript()).toContain("button#flow-end-msg-send");
|
|
225
|
+
expect(__test__.clickSendButtonScript()).toContain("getAttribute('disabled') !== null");
|
|
226
|
+
expect(__test__.clickSendButtonScript()).toContain("getAttribute('aria-disabled') === 'true'");
|
|
206
227
|
expect(__test__.clickSendButtonScript()).toContain('bestScore >= 200');
|
|
207
|
-
expect(__test__.clickSendButtonScript()).not.toContain("|| !!button.closest('.chat-input-button')");
|
|
208
228
|
expect(__test__.clickSendButtonScript()).toContain("button.getAttribute('type') === 'submit') score += 1200");
|
|
209
229
|
expect(__test__.composerStateScript()).toContain("(composer.innerText || '').trim() || (composer.textContent || '').trim()");
|
|
210
230
|
expect(__test__.detectDoubaoVerificationScript()).not.toContain('document.body?.innerText');
|
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
19
19
|
],
|
|
20
20
|
columns: ['time', 'code', 'name', 'title', 'category', 'url'],
|
|
21
|
-
func: async (
|
|
21
|
+
func: async (args) => {
|
|
22
22
|
const market = String(args.market ?? 'SHA,SZA,BJA').trim() || 'SHA,SZA,BJA';
|
|
23
23
|
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ cli({
|
|
|
28
28
|
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
29
29
|
],
|
|
30
30
|
columns: ['rank', 'bondCode', 'bondName', 'bondPrice', 'bondChangePct', 'stockCode', 'stockName', 'stockPrice', 'stockChangePct', 'convPrice', 'convValue', 'convPremiumPct', 'remainingYears', 'ytm', 'listDate'],
|
|
31
|
-
func: async (
|
|
31
|
+
func: async (args) => {
|
|
32
32
|
const sortKey = String(args.sort ?? 'turnover').toLowerCase();
|
|
33
33
|
const sort = SORTS[sortKey];
|
|
34
34
|
if (!sort) throw new CliError('INVALID_ARGUMENT', `Unknown sort "${sortKey}". Valid: ${Object.keys(SORTS).join(', ')}`);
|
package/clis/eastmoney/etf.js
CHANGED
|
@@ -26,7 +26,7 @@ cli({
|
|
|
26
26
|
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
27
27
|
],
|
|
28
28
|
columns: ['rank', 'code', 'name', 'price', 'changePercent', 'change', 'turnover', 'volume', 'turnoverRate'],
|
|
29
|
-
func: async (
|
|
29
|
+
func: async (args) => {
|
|
30
30
|
const sortKey = String(args.sort ?? 'turnover').toLowerCase();
|
|
31
31
|
const sort = SORTS[sortKey];
|
|
32
32
|
if (!sort) throw new CliError('INVALID_ARGUMENT', `Unknown sort "${sortKey}". Valid: ${Object.keys(SORTS).join(', ')}`);
|
|
@@ -37,7 +37,7 @@ cli({
|
|
|
37
37
|
{ name: 'limit', type: 'int', default: 10, help: '返回股东数(默认十大流通股东)' },
|
|
38
38
|
],
|
|
39
39
|
columns: ['rank', 'reportDate', 'name', 'holdNum', 'floatRatio', 'change'],
|
|
40
|
-
func: async (
|
|
40
|
+
func: async (args) => {
|
|
41
41
|
/** @type {string} */
|
|
42
42
|
let secucode;
|
|
43
43
|
try { secucode = toSecucode(args.symbol); }
|
|
@@ -47,7 +47,7 @@ cli({
|
|
|
47
47
|
},
|
|
48
48
|
],
|
|
49
49
|
columns: ['code', 'name', 'price', 'changePercent', 'change', 'open', 'high', 'low', 'prevClose'],
|
|
50
|
-
func: async (
|
|
50
|
+
func: async (args) => {
|
|
51
51
|
const group = String(args.group ?? 'main').toLowerCase();
|
|
52
52
|
/** @type {[string,string][]} */
|
|
53
53
|
let entries;
|
package/clis/eastmoney/kline.js
CHANGED
|
@@ -36,7 +36,7 @@ cli({
|
|
|
36
36
|
{ name: 'limit', type: 'int', default: 30, help: '返回最近 N 根(末尾)' },
|
|
37
37
|
],
|
|
38
38
|
columns: ['date', 'open', 'close', 'high', 'low', 'volume', 'turnover', 'amplitude', 'changePercent', 'change', 'turnoverRate'],
|
|
39
|
-
func: async (
|
|
39
|
+
func: async (args) => {
|
|
40
40
|
const secid = resolveSecid(args.symbol);
|
|
41
41
|
const periodKey = String(args.period ?? 'day').toLowerCase();
|
|
42
42
|
const klt = PERIOD_MAP[periodKey];
|
|
@@ -26,7 +26,7 @@ cli({
|
|
|
26
26
|
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
27
27
|
],
|
|
28
28
|
columns: ['time', 'title', 'summary', 'stocks'],
|
|
29
|
-
func: async (
|
|
29
|
+
func: async (args) => {
|
|
30
30
|
const column = String(args.column ?? '102').trim();
|
|
31
31
|
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
|
|
32
32
|
|
package/clis/eastmoney/longhu.js
CHANGED
|
@@ -26,7 +26,7 @@ cli({
|
|
|
26
26
|
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
27
27
|
],
|
|
28
28
|
columns: ['tradeDate', 'code', 'name', 'closePrice', 'changeRate', 'boardAmt', 'buyAmt', 'sellAmt', 'netAmt', 'turnover', 'dealRatio', 'market', 'reason'],
|
|
29
|
-
func: async (
|
|
29
|
+
func: async (args) => {
|
|
30
30
|
const sinceDate = String(args.date || '').trim() || defaultTradeDate();
|
|
31
31
|
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
|
|
32
32
|
|
|
@@ -28,7 +28,7 @@ cli({
|
|
|
28
28
|
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
29
29
|
],
|
|
30
30
|
columns: ['rank', 'code', 'name', 'price', 'changePercent', 'mainNet', 'mainNetRatio', 'superNet', 'bigNet', 'mediumNet', 'smallNet'],
|
|
31
|
-
func: async (
|
|
31
|
+
func: async (args) => {
|
|
32
32
|
const rangeKey = String(args.range ?? 'today').toLowerCase();
|
|
33
33
|
const range = RANGES[rangeKey];
|
|
34
34
|
if (!range) {
|
|
@@ -19,7 +19,7 @@ cli({
|
|
|
19
19
|
{ name: 'limit', type: 'int', default: 10, help: '返回最近 N 分钟' },
|
|
20
20
|
],
|
|
21
21
|
columns: ['time', 'cumulativeNetYi', 'minuteNetYi', 'totalNetYi'],
|
|
22
|
-
func: async (
|
|
22
|
+
func: async (args) => {
|
|
23
23
|
const dir = String(args.direction ?? 'north').toLowerCase();
|
|
24
24
|
if (!['north', 'south', 'n', 's'].includes(dir)) {
|
|
25
25
|
throw new CliError('INVALID_ARGUMENT', `Unknown direction "${dir}". Valid: north / south`);
|