diff --git a/packages/mcp/src/generated/catalog.json b/packages/mcp/src/generated/catalog.json index 26285237..9f82529e 100644 --- a/packages/mcp/src/generated/catalog.json +++ b/packages/mcp/src/generated/catalog.json @@ -4982,14 +4982,42 @@ { "name": "get_online_clients", "aliases": [], - "summary": "获取在线客户端(占位,OneBot v11 形状)", - "returns": "{ clients }:在线设备列表(占位,clients 恒为空数组)。", + "summary": "获取在线客户端", + "returns": "{ clients }:QQ 本次会话最近推送的在线设备快照。", "returnsSchema": { "type": "object", "properties": { "clients": { "type": "array", - "description": "在线设备列表(占位,恒空)" + "description": "在线设备列表", + "items": { + "type": "object", + "properties": { + "app_id": { + "type": "integer", + "description": "客户端 ID" + }, + "device_name": { + "type": "string", + "description": "设备名称" + }, + "device_kind": { + "type": "string", + "enum": [ + "电脑", + "Pad", + "手机", + "未知设备" + ], + "description": "设备类别" + } + }, + "required": [ + "app_id", + "device_name", + "device_kind" + ] + } } }, "required": [ @@ -4997,11 +5025,28 @@ ] }, "readOnly": true, - "params": [], + "params": [ + { + "name": "no_cache", + "type": "bool", + "required": false, + "schema": { + "type": "boolean" + }, + "default": false, + "desc": "是否强制刷新;QQ 当前仅暴露本地快照,true 会明确失败" + } + ], "invariants": [], "inputSchema": { "type": "object", - "properties": {}, + "properties": { + "no_cache": { + "type": "boolean", + "description": "是否强制刷新;QQ 当前仅暴露本地快照,true 会明确失败", + "default": false + } + }, "additionalProperties": true }, "category": "扩展" diff --git a/packages/mcp/src/generated/catalog.ts b/packages/mcp/src/generated/catalog.ts index 1801ee09..6fc7c859 100644 --- a/packages/mcp/src/generated/catalog.ts +++ b/packages/mcp/src/generated/catalog.ts @@ -4985,14 +4985,42 @@ export const ACTIONS: CatalogAction[] = [ { "name": "get_online_clients", "aliases": [], - "summary": "获取在线客户端(占位,OneBot v11 形状)", - "returns": "{ clients }:在线设备列表(占位,clients 恒为空数组)。", + "summary": "获取在线客户端", + "returns": "{ clients }:QQ 本次会话最近推送的在线设备快照。", "returnsSchema": { "type": "object", "properties": { "clients": { "type": "array", - "description": "在线设备列表(占位,恒空)" + "description": "在线设备列表", + "items": { + "type": "object", + "properties": { + "app_id": { + "type": "integer", + "description": "客户端 ID" + }, + "device_name": { + "type": "string", + "description": "设备名称" + }, + "device_kind": { + "type": "string", + "enum": [ + "电脑", + "Pad", + "手机", + "未知设备" + ], + "description": "设备类别" + } + }, + "required": [ + "app_id", + "device_name", + "device_kind" + ] + } } }, "required": [ @@ -5000,11 +5028,28 @@ export const ACTIONS: CatalogAction[] = [ ] }, "readOnly": true, - "params": [], + "params": [ + { + "name": "no_cache", + "type": "bool", + "required": false, + "schema": { + "type": "boolean" + }, + "default": false, + "desc": "是否强制刷新;QQ 当前仅暴露本地快照,true 会明确失败" + } + ], "invariants": [], "inputSchema": { "type": "object", - "properties": {}, + "properties": { + "no_cache": { + "type": "boolean", + "description": "是否强制刷新;QQ 当前仅暴露本地快照,true 会明确失败", + "default": false + } + }, "additionalProperties": true }, "category": "扩展" diff --git a/packages/onebot/src/actions/extended.ts b/packages/onebot/src/actions/extended.ts index 0c46e9ad..24e42bc4 100644 --- a/packages/onebot/src/actions/extended.ts +++ b/packages/onebot/src/actions/extended.ts @@ -1199,25 +1199,60 @@ export const actions = [ }, }), - // get_online_clients — 在线设备列表。NapCat 也拿不到(注册监听后 sleep 500ms - // 直接返回空),且其返回是裸数组 [],偏离 OneBot v11 / go-cqhttp 规范。 - // SnowLuma 暂无对应单包 SSO cmd,返回 OneBot v11 标准空壳 { clients: [] } - // ——刻意采用规范形状而非 NapCat 的裸 [],故对照 NapCat 客户端此处不严格 parity。 defineAction({ name: 'get_online_clients', - summary: '获取在线客户端(占位,OneBot v11 形状)', + summary: '获取在线客户端', readOnly: true, - returns: '{ clients }:在线设备列表(占位,clients 恒为空数组)。', + returns: '{ clients }:QQ 本次会话最近推送的在线设备快照。', returnsSchema: { type: 'object', properties: { - clients: { type: 'array', description: '在线设备列表(占位,恒空)' }, + clients: { + type: 'array', + description: '在线设备列表', + items: { + type: 'object', + properties: { + app_id: { type: 'integer', description: '客户端 ID' }, + device_name: { type: 'string', description: '设备名称' }, + device_kind: { + type: 'string', + enum: ['电脑', 'Pad', '手机', '未知设备'], + description: '设备类别', + }, + }, + required: ['app_id', 'device_name', 'device_kind'], + }, + }, }, required: ['clients'], }, - params: {}, - run: async () => { - return okResponse({ clients: [] }); + params: { + no_cache: f.bool().default(false).describe('是否强制刷新;QQ 当前仅暴露本地快照,true 会明确失败'), + }, + run: async (p, ctx) => { + if (p.no_cache) { + return failedResponse(RETCODE.ACTION_FAILED, + 'online-client fresh refresh is unavailable; QQ exposes only its local snapshot'); + } + const snapshot = ctx.bridge.getOnlineClients(); + if (snapshot === null) { + return failedResponse(RETCODE.ACTION_FAILED, + 'online-client snapshot has not been observed in this session'); + } + const kindLabels = { + computer: '电脑', + pad: 'Pad', + phone: '手机', + unknown: '未知设备', + } as const; + return okResponse({ + clients: snapshot.map((device) => ({ + app_id: device.appId, + device_name: device.deviceName, + device_kind: kindLabels[device.deviceKind], + })), + }); }, }), diff --git a/packages/onebot/tests/extended-actions.test.ts b/packages/onebot/tests/extended-actions.test.ts index 50399ecb..738a6fcd 100644 --- a/packages/onebot/tests/extended-actions.test.ts +++ b/packages/onebot/tests/extended-actions.test.ts @@ -1059,9 +1059,9 @@ describe('extended-actions / ocr_image', () => { // NOTE two deliberate divergences from NapCat: // • _get_model_show reuses NapCat's array/variants shape but ECHOES the // requested model instead of NapCat's hardcoded 'napcat'. -// • get_online_clients returns the OneBot-v11/go-cqhttp { clients: [] } -// envelope, NOT NapCat's (non-standard) bare []. A strict-NapCat client -// would expect an array here; we intentionally follow the spec instead. +// • get_online_clients returns the OneBot-v11/go-cqhttp { clients } +// envelope, NOT NapCat's (non-standard) bare array. Its contents come from +// the latest QQ online-device snapshot observed by this Bridge lifecycle. describe('extended-actions / TierB compat stubs', () => { it('_get_model_show returns the napcat-shaped variants array, echoing the model', async () => { @@ -1087,10 +1087,55 @@ describe('extended-actions / TierB compat stubs', () => { expect(res).toMatchObject({ status: 'ok', retcode: 0, data: null }); }); - it('get_online_clients returns the OneBot-standard {clients:[]} envelope', async () => { - const res = await makeHandler(fakeCtx(fakeBridge())).handle('get_online_clients', {}); - expect(res.status).toBe('ok'); - expect(res.data).toMatchObject({ clients: [] }); + it('get_online_clients returns the latest observed QQ device snapshot', async () => { + const getOnlineClients = vi.fn(() => [{ + appId: 537242075, + instanceId: 202, + clientType: 1, + platform: 3, + deviceName: 'DESKTOP-TEST', + deviceKind: 'computer', + }]); + const res = await makeHandler(fakeCtx(fakeBridge({ getOnlineClients }))) + .handle('get_online_clients', {}); + + expect(getOnlineClients).toHaveBeenCalledOnce(); + expect(res).toMatchObject({ + status: 'ok', + retcode: 0, + data: { + clients: [{ + app_id: 537242075, + device_name: 'DESKTOP-TEST', + device_kind: '电脑', + }], + }, + }); + }); + + it('get_online_clients reports an unavailable snapshot instead of a false empty list', async () => { + const res = await makeHandler(fakeCtx(fakeBridge({ getOnlineClients: () => null }))) + .handle('get_online_clients', {}); + + expect(res).toMatchObject({ status: 'failed', retcode: 100 }); + expect(res.wording).toMatch(/snapshot has not been observed/i); + }); + + it('get_online_clients preserves an observed empty snapshot as a successful result', async () => { + const res = await makeHandler(fakeCtx(fakeBridge({ getOnlineClients: () => [] }))) + .handle('get_online_clients', {}); + + expect(res).toMatchObject({ status: 'ok', retcode: 0, data: { clients: [] } }); + }); + + it('get_online_clients rejects a forced refresh that QQ does not expose on the packet wire', async () => { + const getOnlineClients = vi.fn(() => []); + const res = await makeHandler(fakeCtx(fakeBridge({ getOnlineClients }))) + .handle('get_online_clients', { no_cache: true }); + + expect(res).toMatchObject({ status: 'failed', retcode: 100 }); + expect(res.wording).toMatch(/fresh refresh is unavailable/i); + expect(getOnlineClients).not.toHaveBeenCalled(); }); it('_mark_all_as_read passes every observed session to the batched SSO implementation', async () => { diff --git a/packages/sdk/src/types/actions.ts b/packages/sdk/src/types/actions.ts index 7b129b9b..7c01f65b 100644 --- a/packages/sdk/src/types/actions.ts +++ b/packages/sdk/src/types/actions.ts @@ -22,6 +22,7 @@ import type { GetGroupMemberListParams, GetGroupMessageHistoryParams, GetMediaParams, + GetOnlineClientsParams, GetPrivateFileUrlParams, GroupForwardMessageParams, GroupIdParams, @@ -199,7 +200,7 @@ export interface SnowLumaActionMap { get_emoji_likes: { params: GetEmojiLikesParams; data: JsonObject }; fetch_emoji_like: { params: FetchEmojiLikeParams; data: JsonObject }; get_friends_with_category: { params: JsonObject; data: FriendCategoryResult[] }; - get_online_clients: { params: JsonObject; data: OnlineClientsInfo }; + get_online_clients: { params: GetOnlineClientsParams; data: OnlineClientsInfo }; _get_model_show: { params: JsonObject; data: { variants: JsonArray } }; _set_model_show: { params: JsonObject; data: EmptyData }; '.get_word_slices': { params: JsonObject; data: JsonValue }; diff --git a/packages/sdk/src/types/params.ts b/packages/sdk/src/types/params.ts index 13e5cfb1..5f350a09 100644 --- a/packages/sdk/src/types/params.ts +++ b/packages/sdk/src/types/params.ts @@ -36,6 +36,8 @@ export interface NoCacheParams extends JsonObject { no_cache?: boolean; } +export interface GetOnlineClientsParams extends NoCacheParams {} + export interface GetGroupListParams extends NoCacheParams {} export interface GetGroupInfoParams extends GroupIdParams { diff --git a/packages/sdk/src/types/results.ts b/packages/sdk/src/types/results.ts index 52fd162b..3b1c84f9 100644 --- a/packages/sdk/src/types/results.ts +++ b/packages/sdk/src/types/results.ts @@ -1,4 +1,4 @@ -import type { JsonArray, JsonObject, JsonValue } from './json'; +import type { JsonObject, JsonValue } from './json'; export interface SendMessageResult { message_id: number; @@ -116,8 +116,14 @@ export interface ClientKeyInfo { expireTime: string; } +export interface OnlineClientInfo { + app_id: number; + device_name: string; + device_kind: '电脑' | 'Pad' | '手机' | '未知设备'; +} + export interface OnlineClientsInfo { - clients: JsonArray; + clients: OnlineClientInfo[]; } export interface UrlSafetyInfo {