@jackwener/opencli 1.5.5 → 1.5.6
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 +27 -2
- package/README.zh-CN.md +36 -4
- package/dist/browser/daemon-client.d.ts +5 -1
- package/dist/browser/page.d.ts +6 -0
- package/dist/browser/page.js +15 -0
- package/dist/cli-manifest.json +1229 -67
- package/dist/clis/band/bands.d.ts +1 -0
- package/dist/clis/band/bands.js +72 -0
- package/dist/clis/band/mentions.d.ts +1 -0
- package/dist/clis/band/mentions.js +127 -0
- package/dist/clis/band/post.d.ts +1 -0
- package/dist/clis/band/post.js +175 -0
- package/dist/clis/band/posts.d.ts +1 -0
- package/dist/clis/band/posts.js +94 -0
- package/dist/clis/doubao/detail.d.ts +1 -0
- package/dist/clis/doubao/detail.js +33 -0
- package/dist/clis/doubao/detail.test.d.ts +1 -0
- package/dist/clis/doubao/detail.test.js +42 -0
- package/dist/clis/doubao/history.d.ts +1 -0
- package/dist/clis/doubao/history.js +28 -0
- package/dist/clis/doubao/history.test.d.ts +1 -0
- package/dist/clis/doubao/history.test.js +37 -0
- package/dist/clis/doubao/meeting-summary.d.ts +1 -0
- package/dist/clis/doubao/meeting-summary.js +39 -0
- package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
- package/dist/clis/doubao/meeting-transcript.js +36 -0
- package/dist/clis/doubao/utils.d.ts +27 -0
- package/dist/clis/doubao/utils.js +317 -0
- package/dist/clis/doubao/utils.test.d.ts +1 -0
- package/dist/clis/doubao/utils.test.js +24 -0
- package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
- package/dist/clis/douyin/_shared/public-api.js +29 -0
- package/dist/clis/douyin/user-videos.d.ts +5 -0
- package/dist/clis/douyin/user-videos.js +74 -0
- package/dist/clis/douyin/user-videos.test.d.ts +1 -0
- package/dist/clis/douyin/user-videos.test.js +108 -0
- package/dist/clis/ones/common.d.ts +32 -0
- package/dist/clis/ones/common.js +144 -0
- package/dist/clis/ones/enrich-tasks.d.ts +5 -0
- package/dist/clis/ones/enrich-tasks.js +37 -0
- package/dist/clis/ones/login.d.ts +1 -0
- package/dist/clis/ones/login.js +80 -0
- package/dist/clis/ones/logout.d.ts +1 -0
- package/dist/clis/ones/logout.js +17 -0
- package/dist/clis/ones/me.d.ts +1 -0
- package/dist/clis/ones/me.js +30 -0
- package/dist/clis/ones/my-tasks.d.ts +1 -0
- package/dist/clis/ones/my-tasks.js +120 -0
- package/dist/clis/ones/resolve-labels.d.ts +10 -0
- package/dist/clis/ones/resolve-labels.js +64 -0
- package/dist/clis/ones/task-helpers.d.ts +29 -0
- package/dist/clis/ones/task-helpers.js +212 -0
- package/dist/clis/ones/task-helpers.test.d.ts +1 -0
- package/dist/clis/ones/task-helpers.test.js +12 -0
- package/dist/clis/ones/task.d.ts +1 -0
- package/dist/clis/ones/task.js +66 -0
- package/dist/clis/ones/tasks.d.ts +1 -0
- package/dist/clis/ones/tasks.js +79 -0
- package/dist/clis/ones/token-info.d.ts +1 -0
- package/dist/clis/ones/token-info.js +42 -0
- package/dist/clis/ones/worklog.d.ts +11 -0
- package/dist/clis/ones/worklog.js +267 -0
- package/dist/clis/ones/worklog.test.d.ts +1 -0
- package/dist/clis/ones/worklog.test.js +20 -0
- package/dist/clis/spotify/spotify.d.ts +1 -0
- package/dist/clis/spotify/spotify.js +316 -0
- package/dist/clis/spotify/utils.d.ts +21 -0
- package/dist/clis/spotify/utils.js +66 -0
- package/dist/clis/spotify/utils.test.d.ts +1 -0
- package/dist/clis/spotify/utils.test.js +67 -0
- package/dist/clis/tieba/commands.test.d.ts +4 -0
- package/dist/clis/tieba/commands.test.js +79 -0
- package/dist/clis/tieba/hot.d.ts +1 -0
- package/dist/clis/tieba/hot.js +48 -0
- package/dist/clis/tieba/posts.d.ts +1 -0
- package/dist/clis/tieba/posts.js +85 -0
- package/dist/clis/tieba/read.d.ts +1 -0
- package/dist/clis/tieba/read.js +140 -0
- package/dist/clis/tieba/search.d.ts +1 -0
- package/dist/clis/tieba/search.js +108 -0
- package/dist/clis/tieba/utils.d.ts +101 -0
- package/dist/clis/tieba/utils.js +240 -0
- package/dist/clis/tieba/utils.test.d.ts +1 -0
- package/dist/clis/tieba/utils.test.js +290 -0
- package/dist/clis/weread/book.js +100 -13
- package/dist/clis/weread/commands.test.js +221 -0
- package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
- package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
- package/dist/clis/weread/search-regression.test.d.ts +1 -0
- package/dist/clis/weread/search-regression.test.js +407 -0
- package/dist/clis/weread/search.js +143 -7
- package/dist/clis/weread/shelf.js +13 -95
- package/dist/clis/weread/utils.d.ts +46 -0
- package/dist/clis/weread/utils.js +214 -7
- package/dist/clis/weread/utils.test.js +71 -1
- package/dist/clis/xiaohongshu/publish.d.ts +1 -1
- package/dist/clis/xiaohongshu/publish.js +78 -31
- package/dist/clis/xiaohongshu/publish.test.js +66 -1
- package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.js +2 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
- package/dist/clis/xueqiu/comments.d.ts +118 -0
- package/dist/clis/xueqiu/comments.js +354 -0
- package/dist/clis/xueqiu/comments.test.d.ts +1 -0
- package/dist/clis/xueqiu/comments.test.js +696 -0
- package/dist/clis/youtube/transcript.js +2 -4
- package/dist/clis/youtube/utils.d.ts +9 -0
- package/dist/clis/youtube/utils.js +67 -3
- package/dist/clis/youtube/utils.test.d.ts +1 -0
- package/dist/clis/youtube/utils.test.js +37 -0
- package/dist/clis/youtube/video.js +16 -15
- package/dist/clis/zsxq/dynamics.d.ts +1 -0
- package/dist/clis/zsxq/dynamics.js +47 -0
- package/dist/clis/zsxq/groups.d.ts +1 -0
- package/dist/clis/zsxq/groups.js +32 -0
- package/dist/clis/zsxq/search.d.ts +1 -0
- package/dist/clis/zsxq/search.js +43 -0
- package/dist/clis/zsxq/search.test.d.ts +1 -0
- package/dist/clis/zsxq/search.test.js +24 -0
- package/dist/clis/zsxq/topic.d.ts +1 -0
- package/dist/clis/zsxq/topic.js +47 -0
- package/dist/clis/zsxq/topic.test.d.ts +1 -0
- package/dist/clis/zsxq/topic.test.js +29 -0
- package/dist/clis/zsxq/topics.d.ts +1 -0
- package/dist/clis/zsxq/topics.js +25 -0
- package/dist/clis/zsxq/topics.test.d.ts +1 -0
- package/dist/clis/zsxq/topics.test.js +24 -0
- package/dist/clis/zsxq/utils.d.ts +97 -0
- package/dist/clis/zsxq/utils.js +230 -0
- package/dist/commanderAdapter.js +1 -1
- package/dist/commanderAdapter.test.js +39 -0
- package/dist/external-clis.yaml +17 -0
- package/dist/types.d.ts +5 -0
- package/docs/.vitepress/config.mts +3 -0
- package/docs/adapters/browser/band.md +63 -0
- package/docs/adapters/browser/ones.md +59 -0
- package/docs/adapters/browser/spotify.md +62 -0
- package/docs/adapters/browser/tieba.md +45 -0
- package/docs/adapters/browser/xueqiu.md +5 -0
- package/docs/adapters/browser/zsxq.md +49 -0
- package/docs/adapters/index.md +5 -2
- package/docs/adapters-doc/ones.md +32 -0
- package/extension/src/background.ts +15 -0
- package/extension/src/cdp.ts +42 -0
- package/extension/src/protocol.ts +5 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +16 -0
- package/src/browser/daemon-client.ts +5 -1
- package/src/browser/page.ts +16 -0
- package/src/clis/band/bands.ts +76 -0
- package/src/clis/band/mentions.ts +134 -0
- package/src/clis/band/post.ts +187 -0
- package/src/clis/band/posts.ts +106 -0
- package/src/clis/doubao/detail.test.ts +53 -0
- package/src/clis/doubao/detail.ts +41 -0
- package/src/clis/doubao/history.test.ts +45 -0
- package/src/clis/doubao/history.ts +32 -0
- package/src/clis/doubao/meeting-summary.ts +53 -0
- package/src/clis/doubao/meeting-transcript.ts +48 -0
- package/src/clis/doubao/utils.test.ts +45 -0
- package/src/clis/doubao/utils.ts +371 -0
- package/src/clis/douyin/_shared/public-api.ts +84 -0
- package/src/clis/douyin/user-videos.test.ts +122 -0
- package/src/clis/douyin/user-videos.ts +101 -0
- package/src/clis/ones/common.ts +187 -0
- package/src/clis/ones/enrich-tasks.ts +47 -0
- package/src/clis/ones/login.ts +103 -0
- package/src/clis/ones/logout.ts +19 -0
- package/src/clis/ones/me.ts +34 -0
- package/src/clis/ones/my-tasks.ts +148 -0
- package/src/clis/ones/resolve-labels.ts +80 -0
- package/src/clis/ones/task-helpers.test.ts +14 -0
- package/src/clis/ones/task-helpers.ts +214 -0
- package/src/clis/ones/task.ts +79 -0
- package/src/clis/ones/tasks.ts +92 -0
- package/src/clis/ones/token-info.ts +46 -0
- package/src/clis/ones/worklog.test.ts +24 -0
- package/src/clis/ones/worklog.ts +306 -0
- package/src/clis/spotify/spotify.ts +328 -0
- package/src/clis/spotify/utils.test.ts +87 -0
- package/src/clis/spotify/utils.ts +92 -0
- package/src/clis/tieba/commands.test.ts +86 -0
- package/src/clis/tieba/hot.ts +52 -0
- package/src/clis/tieba/posts.ts +108 -0
- package/src/clis/tieba/read.ts +158 -0
- package/src/clis/tieba/search.ts +119 -0
- package/src/clis/tieba/utils.test.ts +322 -0
- package/src/clis/tieba/utils.ts +348 -0
- package/src/clis/weread/book.ts +116 -13
- package/src/clis/weread/commands.test.ts +249 -0
- package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
- package/src/clis/weread/search-regression.test.ts +440 -0
- package/src/clis/weread/search.ts +189 -9
- package/src/clis/weread/shelf.ts +20 -122
- package/src/clis/weread/utils.test.ts +81 -1
- package/src/clis/weread/utils.ts +264 -7
- package/src/clis/xiaohongshu/publish.test.ts +79 -1
- package/src/clis/xiaohongshu/publish.ts +84 -30
- package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
- package/src/clis/xiaohongshu/user-helpers.ts +4 -0
- package/src/clis/xueqiu/comments.test.ts +823 -0
- package/src/clis/xueqiu/comments.ts +461 -0
- package/src/clis/youtube/transcript.ts +2 -4
- package/src/clis/youtube/utils.test.ts +43 -0
- package/src/clis/youtube/utils.ts +69 -0
- package/src/clis/youtube/video.ts +16 -15
- package/src/clis/zsxq/dynamics.ts +60 -0
- package/src/clis/zsxq/groups.ts +41 -0
- package/src/clis/zsxq/search.test.ts +29 -0
- package/src/clis/zsxq/search.ts +54 -0
- package/src/clis/zsxq/topic.test.ts +34 -0
- package/src/clis/zsxq/topic.ts +68 -0
- package/src/clis/zsxq/topics.test.ts +29 -0
- package/src/clis/zsxq/topics.ts +36 -0
- package/src/clis/zsxq/utils.ts +351 -0
- package/src/commanderAdapter.test.ts +47 -0
- package/src/commanderAdapter.ts +1 -1
- package/src/external-clis.yaml +17 -0
- package/src/types.ts +5 -0
- package/tests/e2e/band-auth.test.ts +20 -0
- package/tests/e2e/browser-auth-helpers.ts +18 -0
- package/tests/e2e/browser-auth.test.ts +35 -47
- package/tests/e2e/browser-public.test.ts +288 -0
- package/tests/e2e/management.test.ts +1 -1
- package/tests/e2e/plugin-management.test.ts +1 -1
- package/vitest.config.ts +1 -0
- package/SKILL.md +0 -879
- package/dist/weread-private-api-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.js +0 -39
- package/src/weread-search-regression.test.ts +0 -44
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* band bands — List all Bands you belong to.
|
|
5
|
+
*
|
|
6
|
+
* Band.us renders the full band list in the left sidebar of the home page for
|
|
7
|
+
* logged-in users, so we can extract everything we need from the DOM without
|
|
8
|
+
* XHR interception or any secondary navigation.
|
|
9
|
+
*
|
|
10
|
+
* Each sidebar item is an <a href="/band/{band_no}/..."> link whose text and
|
|
11
|
+
* data attributes carry the band name and member count.
|
|
12
|
+
*/
|
|
13
|
+
cli({
|
|
14
|
+
site: 'band',
|
|
15
|
+
name: 'bands',
|
|
16
|
+
description: 'List all Bands you belong to',
|
|
17
|
+
domain: 'www.band.us',
|
|
18
|
+
strategy: Strategy.COOKIE,
|
|
19
|
+
browser: true,
|
|
20
|
+
args: [],
|
|
21
|
+
columns: ['band_no', 'name', 'members'],
|
|
22
|
+
func: async (page, _kwargs) => {
|
|
23
|
+
const cookies = await page.getCookies({ domain: 'band.us' });
|
|
24
|
+
const isLoggedIn = cookies.some(c => c.name === 'band_session');
|
|
25
|
+
if (!isLoggedIn)
|
|
26
|
+
throw new AuthRequiredError('band.us', 'Not logged in to Band');
|
|
27
|
+
// Extract the band list from the sidebar. Poll until at least one band card
|
|
28
|
+
// appears (React hydration may take a moment after navigation).
|
|
29
|
+
// Sidebar band cards use class "bandCover _link" with hrefs like /band/{id}/post.
|
|
30
|
+
const bands = await page.evaluate(`
|
|
31
|
+
(async () => {
|
|
32
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
33
|
+
|
|
34
|
+
// Wait up to 9 s for sidebar band cards to render.
|
|
35
|
+
for (let i = 0; i < 30; i++) {
|
|
36
|
+
if (document.querySelector('a.bandCover._link')) break;
|
|
37
|
+
await sleep(300);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const results = [];
|
|
43
|
+
|
|
44
|
+
for (const a of Array.from(document.querySelectorAll('a.bandCover._link'))) {
|
|
45
|
+
// Extract band_no from href: /band/{id} or /band/{id}/post only.
|
|
46
|
+
const m = (a.getAttribute('href') || '').match(/^\\/band\\/(\\d+)(?:\\/post)?\\/?$/);
|
|
47
|
+
if (!m) continue;
|
|
48
|
+
const bandNo = Number(m[1]);
|
|
49
|
+
if (seen.has(bandNo)) continue;
|
|
50
|
+
seen.add(bandNo);
|
|
51
|
+
|
|
52
|
+
// Band name lives in p.uriText inside div.bandName.
|
|
53
|
+
const nameEl = a.querySelector('p.uriText');
|
|
54
|
+
const name = nameEl ? norm(nameEl.textContent) : '';
|
|
55
|
+
if (!name) continue;
|
|
56
|
+
|
|
57
|
+
// Member count is the <em> inside span.member.
|
|
58
|
+
const memberEl = a.querySelector('span.member em');
|
|
59
|
+
const members = memberEl ? parseInt((memberEl.textContent || '').replace(/[^0-9]/g, ''), 10) || 0 : 0;
|
|
60
|
+
|
|
61
|
+
results.push({ band_no: bandNo, name, members });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return results;
|
|
65
|
+
})()
|
|
66
|
+
`);
|
|
67
|
+
if (!bands || bands.length === 0) {
|
|
68
|
+
throw new EmptyResultError('band bands', 'No bands found in sidebar — are you logged in?');
|
|
69
|
+
}
|
|
70
|
+
return bands;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { AuthRequiredError, EmptyResultError, SelectorError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* band mentions — Show Band notifications where you were @mentioned.
|
|
5
|
+
*
|
|
6
|
+
* Band.us signs every API request with a per-request HMAC (`md` header) generated
|
|
7
|
+
* by its own JavaScript, so we cannot replicate it externally. Instead we use
|
|
8
|
+
* Strategy.INTERCEPT: install an XHR interceptor, open the notification panel by
|
|
9
|
+
* clicking the bell to trigger the get_news XHR call, then apply client-side
|
|
10
|
+
* filtering to extract notifications matching the requested filter/unread options.
|
|
11
|
+
*/
|
|
12
|
+
cli({
|
|
13
|
+
site: 'band',
|
|
14
|
+
name: 'mentions',
|
|
15
|
+
description: 'Show Band notifications where you are @mentioned',
|
|
16
|
+
domain: 'www.band.us',
|
|
17
|
+
strategy: Strategy.INTERCEPT,
|
|
18
|
+
browser: true,
|
|
19
|
+
args: [
|
|
20
|
+
{
|
|
21
|
+
name: 'filter',
|
|
22
|
+
default: 'mentioned',
|
|
23
|
+
choices: ['mentioned', 'all', 'post', 'comment'],
|
|
24
|
+
help: 'Filter: mentioned (default) | all | post | comment',
|
|
25
|
+
},
|
|
26
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max results' },
|
|
27
|
+
{ name: 'unread', type: 'bool', default: false, help: 'Show only unread notifications' },
|
|
28
|
+
],
|
|
29
|
+
columns: ['time', 'band', 'type', 'from', 'text', 'url'],
|
|
30
|
+
func: async (page, kwargs) => {
|
|
31
|
+
const filter = kwargs.filter;
|
|
32
|
+
const limit = kwargs.limit;
|
|
33
|
+
const unreadOnly = kwargs.unread;
|
|
34
|
+
// Navigate with a timestamp param to force a fresh page load each run.
|
|
35
|
+
// Without this, same-URL navigation may skip the reload (preserving the JS context
|
|
36
|
+
// and leaving the notification panel open from a previous run).
|
|
37
|
+
await page.goto(`https://www.band.us/?_=${Date.now()}`);
|
|
38
|
+
const cookies = await page.getCookies({ domain: 'band.us' });
|
|
39
|
+
const isLoggedIn = cookies.some(c => c.name === 'band_session');
|
|
40
|
+
if (!isLoggedIn)
|
|
41
|
+
throw new AuthRequiredError('band.us', 'Not logged in to Band');
|
|
42
|
+
// Install XHR interceptor before any clicks so all get_news responses are captured.
|
|
43
|
+
await page.installInterceptor('get_news');
|
|
44
|
+
// Wait for the bell button to appear (React hydration) instead of a fixed sleep.
|
|
45
|
+
let bellReady = false;
|
|
46
|
+
for (let i = 0; i < 20; i++) {
|
|
47
|
+
const exists = await page.evaluate(`() => !!document.querySelector('button._btnWidgetIcon')`);
|
|
48
|
+
if (exists) {
|
|
49
|
+
bellReady = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
await page.wait(0.5);
|
|
53
|
+
}
|
|
54
|
+
if (!bellReady) {
|
|
55
|
+
throw new SelectorError('button._btnWidgetIcon', 'Notification bell not found. The Band.us UI may have changed.');
|
|
56
|
+
}
|
|
57
|
+
// Poll until a capture containing result_data.news arrives, up to maxSecs seconds.
|
|
58
|
+
// getInterceptedRequests() clears the array on each call, so captures are accumulated
|
|
59
|
+
// locally. The interceptor pattern 'get_news' also matches 'get_news_count' responses
|
|
60
|
+
// which don't have result_data.news — keep polling until the real news response arrives.
|
|
61
|
+
const waitForOneCapture = async (maxSecs = 8) => {
|
|
62
|
+
const captures = [];
|
|
63
|
+
for (let i = 0; i < maxSecs * 2; i++) {
|
|
64
|
+
await page.wait(0.5); // 0.5 seconds per iteration (page.wait takes seconds)
|
|
65
|
+
const reqs = await page.getInterceptedRequests();
|
|
66
|
+
if (reqs.length > 0) {
|
|
67
|
+
captures.push(...reqs);
|
|
68
|
+
if (captures.some((r) => Array.isArray(r?.result_data?.news)))
|
|
69
|
+
return captures;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return captures;
|
|
73
|
+
};
|
|
74
|
+
// Click the bell. Guard against the element disappearing between the readiness
|
|
75
|
+
// check and the click (e.g. due to a React re-render) to surface a clear error.
|
|
76
|
+
const bellClicked = await page.evaluate(`() => {
|
|
77
|
+
const el = document.querySelector('button._btnWidgetIcon');
|
|
78
|
+
if (!el) return false;
|
|
79
|
+
el.click();
|
|
80
|
+
return true;
|
|
81
|
+
}`);
|
|
82
|
+
if (!bellClicked) {
|
|
83
|
+
throw new SelectorError('button._btnWidgetIcon', 'Notification bell disappeared before click. The Band.us UI may have changed.');
|
|
84
|
+
}
|
|
85
|
+
const requests = await waitForOneCapture();
|
|
86
|
+
// Find the get_news response (has result_data.news); get_news_count responses do not.
|
|
87
|
+
const newsReq = requests.find((r) => Array.isArray(r?.result_data?.news));
|
|
88
|
+
if (!newsReq) {
|
|
89
|
+
throw new EmptyResultError('band mentions', 'Failed to capture get_news response from Band.us. Try running the command again.');
|
|
90
|
+
}
|
|
91
|
+
let items = newsReq.result_data.news ?? [];
|
|
92
|
+
if (items.length === 0) {
|
|
93
|
+
throw new EmptyResultError('band mentions', 'No notifications found');
|
|
94
|
+
}
|
|
95
|
+
// Apply filters client-side from the full notification list.
|
|
96
|
+
if (unreadOnly) {
|
|
97
|
+
items = items.filter((n) => n.is_new === true);
|
|
98
|
+
}
|
|
99
|
+
if (filter === 'mentioned') {
|
|
100
|
+
// 'filters' is Band's server-side tag array; 'referred' means you were @mentioned.
|
|
101
|
+
items = items.filter((n) => n.filters?.includes('referred'));
|
|
102
|
+
}
|
|
103
|
+
else if (filter === 'post') {
|
|
104
|
+
items = items.filter((n) => n.category === 'post');
|
|
105
|
+
}
|
|
106
|
+
else if (filter === 'comment') {
|
|
107
|
+
items = items.filter((n) => n.category === 'comment');
|
|
108
|
+
}
|
|
109
|
+
// Band markup tags (<band:mention uid="...">, <band:sticker>, etc.) appear in
|
|
110
|
+
// notification text; strip them to get plain readable content.
|
|
111
|
+
const stripBandTags = (s) => s.replace(/<\/?band:[^>]+>/g, '');
|
|
112
|
+
return items.slice(0, limit).map((n) => {
|
|
113
|
+
const ts = n.created_at ? new Date(n.created_at) : null;
|
|
114
|
+
return {
|
|
115
|
+
time: ts
|
|
116
|
+
? ts.toLocaleString('ja-JP', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
117
|
+
: '',
|
|
118
|
+
band: n.band?.name ?? '',
|
|
119
|
+
// 'filters' is Band's server-side tag array; 'referred' means you were @mentioned.
|
|
120
|
+
type: n.filters?.includes('referred') ? '@mention' : n.category ?? '',
|
|
121
|
+
from: n.actor?.name ?? '',
|
|
122
|
+
text: stripBandTags(n.subtext ?? '').slice(0, 100),
|
|
123
|
+
url: n.action?.pc ?? '',
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
2
|
+
import { formatCookieHeader } from '../../download/index.js';
|
|
3
|
+
import { downloadMedia } from '../../download/media-download.js';
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* band post — Export full content of a Band post: body, comments, and optional photo download.
|
|
7
|
+
*
|
|
8
|
+
* Navigates directly to the post URL and extracts everything from the DOM.
|
|
9
|
+
* No XHR interception needed — Band renders the full post for logged-in users.
|
|
10
|
+
*
|
|
11
|
+
* Output rows:
|
|
12
|
+
* type=post → the post itself (author, date, body text)
|
|
13
|
+
* type=comment → top-level comment
|
|
14
|
+
* type=reply → reply to a comment (nested under its parent)
|
|
15
|
+
*
|
|
16
|
+
* Photo thumbnail URLs carry a ?type=sNNN suffix; stripping it yields full-res.
|
|
17
|
+
*/
|
|
18
|
+
cli({
|
|
19
|
+
site: 'band',
|
|
20
|
+
name: 'post',
|
|
21
|
+
description: 'Export full content of a post including comments',
|
|
22
|
+
domain: 'www.band.us',
|
|
23
|
+
strategy: Strategy.COOKIE,
|
|
24
|
+
navigateBefore: false,
|
|
25
|
+
browser: true,
|
|
26
|
+
args: [
|
|
27
|
+
{ name: 'band_no', positional: true, required: true, type: 'int', help: 'Band number' },
|
|
28
|
+
{ name: 'post_no', positional: true, required: true, type: 'int', help: 'Post number' },
|
|
29
|
+
{ name: 'output', type: 'str', default: '', help: 'Directory to save attached photos' },
|
|
30
|
+
{ name: 'comments', type: 'bool', default: true, help: 'Include comments (default: true)' },
|
|
31
|
+
],
|
|
32
|
+
columns: ['type', 'author', 'date', 'text'],
|
|
33
|
+
func: async (page, kwargs) => {
|
|
34
|
+
const bandNo = Number(kwargs.band_no);
|
|
35
|
+
const postNo = Number(kwargs.post_no);
|
|
36
|
+
const outputDir = kwargs.output;
|
|
37
|
+
const withComments = kwargs.comments;
|
|
38
|
+
await page.goto(`https://www.band.us/band/${bandNo}/post/${postNo}`);
|
|
39
|
+
const cookies = await page.getCookies({ domain: 'band.us' });
|
|
40
|
+
const isLoggedIn = cookies.some(c => c.name === 'band_session');
|
|
41
|
+
if (!isLoggedIn)
|
|
42
|
+
throw new AuthRequiredError('band.us', 'Not logged in to Band');
|
|
43
|
+
const data = await page.evaluate(`
|
|
44
|
+
(async () => {
|
|
45
|
+
const withComments = ${withComments};
|
|
46
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
47
|
+
const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
|
|
48
|
+
// Band embeds <band:mention>, <band:sticker>, etc. in content — strip to plain text.
|
|
49
|
+
const stripTags = s => s.replace(/<\\/?band:[^>]+>/g, '');
|
|
50
|
+
|
|
51
|
+
// Wait up to 9 s for the post content to render (poll for the author link,
|
|
52
|
+
// which appears after React hydration fills the post header).
|
|
53
|
+
for (let i = 0; i < 30; i++) {
|
|
54
|
+
if (document.querySelector('._postWrapper a.text')) break;
|
|
55
|
+
await sleep(300);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const postCard = document.querySelector('._postWrapper');
|
|
59
|
+
const commentSection = postCard?.querySelector('.dPostCommentMainView');
|
|
60
|
+
|
|
61
|
+
// Author and date live in the post header, above the comment section.
|
|
62
|
+
// Exclude any matches inside the comment section to avoid picking up comment authors.
|
|
63
|
+
let author = '', date = '';
|
|
64
|
+
for (const el of (postCard?.querySelectorAll('a.text') || [])) {
|
|
65
|
+
if (!commentSection?.contains(el)) { author = norm(el.textContent); break; }
|
|
66
|
+
}
|
|
67
|
+
for (const el of (postCard?.querySelectorAll('time.time') || [])) {
|
|
68
|
+
if (!commentSection?.contains(el)) { date = norm(el.textContent); break; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bodyEl = postCard?.querySelector('.postText._postText');
|
|
72
|
+
const text = bodyEl ? stripTags(norm(bodyEl.innerText || bodyEl.textContent)) : '';
|
|
73
|
+
|
|
74
|
+
// Photo thumbnails have a ?type=sNNN query param; strip it for full-res URL.
|
|
75
|
+
// Use location.href as base so protocol-relative or relative URLs resolve correctly.
|
|
76
|
+
const photos = Array.from(postCard?.querySelectorAll('img._imgRecentPhoto, img._imgPhoto') || [])
|
|
77
|
+
.map(img => {
|
|
78
|
+
const src = img.getAttribute('src') || '';
|
|
79
|
+
if (!src) return '';
|
|
80
|
+
try { const u = new URL(src, location.href); return u.origin + u.pathname; }
|
|
81
|
+
catch { return ''; }
|
|
82
|
+
})
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
|
|
85
|
+
if (!withComments) return { author, date, text, photos, comments: [] };
|
|
86
|
+
|
|
87
|
+
// Wait up to 6 s for the comment list container to render.
|
|
88
|
+
// Wait for the container itself (not .cComment) so posts with zero comments
|
|
89
|
+
// don't incur a fixed 6s delay waiting for an element that never appears.
|
|
90
|
+
for (let i = 0; i < 20; i++) {
|
|
91
|
+
if (postCard?.querySelector('.sCommentList._heightDetectAreaForComment')) break;
|
|
92
|
+
await sleep(300);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Recursively collect comments and their replies.
|
|
96
|
+
// Replies live in .sReplyList > .sCommentList, not in ._replyRegion.
|
|
97
|
+
function extractComments(container, depth) {
|
|
98
|
+
const results = [];
|
|
99
|
+
for (const el of container.querySelectorAll(':scope > .cComment')) {
|
|
100
|
+
results.push({
|
|
101
|
+
depth,
|
|
102
|
+
author: norm(el.querySelector('strong.name')?.textContent),
|
|
103
|
+
date: norm(el.querySelector('time.time')?.textContent),
|
|
104
|
+
text: stripTags(norm(el.querySelector('p.txt._commentContent')?.innerText || '')),
|
|
105
|
+
});
|
|
106
|
+
const replyList = el.querySelector('.sReplyList .sCommentList._heightDetectAreaForComment');
|
|
107
|
+
if (replyList) results.push(...extractComments(replyList, depth + 1));
|
|
108
|
+
}
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const commentList = postCard?.querySelector('.sCommentList._heightDetectAreaForComment');
|
|
113
|
+
const comments = commentList ? extractComments(commentList, 0) : [];
|
|
114
|
+
|
|
115
|
+
return { author, date, text, photos, comments };
|
|
116
|
+
})()
|
|
117
|
+
`);
|
|
118
|
+
if (!data?.text && !data?.comments?.length && !data?.photos?.length) {
|
|
119
|
+
throw new EmptyResultError('band post', 'Post not found or not accessible');
|
|
120
|
+
}
|
|
121
|
+
const photos = data.photos ?? [];
|
|
122
|
+
// Download photos when --output is specified, using the shared downloadMedia utility
|
|
123
|
+
// which handles redirects, timeouts, and stream errors correctly.
|
|
124
|
+
// Pass browser cookies so Band's login-protected photo URLs don't fail with 401/403.
|
|
125
|
+
if (outputDir && photos.length > 0) {
|
|
126
|
+
// Only send Band cookies to Band-hosted URLs; avoid leaking auth cookies to third-party CDNs.
|
|
127
|
+
// Use a global index across both batches so filenames don't collide (photo_1, photo_2, ...).
|
|
128
|
+
const cookieHeader = formatCookieHeader(await page.getCookies({ url: 'https://www.band.us' }));
|
|
129
|
+
const isBandUrl = (u) => { try {
|
|
130
|
+
const h = new URL(u).hostname;
|
|
131
|
+
return h === 'band.us' || h.endsWith('.band.us');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
} };
|
|
136
|
+
// Derive extension from URL path so downloaded files have correct extensions (e.g. photo_1.jpg).
|
|
137
|
+
const urlExt = (u) => { try {
|
|
138
|
+
return new URL(u).pathname.match(/\.(\w+)$/)?.[1] ?? 'jpg';
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return 'jpg';
|
|
142
|
+
} };
|
|
143
|
+
let globalIndex = 1;
|
|
144
|
+
const bandPhotos = photos.filter(isBandUrl);
|
|
145
|
+
const otherPhotos = photos.filter(u => !isBandUrl(u));
|
|
146
|
+
if (bandPhotos.length > 0) {
|
|
147
|
+
await downloadMedia(bandPhotos.map(url => ({ type: 'image', url, filename: `photo_${globalIndex++}.${urlExt(url)}` })), { output: outputDir, verbose: false, cookies: cookieHeader });
|
|
148
|
+
}
|
|
149
|
+
if (otherPhotos.length > 0) {
|
|
150
|
+
await downloadMedia(otherPhotos.map(url => ({ type: 'image', url, filename: `photo_${globalIndex++}.${urlExt(url)}` })), { output: outputDir, verbose: false });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const rows = [];
|
|
154
|
+
// Post row — append photo URLs inline when not downloading to disk.
|
|
155
|
+
rows.push({
|
|
156
|
+
type: 'post',
|
|
157
|
+
author: data.author ?? '',
|
|
158
|
+
date: data.date ?? '',
|
|
159
|
+
text: [
|
|
160
|
+
data.text ?? '',
|
|
161
|
+
...(outputDir ? [] : photos.map((u, i) => `[photo${i + 1}] ${u}`)),
|
|
162
|
+
].filter(Boolean).join('\n'),
|
|
163
|
+
});
|
|
164
|
+
// Comment rows — depth=0 → type 'comment', depth≥1 → type 'reply'.
|
|
165
|
+
for (const c of data.comments ?? []) {
|
|
166
|
+
rows.push({
|
|
167
|
+
type: c.depth === 0 ? 'comment' : 'reply',
|
|
168
|
+
author: c.author ?? '',
|
|
169
|
+
date: c.date ?? '',
|
|
170
|
+
text: c.depth > 0 ? ' '.repeat(c.depth) + '└ ' + (c.text ?? '') : (c.text ?? ''),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return rows;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* band posts — List posts from a specific Band.
|
|
5
|
+
*
|
|
6
|
+
* Band.us renders the post list in the DOM for logged-in users, so we navigate
|
|
7
|
+
* directly to the band's post page and extract everything from the DOM — no XHR
|
|
8
|
+
* interception or home-page detour required.
|
|
9
|
+
*/
|
|
10
|
+
cli({
|
|
11
|
+
site: 'band',
|
|
12
|
+
name: 'posts',
|
|
13
|
+
description: 'List posts from a Band',
|
|
14
|
+
domain: 'www.band.us',
|
|
15
|
+
strategy: Strategy.COOKIE,
|
|
16
|
+
navigateBefore: false,
|
|
17
|
+
browser: true,
|
|
18
|
+
args: [
|
|
19
|
+
{
|
|
20
|
+
name: 'band_no',
|
|
21
|
+
positional: true,
|
|
22
|
+
required: true,
|
|
23
|
+
type: 'int',
|
|
24
|
+
help: 'Band number (get it from: band bands)',
|
|
25
|
+
},
|
|
26
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max results' },
|
|
27
|
+
],
|
|
28
|
+
columns: ['date', 'author', 'content', 'comments', 'url'],
|
|
29
|
+
func: async (page, kwargs) => {
|
|
30
|
+
const bandNo = Number(kwargs.band_no);
|
|
31
|
+
const limit = Number(kwargs.limit);
|
|
32
|
+
// Navigate directly to the band's post page — no home-page detour needed.
|
|
33
|
+
await page.goto(`https://www.band.us/band/${bandNo}/post`);
|
|
34
|
+
const cookies = await page.getCookies({ domain: 'band.us' });
|
|
35
|
+
const isLoggedIn = cookies.some(c => c.name === 'band_session');
|
|
36
|
+
if (!isLoggedIn)
|
|
37
|
+
throw new AuthRequiredError('band.us', 'Not logged in to Band');
|
|
38
|
+
// Extract post list from the DOM. Poll until post items appear (React hydration).
|
|
39
|
+
const posts = await page.evaluate(`
|
|
40
|
+
(async () => {
|
|
41
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
42
|
+
const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
|
|
43
|
+
const limit = ${limit};
|
|
44
|
+
|
|
45
|
+
// Wait up to 9 s for post items to render.
|
|
46
|
+
for (let i = 0; i < 30; i++) {
|
|
47
|
+
if (document.querySelector('article.cContentsCard._postMainWrap')) break;
|
|
48
|
+
await sleep(300);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Band embeds custom <band:mention>, <band:sticker>, etc. tags in content.
|
|
52
|
+
const stripTags = s => s.replace(/<\\/?band:[^>]+>/g, '');
|
|
53
|
+
|
|
54
|
+
const results = [];
|
|
55
|
+
const postEls = Array.from(
|
|
56
|
+
document.querySelectorAll('article.cContentsCard._postMainWrap')
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
for (const el of postEls) {
|
|
60
|
+
// URL: first post permalink link (absolute or relative).
|
|
61
|
+
const linkEl = el.querySelector('a[href*="/post/"]');
|
|
62
|
+
const href = linkEl?.getAttribute('href') || '';
|
|
63
|
+
if (!href) continue;
|
|
64
|
+
const url = href.startsWith('http') ? href : 'https://www.band.us' + href;
|
|
65
|
+
|
|
66
|
+
// Author name — a.text in the post header area.
|
|
67
|
+
const author = norm(el.querySelector('a.text')?.textContent);
|
|
68
|
+
|
|
69
|
+
// Date / timestamp.
|
|
70
|
+
const date = norm(el.querySelector('time')?.textContent);
|
|
71
|
+
|
|
72
|
+
// Post body text (strip Band markup tags, truncate for listing).
|
|
73
|
+
const bodyEl = el.querySelector('.postText._postText');
|
|
74
|
+
const content = bodyEl
|
|
75
|
+
? stripTags(norm(bodyEl.innerText || bodyEl.textContent)).slice(0, 120)
|
|
76
|
+
: '';
|
|
77
|
+
|
|
78
|
+
// Comment count is in span.count inside the count area.
|
|
79
|
+
const commentEl = el.querySelector('span.count');
|
|
80
|
+
const comments = commentEl ? parseInt((commentEl.textContent || '').replace(/[^0-9]/g, ''), 10) || 0 : 0;
|
|
81
|
+
|
|
82
|
+
if (results.length >= limit) break;
|
|
83
|
+
results.push({ date, author, content, comments, url });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
})()
|
|
88
|
+
`);
|
|
89
|
+
if (!posts || posts.length === 0) {
|
|
90
|
+
throw new EmptyResultError('band posts', 'No posts found in this Band');
|
|
91
|
+
}
|
|
92
|
+
return posts;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const detailCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { DOUBAO_DOMAIN, getConversationDetail, parseDoubaoConversationId } from './utils.js';
|
|
3
|
+
export const detailCommand = cli({
|
|
4
|
+
site: 'doubao',
|
|
5
|
+
name: 'detail',
|
|
6
|
+
description: 'Read a specific Doubao conversation by ID',
|
|
7
|
+
domain: DOUBAO_DOMAIN,
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
navigateBefore: false,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['Role', 'Text'],
|
|
15
|
+
func: async (page, kwargs) => {
|
|
16
|
+
const conversationId = parseDoubaoConversationId(kwargs.id);
|
|
17
|
+
const { messages, meeting } = await getConversationDetail(page, conversationId);
|
|
18
|
+
if (messages.length === 0 && !meeting) {
|
|
19
|
+
return [{ Role: 'System', Text: 'No messages found. Verify the conversation ID.' }];
|
|
20
|
+
}
|
|
21
|
+
const result = [];
|
|
22
|
+
if (meeting) {
|
|
23
|
+
result.push({
|
|
24
|
+
Role: 'Meeting',
|
|
25
|
+
Text: `${meeting.title}${meeting.time ? ` (${meeting.time})` : ''}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const m of messages) {
|
|
29
|
+
result.push({ Role: m.Role, Text: m.Text });
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './detail.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockGetConversationDetail } = vi.hoisted(() => ({
|
|
3
|
+
mockGetConversationDetail: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('./utils.js', async () => {
|
|
6
|
+
const actual = await vi.importActual('./utils.js');
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
getConversationDetail: mockGetConversationDetail,
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
import { getRegistry } from '../../registry.js';
|
|
13
|
+
import './detail.js';
|
|
14
|
+
describe('doubao detail', () => {
|
|
15
|
+
const detail = getRegistry().get('doubao/detail');
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockGetConversationDetail.mockReset();
|
|
18
|
+
});
|
|
19
|
+
it('returns meeting metadata even when the conversation has no chat messages', async () => {
|
|
20
|
+
mockGetConversationDetail.mockResolvedValue({
|
|
21
|
+
messages: [],
|
|
22
|
+
meeting: {
|
|
23
|
+
title: 'Weekly Sync',
|
|
24
|
+
time: '2026-03-28 10:00',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const result = await detail.func({}, { id: '1234567890' });
|
|
28
|
+
expect(result).toEqual([
|
|
29
|
+
{ Role: 'Meeting', Text: 'Weekly Sync (2026-03-28 10:00)' },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
it('still returns an error row for a truly empty conversation', async () => {
|
|
33
|
+
mockGetConversationDetail.mockResolvedValue({
|
|
34
|
+
messages: [],
|
|
35
|
+
meeting: null,
|
|
36
|
+
});
|
|
37
|
+
const result = await detail.func({}, { id: '1234567890' });
|
|
38
|
+
expect(result).toEqual([
|
|
39
|
+
{ Role: 'System', Text: 'No messages found. Verify the conversation ID.' },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const historyCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { DOUBAO_DOMAIN, getDoubaoConversationList } from './utils.js';
|
|
3
|
+
export const historyCommand = cli({
|
|
4
|
+
site: 'doubao',
|
|
5
|
+
name: 'history',
|
|
6
|
+
description: 'List conversation history from Doubao sidebar',
|
|
7
|
+
domain: DOUBAO_DOMAIN,
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
navigateBefore: false,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'limit', required: false, help: 'Max number of conversations to show', default: '50' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['Index', 'Id', 'Title', 'Url'],
|
|
15
|
+
func: async (page, kwargs) => {
|
|
16
|
+
const limit = parseInt(kwargs.limit, 10) || 50;
|
|
17
|
+
const conversations = await getDoubaoConversationList(page);
|
|
18
|
+
if (conversations.length === 0) {
|
|
19
|
+
return [{ Index: 0, Id: '', Title: 'No conversation history found. Make sure you are logged in.', Url: '' }];
|
|
20
|
+
}
|
|
21
|
+
return conversations.slice(0, limit).map((conv, i) => ({
|
|
22
|
+
Index: i + 1,
|
|
23
|
+
Id: conv.Id,
|
|
24
|
+
Title: conv.Title,
|
|
25
|
+
Url: conv.Url,
|
|
26
|
+
}));
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './history.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockGetDoubaoConversationList } = vi.hoisted(() => ({
|
|
3
|
+
mockGetDoubaoConversationList: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('./utils.js', async () => {
|
|
6
|
+
const actual = await vi.importActual('./utils.js');
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
getDoubaoConversationList: mockGetDoubaoConversationList,
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
import { getRegistry } from '../../registry.js';
|
|
13
|
+
import './history.js';
|
|
14
|
+
describe('doubao history', () => {
|
|
15
|
+
const history = getRegistry().get('doubao/history');
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockGetDoubaoConversationList.mockReset();
|
|
18
|
+
});
|
|
19
|
+
it('includes the conversation id in the tabular output', async () => {
|
|
20
|
+
mockGetDoubaoConversationList.mockResolvedValue([
|
|
21
|
+
{
|
|
22
|
+
Id: '1234567890123',
|
|
23
|
+
Title: 'Weekly Sync',
|
|
24
|
+
Url: 'https://www.doubao.com/chat/1234567890123',
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
const result = await history.func({}, {});
|
|
28
|
+
expect(result).toEqual([
|
|
29
|
+
{
|
|
30
|
+
Index: 1,
|
|
31
|
+
Id: '1234567890123',
|
|
32
|
+
Title: 'Weekly Sync',
|
|
33
|
+
Url: 'https://www.doubao.com/chat/1234567890123',
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const meetingSummaryCommand: import("../../registry.js").CliCommand;
|