mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
80eef46b4e
* refactor: move adapters from src/clis/ to root clis/ for monorepo separation Separates CLI adapters from the core runtime to prepare for independent adapter distribution via postinstall fetch. Key changes: - Move src/clis/ → clis/ (adapters at repo root) - Change tsconfig rootDir from "src" to "." so tsc compiles both - Create root-level shim files (registry.ts, errors.ts, etc.) so adapter relative imports (../../registry.js) resolve correctly - Update build-manifest.ts, main.ts paths for new dist/src/ structure - Expand ensureUserCliCompatShims() to cover all adapter import targets (types, utils, logger, launcher, browser/*, download/*, pipeline/*) - Add scripts/fetch-adapters.js postinstall for ~/.opencli/clis/ sync - Update vitest.config.ts adapter test paths - Add package.json files field to exclude adapters from npm package Official adapter files are unconditionally overwritten on update; user-created files not in the manifest are preserved. * fix: add dist/clis/ and cli-manifest.json to npm files, harden fetch-adapters - Add dist/clis/ and dist/cli-manifest.json to package.json files field so built-in adapters and manifest ship with the npm package - Replace execSync with execFileSync to prevent command injection - Add version check to skip redundant adapter fetches - Track tmpRoot explicitly for reliable cleanup * fix: address review blockers — manifest-based updates, global-only fetch, first-run fallback 1. Manifest-based update strategy: - Read old manifest to identify previously-official files - Clean up files removed upstream (in old manifest but not new) - User-created files (never in any manifest) remain untouched 2. Only run fetch-adapters on global install (npm_config_global=true) or explicit OPENCLI_FETCH=1, preventing heavy side effects for local/dev installs 3. First-run fallback in discovery.ts: - ensureUserAdapters() checks for adapter-manifest.json - If missing and ~/.opencli/clis/ is empty, spawns fetch-adapters.js - Guarantees adapters are available even with --ignore-scripts * fix: remove OPENCLI_FETCH env var, use internal _OPENCLI_FIRST_RUN instead * feat: also support OPENCLI_FETCH=1 for explicit adapter fetch trigger * simplify: replace git clone with local copy from dist/clis/ Adapters already ship in the npm package (dist/clis/), so there's no need to clone from GitHub. Copy directly from the installed package: - Eliminates git, curl, tar dependencies - No network calls in postinstall - No timeout/offline issues - Version always matches the installed CLI - ~65 lines of clone/download code replaced by one cpSync loop
323 lines
9.0 KiB
TypeScript
323 lines
9.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
MAX_TIEBA_LIMIT,
|
|
buildTiebaPostCardsFromPagePc,
|
|
buildTiebaPostItems,
|
|
buildTiebaSearchItems,
|
|
buildTiebaReadItems,
|
|
normalizeTiebaLimit,
|
|
signTiebaPcParams,
|
|
} from './utils.js';
|
|
|
|
describe('normalizeTiebaLimit', () => {
|
|
it('caps list commands at the declared tieba maximum', () => {
|
|
expect(MAX_TIEBA_LIMIT).toBe(20);
|
|
expect(normalizeTiebaLimit(undefined)).toBe(20);
|
|
expect(normalizeTiebaLimit(25)).toBe(20);
|
|
expect(normalizeTiebaLimit(7)).toBe(7);
|
|
});
|
|
});
|
|
|
|
describe('signTiebaPcParams', () => {
|
|
it('matches Tieba PC forum-list signing for stable page_pc requests', () => {
|
|
expect(signTiebaPcParams({
|
|
kw: encodeURIComponent('李毅'),
|
|
pn: '1',
|
|
sort_type: '-1',
|
|
is_newfrs: '1',
|
|
is_newfeed: '1',
|
|
rn: '30',
|
|
rn_need: '20',
|
|
tbs: '',
|
|
subapp_type: 'pc',
|
|
_client_type: '20',
|
|
})).toBe('466f2e091dd4ed17c6661a842b5ec342');
|
|
});
|
|
});
|
|
|
|
describe('buildTiebaPostCardsFromPagePc', () => {
|
|
it('extracts thread cards from signed page_pc feed payloads', () => {
|
|
const cards = buildTiebaPostCardsFromPagePc([
|
|
{
|
|
layout: 'feed',
|
|
feed: {
|
|
schema: 'tiebaapp://router/portal?params=%7B%22pageParams%22%3A%7B%22tid%22%3A10596901456%7D%7D',
|
|
log_param: [
|
|
{ key: 'tid', value: '10596901456' },
|
|
],
|
|
business_info_map: {
|
|
thread_id: '10596901456',
|
|
title: '崇拜希特勒的人都是日本的汉奸走狗',
|
|
},
|
|
components: [
|
|
{
|
|
component: 'feed_head',
|
|
feed_head: {
|
|
extra_data: [
|
|
{
|
|
business_info_map: { time_prefix: '回复于' },
|
|
text: { text: '1774343231' },
|
|
},
|
|
],
|
|
main_data: [
|
|
{
|
|
text: { text: '上帝的子民º♬' },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
component: 'feed_title',
|
|
feed_title: {
|
|
data: [{ text_info: { text: '崇拜希特勒的人都是日本的汉奸走狗' } }],
|
|
},
|
|
},
|
|
{
|
|
component: 'feed_social',
|
|
feed_social: {
|
|
comment_num: 12,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(cards).toEqual([
|
|
{
|
|
title: '崇拜希特勒的人都是日本的汉奸走狗',
|
|
author: '上帝的子民º♬',
|
|
descInfo: '回复于2026-03-24 17:07',
|
|
commentCount: 12,
|
|
actionTexts: [],
|
|
threadId: '10596901456',
|
|
url: 'https://tieba.baidu.com/p/10596901456',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('buildTiebaPostItems', () => {
|
|
it('builds stable thread ids and urls from card props without page hops', () => {
|
|
const items = buildTiebaPostItems([
|
|
{
|
|
title: '我来说个事',
|
|
author: '暴躁的小伙子',
|
|
descInfo: '回复于2分钟前',
|
|
actionTexts: ['分享', '评论 5', '点赞 2'],
|
|
threadId: '10590564788',
|
|
},
|
|
], 5);
|
|
|
|
expect(items).toEqual([
|
|
{
|
|
rank: 1,
|
|
title: '我来说个事',
|
|
author: '暴躁的小伙子',
|
|
replies: 5,
|
|
last_reply: '2分钟前',
|
|
id: '10590564788',
|
|
url: 'https://tieba.baidu.com/p/10590564788',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('honors the public 20-item limit contract', () => {
|
|
const raw = Array.from({ length: 25 }, (_, index) => ({
|
|
title: `帖子 ${index + 1}`,
|
|
author: `作者 ${index + 1}`,
|
|
descInfo: '回复于刚刚',
|
|
actionTexts: ['分享', `评论 ${index + 1}`],
|
|
threadId: String(1000 + index),
|
|
}));
|
|
|
|
const items = buildTiebaPostItems(raw, 25);
|
|
expect(items).toHaveLength(20);
|
|
expect(items[19]).toMatchObject({
|
|
rank: 20,
|
|
id: '1019',
|
|
url: 'https://tieba.baidu.com/p/1019',
|
|
});
|
|
});
|
|
|
|
it('parses Chinese count units and keeps date-time last-reply text intact', () => {
|
|
const items = buildTiebaPostItems([
|
|
{
|
|
title: '复杂格式帖子',
|
|
author: '作者',
|
|
descInfo: '回复于03-29 11:35',
|
|
actionTexts: ['分享', '评论 1.2万'],
|
|
url: 'https://tieba.baidu.com/p/123456',
|
|
},
|
|
], 5);
|
|
|
|
expect(items[0]).toMatchObject({
|
|
replies: 12000,
|
|
last_reply: '03-29 11:35',
|
|
id: '123456',
|
|
url: 'https://tieba.baidu.com/p/123456',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('buildTiebaSearchItems', () => {
|
|
it('keeps up to 20 search results when the page provides more than 10 cards', () => {
|
|
const raw = Array.from({ length: 25 }, (_, index) => ({
|
|
title: `结果 ${index + 1}`,
|
|
forum: '编程吧',
|
|
author: `作者 ${index + 1}`,
|
|
time: '2026-03-29',
|
|
snippet: `摘要 ${index + 1}`,
|
|
id: String(2000 + index),
|
|
url: `https://tieba.baidu.com/p/${2000 + index}`,
|
|
}));
|
|
|
|
const items = buildTiebaSearchItems(raw, 25);
|
|
expect(items).toHaveLength(20);
|
|
expect(items[19]).toMatchObject({
|
|
rank: 20,
|
|
id: '2019',
|
|
url: 'https://tieba.baidu.com/p/2019',
|
|
});
|
|
});
|
|
|
|
it('fills missing search ids from stable thread urls', () => {
|
|
const items = buildTiebaSearchItems([
|
|
{
|
|
title: '搜索结果',
|
|
forum: '编程吧',
|
|
author: '作者',
|
|
time: '2026-03-29 11:35',
|
|
snippet: '摘要',
|
|
id: '',
|
|
url: 'https://tieba.baidu.com/p/654321',
|
|
},
|
|
], 5);
|
|
|
|
expect(items[0]).toMatchObject({
|
|
id: '654321',
|
|
url: 'https://tieba.baidu.com/p/654321',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('buildTiebaReadItems', () => {
|
|
it('prefers visible main-post fields and still keeps floor 1 for media-only threads', () => {
|
|
const items = buildTiebaReadItems({
|
|
mainPost: {
|
|
title: '刚开始读博士的人据说都这样',
|
|
author: '湖水之岸',
|
|
contentText: '',
|
|
structuredText: '',
|
|
visibleTime: '03-24',
|
|
structuredTime: 1774343231,
|
|
hasMedia: true,
|
|
},
|
|
replies: [],
|
|
}, { limit: 5, includeMainPost: true });
|
|
|
|
expect(items).toEqual([
|
|
{
|
|
floor: 1,
|
|
author: '湖水之岸',
|
|
content: '刚开始读博士的人据说都这样 [media]',
|
|
time: '03-24',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('falls back to structured main-post data when visible text is missing', () => {
|
|
const items = buildTiebaReadItems({
|
|
mainPost: {
|
|
title: '标题',
|
|
author: '',
|
|
fallbackAuthor: '结构化作者',
|
|
contentText: '',
|
|
structuredText: '结构化正文',
|
|
visibleTime: '',
|
|
structuredTime: 1774343231,
|
|
hasMedia: false,
|
|
},
|
|
replies: [
|
|
{ floor: 2, author: '回复者', content: '二楼内容', time: '第2楼 2026-03-25 12:34 广东' },
|
|
],
|
|
}, { limit: 5, includeMainPost: true });
|
|
|
|
expect(items[0]).toMatchObject({
|
|
floor: 1,
|
|
author: '结构化作者',
|
|
content: '标题 结构化正文',
|
|
time: '2026-03-24 17:07',
|
|
});
|
|
expect(items[1]).toMatchObject({
|
|
floor: 2,
|
|
author: '回复者',
|
|
content: '二楼内容',
|
|
time: '2026-03-25 12:34',
|
|
});
|
|
});
|
|
|
|
it('strips trailing location metadata from reply times', () => {
|
|
const items = buildTiebaReadItems({
|
|
mainPost: {
|
|
title: '主楼',
|
|
author: '楼主',
|
|
contentText: '正文',
|
|
visibleTime: '03-24',
|
|
},
|
|
replies: [
|
|
{ floor: 2, author: '二楼', content: '二楼内容', time: '第2楼 3小时前 福建' },
|
|
{ floor: 3, author: '三楼', content: '三楼内容', time: '第3楼 刚刚 江苏' },
|
|
],
|
|
}, { limit: 5, includeMainPost: false });
|
|
|
|
expect(items).toEqual([
|
|
{
|
|
floor: 2,
|
|
author: '二楼',
|
|
content: '二楼内容',
|
|
time: '3小时前',
|
|
},
|
|
{
|
|
floor: 3,
|
|
author: '三楼',
|
|
content: '三楼内容',
|
|
time: '刚刚',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('counts limit as replies and skips main post on later pages', () => {
|
|
const items = buildTiebaReadItems({
|
|
mainPost: {
|
|
title: '主楼',
|
|
author: '楼主',
|
|
contentText: '正文',
|
|
visibleTime: '03-24',
|
|
},
|
|
replies: [
|
|
{ floor: 2, author: '二楼', content: '二楼内容', time: '第2楼 03-25' },
|
|
{ floor: 3, author: '三楼', content: '三楼内容', time: '第3楼 03-26' },
|
|
{ floor: 4, author: '四楼', content: '四楼内容', time: '第4楼 03-27' },
|
|
],
|
|
}, { limit: 2, includeMainPost: true });
|
|
|
|
expect(items).toHaveLength(3);
|
|
expect(items.map((item) => item.floor)).toEqual([1, 2, 3]);
|
|
|
|
const page2 = buildTiebaReadItems({
|
|
mainPost: {
|
|
title: '主楼',
|
|
author: '楼主',
|
|
contentText: '正文',
|
|
visibleTime: '03-24',
|
|
},
|
|
replies: [
|
|
{ floor: 26, author: '二十六楼', content: '二十六楼内容', time: '第26楼 03-29' },
|
|
],
|
|
}, { limit: 2, includeMainPost: false });
|
|
|
|
expect(page2.map((item) => item.floor)).toEqual([26]);
|
|
});
|
|
});
|