@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
|
@@ -1,5 +1,41 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import {
|
|
4
|
+
__test__,
|
|
5
|
+
collectDoubaoTranscriptAdditions,
|
|
6
|
+
mergeTranscriptSnapshots,
|
|
7
|
+
parseDoubaoConversationId,
|
|
8
|
+
sendDoubaoMessage,
|
|
9
|
+
waitForDoubaoResponse,
|
|
10
|
+
} from './utils.js';
|
|
11
|
+
|
|
12
|
+
function createPageMock() {
|
|
13
|
+
return {
|
|
14
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
evaluate: vi.fn(),
|
|
16
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
17
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
getFormState: vi.fn().mockResolvedValue({}),
|
|
23
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
25
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
27
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
28
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
32
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
34
|
+
nativeType: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
nativeKeyPress: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
3
39
|
describe('parseDoubaoConversationId', () => {
|
|
4
40
|
it('extracts the numeric id from a full conversation URL', () => {
|
|
5
41
|
expect(parseDoubaoConversationId('https://www.doubao.com/chat/1234567890123')).toBe('1234567890123');
|
|
@@ -8,6 +44,209 @@ describe('parseDoubaoConversationId', () => {
|
|
|
8
44
|
expect(parseDoubaoConversationId('1234567890123')).toBe('1234567890123');
|
|
9
45
|
});
|
|
10
46
|
});
|
|
47
|
+
describe('doubao send strategy', () => {
|
|
48
|
+
it('prefers native CDP text insertion and button submission when a send button is available', async () => {
|
|
49
|
+
const page = createPageMock();
|
|
50
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
51
|
+
const nativeType = vi.mocked(page.nativeType);
|
|
52
|
+
const nativeKeyPress = vi.mocked(page.nativeKeyPress);
|
|
53
|
+
evaluate
|
|
54
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat')
|
|
55
|
+
.mockResolvedValueOnce({ ok: true })
|
|
56
|
+
.mockResolvedValueOnce({ hasText: true, text: '你好' })
|
|
57
|
+
.mockResolvedValueOnce({ hasText: true, text: '你好' })
|
|
58
|
+
.mockResolvedValueOnce(true)
|
|
59
|
+
.mockResolvedValueOnce({ detected: false });
|
|
60
|
+
const result = await sendDoubaoMessage(page, '你好');
|
|
61
|
+
expect(nativeType).toHaveBeenCalledWith('你好');
|
|
62
|
+
expect(nativeKeyPress).not.toHaveBeenCalled();
|
|
63
|
+
expect(result).toBe('button');
|
|
64
|
+
});
|
|
65
|
+
it('falls back to DOM insertion when native insertion does not update the composer', async () => {
|
|
66
|
+
const page = createPageMock();
|
|
67
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
68
|
+
const nativeType = vi.mocked(page.nativeType);
|
|
69
|
+
evaluate
|
|
70
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat')
|
|
71
|
+
.mockResolvedValueOnce({ ok: true })
|
|
72
|
+
.mockResolvedValueOnce({ hasText: false, text: '' })
|
|
73
|
+
.mockResolvedValueOnce({ hasText: false, text: '' })
|
|
74
|
+
.mockResolvedValueOnce({ hasText: true, text: '你好' })
|
|
75
|
+
.mockResolvedValueOnce(true)
|
|
76
|
+
.mockResolvedValueOnce({ detected: false });
|
|
77
|
+
const result = await sendDoubaoMessage(page, '你好');
|
|
78
|
+
expect(nativeType).toHaveBeenCalledWith('你好');
|
|
79
|
+
expect(evaluate).toHaveBeenCalledTimes(7);
|
|
80
|
+
expect(result).toBe('button');
|
|
81
|
+
});
|
|
82
|
+
it('falls back to DOM insertion when native insertion text does not match the requested prompt', async () => {
|
|
83
|
+
const page = createPageMock();
|
|
84
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
85
|
+
evaluate
|
|
86
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat')
|
|
87
|
+
.mockResolvedValueOnce({ ok: true })
|
|
88
|
+
.mockResolvedValueOnce({ hasText: true, text: '你' })
|
|
89
|
+
.mockResolvedValueOnce({ hasText: true, text: '你好' })
|
|
90
|
+
.mockResolvedValueOnce(true)
|
|
91
|
+
.mockResolvedValueOnce({ detected: false });
|
|
92
|
+
const result = await sendDoubaoMessage(page, '你好');
|
|
93
|
+
expect(result).toBe('button');
|
|
94
|
+
});
|
|
95
|
+
it('falls back to native Enter when no clickable submit button is found', async () => {
|
|
96
|
+
const page = createPageMock();
|
|
97
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
98
|
+
const nativeKeyPress = vi.mocked(page.nativeKeyPress);
|
|
99
|
+
evaluate
|
|
100
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat')
|
|
101
|
+
.mockResolvedValueOnce({ ok: true })
|
|
102
|
+
.mockResolvedValueOnce({ hasText: true, text: '你好' })
|
|
103
|
+
.mockResolvedValueOnce({ hasText: true, text: '你好' })
|
|
104
|
+
.mockResolvedValueOnce(false)
|
|
105
|
+
.mockResolvedValueOnce({ detected: false });
|
|
106
|
+
const result = await sendDoubaoMessage(page, '你好');
|
|
107
|
+
expect(nativeKeyPress).toHaveBeenCalledWith('Enter');
|
|
108
|
+
expect(result).toBe('enter');
|
|
109
|
+
});
|
|
110
|
+
it('does not throw verification errors just because the prompt mentions verification terms', async () => {
|
|
111
|
+
const page = createPageMock();
|
|
112
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
113
|
+
evaluate
|
|
114
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat')
|
|
115
|
+
.mockResolvedValueOnce({ ok: true })
|
|
116
|
+
.mockResolvedValueOnce({ hasText: true, text: '请解释 CAPTCHA verification 是什么' })
|
|
117
|
+
.mockResolvedValueOnce({ hasText: true, text: '请解释 CAPTCHA verification 是什么' })
|
|
118
|
+
.mockResolvedValueOnce(true)
|
|
119
|
+
.mockResolvedValueOnce({ detected: false, reason: '' });
|
|
120
|
+
await expect(sendDoubaoMessage(page, '请解释 CAPTCHA verification 是什么')).resolves.toBe('button');
|
|
121
|
+
});
|
|
122
|
+
it('does not throw verification errors for ordinary chinese prompts mentioning security terms', async () => {
|
|
123
|
+
const page = createPageMock();
|
|
124
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
125
|
+
evaluate
|
|
126
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat')
|
|
127
|
+
.mockResolvedValueOnce({ ok: true })
|
|
128
|
+
.mockResolvedValueOnce({ hasText: true, text: '请解释人机验证和完成安全验证的区别' })
|
|
129
|
+
.mockResolvedValueOnce({ hasText: true, text: '请解释人机验证和完成安全验证的区别' })
|
|
130
|
+
.mockResolvedValueOnce(true)
|
|
131
|
+
.mockResolvedValueOnce({ detected: false, reason: '' });
|
|
132
|
+
await expect(sendDoubaoMessage(page, '请解释人机验证和完成安全验证的区别')).resolves.toBe('button');
|
|
133
|
+
});
|
|
134
|
+
it('throws a command error when Doubao shows a verification challenge after submit', async () => {
|
|
135
|
+
const page = createPageMock();
|
|
136
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
137
|
+
evaluate
|
|
138
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat')
|
|
139
|
+
.mockResolvedValueOnce({ ok: true })
|
|
140
|
+
.mockResolvedValueOnce({ hasText: true, text: '你好' })
|
|
141
|
+
.mockResolvedValueOnce({ hasText: true, text: '你好' })
|
|
142
|
+
.mockResolvedValueOnce(true)
|
|
143
|
+
.mockResolvedValueOnce({ detected: true, reason: '请完成安全验证' });
|
|
144
|
+
await expect(sendDoubaoMessage(page, '你好')).rejects.toBeInstanceOf(CommandExecutionError);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('collectDoubaoTranscriptAdditions', () => {
|
|
148
|
+
it('ignores landing-page capability chips that are not assistant content', () => {
|
|
149
|
+
const before = ['older'];
|
|
150
|
+
const current = [
|
|
151
|
+
'older',
|
|
152
|
+
'测试一下,只回复OK快速视频生成深入研究图像生成帮我写作音乐生成更多',
|
|
153
|
+
'测试一下,只回复OK',
|
|
154
|
+
];
|
|
155
|
+
expect(collectDoubaoTranscriptAdditions(before, current, '测试一下,只回复OK')).toBe('');
|
|
156
|
+
});
|
|
157
|
+
it('filters prompt-contaminated chip lines for arbitrary prompts', () => {
|
|
158
|
+
const before = ['older'];
|
|
159
|
+
const current = [
|
|
160
|
+
'older',
|
|
161
|
+
'你好快速视频生成深入研究图像生成帮我写作音乐生成更多',
|
|
162
|
+
];
|
|
163
|
+
expect(collectDoubaoTranscriptAdditions(before, current, '你好')).toBe('');
|
|
164
|
+
});
|
|
165
|
+
it('filters whitespace-normalized multiline prompt echoes and prompt-plus-chip artifacts', () => {
|
|
166
|
+
const before = ['older'];
|
|
167
|
+
const prompt = '第一行\n第二行';
|
|
168
|
+
expect(collectDoubaoTranscriptAdditions(before, ['older', '第一行 第二行'], prompt)).toBe('');
|
|
169
|
+
expect(collectDoubaoTranscriptAdditions(before, ['older', '第一行 第二行快速视频生成深入研究图像生成帮我写作音乐生成更多'], prompt)).toBe('');
|
|
170
|
+
});
|
|
171
|
+
it('keeps legitimate replies that discuss Doubao features', () => {
|
|
172
|
+
const before = ['older'];
|
|
173
|
+
const current = [
|
|
174
|
+
'older',
|
|
175
|
+
'图像生成和音乐生成目前都支持,但适用场景不同。',
|
|
176
|
+
];
|
|
177
|
+
expect(collectDoubaoTranscriptAdditions(before, current, 'irrelevant prompt')).toBe('图像生成和音乐生成目前都支持,但适用场景不同。');
|
|
178
|
+
});
|
|
179
|
+
it('keeps an exact chip string when it is the assistant reply rather than prompt contamination', () => {
|
|
180
|
+
const before = ['older'];
|
|
181
|
+
const current = [
|
|
182
|
+
'older',
|
|
183
|
+
'快速视频生成深入研究图像生成帮我写作音乐生成更多',
|
|
184
|
+
];
|
|
185
|
+
expect(collectDoubaoTranscriptAdditions(before, current, '测试一下,只回复OK')).toBe('快速视频生成深入研究图像生成帮我写作音乐生成更多');
|
|
186
|
+
});
|
|
187
|
+
it('filters combined sidebar chrome that appears as a new transcript line', () => {
|
|
188
|
+
const before = ['older'];
|
|
189
|
+
const current = [
|
|
190
|
+
'older',
|
|
191
|
+
'AI 创作云盘更多历史对话',
|
|
192
|
+
];
|
|
193
|
+
expect(collectDoubaoTranscriptAdditions(before, current, '测试一下,只回复OK')).toBe('');
|
|
194
|
+
});
|
|
195
|
+
it('filters transcript lines that only differ because the prompt was appended to existing page chrome', () => {
|
|
196
|
+
const before = [
|
|
197
|
+
'有什么我能帮你的吗?资讯:韩国三大运营商允许超流量用基本数据服务快速视频生成深入研究图像生成帮我写作音乐生成更多',
|
|
198
|
+
];
|
|
199
|
+
const current = [
|
|
200
|
+
'有什么我能帮你的吗?资讯:韩国三大运营商允许超流量用基本数据服务快速视频生成深入研究图像生成帮我写作音乐生成更多测试一下,只回复OK',
|
|
201
|
+
];
|
|
202
|
+
expect(collectDoubaoTranscriptAdditions(before, current, '测试一下,只回复OK', (value) => value.replace('测试一下,只回复OK', '').trim())).toBe('');
|
|
203
|
+
});
|
|
204
|
+
it('treats only the exact landing-page chip string as UI noise', () => {
|
|
205
|
+
expect(__test__.clickSendButtonScript()).not.toContain('document,');
|
|
206
|
+
expect(__test__.clickSendButtonScript()).toContain('bestScore >= 200');
|
|
207
|
+
expect(__test__.clickSendButtonScript()).not.toContain("|| !!button.closest('.chat-input-button')");
|
|
208
|
+
expect(__test__.clickSendButtonScript()).toContain("button.getAttribute('type') === 'submit') score += 1200");
|
|
209
|
+
expect(__test__.composerStateScript()).toContain("(composer.innerText || '').trim() || (composer.textContent || '').trim()");
|
|
210
|
+
expect(__test__.detectDoubaoVerificationScript()).not.toContain('document.body?.innerText');
|
|
211
|
+
expect(__test__.detectDoubaoVerificationScript()).not.toContain('[class*=\"verify\"]');
|
|
212
|
+
expect(__test__.detectDoubaoVerificationScript()).not.toContain('[class*=\"captcha\"]');
|
|
213
|
+
expect(__test__.detectDoubaoVerificationScript()).not.toContain('document.body?.children');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
describe('waitForDoubaoResponse', () => {
|
|
217
|
+
it('allows transcript fallback on local chat urls when new transcript lines appear', async () => {
|
|
218
|
+
const page = createPageMock();
|
|
219
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
220
|
+
const wait = vi.mocked(page.wait);
|
|
221
|
+
evaluate
|
|
222
|
+
.mockResolvedValueOnce({ detected: false })
|
|
223
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat/local_123')
|
|
224
|
+
.mockResolvedValueOnce([])
|
|
225
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat/local_123')
|
|
226
|
+
.mockResolvedValueOnce(['older', '真正的回答']);
|
|
227
|
+
const result = await waitForDoubaoResponse(page, ['older'], [], '测试一下,只回复OK', 2);
|
|
228
|
+
expect(wait).toHaveBeenCalled();
|
|
229
|
+
expect(result).toBe('真正的回答');
|
|
230
|
+
});
|
|
231
|
+
it('does not suppress assistant turns that happen to match landing-page chip text', async () => {
|
|
232
|
+
const page = createPageMock();
|
|
233
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
234
|
+
evaluate
|
|
235
|
+
.mockResolvedValueOnce({ detected: false })
|
|
236
|
+
.mockResolvedValueOnce('https://www.doubao.com/chat')
|
|
237
|
+
.mockResolvedValueOnce([
|
|
238
|
+
{ Role: 'Assistant', Text: '快速视频生成深入研究图像生成帮我写作音乐生成更多' },
|
|
239
|
+
]);
|
|
240
|
+
const result = await waitForDoubaoResponse(page, [], [], '测试一下,只回复OK', 2);
|
|
241
|
+
expect(result).toBe('快速视频生成深入研究图像生成帮我写作音乐生成更多');
|
|
242
|
+
});
|
|
243
|
+
it('raises a command error when a verification challenge appears during polling', async () => {
|
|
244
|
+
const page = createPageMock();
|
|
245
|
+
const evaluate = vi.mocked(page.evaluate);
|
|
246
|
+
evaluate.mockResolvedValueOnce({ detected: true, reason: '请完成安全验证' });
|
|
247
|
+
await expect(waitForDoubaoResponse(page, [], [], '你好', 2)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
11
250
|
describe('mergeTranscriptSnapshots', () => {
|
|
12
251
|
it('extends the transcript when the next snapshot overlaps with the tail', () => {
|
|
13
252
|
const merged = mergeTranscriptSnapshots('Alice 00:00\nHello team\nBob 00:05\nHi', 'Bob 00:05\nHi\nAlice 00:10\nNext topic');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Shared helpers for resolving eastmoney "secid" (市场.代码).
|
|
2
|
+
//
|
|
3
|
+
// Markets:
|
|
4
|
+
// 1.XXXXXX → Shanghai A (SSE)
|
|
5
|
+
// 0.XXXXXX → Shenzhen A (SZSE) or Beijing (BSE) — eastmoney groups both under 0
|
|
6
|
+
// 116.XXXXX → Hong Kong
|
|
7
|
+
// 105.SYMBOL → NASDAQ
|
|
8
|
+
// 106.SYMBOL → NYSE
|
|
9
|
+
// 107.SYMBOL → AMEX (US)
|
|
10
|
+
|
|
11
|
+
const A_PREFIX_TO_MARKET = /** @param {string} c */ (c) => {
|
|
12
|
+
if (/^(60|68|90|113|900)/.test(c)) return '1'; // SH (A + STAR + old B)
|
|
13
|
+
if (/^(00|30|20)/.test(c)) return '0'; // SZ (A + ChiNext + B)
|
|
14
|
+
if (/^(4|8|920|83|87)/.test(c)) return '0'; // BJ (eastmoney uses 0.)
|
|
15
|
+
return '0';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve various user inputs to an eastmoney `secid`.
|
|
20
|
+
* - "600000" → "1.600000"
|
|
21
|
+
* - "sh600000" → "1.600000"
|
|
22
|
+
* - "sz000001" → "0.000001"
|
|
23
|
+
* - "bj430047" → "0.430047"
|
|
24
|
+
* - "hk00700" / "00700.HK" → "116.00700"
|
|
25
|
+
* - "us.AAPL" / "AAPL" → "105.AAPL"
|
|
26
|
+
* - "1.600000" → passed through
|
|
27
|
+
* @param {string} input
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
// Known eastmoney market numeric prefixes. Narrow whitelist so that inputs like
|
|
31
|
+
// "00700.HK" are NOT mistakenly treated as secids just because they look like
|
|
32
|
+
// "<digits>.<alphanumeric>".
|
|
33
|
+
const KNOWN_MARKET_PREFIXES = new Set(['0', '1', '100', '105', '106', '107', '116', '140', '150', '151', '152', '155', '156']);
|
|
34
|
+
|
|
35
|
+
export function resolveSecid(input) {
|
|
36
|
+
const raw = String(input || '').trim();
|
|
37
|
+
if (!raw) throw new Error('empty symbol');
|
|
38
|
+
const secidMatch = raw.match(/^(\d{1,3})\.([A-Za-z0-9]+)$/);
|
|
39
|
+
if (secidMatch && KNOWN_MARKET_PREFIXES.has(secidMatch[1])) return raw; // already a secid
|
|
40
|
+
const lower = raw.toLowerCase();
|
|
41
|
+
|
|
42
|
+
// market-prefixed Chinese code
|
|
43
|
+
const pref = lower.match(/^(sh|sz|bj)(\d{6})$/);
|
|
44
|
+
if (pref) {
|
|
45
|
+
const [, mk, code] = pref;
|
|
46
|
+
return (mk === 'sh' ? '1' : '0') + '.' + code;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// hk prefix
|
|
50
|
+
const hk = lower.match(/^hk(\d{4,5})$/) || lower.match(/^(\d{4,5})\.hk$/);
|
|
51
|
+
if (hk) return '116.' + hk[1].padStart(5, '0');
|
|
52
|
+
|
|
53
|
+
// us.SYMBOL or SYMBOL.N/.O (treat all as NASDAQ by default; .N as NYSE)
|
|
54
|
+
const usDot = lower.match(/^([a-z.\-]+)\.([no])$/);
|
|
55
|
+
if (usDot) return (usDot[2] === 'n' ? '106' : '105') + '.' + usDot[1].toUpperCase();
|
|
56
|
+
const usPref = lower.match(/^us\.([a-z.\-]+)$/);
|
|
57
|
+
if (usPref) return '105.' + usPref[1].toUpperCase();
|
|
58
|
+
|
|
59
|
+
// bare 6-digit Chinese code
|
|
60
|
+
if (/^\d{6}$/.test(raw)) return A_PREFIX_TO_MARKET(raw) + '.' + raw;
|
|
61
|
+
|
|
62
|
+
// bare US ticker — uppercase letters only
|
|
63
|
+
if (/^[A-Z.\-]{1,8}$/.test(raw)) return '105.' + raw;
|
|
64
|
+
|
|
65
|
+
throw new Error(`Unrecognized symbol: ${input}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Normalize a list of user inputs separated by comma / space / Chinese comma.
|
|
70
|
+
* @param {string} s
|
|
71
|
+
* @returns {string[]}
|
|
72
|
+
*/
|
|
73
|
+
export function splitSymbols(s) {
|
|
74
|
+
return String(s || '')
|
|
75
|
+
.split(/[,,\s]+/)
|
|
76
|
+
.map((x) => x.trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// eastmoney announcement — listed company filings/announcements feed.
|
|
2
|
+
//
|
|
3
|
+
// opencli eastmoney announcement
|
|
4
|
+
// opencli eastmoney announcement --market SHA --limit 30
|
|
5
|
+
|
|
6
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { CliError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
cli({
|
|
10
|
+
site: 'eastmoney',
|
|
11
|
+
name: 'announcement',
|
|
12
|
+
description: '上市公司公告(按交易所筛选)',
|
|
13
|
+
domain: 'np-anotice-stock.eastmoney.com',
|
|
14
|
+
strategy: Strategy.PUBLIC,
|
|
15
|
+
browser: false,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'market', type: 'string', default: 'SHA,SZA,BJA', help: '交易所:SHA (沪) / SZA (深) / BJA (北) 可逗号分隔' },
|
|
18
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['time', 'code', 'name', 'title', 'category', 'url'],
|
|
21
|
+
func: async (_page, args) => {
|
|
22
|
+
const market = String(args.market ?? 'SHA,SZA,BJA').trim() || 'SHA,SZA,BJA';
|
|
23
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
|
|
24
|
+
|
|
25
|
+
const url = new URL('https://np-anotice-stock.eastmoney.com/api/security/ann');
|
|
26
|
+
url.searchParams.set('page_size', String(limit));
|
|
27
|
+
url.searchParams.set('page_index', '1');
|
|
28
|
+
url.searchParams.set('ann_type', market);
|
|
29
|
+
url.searchParams.set('client_source', 'web');
|
|
30
|
+
url.searchParams.set('f_node', '0');
|
|
31
|
+
url.searchParams.set('s_node', '0');
|
|
32
|
+
|
|
33
|
+
const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
34
|
+
if (!resp.ok) throw new CliError('HTTP_ERROR', `announcement failed: HTTP ${resp.status}`);
|
|
35
|
+
const data = await resp.json();
|
|
36
|
+
const list = Array.isArray(data?.data?.list) ? data.data.list : [];
|
|
37
|
+
if (list.length === 0) throw new CliError('NO_DATA', 'eastmoney returned no announcement data');
|
|
38
|
+
|
|
39
|
+
return list.slice(0, limit).map((it) => {
|
|
40
|
+
const primary = Array.isArray(it.codes) && it.codes.length > 0 ? it.codes[0] : {};
|
|
41
|
+
const cat = Array.isArray(it.columns) && it.columns.length > 0 ? it.columns[0]?.column_name : '';
|
|
42
|
+
return {
|
|
43
|
+
time: String(it.notice_date || it.display_time || '').slice(0, 19),
|
|
44
|
+
code: primary.stock_code || '',
|
|
45
|
+
name: primary.short_name || '',
|
|
46
|
+
title: it.title || it.title_ch || '',
|
|
47
|
+
category: cat || '',
|
|
48
|
+
url: `https://data.eastmoney.com/notices/detail/${primary.stock_code || ''}/${it.art_code || ''}.html`,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// eastmoney convertible — on-market convertible bond listing.
|
|
2
|
+
//
|
|
3
|
+
// opencli eastmoney convertible
|
|
4
|
+
// opencli eastmoney convertible --sort premium --limit 30
|
|
5
|
+
|
|
6
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { CliError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
const SORTS = {
|
|
10
|
+
change: { fid: 'f3', order: 'desc' },
|
|
11
|
+
drop: { fid: 'f3', order: 'asc' },
|
|
12
|
+
turnover: { fid: 'f6', order: 'desc' },
|
|
13
|
+
price: { fid: 'f2', order: 'desc' },
|
|
14
|
+
premium: { fid: 'f237', order: 'desc' }, // 转股溢价率
|
|
15
|
+
value: { fid: 'f236', order: 'desc' }, // 转股价值
|
|
16
|
+
ytm: { fid: 'f239', order: 'desc' }, // 到期收益率
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
cli({
|
|
20
|
+
site: 'eastmoney',
|
|
21
|
+
name: 'convertible',
|
|
22
|
+
description: '可转债行情列表(默认按成交额排序)',
|
|
23
|
+
domain: 'push2.eastmoney.com',
|
|
24
|
+
strategy: Strategy.PUBLIC,
|
|
25
|
+
browser: false,
|
|
26
|
+
args: [
|
|
27
|
+
{ name: 'sort', type: 'string', default: 'turnover', help: '排序:turnover / change / drop / price / premium' },
|
|
28
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
29
|
+
],
|
|
30
|
+
columns: ['rank', 'bondCode', 'bondName', 'bondPrice', 'bondChangePct', 'stockCode', 'stockName', 'stockPrice', 'stockChangePct', 'convPrice', 'convValue', 'convPremiumPct', 'remainingYears', 'ytm', 'listDate'],
|
|
31
|
+
func: async (_page, args) => {
|
|
32
|
+
const sortKey = String(args.sort ?? 'turnover').toLowerCase();
|
|
33
|
+
const sort = SORTS[sortKey];
|
|
34
|
+
if (!sort) throw new CliError('INVALID_ARGUMENT', `Unknown sort "${sortKey}". Valid: ${Object.keys(SORTS).join(', ')}`);
|
|
35
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
|
|
36
|
+
|
|
37
|
+
const url = new URL('https://push2.eastmoney.com/api/qt/clist/get');
|
|
38
|
+
url.searchParams.set('pn', '1');
|
|
39
|
+
url.searchParams.set('pz', String(limit));
|
|
40
|
+
url.searchParams.set('po', sort.order === 'desc' ? '1' : '0');
|
|
41
|
+
url.searchParams.set('np', '1');
|
|
42
|
+
url.searchParams.set('fltt', '2');
|
|
43
|
+
url.searchParams.set('invt', '2');
|
|
44
|
+
url.searchParams.set('fid', sort.fid);
|
|
45
|
+
url.searchParams.set('fs', 'b:MK0354');
|
|
46
|
+
url.searchParams.set('fields', 'f12,f14,f2,f3,f6,f229,f230,f232,f234,f235,f236,f237,f238,f239,f243');
|
|
47
|
+
url.searchParams.set('ut', 'bd1d9ddb04089700cf9c27f6f7426281');
|
|
48
|
+
|
|
49
|
+
const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
50
|
+
if (!resp.ok) throw new CliError('HTTP_ERROR', `convertible failed: HTTP ${resp.status}`);
|
|
51
|
+
const data = await resp.json();
|
|
52
|
+
const diff = Array.isArray(data?.data?.diff) ? data.data.diff : [];
|
|
53
|
+
if (diff.length === 0) throw new CliError('NO_DATA', 'eastmoney returned no convertible data');
|
|
54
|
+
|
|
55
|
+
return diff.slice(0, limit).map((it, i) => ({
|
|
56
|
+
rank: i + 1,
|
|
57
|
+
bondCode: it.f12,
|
|
58
|
+
bondName: it.f14,
|
|
59
|
+
bondPrice: it.f2,
|
|
60
|
+
bondChangePct: it.f3,
|
|
61
|
+
stockCode: it.f232,
|
|
62
|
+
stockName: it.f234,
|
|
63
|
+
stockPrice: it.f229,
|
|
64
|
+
stockChangePct: it.f230,
|
|
65
|
+
convPrice: it.f235,
|
|
66
|
+
convValue: it.f236,
|
|
67
|
+
convPremiumPct: it.f237,
|
|
68
|
+
remainingYears: it.f238,
|
|
69
|
+
ytm: it.f239,
|
|
70
|
+
listDate: String(it.f243 ?? ''),
|
|
71
|
+
}));
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// eastmoney etf — ETF ranking by change / turnover.
|
|
2
|
+
//
|
|
3
|
+
// opencli eastmoney etf
|
|
4
|
+
// opencli eastmoney etf --sort change --limit 30
|
|
5
|
+
|
|
6
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { CliError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
const SORTS = {
|
|
10
|
+
turnover: { fid: 'f6', order: 'desc' },
|
|
11
|
+
change: { fid: 'f3', order: 'desc' },
|
|
12
|
+
drop: { fid: 'f3', order: 'asc' },
|
|
13
|
+
volume: { fid: 'f5', order: 'desc' },
|
|
14
|
+
rate: { fid: 'f8', order: 'desc' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
cli({
|
|
18
|
+
site: 'eastmoney',
|
|
19
|
+
name: 'etf',
|
|
20
|
+
description: 'ETF 列表按成交额/涨跌幅排行',
|
|
21
|
+
domain: 'push2.eastmoney.com',
|
|
22
|
+
strategy: Strategy.PUBLIC,
|
|
23
|
+
browser: false,
|
|
24
|
+
args: [
|
|
25
|
+
{ name: 'sort', type: 'string', default: 'turnover', help: '排序:turnover / change / drop / volume / rate' },
|
|
26
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
|
|
27
|
+
],
|
|
28
|
+
columns: ['rank', 'code', 'name', 'price', 'changePercent', 'change', 'turnover', 'volume', 'turnoverRate'],
|
|
29
|
+
func: async (_page, args) => {
|
|
30
|
+
const sortKey = String(args.sort ?? 'turnover').toLowerCase();
|
|
31
|
+
const sort = SORTS[sortKey];
|
|
32
|
+
if (!sort) throw new CliError('INVALID_ARGUMENT', `Unknown sort "${sortKey}". Valid: ${Object.keys(SORTS).join(', ')}`);
|
|
33
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
|
|
34
|
+
|
|
35
|
+
const url = new URL('https://push2.eastmoney.com/api/qt/clist/get');
|
|
36
|
+
url.searchParams.set('pn', '1');
|
|
37
|
+
url.searchParams.set('pz', String(limit));
|
|
38
|
+
url.searchParams.set('po', sort.order === 'desc' ? '1' : '0');
|
|
39
|
+
url.searchParams.set('np', '1');
|
|
40
|
+
url.searchParams.set('fltt', '2');
|
|
41
|
+
url.searchParams.set('invt', '2');
|
|
42
|
+
url.searchParams.set('fid', sort.fid);
|
|
43
|
+
url.searchParams.set('fs', 'b:MK0021'); // 场内ETF
|
|
44
|
+
url.searchParams.set('fields', 'f12,f14,f2,f3,f4,f5,f6,f8');
|
|
45
|
+
url.searchParams.set('ut', 'bd1d9ddb04089700cf9c27f6f7426281');
|
|
46
|
+
|
|
47
|
+
const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
48
|
+
if (!resp.ok) throw new CliError('HTTP_ERROR', `etf failed: HTTP ${resp.status}`);
|
|
49
|
+
const data = await resp.json();
|
|
50
|
+
const diff = Array.isArray(data?.data?.diff) ? data.data.diff : [];
|
|
51
|
+
if (diff.length === 0) throw new CliError('NO_DATA', 'eastmoney returned no ETF data');
|
|
52
|
+
|
|
53
|
+
return diff.slice(0, limit).map((it, i) => ({
|
|
54
|
+
rank: i + 1,
|
|
55
|
+
code: it.f12,
|
|
56
|
+
name: it.f14,
|
|
57
|
+
price: it.f2,
|
|
58
|
+
changePercent: it.f3,
|
|
59
|
+
change: it.f4,
|
|
60
|
+
turnover: it.f6,
|
|
61
|
+
volume: it.f5,
|
|
62
|
+
turnoverRate: it.f8,
|
|
63
|
+
}));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// eastmoney holders — top-10 float shareholders of an A-share (F10 data).
|
|
2
|
+
//
|
|
3
|
+
// opencli eastmoney holders 600519
|
|
4
|
+
// opencli eastmoney holders sh600519 --limit 10
|
|
5
|
+
|
|
6
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { CliError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a bare A-share symbol to eastmoney's SECUCODE form ("600519.SH").
|
|
11
|
+
* Accepts "600519", "sh600519", "sz000001", "bj430047", or full "600519.SH".
|
|
12
|
+
* @param {string} input
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function toSecucode(input) {
|
|
16
|
+
const raw = String(input || '').trim().toUpperCase();
|
|
17
|
+
if (/^\d{6}\.(SH|SZ|BJ)$/.test(raw)) return raw;
|
|
18
|
+
const pref = raw.match(/^(SH|SZ|BJ)(\d{6})$/);
|
|
19
|
+
if (pref) return `${pref[2]}.${pref[1]}`;
|
|
20
|
+
if (/^\d{6}$/.test(raw)) {
|
|
21
|
+
if (/^(60|68|90|113|900)/.test(raw)) return `${raw}.SH`;
|
|
22
|
+
if (/^(4|8|920|83|87)/.test(raw)) return `${raw}.BJ`;
|
|
23
|
+
return `${raw}.SZ`;
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Unrecognized A-share symbol: ${input}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cli({
|
|
29
|
+
site: 'eastmoney',
|
|
30
|
+
name: 'holders',
|
|
31
|
+
description: '十大流通股东(A股 F10 数据)',
|
|
32
|
+
domain: 'datacenter-web.eastmoney.com',
|
|
33
|
+
strategy: Strategy.PUBLIC,
|
|
34
|
+
browser: false,
|
|
35
|
+
args: [
|
|
36
|
+
{ name: 'symbol', required: true, positional: true, help: 'A股代码(600519 / sh600519 等)' },
|
|
37
|
+
{ name: 'limit', type: 'int', default: 10, help: '返回股东数(默认十大流通股东)' },
|
|
38
|
+
],
|
|
39
|
+
columns: ['rank', 'reportDate', 'name', 'holdNum', 'floatRatio', 'change'],
|
|
40
|
+
func: async (_page, args) => {
|
|
41
|
+
/** @type {string} */
|
|
42
|
+
let secucode;
|
|
43
|
+
try { secucode = toSecucode(args.symbol); }
|
|
44
|
+
catch (err) { throw new CliError('INVALID_ARGUMENT', `${err instanceof Error ? err.message : err}`); }
|
|
45
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 10, 50));
|
|
46
|
+
|
|
47
|
+
const url = new URL('https://datacenter-web.eastmoney.com/api/data/v1/get');
|
|
48
|
+
url.searchParams.set('sortColumns', 'END_DATE,HOLDER_RANK');
|
|
49
|
+
url.searchParams.set('sortTypes', '-1,1');
|
|
50
|
+
url.searchParams.set('pageSize', String(Math.max(limit, 10)));
|
|
51
|
+
url.searchParams.set('pageNumber', '1');
|
|
52
|
+
url.searchParams.set('reportName', 'RPT_F10_EH_FREEHOLDERS');
|
|
53
|
+
url.searchParams.set('columns', 'SECUCODE,SECURITY_CODE,END_DATE,HOLDER_RANK,HOLDER_NAME,HOLD_NUM,FREE_HOLDNUM_RATIO,HOLD_NUM_CHANGE');
|
|
54
|
+
url.searchParams.set('source', 'HSF10');
|
|
55
|
+
url.searchParams.set('client', 'PC');
|
|
56
|
+
url.searchParams.set('filter', `(SECUCODE="${secucode}")`);
|
|
57
|
+
|
|
58
|
+
const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
59
|
+
if (!resp.ok) throw new CliError('HTTP_ERROR', `holders failed: HTTP ${resp.status}`);
|
|
60
|
+
const data = await resp.json();
|
|
61
|
+
const rows = Array.isArray(data?.result?.data) ? data.result.data : [];
|
|
62
|
+
if (rows.length === 0) throw new CliError('NO_DATA', `No shareholder data for ${secucode}`);
|
|
63
|
+
|
|
64
|
+
// Only the most recent reporting period
|
|
65
|
+
const latest = String(rows[0].END_DATE || '').slice(0, 10);
|
|
66
|
+
return rows
|
|
67
|
+
.filter((it) => String(it.END_DATE || '').slice(0, 10) === latest)
|
|
68
|
+
.slice(0, limit)
|
|
69
|
+
.map((it) => ({
|
|
70
|
+
rank: it.HOLDER_RANK,
|
|
71
|
+
reportDate: latest,
|
|
72
|
+
name: it.HOLDER_NAME,
|
|
73
|
+
holdNum: it.HOLD_NUM,
|
|
74
|
+
floatRatio: it.FREE_HOLDNUM_RATIO,
|
|
75
|
+
change: it.HOLD_NUM_CHANGE,
|
|
76
|
+
}));
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'eastmoney',
|
|
5
|
+
name: 'hot-rank',
|
|
6
|
+
description: '东方财富热股榜',
|
|
7
|
+
domain: 'guba.eastmoney.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
navigateBefore: true,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'symbol', 'name', 'price', 'changePercent', 'heat', 'url'],
|
|
14
|
+
func: async (page, kwargs) => {
|
|
15
|
+
await page.goto('https://guba.eastmoney.com/rank/');
|
|
16
|
+
await page.wait({ selector: '#rankCont', timeout: 15000 });
|
|
17
|
+
const data = await page.evaluate(`
|
|
18
|
+
(() => {
|
|
19
|
+
const cleanText = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
20
|
+
const rows = document.querySelectorAll('table.rank_table tbody tr');
|
|
21
|
+
const results = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
let rank = 0;
|
|
24
|
+
rows.forEach((row) => {
|
|
25
|
+
const codeEl = row.querySelector('a.stock_code');
|
|
26
|
+
const href = codeEl?.getAttribute('href') || '';
|
|
27
|
+
const symbolMatch = href.match(/(\\d{6})/);
|
|
28
|
+
if (!symbolMatch) return;
|
|
29
|
+
const symbol = symbolMatch[1];
|
|
30
|
+
if (seen.has(symbol)) return;
|
|
31
|
+
seen.add(symbol);
|
|
32
|
+
rank++;
|
|
33
|
+
const tds = row.querySelectorAll('td');
|
|
34
|
+
results.push({
|
|
35
|
+
rank,
|
|
36
|
+
symbol,
|
|
37
|
+
name: row.querySelector('td.nametd a[title]')?.getAttribute('title') || cleanText(row.querySelector('td.nametd')),
|
|
38
|
+
price: tds[6] ? cleanText(tds[6]) : '',
|
|
39
|
+
changePercent: tds[8] ? cleanText(tds[8]) : '',
|
|
40
|
+
heat: cleanText(row.querySelector('td.fans')),
|
|
41
|
+
url: 'https://guba.eastmoney.com/list,' + symbol + '.html',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
return results;
|
|
45
|
+
})()
|
|
46
|
+
`);
|
|
47
|
+
if (!Array.isArray(data)) return [];
|
|
48
|
+
return data.slice(0, kwargs.limit);
|
|
49
|
+
},
|
|
50
|
+
});
|