From 13a8a2b8dd962fa63b3bc99a2d29eac0bf78e8ec Mon Sep 17 00:00:00 2001 From: sj817 <74231782+sj817@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:07:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(onebot):=20get=5Fgroup=5Fnotice=20=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E3=80=8C=E5=8F=91=E7=BB=99=E6=96=B0=E6=88=90=E5=91=98?= =?UTF-8?q?=E3=80=8D=E5=85=AC=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_t_list 会把「发给新成员」类公告单独放在 inst 数组里,之前只遍历 feeds, 这类公告(群规通常发在这里)完全拿不到。现在把 inst 条目追加在 feeds 之后返回, 已有条目的顺序不变,并按 fid 去重。 新增字段: - send_to_new_member:是否为「发给新成员」公告 - pinned:由 inst 条目的 settings.inst_no_pinned 映射(0 = 置顶), feeds 条目读不到置顶状态,不带该字段 Closes #2082 --- packages/napcat-core/types/webapi.ts | 2 + .../action/group/GetGroupNotice.ts | 58 +++++++++------ packages/napcat-test/getGroupNotice.test.ts | 71 +++++++++++++++++++ 3 files changed, 110 insertions(+), 21 deletions(-) create mode 100644 packages/napcat-test/getGroupNotice.test.ts diff --git a/packages/napcat-core/types/webapi.ts b/packages/napcat-core/types/webapi.ts index 4424ce9e..2c708c2e 100644 --- a/packages/napcat-core/types/webapi.ts +++ b/packages/napcat-core/types/webapi.ts @@ -78,6 +78,7 @@ export interface WebApiGroupNoticeFeed { remind_ts: number tip_window_type: number confirm_required: number + inst_no_pinned?: number // 仅 inst 条目有:0 = 置顶中,1 = 未置顶 }; read_num: number; is_read: number; @@ -92,6 +93,7 @@ export interface WebApiGroupNoticeRet { read_only: number role: number feeds: WebApiGroupNoticeFeed[] + inst?: WebApiGroupNoticeFeed[] // 「发给新成员」类公告,不在 feeds 里 group: { group_id: number class_ext: number diff --git a/packages/napcat-onebot/action/group/GetGroupNotice.ts b/packages/napcat-onebot/action/group/GetGroupNotice.ts index 42ad9f74..208426b1 100644 --- a/packages/napcat-onebot/action/group/GetGroupNotice.ts +++ b/packages/napcat-onebot/action/group/GetGroupNotice.ts @@ -21,6 +21,8 @@ const ReturnSchema = Type.Array(Type.Object({ }, { description: '公告内容' }), settings: Type.Optional(Type.Any({ description: '设置项' })), read_num: Type.Optional(Type.Number({ description: '阅读数' })), + send_to_new_member: Type.Optional(Type.Boolean({ description: '是否为「发给新成员」公告' })), + pinned: Type.Optional(Type.Boolean({ description: '是否置顶(仅「发给新成员」公告可读到)' })), }), { description: '群公告列表' }); type ReturnType = Static; @@ -44,30 +46,44 @@ export class GetGroupNotice extends OneBotAction { throw new Error('获取公告失败'); } const retNotices: ReturnType = []; - for (const key in ret.feeds) { - if (!ret.feeds[key]) { - continue; + const seen = new Set(); + // 「发给新成员」类公告只出现在 inst 里,追加在 feeds 之后,保持原有条目的顺序不变 + for (const [list, sendToNewMember] of [[ret.feeds, false], [ret.inst, true]] as const) { + for (const retApiNotice of Array.isArray(list) ? list : []) { + if (!retApiNotice || seen.has(retApiNotice.fid)) { + continue; + } + seen.add(retApiNotice.fid); + retNotices.push(this.toNotice(retApiNotice, sendToNewMember)); } - const retApiNotice: WebApiGroupNoticeFeed = ret.feeds[key]; - const image = retApiNotice.msg.pics?.map((pic) => { - return { id: pic.id, height: pic.h, width: pic.w }; - }) || []; - - const retNotice: ReturnType[number] = { - notice_id: retApiNotice.fid, - sender_id: retApiNotice.u, - publish_time: retApiNotice.pubt, - message: { - text: retApiNotice.msg.text, - image, - images: image, - }, - settings: retApiNotice.settings, - read_num: retApiNotice.read_num, - }; - retNotices.push(retNotice); } return retNotices; } + + private toNotice (retApiNotice: WebApiGroupNoticeFeed, sendToNewMember: boolean): ReturnType[number] { + const image = retApiNotice.msg.pics?.map((pic) => { + return { id: pic.id, height: pic.h, width: pic.w }; + }) || []; + + const retNotice: ReturnType[number] = { + notice_id: retApiNotice.fid, + sender_id: retApiNotice.u, + publish_time: retApiNotice.pubt, + message: { + text: retApiNotice.msg.text, + image, + images: image, + }, + settings: retApiNotice.settings, + read_num: retApiNotice.read_num, + send_to_new_member: sendToNewMember, + }; + // 置顶状态只在 inst 条目的 settings.inst_no_pinned 里:0 = 置顶中,1 = 未置顶 + const instNoPinned = retApiNotice.settings?.inst_no_pinned; + if (typeof instNoPinned === 'number') { + retNotice.pinned = instNoPinned === 0; + } + return retNotice; + } } diff --git a/packages/napcat-test/getGroupNotice.test.ts b/packages/napcat-test/getGroupNotice.test.ts new file mode 100644 index 00000000..e22c6229 --- /dev/null +++ b/packages/napcat-test/getGroupNotice.test.ts @@ -0,0 +1,71 @@ +// StreamBasic must be evaluated before OneBotAction: the two modules import each other, +// and loading OneBotAction first leaves BasicStream extending an uninitialized binding. +import '../napcat-onebot/action/stream/StreamBasic'; +import { describe, expect, test, vi } from 'vitest'; +import { GetGroupNotice } from '../napcat-onebot/action/group/GetGroupNotice'; + +function feed (fid: string, settings?: Record) { + return { + u: 10001, + fid, + pubt: 1750000000, + msg: { text: `notice ${fid}`, text_face: '', title: '' }, + type: 1, + fn: 0, + cn: 0, + vn: 0, + settings, + read_num: 3, + is_read: 0, + is_all_confirm: 0, + }; +} + +function createAction (ret: unknown) { + return new GetGroupNotice( + undefined as never, + { apis: { WebApi: { getGroupNotice: vi.fn().mockResolvedValue(ret) } } } as never + ); +} + +describe('_get_group_notice', () => { + test('returns "send to new member" notices from inst after feeds', async () => { + const action = createAction({ + ec: 0, + feeds: [feed('feed-1'), feed('feed-2')], + inst: [feed('inst-1', { inst_no_pinned: 0 })], + }); + + const notices = await action._handle({ group_id: '123' }); + + expect(notices.map(n => n.notice_id)).toEqual(['feed-1', 'feed-2', 'inst-1']); + expect(notices.map(n => n.send_to_new_member)).toEqual([false, false, true]); + expect(notices[2]?.message.text).toBe('notice inst-1'); + }); + + test('maps inst_no_pinned to pinned only when the field is present', async () => { + const action = createAction({ + ec: 0, + feeds: [feed('feed-1', { tip_window_type: 1 })], + inst: [feed('pinned', { inst_no_pinned: 0 }), feed('unpinned', { inst_no_pinned: 1 })], + }); + + const notices = await action._handle({ group_id: '123' }); + + expect(notices.find(n => n.notice_id === 'feed-1')?.pinned).toBeUndefined(); + expect(notices.find(n => n.notice_id === 'pinned')?.pinned).toBe(true); + expect(notices.find(n => n.notice_id === 'unpinned')?.pinned).toBe(false); + }); + + test('keeps working when inst is missing and skips duplicated notices', async () => { + const withoutInst = await createAction({ ec: 0, feeds: [feed('feed-1')] })._handle({ group_id: '123' }); + expect(withoutInst.map(n => n.notice_id)).toEqual(['feed-1']); + + const duplicated = await createAction({ + ec: 0, + feeds: [feed('same')], + inst: [feed('same', { inst_no_pinned: 0 })], + })._handle({ group_id: '123' }); + expect(duplicated.map(n => n.notice_id)).toEqual(['same']); + }); +});