@jackwener/opencli 1.3.0 → 1.3.2
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/CHANGELOG.md +128 -0
- package/README.md +44 -5
- package/README.zh-CN.md +44 -5
- package/SKILL.md +317 -5
- package/TESTING.md +4 -4
- package/dist/browser/errors.d.ts +2 -1
- package/dist/browser/errors.js +9 -10
- package/dist/build-manifest.js +1 -3
- package/dist/cli-manifest.json +2573 -989
- package/dist/cli.js +42 -2
- package/dist/clis/bilibili/download.js +20 -65
- package/dist/clis/bilibili/utils.js +2 -1
- package/dist/clis/chaoxing/assignments.js +2 -1
- package/dist/clis/doubao/ask.d.ts +1 -0
- package/dist/clis/doubao/ask.js +35 -0
- package/dist/clis/doubao/common.d.ts +23 -0
- package/dist/clis/doubao/common.js +564 -0
- package/dist/clis/doubao/new.d.ts +1 -0
- package/dist/clis/doubao/new.js +20 -0
- package/dist/clis/doubao/read.d.ts +1 -0
- package/dist/clis/doubao/read.js +19 -0
- package/dist/clis/doubao/send.d.ts +1 -0
- package/dist/clis/doubao/send.js +22 -0
- package/dist/clis/doubao/status.d.ts +1 -0
- package/dist/clis/doubao/status.js +24 -0
- package/dist/clis/doubao-app/ask.d.ts +1 -0
- package/dist/clis/doubao-app/ask.js +53 -0
- package/dist/clis/doubao-app/common.d.ts +37 -0
- package/dist/clis/doubao-app/common.js +110 -0
- package/dist/clis/doubao-app/dump.d.ts +1 -0
- package/dist/clis/doubao-app/dump.js +24 -0
- package/dist/clis/doubao-app/new.d.ts +1 -0
- package/dist/clis/doubao-app/new.js +20 -0
- package/dist/clis/doubao-app/read.d.ts +1 -0
- package/dist/clis/doubao-app/read.js +18 -0
- package/dist/clis/doubao-app/screenshot.d.ts +1 -0
- package/dist/clis/doubao-app/screenshot.js +18 -0
- package/dist/clis/doubao-app/send.d.ts +1 -0
- package/dist/clis/doubao-app/send.js +27 -0
- package/dist/clis/doubao-app/status.d.ts +1 -0
- package/dist/clis/doubao-app/status.js +16 -0
- package/dist/clis/hackernews/ask.yaml +38 -0
- package/dist/clis/hackernews/best.yaml +38 -0
- package/dist/clis/hackernews/jobs.yaml +36 -0
- package/dist/clis/hackernews/new.yaml +38 -0
- package/dist/clis/hackernews/search.yaml +44 -0
- package/dist/clis/hackernews/show.yaml +38 -0
- package/dist/clis/hackernews/top.yaml +3 -1
- package/dist/clis/hackernews/user.yaml +25 -0
- package/dist/clis/twitter/download.js +13 -97
- package/dist/clis/twitter/thread.js +2 -1
- package/dist/clis/v2ex/member.yaml +29 -0
- package/dist/clis/v2ex/node.yaml +34 -0
- package/dist/clis/v2ex/nodes.yaml +31 -0
- package/dist/clis/v2ex/replies.yaml +32 -0
- package/dist/clis/v2ex/user.yaml +34 -0
- package/dist/clis/weibo/search.d.ts +1 -0
- package/dist/clis/weibo/search.js +73 -0
- package/dist/clis/weixin/download.d.ts +12 -0
- package/dist/clis/weixin/download.js +183 -0
- package/dist/clis/xiaohongshu/download.js +12 -60
- package/dist/clis/xiaohongshu/publish.d.ts +18 -0
- package/dist/clis/xiaohongshu/publish.js +352 -0
- package/dist/clis/xiaohongshu/search.js +47 -15
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/search.test.js +114 -0
- package/dist/clis/yollomi/background.d.ts +4 -0
- package/dist/clis/yollomi/background.js +45 -0
- package/dist/clis/yollomi/edit.d.ts +5 -0
- package/dist/clis/yollomi/edit.js +56 -0
- package/dist/clis/yollomi/face-swap.d.ts +5 -0
- package/dist/clis/yollomi/face-swap.js +43 -0
- package/dist/clis/yollomi/generate.d.ts +9 -0
- package/dist/clis/yollomi/generate.js +100 -0
- package/dist/clis/yollomi/models.d.ts +1 -0
- package/dist/clis/yollomi/models.js +33 -0
- package/dist/clis/yollomi/object-remover.d.ts +4 -0
- package/dist/clis/yollomi/object-remover.js +42 -0
- package/dist/clis/yollomi/remove-bg.d.ts +4 -0
- package/dist/clis/yollomi/remove-bg.js +38 -0
- package/dist/clis/yollomi/restore.d.ts +4 -0
- package/dist/clis/yollomi/restore.js +38 -0
- package/dist/clis/yollomi/try-on.d.ts +4 -0
- package/dist/clis/yollomi/try-on.js +46 -0
- package/dist/clis/yollomi/upload.d.ts +7 -0
- package/dist/clis/yollomi/upload.js +71 -0
- package/dist/clis/yollomi/upscale.d.ts +4 -0
- package/dist/clis/yollomi/upscale.js +53 -0
- package/dist/clis/yollomi/utils.d.ts +45 -0
- package/dist/clis/yollomi/utils.js +180 -0
- package/dist/clis/yollomi/video.d.ts +5 -0
- package/dist/clis/yollomi/video.js +56 -0
- package/dist/clis/zhihu/download.d.ts +1 -5
- package/dist/clis/zhihu/download.js +20 -126
- package/dist/clis/zhihu/download.test.js +7 -5
- package/dist/clis/zhihu/question.js +2 -1
- package/dist/commanderAdapter.js +4 -6
- package/dist/daemon.js +5 -2
- package/dist/discovery.js +10 -10
- package/dist/download/article-download.d.ts +59 -0
- package/dist/download/article-download.js +178 -0
- package/dist/download/media-download.d.ts +49 -0
- package/dist/download/media-download.js +112 -0
- package/dist/errors.d.ts +23 -2
- package/dist/errors.js +58 -2
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +59 -0
- package/dist/execution.js +9 -10
- package/dist/explore.js +4 -2
- package/dist/external.d.ts +15 -0
- package/dist/external.js +48 -2
- package/dist/external.test.d.ts +1 -0
- package/dist/external.test.js +64 -0
- package/dist/main.js +10 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.js +45 -23
- package/dist/plugin.test.js +6 -1
- package/dist/record.d.ts +47 -0
- package/dist/record.js +545 -0
- package/dist/registry.d.ts +7 -2
- package/dist/registry.js +2 -6
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +10 -3
- package/dist/validate.js +1 -3
- package/docs/.vitepress/config.mts +1 -0
- package/docs/adapters/browser/doubao.md +35 -0
- package/docs/adapters/browser/hackernews.md +20 -4
- package/docs/adapters/browser/tiktok.md +1 -1
- package/docs/adapters/browser/v2ex.md +31 -10
- package/docs/adapters/browser/weibo.md +4 -0
- package/docs/adapters/browser/weixin.md +33 -0
- package/docs/adapters/browser/xiaohongshu.md +8 -6
- package/docs/adapters/browser/yollomi.md +69 -0
- package/docs/adapters/desktop/doubao-app.md +35 -0
- package/docs/adapters/index.md +16 -5
- package/docs/advanced/download.md +4 -0
- package/package.json +3 -1
- package/src/browser/errors.ts +17 -11
- package/src/build-manifest.ts +2 -3
- package/src/cli.ts +45 -2
- package/src/clis/bilibili/download.ts +25 -83
- package/src/clis/bilibili/utils.ts +2 -1
- package/src/clis/chaoxing/assignments.ts +2 -1
- package/src/clis/doubao/ask.ts +40 -0
- package/src/clis/doubao/common.ts +619 -0
- package/src/clis/doubao/new.ts +22 -0
- package/src/clis/doubao/read.ts +20 -0
- package/src/clis/doubao/send.ts +25 -0
- package/src/clis/doubao/status.ts +27 -0
- package/src/clis/doubao-app/ask.ts +60 -0
- package/src/clis/doubao-app/common.ts +116 -0
- package/src/clis/doubao-app/dump.ts +28 -0
- package/src/clis/doubao-app/new.ts +21 -0
- package/src/clis/doubao-app/read.ts +21 -0
- package/src/clis/doubao-app/screenshot.ts +19 -0
- package/src/clis/doubao-app/send.ts +30 -0
- package/src/clis/doubao-app/status.ts +17 -0
- package/src/clis/hackernews/ask.yaml +38 -0
- package/src/clis/hackernews/best.yaml +38 -0
- package/src/clis/hackernews/jobs.yaml +36 -0
- package/src/clis/hackernews/new.yaml +38 -0
- package/src/clis/hackernews/search.yaml +44 -0
- package/src/clis/hackernews/show.yaml +38 -0
- package/src/clis/hackernews/top.yaml +3 -1
- package/src/clis/hackernews/user.yaml +25 -0
- package/src/clis/twitter/download.ts +13 -111
- package/src/clis/twitter/thread.ts +2 -1
- package/src/clis/v2ex/member.yaml +29 -0
- package/src/clis/v2ex/node.yaml +34 -0
- package/src/clis/v2ex/nodes.yaml +31 -0
- package/src/clis/v2ex/replies.yaml +32 -0
- package/src/clis/v2ex/user.yaml +34 -0
- package/src/clis/weibo/search.ts +78 -0
- package/src/clis/weixin/download.ts +199 -0
- package/src/clis/xiaohongshu/download.ts +12 -71
- package/src/clis/xiaohongshu/publish.ts +392 -0
- package/src/clis/xiaohongshu/search.test.ts +134 -0
- package/src/clis/xiaohongshu/search.ts +49 -15
- package/src/clis/yollomi/background.ts +48 -0
- package/src/clis/yollomi/edit.ts +58 -0
- package/src/clis/yollomi/face-swap.ts +45 -0
- package/src/clis/yollomi/generate.ts +95 -0
- package/src/clis/yollomi/models.ts +38 -0
- package/src/clis/yollomi/object-remover.ts +44 -0
- package/src/clis/yollomi/remove-bg.ts +40 -0
- package/src/clis/yollomi/restore.ts +40 -0
- package/src/clis/yollomi/try-on.ts +48 -0
- package/src/clis/yollomi/upload.ts +78 -0
- package/src/clis/yollomi/upscale.ts +49 -0
- package/src/clis/yollomi/utils.ts +202 -0
- package/src/clis/yollomi/video.ts +61 -0
- package/src/clis/zhihu/download.test.ts +7 -5
- package/src/clis/zhihu/download.ts +23 -158
- package/src/clis/zhihu/question.ts +2 -1
- package/src/commanderAdapter.ts +4 -7
- package/src/daemon.ts +5 -2
- package/src/discovery.ts +26 -26
- package/src/download/article-download.ts +272 -0
- package/src/download/media-download.ts +178 -0
- package/src/errors.test.ts +79 -0
- package/src/errors.ts +92 -2
- package/src/execution.ts +14 -10
- package/src/explore.ts +4 -2
- package/src/external.test.ts +88 -0
- package/src/external.ts +56 -2
- package/src/generate.ts +2 -1
- package/src/main.ts +10 -0
- package/src/plugin.test.ts +7 -1
- package/src/plugin.ts +49 -25
- package/src/record.ts +617 -0
- package/src/registry.ts +9 -5
- package/src/runtime.ts +16 -4
- package/src/validate.ts +2 -3
- package/tests/e2e/browser-auth.test.ts +10 -1
- package/tests/e2e/browser-public.test.ts +13 -8
- package/tests/e2e/public-commands.test.ts +209 -21
- package/tests/smoke/api-health.test.ts +65 -6
- package/.github/workflows/release-please.yml +0 -25
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
export const DOUBAO_DOMAIN = 'www.doubao.com';
|
|
2
|
+
export const DOUBAO_CHAT_URL = 'https://www.doubao.com/chat';
|
|
3
|
+
export const DOUBAO_NEW_CHAT_URL = 'https://www.doubao.com/chat/new-thread/create-by-msg';
|
|
4
|
+
function getTranscriptLinesScript() {
|
|
5
|
+
return `
|
|
6
|
+
(() => {
|
|
7
|
+
const clean = (value) => (value || '')
|
|
8
|
+
.replace(/\\u00a0/g, ' ')
|
|
9
|
+
.replace(/\\n{3,}/g, '\\n\\n')
|
|
10
|
+
.trim();
|
|
11
|
+
|
|
12
|
+
const root = document.body.cloneNode(true);
|
|
13
|
+
const removableSelectors = [
|
|
14
|
+
'[data-testid="flow_chat_sidebar"]',
|
|
15
|
+
'[data-testid="chat_input"]',
|
|
16
|
+
'[data-testid="flow_chat_guidance_page"]',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const selector of removableSelectors) {
|
|
20
|
+
root.querySelectorAll(selector).forEach((node) => node.remove());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
root.querySelectorAll('script, style, noscript').forEach((node) => node.remove());
|
|
24
|
+
|
|
25
|
+
const stopLines = new Set([
|
|
26
|
+
'豆包',
|
|
27
|
+
'新对话',
|
|
28
|
+
'内容由豆包 AI 生成',
|
|
29
|
+
'AI 创作',
|
|
30
|
+
'云盘',
|
|
31
|
+
'更多',
|
|
32
|
+
'历史对话',
|
|
33
|
+
'手机版对话',
|
|
34
|
+
'快速',
|
|
35
|
+
'超能模式',
|
|
36
|
+
'Beta',
|
|
37
|
+
'PPT 生成',
|
|
38
|
+
'图像生成',
|
|
39
|
+
'帮我写作',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const noisyPatterns = [
|
|
43
|
+
/^window\\._SSR_DATA/,
|
|
44
|
+
/^window\\._ROUTER_DATA/,
|
|
45
|
+
/^\{"namedChunks"/,
|
|
46
|
+
/^在此处拖放文件/,
|
|
47
|
+
/^文件数量:/,
|
|
48
|
+
/^文件类型:/,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const transcriptText = clean(root.innerText || root.textContent || '')
|
|
52
|
+
.replace(/新对话/g, '\\n')
|
|
53
|
+
.replace(/内容由豆包 AI 生成/g, '\\n')
|
|
54
|
+
.replace(/在此处拖放文件/g, '\\n')
|
|
55
|
+
.replace(/文件数量:[^\\n]*/g, '')
|
|
56
|
+
.replace(/文件类型:[^\\n]*/g, '');
|
|
57
|
+
|
|
58
|
+
return clean(transcriptText)
|
|
59
|
+
.split('\\n')
|
|
60
|
+
.map((line) => clean(line))
|
|
61
|
+
.filter((line) => line
|
|
62
|
+
&& line.length <= 400
|
|
63
|
+
&& !stopLines.has(line)
|
|
64
|
+
&& !noisyPatterns.some((pattern) => pattern.test(line)));
|
|
65
|
+
})()
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
function getStateScript() {
|
|
69
|
+
return `
|
|
70
|
+
(() => {
|
|
71
|
+
const routerData = window._ROUTER_DATA?.loaderData?.chat_layout;
|
|
72
|
+
const placeholderNode = document.querySelector(
|
|
73
|
+
'textarea[data-testid="chat_input_input"], textarea[placeholder], [contenteditable="true"][placeholder], [aria-label*="发消息"], [aria-label*="Message"]'
|
|
74
|
+
);
|
|
75
|
+
return {
|
|
76
|
+
url: window.location.href,
|
|
77
|
+
title: document.title || '',
|
|
78
|
+
isLogin: typeof routerData?.userSetting?.data?.is_login === 'boolean'
|
|
79
|
+
? routerData.userSetting.data.is_login
|
|
80
|
+
: null,
|
|
81
|
+
accountDescription: routerData?.accountInfo?.data?.description || '',
|
|
82
|
+
placeholder: placeholderNode?.getAttribute('placeholder')
|
|
83
|
+
|| placeholderNode?.getAttribute('aria-label')
|
|
84
|
+
|| '',
|
|
85
|
+
};
|
|
86
|
+
})()
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
function getTurnsScript() {
|
|
90
|
+
return `
|
|
91
|
+
(() => {
|
|
92
|
+
const clean = (value) => (value || '')
|
|
93
|
+
.replace(/\\u00a0/g, ' ')
|
|
94
|
+
.replace(/\\n{3,}/g, '\\n\\n')
|
|
95
|
+
.trim();
|
|
96
|
+
|
|
97
|
+
const isVisible = (el) => {
|
|
98
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
99
|
+
const style = window.getComputedStyle(el);
|
|
100
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
101
|
+
const rect = el.getBoundingClientRect();
|
|
102
|
+
return rect.width > 0 && rect.height > 0;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getRole = (root) => {
|
|
106
|
+
if (
|
|
107
|
+
root.matches('[data-testid="send_message"], [class*="send-message"]')
|
|
108
|
+
|| root.querySelector('[data-testid="send_message"], [class*="send-message"]')
|
|
109
|
+
) {
|
|
110
|
+
return 'User';
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
root.matches('[data-testid="receive_message"], [data-testid*="receive_message"], [class*="receive-message"]')
|
|
114
|
+
|| root.querySelector('[data-testid="receive_message"], [data-testid*="receive_message"], [class*="receive-message"]')
|
|
115
|
+
) {
|
|
116
|
+
return 'Assistant';
|
|
117
|
+
}
|
|
118
|
+
return '';
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const extractText = (root) => {
|
|
122
|
+
const selectors = [
|
|
123
|
+
'[data-testid="message_text_content"]',
|
|
124
|
+
'[data-testid="message_content"]',
|
|
125
|
+
'[data-testid*="message_text"]',
|
|
126
|
+
'[data-testid*="message_content"]',
|
|
127
|
+
'[class*="message-text"]',
|
|
128
|
+
'[class*="message-content"]',
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const chunks = [];
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
for (const selector of selectors) {
|
|
134
|
+
const nodes = Array.from(root.querySelectorAll(selector))
|
|
135
|
+
.filter((el) => isVisible(el))
|
|
136
|
+
.map((el) => clean(el.innerText || el.textContent || ''))
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
for (const nodeText of nodes) {
|
|
139
|
+
if (seen.has(nodeText)) continue;
|
|
140
|
+
seen.add(nodeText);
|
|
141
|
+
chunks.push(nodeText);
|
|
142
|
+
}
|
|
143
|
+
if (chunks.length > 0) break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (chunks.length > 0) return clean(chunks.join('\\n'));
|
|
147
|
+
return clean(root.innerText || root.textContent || '');
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const messageList = document.querySelector('[data-testid="message-list"]');
|
|
151
|
+
if (!messageList) return [];
|
|
152
|
+
|
|
153
|
+
const unionRoots = Array.from(messageList.querySelectorAll('[data-testid="union_message"]'))
|
|
154
|
+
.filter((el) => isVisible(el));
|
|
155
|
+
const blockRoots = Array.from(messageList.querySelectorAll('[data-testid="message-block-container"]'))
|
|
156
|
+
.filter((el) => isVisible(el) && !el.closest('[data-testid="union_message"]'));
|
|
157
|
+
const roots = (unionRoots.length > 0 ? unionRoots : blockRoots)
|
|
158
|
+
.filter((el, index, items) => !items.some((other, otherIndex) => otherIndex !== index && other.contains(el)));
|
|
159
|
+
|
|
160
|
+
const turns = roots
|
|
161
|
+
.map((el) => {
|
|
162
|
+
const role = getRole(el);
|
|
163
|
+
const text = extractText(el);
|
|
164
|
+
return { el, role, text };
|
|
165
|
+
})
|
|
166
|
+
.filter((item) => (item.role === 'User' || item.role === 'Assistant') && item.text);
|
|
167
|
+
|
|
168
|
+
turns.sort((a, b) => {
|
|
169
|
+
if (a.el === b.el) return 0;
|
|
170
|
+
const pos = a.el.compareDocumentPosition(b.el);
|
|
171
|
+
return pos & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const deduped = [];
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
for (const turn of turns) {
|
|
177
|
+
const key = turn.role + '::' + turn.text;
|
|
178
|
+
if (seen.has(key)) continue;
|
|
179
|
+
seen.add(key);
|
|
180
|
+
deduped.push({ Role: turn.role, Text: turn.text });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (deduped.length > 0) return deduped;
|
|
184
|
+
return [];
|
|
185
|
+
})()
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
function fillComposerScript(text) {
|
|
189
|
+
return `
|
|
190
|
+
((inputText) => {
|
|
191
|
+
const isVisible = (el) => {
|
|
192
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
193
|
+
const style = window.getComputedStyle(el);
|
|
194
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
195
|
+
const rect = el.getBoundingClientRect();
|
|
196
|
+
return rect.width > 0 && rect.height > 0;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const candidates = [
|
|
200
|
+
'textarea[data-testid="chat_input_input"]',
|
|
201
|
+
'.chat-input textarea',
|
|
202
|
+
'.chat-input [contenteditable="true"]',
|
|
203
|
+
'.chat-editor textarea',
|
|
204
|
+
'.chat-editor [contenteditable="true"]',
|
|
205
|
+
'textarea[placeholder*="发消息"]',
|
|
206
|
+
'textarea[placeholder*="Message"]',
|
|
207
|
+
'[contenteditable="true"][placeholder*="发消息"]',
|
|
208
|
+
'[contenteditable="true"][placeholder*="Message"]',
|
|
209
|
+
'[contenteditable="true"][aria-label*="发消息"]',
|
|
210
|
+
'[contenteditable="true"][aria-label*="Message"]',
|
|
211
|
+
'textarea',
|
|
212
|
+
'[contenteditable="true"]',
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
let composer = null;
|
|
216
|
+
for (const selector of candidates) {
|
|
217
|
+
const node = Array.from(document.querySelectorAll(selector)).find(isVisible);
|
|
218
|
+
if (node) {
|
|
219
|
+
composer = node;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!composer) throw new Error('Could not find Doubao input element');
|
|
225
|
+
|
|
226
|
+
composer.focus();
|
|
227
|
+
|
|
228
|
+
if (composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement) {
|
|
229
|
+
const proto = composer instanceof HTMLTextAreaElement
|
|
230
|
+
? window.HTMLTextAreaElement.prototype
|
|
231
|
+
: window.HTMLInputElement.prototype;
|
|
232
|
+
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
233
|
+
setter?.call(composer, inputText);
|
|
234
|
+
composer.dispatchEvent(new Event('input', { bubbles: true }));
|
|
235
|
+
composer.dispatchEvent(new Event('change', { bubbles: true }));
|
|
236
|
+
return 'text-input';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (composer instanceof HTMLElement) {
|
|
240
|
+
composer.textContent = '';
|
|
241
|
+
const selection = window.getSelection();
|
|
242
|
+
const range = document.createRange();
|
|
243
|
+
range.selectNodeContents(composer);
|
|
244
|
+
range.collapse(false);
|
|
245
|
+
selection?.removeAllRanges();
|
|
246
|
+
selection?.addRange(range);
|
|
247
|
+
document.execCommand('insertText', false, inputText);
|
|
248
|
+
composer.dispatchEvent(new Event('input', { bubbles: true }));
|
|
249
|
+
composer.dispatchEvent(new Event('change', { bubbles: true }));
|
|
250
|
+
return 'contenteditable';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw new Error('Unsupported Doubao input element');
|
|
254
|
+
})(${JSON.stringify(text)})
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
function fillAndSubmitComposerScript(text) {
|
|
258
|
+
return `
|
|
259
|
+
((inputText) => {
|
|
260
|
+
const isVisible = (el) => {
|
|
261
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
262
|
+
const style = window.getComputedStyle(el);
|
|
263
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
264
|
+
const rect = el.getBoundingClientRect();
|
|
265
|
+
return rect.width > 0 && rect.height > 0;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const candidates = [
|
|
269
|
+
'textarea[data-testid="chat_input_input"]',
|
|
270
|
+
'[data-testid="chat_input"] textarea',
|
|
271
|
+
'.chat-input textarea',
|
|
272
|
+
'textarea[placeholder*="发消息"]',
|
|
273
|
+
'textarea[placeholder*="Message"]',
|
|
274
|
+
'textarea',
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
let composer = null;
|
|
278
|
+
for (const selector of candidates) {
|
|
279
|
+
const node = Array.from(document.querySelectorAll(selector)).find(isVisible);
|
|
280
|
+
if (node) {
|
|
281
|
+
composer = node;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!(composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement)) {
|
|
287
|
+
throw new Error('Could not find Doubao textarea input element');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
composer.focus();
|
|
291
|
+
const proto = composer instanceof HTMLTextAreaElement
|
|
292
|
+
? window.HTMLTextAreaElement.prototype
|
|
293
|
+
: window.HTMLInputElement.prototype;
|
|
294
|
+
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
295
|
+
setter?.call(composer, inputText);
|
|
296
|
+
composer.dispatchEvent(new Event('input', { bubbles: true }));
|
|
297
|
+
composer.dispatchEvent(new Event('change', { bubbles: true }));
|
|
298
|
+
|
|
299
|
+
const root = document.querySelector('[data-testid="chat_input"], .chat-input') || document.body;
|
|
300
|
+
const buttons = Array.from(root.querySelectorAll('button, [role="button"]')).filter(isVisible);
|
|
301
|
+
const target = buttons[buttons.length - 1];
|
|
302
|
+
|
|
303
|
+
if (target) {
|
|
304
|
+
target.click();
|
|
305
|
+
return 'button';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return 'enter';
|
|
309
|
+
})(${JSON.stringify(text)})
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
function clickSendButtonScript() {
|
|
313
|
+
return `
|
|
314
|
+
(() => {
|
|
315
|
+
const isVisible = (el) => {
|
|
316
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
317
|
+
const style = window.getComputedStyle(el);
|
|
318
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
319
|
+
const rect = el.getBoundingClientRect();
|
|
320
|
+
return rect.width > 0 && rect.height > 0;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const labels = ['发送', 'Send', '发消息...', 'Message...'];
|
|
324
|
+
const root = document.querySelector('[data-testid="chat_input"], .chat-input') || document;
|
|
325
|
+
const buttons = Array.from(root.querySelectorAll(
|
|
326
|
+
'.chat-input-button button, .chat-input-button [role="button"], .chat-input button, button[type="submit"], [role="button"]'
|
|
327
|
+
));
|
|
328
|
+
|
|
329
|
+
for (const button of buttons) {
|
|
330
|
+
if (!isVisible(button)) continue;
|
|
331
|
+
const disabled = button.getAttribute('disabled') !== null
|
|
332
|
+
|| button.getAttribute('aria-disabled') === 'true';
|
|
333
|
+
if (disabled) continue;
|
|
334
|
+
const text = (button.innerText || button.textContent || '').trim();
|
|
335
|
+
const aria = (button.getAttribute('aria-label') || '').trim();
|
|
336
|
+
const title = (button.getAttribute('title') || '').trim();
|
|
337
|
+
const haystacks = [text, aria, title];
|
|
338
|
+
if (haystacks.some((value) => labels.some((label) => value.includes(label)))) {
|
|
339
|
+
button.click();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const styledCandidate = [...buttons].reverse().find((button) => {
|
|
345
|
+
if (!isVisible(button)) return false;
|
|
346
|
+
const disabled = button.getAttribute('disabled') !== null
|
|
347
|
+
|| button.getAttribute('aria-disabled') === 'true';
|
|
348
|
+
if (disabled) return false;
|
|
349
|
+
const className = button.className || '';
|
|
350
|
+
return className.includes('bg-dbx-text-highlight')
|
|
351
|
+
|| className.includes('bg-dbx-fill-highlight')
|
|
352
|
+
|| className.includes('text-dbx-text-static-white-primary');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (styledCandidate) {
|
|
356
|
+
styledCandidate.click();
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const inputButton = [...buttons].reverse().find((button) => {
|
|
361
|
+
if (!isVisible(button)) return false;
|
|
362
|
+
const disabled = button.getAttribute('disabled') !== null
|
|
363
|
+
|| button.getAttribute('aria-disabled') === 'true';
|
|
364
|
+
if (disabled) return false;
|
|
365
|
+
return !!button.closest('.chat-input-button');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (inputButton) {
|
|
369
|
+
inputButton.click();
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const lastEnabledButton = [...buttons].reverse().find((button) => {
|
|
374
|
+
if (!isVisible(button)) return false;
|
|
375
|
+
return button.getAttribute('disabled') === null
|
|
376
|
+
&& button.getAttribute('aria-disabled') !== 'true';
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
if (lastEnabledButton) {
|
|
380
|
+
lastEnabledButton.click();
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return false;
|
|
385
|
+
})()
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
function clickNewChatScript() {
|
|
389
|
+
return `
|
|
390
|
+
(() => {
|
|
391
|
+
const isVisible = (el) => {
|
|
392
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
393
|
+
const style = window.getComputedStyle(el);
|
|
394
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
395
|
+
const rect = el.getBoundingClientRect();
|
|
396
|
+
return rect.width > 0 && rect.height > 0;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const labels = ['新对话', 'New Chat', '创建新对话'];
|
|
400
|
+
const buttons = Array.from(document.querySelectorAll('button, a, [role="button"]'));
|
|
401
|
+
|
|
402
|
+
for (const button of buttons) {
|
|
403
|
+
if (!isVisible(button)) continue;
|
|
404
|
+
const text = (button.innerText || button.textContent || '').trim();
|
|
405
|
+
const aria = (button.getAttribute('aria-label') || '').trim();
|
|
406
|
+
const title = (button.getAttribute('title') || '').trim();
|
|
407
|
+
const haystacks = [text, aria, title];
|
|
408
|
+
if (haystacks.some((value) => labels.some((label) => value.includes(label)))) {
|
|
409
|
+
button.click();
|
|
410
|
+
return text || aria || title || 'new-chat';
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return '';
|
|
415
|
+
})()
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
function normalizeDoubaoTabs(rawTabs) {
|
|
419
|
+
return rawTabs
|
|
420
|
+
.map((tab, index) => {
|
|
421
|
+
const record = (tab || {});
|
|
422
|
+
return {
|
|
423
|
+
index: typeof record.index === 'number' ? record.index : index,
|
|
424
|
+
url: typeof record.url === 'string' ? record.url : '',
|
|
425
|
+
title: typeof record.title === 'string' ? record.title : '',
|
|
426
|
+
active: record.active === true,
|
|
427
|
+
};
|
|
428
|
+
})
|
|
429
|
+
.filter((tab) => tab.url.includes('doubao.com/chat'));
|
|
430
|
+
}
|
|
431
|
+
async function selectPreferredDoubaoTab(page) {
|
|
432
|
+
const rawTabs = await page.tabs().catch(() => []);
|
|
433
|
+
if (!Array.isArray(rawTabs) || rawTabs.length === 0)
|
|
434
|
+
return false;
|
|
435
|
+
const tabs = normalizeDoubaoTabs(rawTabs);
|
|
436
|
+
if (tabs.length === 0)
|
|
437
|
+
return false;
|
|
438
|
+
const preferred = [...tabs].sort((left, right) => {
|
|
439
|
+
const score = (tab) => {
|
|
440
|
+
let value = tab.index;
|
|
441
|
+
if (/https:\/\/www\.doubao\.com\/chat\/[A-Za-z0-9_-]+/.test(tab.url))
|
|
442
|
+
value += 1000;
|
|
443
|
+
else if (tab.url.startsWith(DOUBAO_CHAT_URL))
|
|
444
|
+
value += 100;
|
|
445
|
+
if (tab.active)
|
|
446
|
+
value += 25;
|
|
447
|
+
return value;
|
|
448
|
+
};
|
|
449
|
+
return score(right) - score(left);
|
|
450
|
+
})[0];
|
|
451
|
+
if (!preferred)
|
|
452
|
+
return false;
|
|
453
|
+
await page.selectTab(preferred.index);
|
|
454
|
+
await page.wait(0.8);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
export async function ensureDoubaoChatPage(page) {
|
|
458
|
+
let currentUrl = await page.evaluate('window.location.href').catch(() => '');
|
|
459
|
+
if (typeof currentUrl === 'string' && currentUrl.includes('doubao.com/chat')) {
|
|
460
|
+
await page.wait(1);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const reusedTab = await selectPreferredDoubaoTab(page);
|
|
464
|
+
if (reusedTab) {
|
|
465
|
+
currentUrl = await page.evaluate('window.location.href').catch(() => '');
|
|
466
|
+
if (typeof currentUrl === 'string' && currentUrl.includes('doubao.com/chat')) {
|
|
467
|
+
await page.wait(1);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
await page.goto(DOUBAO_CHAT_URL, { waitUntil: 'load', settleMs: 2500 });
|
|
472
|
+
await page.wait(1.5);
|
|
473
|
+
}
|
|
474
|
+
export async function getDoubaoPageState(page) {
|
|
475
|
+
await ensureDoubaoChatPage(page);
|
|
476
|
+
return await page.evaluate(getStateScript());
|
|
477
|
+
}
|
|
478
|
+
export async function getDoubaoTurns(page) {
|
|
479
|
+
await ensureDoubaoChatPage(page);
|
|
480
|
+
const turns = await page.evaluate(getTurnsScript());
|
|
481
|
+
if (turns.length > 0)
|
|
482
|
+
return turns;
|
|
483
|
+
const lines = await page.evaluate(getTranscriptLinesScript());
|
|
484
|
+
return lines.map((line) => ({ Role: 'System', Text: line }));
|
|
485
|
+
}
|
|
486
|
+
export async function getDoubaoVisibleTurns(page) {
|
|
487
|
+
await ensureDoubaoChatPage(page);
|
|
488
|
+
return await page.evaluate(getTurnsScript());
|
|
489
|
+
}
|
|
490
|
+
export async function getDoubaoTranscriptLines(page) {
|
|
491
|
+
await ensureDoubaoChatPage(page);
|
|
492
|
+
return await page.evaluate(getTranscriptLinesScript());
|
|
493
|
+
}
|
|
494
|
+
export async function sendDoubaoMessage(page, text) {
|
|
495
|
+
await ensureDoubaoChatPage(page);
|
|
496
|
+
const submittedBy = await page.evaluate(fillAndSubmitComposerScript(text));
|
|
497
|
+
if (submittedBy === 'enter') {
|
|
498
|
+
await page.pressKey('Enter');
|
|
499
|
+
}
|
|
500
|
+
await page.wait(0.8);
|
|
501
|
+
return submittedBy;
|
|
502
|
+
}
|
|
503
|
+
export async function waitForDoubaoResponse(page, beforeLines, beforeTurns, promptText, timeoutSeconds) {
|
|
504
|
+
const beforeSet = new Set(beforeLines);
|
|
505
|
+
const beforeTurnSet = new Set(beforeTurns
|
|
506
|
+
.filter((turn) => turn.Role === 'Assistant')
|
|
507
|
+
.map((turn) => `${turn.Role}::${turn.Text}`));
|
|
508
|
+
const sanitizeCandidate = (value) => value
|
|
509
|
+
.replace(promptText, '')
|
|
510
|
+
.replace(/内容由豆包 AI 生成/g, '')
|
|
511
|
+
.replace(/在此处拖放文件/g, '')
|
|
512
|
+
.replace(/文件数量:.*$/g, '')
|
|
513
|
+
.replace(/\{"namedChunks".*$/g, '')
|
|
514
|
+
.replace(/window\\._SSR_DATA.*$/g, '')
|
|
515
|
+
.trim();
|
|
516
|
+
const getCandidate = async () => {
|
|
517
|
+
const turns = await getDoubaoVisibleTurns(page);
|
|
518
|
+
const assistantCandidate = [...turns]
|
|
519
|
+
.reverse()
|
|
520
|
+
.find((turn) => turn.Role === 'Assistant' && !beforeTurnSet.has(`${turn.Role}::${turn.Text}`));
|
|
521
|
+
const visibleCandidate = assistantCandidate ? sanitizeCandidate(assistantCandidate.Text) : '';
|
|
522
|
+
if (visibleCandidate)
|
|
523
|
+
return visibleCandidate;
|
|
524
|
+
const lines = await getDoubaoTranscriptLines(page);
|
|
525
|
+
const additions = lines
|
|
526
|
+
.filter((line) => !beforeSet.has(line))
|
|
527
|
+
.map((line) => sanitizeCandidate(line))
|
|
528
|
+
.filter((line) => line && line !== promptText);
|
|
529
|
+
const shortCandidate = additions.find((line) => line.length <= 120);
|
|
530
|
+
return shortCandidate || additions[additions.length - 1] || '';
|
|
531
|
+
};
|
|
532
|
+
const pollIntervalSeconds = 2;
|
|
533
|
+
const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / pollIntervalSeconds));
|
|
534
|
+
let lastCandidate = '';
|
|
535
|
+
let stableCount = 0;
|
|
536
|
+
for (let index = 0; index < maxPolls; index += 1) {
|
|
537
|
+
await page.wait(index === 0 ? 1.5 : pollIntervalSeconds);
|
|
538
|
+
const candidate = await getCandidate();
|
|
539
|
+
if (!candidate)
|
|
540
|
+
continue;
|
|
541
|
+
if (candidate === lastCandidate) {
|
|
542
|
+
stableCount += 1;
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
lastCandidate = candidate;
|
|
546
|
+
stableCount = 1;
|
|
547
|
+
}
|
|
548
|
+
if (stableCount >= 2 || index === maxPolls - 1) {
|
|
549
|
+
return candidate;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return lastCandidate;
|
|
553
|
+
}
|
|
554
|
+
export async function startNewDoubaoChat(page) {
|
|
555
|
+
await ensureDoubaoChatPage(page);
|
|
556
|
+
const clickedLabel = await page.evaluate(clickNewChatScript());
|
|
557
|
+
if (clickedLabel) {
|
|
558
|
+
await page.wait(1.5);
|
|
559
|
+
return clickedLabel;
|
|
560
|
+
}
|
|
561
|
+
await page.goto(DOUBAO_NEW_CHAT_URL, { waitUntil: 'load', settleMs: 2000 });
|
|
562
|
+
await page.wait(1.5);
|
|
563
|
+
return 'navigate';
|
|
564
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const newCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { DOUBAO_DOMAIN, startNewDoubaoChat } from './common.js';
|
|
3
|
+
export const newCommand = cli({
|
|
4
|
+
site: 'doubao',
|
|
5
|
+
name: 'new',
|
|
6
|
+
description: 'Start a new conversation in Doubao web chat',
|
|
7
|
+
domain: DOUBAO_DOMAIN,
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
navigateBefore: false,
|
|
11
|
+
args: [],
|
|
12
|
+
columns: ['Status', 'Action'],
|
|
13
|
+
func: async (page) => {
|
|
14
|
+
const action = await startNewDoubaoChat(page);
|
|
15
|
+
return [{
|
|
16
|
+
Status: 'Success',
|
|
17
|
+
Action: action === 'navigate' ? 'Reloaded /chat as fallback' : `Clicked ${action}`,
|
|
18
|
+
}];
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const readCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { DOUBAO_DOMAIN, getDoubaoVisibleTurns } from './common.js';
|
|
3
|
+
export const readCommand = cli({
|
|
4
|
+
site: 'doubao',
|
|
5
|
+
name: 'read',
|
|
6
|
+
description: 'Read the current Doubao conversation history',
|
|
7
|
+
domain: DOUBAO_DOMAIN,
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
navigateBefore: false,
|
|
11
|
+
args: [],
|
|
12
|
+
columns: ['Role', 'Text'],
|
|
13
|
+
func: async (page) => {
|
|
14
|
+
const turns = await getDoubaoVisibleTurns(page);
|
|
15
|
+
if (turns.length > 0)
|
|
16
|
+
return turns;
|
|
17
|
+
return [{ Role: 'System', Text: 'No visible Doubao messages were found.' }];
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sendCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { DOUBAO_DOMAIN, sendDoubaoMessage } from './common.js';
|
|
3
|
+
export const sendCommand = cli({
|
|
4
|
+
site: 'doubao',
|
|
5
|
+
name: 'send',
|
|
6
|
+
description: 'Send a message to Doubao web chat',
|
|
7
|
+
domain: DOUBAO_DOMAIN,
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
navigateBefore: false,
|
|
11
|
+
args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
|
|
12
|
+
columns: ['Status', 'SubmittedBy', 'InjectedText'],
|
|
13
|
+
func: async (page, kwargs) => {
|
|
14
|
+
const text = kwargs.text;
|
|
15
|
+
const submittedBy = await sendDoubaoMessage(page, text);
|
|
16
|
+
return [{
|
|
17
|
+
Status: 'Success',
|
|
18
|
+
SubmittedBy: submittedBy,
|
|
19
|
+
InjectedText: text,
|
|
20
|
+
}];
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const statusCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { DOUBAO_DOMAIN, getDoubaoPageState } from './common.js';
|
|
3
|
+
export const statusCommand = cli({
|
|
4
|
+
site: 'doubao',
|
|
5
|
+
name: 'status',
|
|
6
|
+
description: 'Check Doubao chat page availability and login state',
|
|
7
|
+
domain: DOUBAO_DOMAIN,
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
navigateBefore: false,
|
|
11
|
+
args: [],
|
|
12
|
+
columns: ['Status', 'Login', 'Url', 'Title'],
|
|
13
|
+
func: async (page) => {
|
|
14
|
+
const state = await getDoubaoPageState(page);
|
|
15
|
+
const loggedIn = state.isLogin === null ? 'Unknown' : state.isLogin ? 'Yes' : 'No';
|
|
16
|
+
const status = state.isLogin === false ? 'Login Required' : 'Connected';
|
|
17
|
+
return [{
|
|
18
|
+
Status: status,
|
|
19
|
+
Login: loggedIn,
|
|
20
|
+
Url: state.url,
|
|
21
|
+
Title: state.title || 'Doubao',
|
|
22
|
+
}];
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const askCommand: import("../../registry.js").CliCommand;
|