重构为Monorepo:拆分xhs/xhh应用与core包并完成双服务部署改造

This commit is contained in:
2026-03-03 16:06:16 +08:00
parent ed7fbdd5c2
commit 2cbd6b28b2
84 changed files with 6332 additions and 7678 deletions
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import {
decodeKeysetCursor,
encodeKeysetCursor,
paginateByKeyset,
} from '../src/platforms/xiaoheihe/cursor.js';
describe('xhh keyset cursor', () => {
it('encodes and decodes cursor payload', () => {
const encoded = encodeKeysetCursor({ key: 'abc-123' });
const decoded = decodeKeysetCursor(encoded);
expect(decoded).toEqual({ key: 'abc-123' });
});
it('throws on invalid cursor payload', () => {
expect(() => decodeKeysetCursor('not-base64')).toThrow();
});
it('paginates deterministically without duplicates', () => {
const items = [
{ id: 'a' },
{ id: 'b' },
{ id: 'c' },
{ id: 'd' },
{ id: 'e' },
];
const page1 = paginateByKeyset(items, 2, undefined, (item) => item.id);
expect(page1.items.map((i) => i.id)).toEqual(['a', 'b']);
expect(page1.nextCursor).toBeTruthy();
const page2 = paginateByKeyset(items, 2, decodeKeysetCursor(page1.nextCursor), (item) => item.id);
expect(page2.items.map((i) => i.id)).toEqual(['c', 'd']);
expect(page2.nextCursor).toBeTruthy();
const page3 = paginateByKeyset(items, 2, decodeKeysetCursor(page2.nextCursor), (item) => item.id);
expect(page3.items.map((i) => i.id)).toEqual(['e']);
expect(page3.hasMore).toBe(false);
const combined = [...page1.items, ...page2.items, ...page3.items].map((i) => i.id);
expect(combined).toEqual(['a', 'b', 'c', 'd', 'e']);
});
});
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import {
detectCaptchaText,
extractLinkIdFromUrl,
extractUserIdFromUrl,
firstNonEmpty,
parseCountString,
} from '../src/platforms/xiaoheihe/extractors.js';
describe('xhh extractors', () => {
it('parses count strings', () => {
expect(parseCountString('123')).toBe(123);
expect(parseCountString('1.2万')).toBe(12000);
expect(parseCountString('')).toBe(0);
});
it('detects captcha text', () => {
expect(detectCaptchaText('show_captcha')).toBe(true);
expect(detectCaptchaText('请完成验证码')).toBe(true);
expect(detectCaptchaText('normal page')).toBe(false);
});
it('extracts link_id and user_id from url', () => {
expect(extractLinkIdFromUrl('https://www.xiaoheihe.cn/app/bbs/link/123456')).toBe('123456');
expect(extractLinkIdFromUrl('/app/bbs/link/998877')).toBe('998877');
expect(extractUserIdFromUrl('https://www.xiaoheihe.cn/app/user/profile/112233')).toBe('112233');
expect(extractUserIdFromUrl('/app/user/profile/778899')).toBe('778899');
});
it('returns first non-empty value', () => {
expect(firstNonEmpty('', ' ', 'x', 'y')).toBe('x');
expect(firstNonEmpty('', ' ')).toBe('');
});
});
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest';
import { z } from 'zod';
import {
GetFeedDetailSchema,
ListFeedsSchema,
PostCommentSchema,
ReplyCommentSchema,
SearchSchema,
SetFavoriteStateSchema,
SetLikeStateSchema,
} from '../src/platforms/xiaoheihe/schemas.js';
describe('xhh schemas', () => {
it('validates list/query boundaries', () => {
const schema = z.object(ListFeedsSchema);
expect(schema.parse({ max_count: 20 }).max_count).toBe(20);
expect(() => schema.parse({ max_count: 0 })).toThrow();
expect(() => schema.parse({ max_count: 201 })).toThrow();
});
it('validates search required keyword', () => {
const schema = z.object(SearchSchema);
expect(() => schema.parse({})).toThrow();
expect(schema.parse({ keyword: 'aaa' }).keyword).toBe('aaa');
});
it('allows feed detail by link_id or url', () => {
const schema = z.object(GetFeedDetailSchema);
expect(schema.parse({ link_id: '123' }).link_id).toBe('123');
expect(schema.parse({ url: 'https://www.xiaoheihe.cn/app/bbs/link/123' }).url).toContain('/app/bbs/link/');
});
it('validates comment payloads', () => {
const postSchema = z.object(PostCommentSchema);
const replySchema = z.object(ReplyCommentSchema);
expect(postSchema.parse({ link_id: '1', content: 'hi' }).content).toBe('hi');
expect(replySchema.parse({ link_id: '1', comment_id: '2', content: 'ok' }).comment_id).toBe('2');
expect(() => postSchema.parse({ link_id: '1', content: '' })).toThrow();
});
it('validates set-state tools', () => {
const likeSchema = z.object(SetLikeStateSchema);
const favSchema = z.object(SetFavoriteStateSchema);
expect(likeSchema.parse({ link_id: '1', liked: true }).liked).toBe(true);
expect(favSchema.parse({ link_id: '1', favorited: false }).favorited).toBe(false);
});
});
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import { resolveFeedTarget, resolveUserTarget } from '../src/platforms/xiaoheihe/target-resolver.js';
describe('xhh target resolver', () => {
it('resolves feed target from link_id', () => {
expect(resolveFeedTarget({ link_id: '123' })).toEqual({ linkId: '123' });
});
it('resolves feed target from url', () => {
expect(resolveFeedTarget({ url: 'https://www.xiaoheihe.cn/app/bbs/link/123' })).toEqual({ linkId: '123' });
});
it('throws on invalid feed target', () => {
expect(() => resolveFeedTarget({})).toThrow();
});
it('resolves user target from user_id', () => {
expect(resolveUserTarget({ user_id: '999' })).toEqual({ userId: '999' });
});
it('resolves user target from url', () => {
expect(resolveUserTarget({ url: 'https://www.xiaoheihe.cn/app/user/profile/888' })).toEqual({ userId: '888' });
});
it('throws on invalid user target', () => {
expect(() => resolveUserTarget({})).toThrow();
});
});