diff --git a/lib/routes/zsxq/group.ts b/lib/routes/zsxq/group.ts index eeaaa462cd..a28381444b 100644 --- a/lib/routes/zsxq/group.ts +++ b/lib/routes/zsxq/group.ts @@ -3,6 +3,7 @@ import type { Context } from 'hono'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; import type { Data, Route } from '@/types'; +import { getRouteLimit } from '@/utils/route-limit'; import type { GroupInfoResponse, TopicsResponse } from './types'; import { customFetch, generateTopicDataItem } from './utils'; @@ -46,10 +47,7 @@ async function handler(ctx: Context): Promise { throw new ConfigNotFoundError('该 RSS 源由于配置不正确而被禁用:令牌丢失。'); } - let count = Number(ctx.req.query('limit')) || 20; - if (count > 30) { - count = 30; - } + const count = Math.min(getRouteLimit(ctx, 20), 30); const { group } = await customFetch(`/groups/${groupId}`); diff --git a/lib/routes/zsxq/user.ts b/lib/routes/zsxq/user.ts index aa4eb0bd1a..d0e520559b 100644 --- a/lib/routes/zsxq/user.ts +++ b/lib/routes/zsxq/user.ts @@ -3,6 +3,7 @@ import type { Context } from 'hono'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; import type { Data, Route } from '@/types'; +import { getRouteLimit } from '@/utils/route-limit'; import type { TopicsResponse, UserInfoResponse } from './types'; import { customFetch, generateTopicDataItem } from './utils'; @@ -41,10 +42,7 @@ async function handler(ctx: Context): Promise { throw new ConfigNotFoundError('该 RSS 源由于配置不正确而被禁用:令牌丢失。'); } - let count = Number(ctx.req.query('limit')) || 20; - if (count > 30) { - count = 30; - } + const count = Math.min(getRouteLimit(ctx, 20), 30); const userInfo = await customFetch(`/users/${uid}`); diff --git a/lib/routes/zsxq/utils.ts b/lib/routes/zsxq/utils.ts index 42ca2f829e..25531aeabf 100644 --- a/lib/routes/zsxq/utils.ts +++ b/lib/routes/zsxq/utils.ts @@ -1,3 +1,5 @@ +import { html } from 'hono/html'; + import { config } from '@/config'; import type { DataItem } from '@/types'; import got from '@/utils/got'; @@ -21,53 +23,81 @@ export async function customFetch>(path: s if (code === 1059 && retryCount < 3) { return customFetch(path, retryCount + 1); } - throw new Error('something wrong'); + throw new Error(`Knowledge Planet API request failed (code ${code ?? 'unknown'}). Check the access token and permission for this group or user.`); +} + +function decodeTopicText(text: string) { + try { + return decodeURIComponent(text); + } catch { + return text; + } +} + +function renderTopicLink(href: string, title: string) { + const label = decodeTopicText(title); + try { + const url = new URL(decodeTopicText(href)); + if (url.protocol === 'https:' || url.protocol === 'http:') { + return String(html`${label}`); + } + } catch { + // Malformed encoded links retain their readable label. + } + return String(html`${label}`); } function parseTopicContent(text: string = '', images: TopicImage[] = []) { let result = text.replaceAll('\n', '
'); - result = result.replaceAll(//g, (_, p1, p2) => `${decodeURIComponent(p2)}`); - result = result.replaceAll(//g, (_, p1) => { - const title = decodeURIComponent(p1); - return `${title}`; - }); - result += images.map((image) => ``).join('
'); + result = result.replaceAll(//g, (_, href, title) => renderTopicLink(href, title)); + result = result.replaceAll(//g, (_, title) => String(html`${decodeTopicText(title)}`)); + result += images + .map((image) => { + const url = image.original?.url ?? image.large?.url ?? image.thumbnail?.url; + return url ? String(html``) : ''; + }) + .filter(Boolean) + .join('
'); return result; } export function generateTopicDataItem(topics: Topic[]): DataItem[] { return topics.map((topic) => { + if (topic.topic_id === undefined || topic.topic_id === null) { + throw new Error('Missing Knowledge Planet topic ID; no unique article URL is available.'); + } let description: string | undefined; let title = ''; const url = `https://wx.zsxq.com/topic/${topic.topic_id}`; switch (topic.type) { case 'talk': - title = topic.talk?.text?.split('\n', 1)[0] ?? '文章'; + title = topic.talk?.text ?? '文章'; description = parseTopicContent(topic.talk?.text, topic.talk?.images); break; case 'q&a': - title = topic.question?.text?.split('\n', 1)[0] ?? '问答'; + title = topic.question?.text ?? '问答'; description = parseTopicContent(topic.question?.text, topic.question?.images); - description = `
${topic.question?.owner?.name ?? '匿名用户'} 提问:${description}
`; + description = `
${String(html`${topic.question?.owner?.name ?? '匿名用户'}`)} 提问:${description}
`; if (topic.answered) { - description += '
' + topic.answer?.owner.name + ' 回答:

'; + description += '
' + String(html`${topic.answer?.owner?.name ?? '匿名用户'}`) + ' 回答:

'; description += parseTopicContent(topic.answer?.text, topic.answer?.images); } break; case 'task': - title = topic.task?.text?.split('\n', 1)[0] ?? '作业'; + title = topic.task?.text ?? '作业'; description = parseTopicContent(topic.task?.text, topic.task?.images); break; case 'solution': - title = topic.solution?.text?.split('\n', 1)[0] ?? '写作业'; + title = topic.solution?.text ?? '写作业'; description = parseTopicContent(topic.solution?.text, topic.solution?.images); break; default: + throw new Error('Unsupported Knowledge Planet topic type; update RSSHub or report this topic format.'); } return { title: topic.title ?? title, description, - pubDate: parseDate(topic.create_time), + pubDate: topic.create_time ? parseDate(topic.create_time) : undefined, link: url, }; });