Enforce no-explicit-any; allow in tests
Enable @typescript-eslint/no-explicit-any as an error in the ESLint config to tighten typing across the codebase, and add an override to disable this rule for test files. Also apply non-functional cleanup in packages/proton/src/runtime.ts: reorder/imports and adjust formatting/indentation and string alignment for readability — no behavioral changes.
This commit is contained in:
+13
-1
@@ -48,7 +48,7 @@ export default defineConfig([
|
||||
rules: {
|
||||
'indent': ['error', 2, { SwitchCase: 1 }],
|
||||
'prefer-const': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
@@ -61,6 +61,18 @@ export default defineConfig([
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/tests/**/*.{ts,tsx}', '**/*.test.{ts,tsx}'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/bench/**/*.{ts,tsx}', 'packages/proton/test/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// OIDB service layer is namespace-per-cmd by design — the
|
||||
// structural-typing contract (namespace IS the OidbCallSpec) and
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
// Plain JSON value types — defined here (not in @snowluma/onebot/types
|
||||
// where they used to live) so @snowluma/protocol can talk about JSON-shaped
|
||||
// payloads without importing OneBot. The OneBot types module keeps the
|
||||
// names exported via re-export, so existing call sites that say
|
||||
// `import type { JsonValue } from '<…>/onebot/types'` continue to work
|
||||
// unchanged after Phase 3.
|
||||
//
|
||||
// Anything actually OneBot-specific (ApiResponse, OneBotRequest, …)
|
||||
// stays in @snowluma/onebot — only the plain JSON quartet moves here.
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
||||
export interface JsonObject {
|
||||
|
||||
@@ -17,7 +17,19 @@ import type { BridgeContext } from '../bridge-context';
|
||||
|
||||
// ─── Helpers (previously in bridge-contacts.ts) ───────────────────
|
||||
|
||||
export function buildFriendProperties(raw: any): Map<number, string> {
|
||||
type FriendPropertySource = {
|
||||
additional?: Array<{
|
||||
type?: number;
|
||||
layer1?: {
|
||||
properties?: Array<{
|
||||
code?: number;
|
||||
value?: string;
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export function buildFriendProperties(raw: FriendPropertySource): Map<number, string> {
|
||||
const props = new Map<number, string>();
|
||||
for (const additional of raw.additional ?? []) {
|
||||
if ((additional.type ?? 0) !== 1 || !additional.layer1) continue;
|
||||
|
||||
@@ -6,32 +6,35 @@ import type {
|
||||
DoQunLikeRequest,
|
||||
DoQunLikeResponse,
|
||||
GetMediaListRequest,
|
||||
MediaInfo,
|
||||
GetMediaListResponse,
|
||||
} from '@snowluma/proto-defs/oidb-actions/group-album';
|
||||
import { getGroupAlbumList, uploadImageToGroupAlbum } from '@snowluma/protocol/web/group-album';
|
||||
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
||||
import type { JsonObject, JsonValue } from '@snowluma/common/json';
|
||||
import type { BridgeContext } from '../bridge-context';
|
||||
|
||||
|
||||
function convertBigIntToString(obj: any): any {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
function convertBigIntToString(obj: unknown): JsonValue {
|
||||
if (obj === null || obj === undefined) return null;
|
||||
if (typeof obj === 'bigint') return obj.toString();
|
||||
if (Array.isArray(obj)) return obj.map(convertBigIntToString);
|
||||
if (typeof obj === 'object') {
|
||||
const result: any = {};
|
||||
for (const key in obj) {
|
||||
result[key] = convertBigIntToString(obj[key]);
|
||||
const result: JsonObject = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
result[key] = convertBigIntToString(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return obj;
|
||||
if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') return obj;
|
||||
return null;
|
||||
}
|
||||
|
||||
export class GroupAlbumApi {
|
||||
constructor(private readonly ctx: BridgeContext) { }
|
||||
|
||||
/** List a group's albums (HTTP-cookie-based, qzone.qq.com). */
|
||||
async list(groupId: number): Promise<any> {
|
||||
// 列出群相册(基于 qzone.qq.com 的 HTTP Cookie)。
|
||||
async list(groupId: number): Promise<GroupAlbumList> {
|
||||
const groupCode = groupId.toString();
|
||||
const uin = this.ctx.identity.uin;
|
||||
const cookieObject = await this.ctx.apis.web.getCookies('qzone.qq.com');
|
||||
@@ -39,7 +42,7 @@ export class GroupAlbumApi {
|
||||
return albumData?.album || [];
|
||||
}
|
||||
|
||||
/** Upload an image into an existing album (HTTP slice-upload). */
|
||||
// 上传图片到现有相册(HTTP 分片上传)。
|
||||
async upload(groupId: number, albumId: string, albumName: string, filePath: string): Promise<void> {
|
||||
const groupCode = groupId.toString();
|
||||
const uin = this.ctx.identity.uin;
|
||||
@@ -85,7 +88,7 @@ export class GroupAlbumApi {
|
||||
const mediaList = data.mediaList ?? [];
|
||||
const nextAttachInfo = data.nextAttachInfo ?? '';
|
||||
|
||||
return convertBigIntToString({ mediaList, nextAttachInfo });
|
||||
return convertBigIntToString({ mediaList, nextAttachInfo }) as unknown as GroupAlbumMediaResult;
|
||||
}
|
||||
|
||||
async comment(groupId: number, albumId: string, lloc: string, content: string): Promise<GroupAlbumCommentResult> {
|
||||
@@ -156,10 +159,10 @@ export class GroupAlbumApi {
|
||||
content: commentData.content ?? [],
|
||||
time: commentData.time ?? '0',
|
||||
clientKey: commentData.clientKey ?? '',
|
||||
});
|
||||
}) as unknown as GroupAlbumCommentResult;
|
||||
}
|
||||
|
||||
async like(groupId: number, albumId: string, batchId: string, lloc: string | undefined, isLike: boolean): Promise<any> {
|
||||
async like(groupId: number, albumId: string, batchId: string, lloc: string | undefined, isLike: boolean): Promise<JsonValue> {
|
||||
const uin = this.ctx.identity.uin;
|
||||
const clientKey = `${uin}_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
|
||||
@@ -221,7 +224,7 @@ export class GroupAlbumApi {
|
||||
return convertBigIntToString(resp.body?.like ?? {});
|
||||
}
|
||||
|
||||
async delete(groupId: number, albumId: string, lloc: string): Promise<any> {
|
||||
async delete(groupId: number, albumId: string, lloc: string): Promise<{ success: true }> {
|
||||
const uin = this.ctx.identity.uin;
|
||||
const clientKey = `${uin}_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
|
||||
@@ -261,10 +264,12 @@ export class GroupAlbumApi {
|
||||
}
|
||||
}
|
||||
export interface GroupAlbumMediaResult {
|
||||
mediaList: any[];
|
||||
mediaList: Array<JsonValue & Partial<MediaInfo>>;
|
||||
nextAttachInfo: string;
|
||||
}
|
||||
|
||||
export type GroupAlbumList = NonNullable<Awaited<ReturnType<typeof getGroupAlbumList>>>['album'];
|
||||
|
||||
export interface GroupAlbumCommentResult {
|
||||
id: string;
|
||||
user: { uin: string };
|
||||
|
||||
@@ -440,7 +440,7 @@ export class GroupFileApi {
|
||||
: toInt(upload.uploadPort);
|
||||
if (!uploadHost || uploadPort <= 0) {
|
||||
const rtpDump = Array.isArray(upload.rtpMediaPlatformUploadAddress)
|
||||
? JSON.stringify(upload.rtpMediaPlatformUploadAddress.map((e: any) => ({
|
||||
? JSON.stringify(upload.rtpMediaPlatformUploadAddress.map((e) => ({
|
||||
outIP: e.outIP, outPort: e.outPort, inIP: e.inIP, inPort: e.inPort,
|
||||
iPType: e.iPType,
|
||||
})))
|
||||
|
||||
@@ -55,7 +55,7 @@ export class MessageApi {
|
||||
richText: {
|
||||
elems: protoElems,
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
clientSequence: 0,
|
||||
random,
|
||||
syncCookie: new Uint8Array(0),
|
||||
@@ -117,7 +117,7 @@ export class MessageApi {
|
||||
richText: {
|
||||
elems: protoElems,
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
clientSequence: clientSeq,
|
||||
random,
|
||||
syncCookie: new Uint8Array(0),
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
MiniAppShareResp,
|
||||
} from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
||||
import type { JsonObject, JsonValue } from '@snowluma/common/json';
|
||||
import type { BridgeContext } from '../bridge-context';
|
||||
// Migrated OIDB cmds — facade methods are one-line forwarders.
|
||||
import { ClickInlineKeyboardButton } from '@snowluma/protocol/oidb-services/misc/click-inline-keyboard-button';
|
||||
@@ -16,7 +17,7 @@ export class MiscApi {
|
||||
return TranslateEnToZh.invoke(this.ctx, { words });
|
||||
}
|
||||
|
||||
async getMiniAppArk(type: string, title: string, desc: string, picUrl: string, jumpUrl: string): Promise<any> {
|
||||
async getMiniAppArk(type: string, title: string, desc: string, picUrl: string, jumpUrl: string): Promise<JsonObject> {
|
||||
let appid = '1109937557'; // default: bilibili
|
||||
let iconUrl = 'http://miniapp.gtimg.cn/public/appicon/51f90239b78a2e4994c11215f4c4ba15_200.jpg';
|
||||
|
||||
@@ -45,7 +46,7 @@ export class MiscApi {
|
||||
throw new Error('mini app share json empty');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
const parsed = JSON.parse(jsonStr) as Record<string, JsonObject | string | number | boolean | null>;
|
||||
|
||||
return {
|
||||
data: {
|
||||
@@ -67,7 +68,7 @@ export class MiscApi {
|
||||
buttonId: string,
|
||||
callbackData: string,
|
||||
msgSeq: number,
|
||||
): Promise<any> {
|
||||
): Promise<JsonValue> {
|
||||
return ClickInlineKeyboardButton.invoke(this.ctx, { groupId, botAppid, buttonId, callbackData, msgSeq });
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import type {
|
||||
} from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import { fetchHighwaySession, uploadHighwayHttp } from '@snowluma/protocol/highway';
|
||||
import { computeHashes, loadBinarySource } from '@snowluma/protocol/highway/utils';
|
||||
import { GetLike } from '@snowluma/protocol/oidb-services/profile/get-like';
|
||||
import { GetUnidirectionalFriendList } from '@snowluma/protocol/oidb-services/profile/get-unidirectional-friend-list';
|
||||
import { GetLike, type LikeInfo } from '@snowluma/protocol/oidb-services/profile/get-like';
|
||||
import { GetUnidirectionalFriendList, type UnidirectionalFriendEntry } from '@snowluma/protocol/oidb-services/profile/get-unidirectional-friend-list';
|
||||
import { SetInputStatus } from '@snowluma/protocol/oidb-services/profile/set-input-status';
|
||||
import { SetProfile } from '@snowluma/protocol/oidb-services/profile/set-profile';
|
||||
import { SetSelfLongNick } from '@snowluma/protocol/oidb-services/profile/set-self-long-nick';
|
||||
@@ -120,11 +120,11 @@ export class ProfileApi {
|
||||
|
||||
// ─────────────── queries on me / my contacts ───────────────
|
||||
|
||||
getLike(userId?: number, start = 0, limit = 10): Promise<any> {
|
||||
getLike(userId?: number, start = 0, limit = 10): Promise<LikeInfo> {
|
||||
return GetLike.invoke(this.ctx, { userId, start, limit });
|
||||
}
|
||||
|
||||
getUnidirectionalFriendList(): Promise<any> {
|
||||
getUnidirectionalFriendList(): Promise<UnidirectionalFriendEntry[]> {
|
||||
return GetUnidirectionalFriendList.invoke(this.ctx);
|
||||
}
|
||||
|
||||
@@ -141,10 +141,10 @@ export class ProfileApi {
|
||||
throw new Error(result.errorMessage || 'fetch custom face failed');
|
||||
}
|
||||
const resp = protobuf_decode<FaceroamOpResp>(result.responseData);
|
||||
if (!resp || (resp as any).retCode !== 0) {
|
||||
throw new Error(`fetch custom face error: ${(resp as any)?.message || 'unknown'}`);
|
||||
if (!resp || resp.retCode !== 0) {
|
||||
throw new Error(`fetch custom face error: ${resp?.message || 'unknown'}`);
|
||||
}
|
||||
const faceIds = (resp as any).item?.faceIds || [];
|
||||
const faceIds = resp.item?.faceIds || [];
|
||||
return faceIds.slice(0, count).map((id: string) => `https://p.qpic.cn/qq_expression/${this.ctx.identity.uin}/${id}/0`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ForceFetchClientKey, type ClientKeyInfo as NamespaceClientKeyInfo } from '@snowluma/protocol/oidb-services/web/force-fetch-client-key';
|
||||
import { GetPskey } from '@snowluma/protocol/oidb-services/web/get-pskey';
|
||||
import { getGroupEssenceMsg, getGroupEssenceMsgAll } from '@snowluma/protocol/web/group-essence';
|
||||
import { getHonorListWebAPI, WebHonorType } from '@snowluma/protocol/web/group-honor';
|
||||
import { getGroupEssenceMsg, getGroupEssenceMsgAll, type GroupEssenceMsgRet } from '@snowluma/protocol/web/group-essence';
|
||||
import { getHonorListWebAPI, WebHonorType, type WebHonorItem } from '@snowluma/protocol/web/group-honor';
|
||||
import {
|
||||
deleteGroupNotice as deleteGroupNoticeHttp,
|
||||
getGroupNoticeWebAPI,
|
||||
setGroupNoticeWebAPI,
|
||||
type SetNoticeRetSuccess,
|
||||
uploadGroupNoticeImage,
|
||||
} from '@snowluma/protocol/web/group-notice';
|
||||
import { RequestUtil } from '@snowluma/protocol/web/request-util';
|
||||
@@ -16,6 +17,31 @@ function asBridge(ctx: BridgeContext): Bridge { return ctx as unknown as Bridge;
|
||||
|
||||
export type ClientKeyInfo = NamespaceClientKeyInfo;
|
||||
|
||||
export interface WebHonorInfo {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
group_id: number;
|
||||
current_talkative: WebHonorItem | null;
|
||||
talkative_list: WebHonorItem[];
|
||||
performer_list: WebHonorItem[];
|
||||
legend_list: WebHonorItem[];
|
||||
emotion_list: WebHonorItem[];
|
||||
strong_newbie_list: WebHonorItem[];
|
||||
}
|
||||
|
||||
export interface WebNoticeInfo {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
notice_id: string;
|
||||
sender_id: number;
|
||||
publish_time: number;
|
||||
message: {
|
||||
text: string;
|
||||
image: Array<{ id: string; height: number; width: number }>;
|
||||
images: Array<{ id: string; height: number; width: number }>;
|
||||
};
|
||||
settings: import('@snowluma/common/json').JsonValue;
|
||||
read_num: number;
|
||||
}
|
||||
|
||||
// ─────────────── private helpers (cookie acquisition) ───────────────
|
||||
|
||||
function forceFetchClientKeyInner(bridge: Bridge): Promise<ClientKeyInfo> {
|
||||
@@ -144,7 +170,7 @@ export class WebApi {
|
||||
// ─────────────── group essence ───────────────
|
||||
|
||||
/** Paginated fetch — `pageStart` is 0-indexed, `pageLimit` is server-capped at 50. */
|
||||
async getEssence(groupId: number, pageStart = 0, pageLimit = 50): Promise<any> {
|
||||
async getEssence(groupId: number, pageStart = 0, pageLimit = 50): Promise<GroupEssenceMsgRet> {
|
||||
const bridge = asBridge(this.ctx);
|
||||
const groupCode = groupId.toString();
|
||||
const cookieObject = await getCookies(bridge, 'qun.qq.com');
|
||||
@@ -153,7 +179,7 @@ export class WebApi {
|
||||
}
|
||||
|
||||
/** Walks every page and returns the concatenated result. */
|
||||
async getEssenceAll(groupId: number): Promise<any> {
|
||||
async getEssenceAll(groupId: number): Promise<GroupEssenceMsgRet[]> {
|
||||
const bridge = asBridge(this.ctx);
|
||||
const groupCode = groupId.toString();
|
||||
const cookieObject = await getCookies(bridge, 'qun.qq.com');
|
||||
@@ -162,12 +188,12 @@ export class WebApi {
|
||||
|
||||
// ─────────────── group honor ───────────────
|
||||
|
||||
async getHonorInfo(groupId: number, type: WebHonorType | string): Promise<any> {
|
||||
async getHonorInfo(groupId: number, type: WebHonorType | string): Promise<WebHonorInfo> {
|
||||
const bridge = asBridge(this.ctx);
|
||||
const groupCode = groupId.toString();
|
||||
const cookieObject = await getCookies(bridge, 'qun.qq.com');
|
||||
|
||||
const honorInfo: any = {
|
||||
const honorInfo: WebHonorInfo = {
|
||||
group_id: groupId,
|
||||
current_talkative: null,
|
||||
talkative_list: [],
|
||||
@@ -211,7 +237,7 @@ export class WebApi {
|
||||
type?: number;
|
||||
confirm_required?: number;
|
||||
},
|
||||
): Promise<any> {
|
||||
): Promise<SetNoticeRetSuccess> {
|
||||
const bridge = asBridge(this.ctx);
|
||||
const groupCode = groupId.toString();
|
||||
const cookieObject = await getCookies(bridge, 'qun.qq.com');
|
||||
@@ -261,7 +287,7 @@ export class WebApi {
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getNotice(groupId: number): Promise<any[]> {
|
||||
async getNotice(groupId: number): Promise<WebNoticeInfo[]> {
|
||||
const bridge = asBridge(this.ctx);
|
||||
const groupCode = groupId.toString();
|
||||
const cookieObject = await getCookies(bridge, 'qun.qq.com');
|
||||
@@ -271,7 +297,7 @@ export class WebApi {
|
||||
throw new Error('获取公告失败');
|
||||
}
|
||||
|
||||
const retNotices: any[] = [];
|
||||
const retNotices: WebNoticeInfo[] = [];
|
||||
|
||||
if (ret.feeds) {
|
||||
for (const key in ret.feeds) {
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('pipeline — runNtv2Upload', () => {
|
||||
});
|
||||
const upload = await runNtv2Upload(baseParams({ bridge }));
|
||||
expect(upload.uKey).toBe('fake-ukey');
|
||||
expect(upload.msgInfo.msgInfoBody).toHaveLength(1);
|
||||
expect(upload.msgInfo?.msgInfoBody).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('throws on transport failure', async () => {
|
||||
@@ -315,7 +315,7 @@ describe('pipeline — finalizeMediaMsgInfo', () => {
|
||||
it('encodes msgInfoBody + extBizInfo from the server response', () => {
|
||||
const out = finalizeMediaMsgInfo({
|
||||
msgInfo: {
|
||||
msgInfoBody: [{ index: { fileType: 1 }, fileExist: true }],
|
||||
msgInfoBody: [{ index: { subType: 1 }, fileExist: true }],
|
||||
extBizInfo: { busiType: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
|
||||
try {
|
||||
const essenceDataAll = await ctx.bridge.apis.web.getEssenceAll(groupId);
|
||||
|
||||
const allMsgs = essenceDataAll.flatMap((res: any) => res.data?.msg_list || []);
|
||||
const allMsgs = essenceDataAll.flatMap((res) => res.data?.msg_list || []);
|
||||
|
||||
return okResponse(allMsgs);
|
||||
} catch (e) {
|
||||
@@ -347,7 +347,7 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
|
||||
message_id: 0,
|
||||
};
|
||||
if (groupId > 0) data.group_id = groupId;
|
||||
return okResponse(data as any);
|
||||
return okResponse(data as import('../types').JsonObject);
|
||||
});
|
||||
|
||||
h.registerAction('upload_foward_msg', async (params) => {
|
||||
@@ -985,7 +985,7 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
|
||||
}
|
||||
});
|
||||
|
||||
const handleGroupSign = async (params: any) => {
|
||||
const handleGroupSign = async (params: import('../types').JsonObject) => {
|
||||
const groupId = asNumber(params.group_id);
|
||||
|
||||
if (!groupId) {
|
||||
|
||||
@@ -26,10 +26,10 @@ export interface GroupEssenceMsgRet {
|
||||
retcode: number;
|
||||
data: {
|
||||
is_end: boolean;
|
||||
msg_list: any[];
|
||||
[key: string]: any;
|
||||
msg_list: JsonObject[];
|
||||
[key: string]: JsonValue;
|
||||
};
|
||||
[key: string]: any;
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
|
||||
export interface ApiActionContext {
|
||||
|
||||
@@ -36,7 +36,6 @@ export class OneBotInstance {
|
||||
private readonly reactionStore: ReactionStore;
|
||||
private readonly networkManager: OneBotNetworkManager;
|
||||
private readonly rkeyCache: RKeyCache;
|
||||
private readonly ctx: OneBotInstanceContext;
|
||||
private disposeEventPipeline: (() => void) | null = null;
|
||||
|
||||
private readonly pids = new Set<number>();
|
||||
@@ -85,7 +84,6 @@ export class OneBotInstance {
|
||||
cacheMessageMeta: (messageId, meta) => this.cacheMessageMeta(messageId, meta),
|
||||
dispatchEvent: (event) => this.dispatchEvent(event),
|
||||
};
|
||||
this.ctx = ctx;
|
||||
|
||||
this.apiHandler = new ApiHandler(buildApiContext(ctx), uinNum > 0 ? uinNum : undefined);
|
||||
this.networkManager = new OneBotNetworkManager();
|
||||
|
||||
@@ -35,13 +35,13 @@ export interface SendContext {
|
||||
|
||||
function makeTextElem(text: string): ProtoElem {
|
||||
return {
|
||||
text: { str: text } as any,
|
||||
text: { str: text },
|
||||
};
|
||||
}
|
||||
|
||||
function makeFaceElem(faceId: number): ProtoElem {
|
||||
return {
|
||||
face: { index: faceId } as any,
|
||||
face: { index: faceId },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,14 +83,14 @@ function makeMentionElem(element: MessageElement, ctx?: SendContext): ProtoElem
|
||||
text: {
|
||||
str,
|
||||
pbReserve: extra,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeReplyElem(element: MessageElement): ProtoElem {
|
||||
const seq = element.replySeq! & 0xFFFFFFFF;
|
||||
|
||||
const srcMsg: any = {
|
||||
const srcMsg: NonNullable<Elem['srcMsg']> = {
|
||||
origSeqs: [seq],
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ function makeJsonElem(element: MessageElement): ProtoElem {
|
||||
richMsg: {
|
||||
serviceId: 1,
|
||||
template1: payload,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ function makeXmlElem(element: MessageElement): ProtoElem {
|
||||
richMsg: {
|
||||
serviceId: element.subType === 0 ? 35 : (element.subType ?? 35),
|
||||
template1: payload,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ function makeMarkdownElem(element: MessageElement): ProtoElem {
|
||||
serviceType: 45,
|
||||
pbElem: data,
|
||||
businessType: 1,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ function makeForwardElem(element: MessageElement): ProtoElem {
|
||||
return {
|
||||
lightApp: {
|
||||
data: payload,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ async function makeImageElem(ctx: SendContext, element: MessageElement): Promise
|
||||
serviceType: 48,
|
||||
pbElem: msgInfo,
|
||||
businessType: isGroup ? 20 : 10,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ async function makePttElem(ctx: SendContext, element: MessageElement): Promise<P
|
||||
serviceType: 48,
|
||||
pbElem: msgInfo,
|
||||
businessType: isGroup ? 22 : 12,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ function makeGroupFileElem(element: MessageElement, ctx?: SendContext): ProtoEle
|
||||
transElem: {
|
||||
elemType: 24,
|
||||
elemValue,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ async function makeVideoElem(ctx: SendContext, element: MessageElement): Promise
|
||||
serviceType: 48,
|
||||
pbElem: msgInfo,
|
||||
businessType: isGroup ? 21 : 11,
|
||||
} as any,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ import type { BridgeContext } from '../bridge-context';
|
||||
import type {
|
||||
HttpConn0x6FF501Request,
|
||||
HttpConn0x6FF501Response,
|
||||
HighwayMsgInfoBody,
|
||||
NTV2IPv4,
|
||||
NTV2RichMediaHighwayExt,
|
||||
NTV2UploadRespMsgInfo,
|
||||
ReqDataHighwayHead,
|
||||
RespDataHighwayHead,
|
||||
} from '@snowluma/proto-defs/highway';
|
||||
@@ -91,8 +94,8 @@ function makeHighwayHead(
|
||||
|
||||
export function buildHighwayExtend(
|
||||
uKey: string,
|
||||
msgInfo: any,
|
||||
ipv4s: any[],
|
||||
msgInfo: NTV2UploadRespMsgInfo,
|
||||
ipv4s: NTV2IPv4[],
|
||||
sha1: Uint8Array | Uint8Array[],
|
||||
fileIndex = 0,
|
||||
): Uint8Array {
|
||||
@@ -100,7 +103,7 @@ export function buildHighwayExtend(
|
||||
if (msgInfoBody.length === 0) throw new Error('upload response missing msg_info body');
|
||||
|
||||
const selected = msgInfoBody[fileIndex] ?? msgInfoBody[0];
|
||||
const networkIpv4s: any[] = [];
|
||||
const networkIpv4s: NonNullable<NonNullable<NTV2RichMediaHighwayExt['network']>['ipv4s']> = [];
|
||||
for (const ipv4 of ipv4s ?? []) {
|
||||
const ip = ipv4.outIp ?? 0;
|
||||
const port = ipv4.outPort ?? 0;
|
||||
@@ -113,7 +116,7 @@ export function buildHighwayExtend(
|
||||
fileUuid: selected?.index?.fileUuid ?? '',
|
||||
uKey,
|
||||
network: { ipv4s: networkIpv4s },
|
||||
msgInfoBody: msgInfoBody.map((b: any) => ({
|
||||
msgInfoBody: msgInfoBody.map((b: HighwayMsgInfoBody) => ({
|
||||
index: b.index, picture: b.picture, fileExist: b.fileExist, hashSum: b.hashSum,
|
||||
})),
|
||||
blockSize: HIGHWAY_BLOCK_SIZE,
|
||||
|
||||
@@ -5,8 +5,12 @@ import type { BridgeContext } from '../bridge-context';
|
||||
import { makeOidbEnvelope } from '../bridge-oidb';
|
||||
import type {
|
||||
EncodableMediaMsgInfo,
|
||||
HighwayMsgInfoBody,
|
||||
NTV2ExtBizInfo,
|
||||
NTV2UploadInfo,
|
||||
NTV2UploadRichMediaReq,
|
||||
NTV2UploadRichMediaResp,
|
||||
NTV2UploadRespBody,
|
||||
} from '@snowluma/proto-defs/highway';
|
||||
import { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
import { buildHighwayExtend, fetchHighwaySession, uploadHighwayHttp } from './highway-client';
|
||||
@@ -64,11 +68,11 @@ export interface NtV2UploadParams {
|
||||
/** `reqHead.scene.businessType` (1=image, 3=voice, 2=video). */
|
||||
businessType: number;
|
||||
/** `upload.uploadInfo` array. Each entry is `{ fileInfo, subFileType }`. */
|
||||
uploadInfo: unknown[];
|
||||
uploadInfo: NTV2UploadInfo[];
|
||||
/** `upload.compatQmsgSceneType`. */
|
||||
compatQmsgSceneType: number;
|
||||
/** `upload.extBizInfo` — type-specific bytes/flags. */
|
||||
extBizInfo: unknown;
|
||||
extBizInfo: NTV2ExtBizInfo;
|
||||
/** Sub-file Highway PUTs to perform after the OIDB response. */
|
||||
uploads: MediaSubFileUpload[];
|
||||
/** Used in error messages. Defaults to 'media'. */
|
||||
@@ -107,7 +111,7 @@ export function hexToBytes(hex: string): Uint8Array {
|
||||
* Sessions are cached across sub-file uploads — video does two PUTs but
|
||||
* only fetches the Highway session once.
|
||||
*/
|
||||
export async function runNtv2Upload(params: NtV2UploadParams): Promise<any> {
|
||||
export async function runNtv2Upload(params: NtV2UploadParams): Promise<NTV2UploadRespBody> {
|
||||
const { bridge, isGroup, targetIdOrUid, oidbCmd, serviceCmd, uploads } = params;
|
||||
const label = params.label ?? 'media';
|
||||
const raw = bridge.identity?.uin;
|
||||
@@ -118,7 +122,7 @@ export async function runNtv2Upload(params: NtV2UploadParams): Promise<any> {
|
||||
|
||||
// Build the full body once. Pulling this in here means each format file
|
||||
// only specifies the seven fields that actually vary.
|
||||
const body: any = {
|
||||
const body: NTV2UploadRichMediaReq = {
|
||||
reqHead: {
|
||||
common: { requestId: params.requestId, command: 100 },
|
||||
scene: {
|
||||
@@ -157,7 +161,7 @@ export async function runNtv2Upload(params: NtV2UploadParams): Promise<any> {
|
||||
throw new Error(`OIDB error ${resp.errorCode}: ${resp.errorMsg ?? ''}`);
|
||||
}
|
||||
|
||||
const uploadBody: any = resp.body;
|
||||
const uploadBody = resp.body;
|
||||
if (!uploadBody) throw new Error(`${label} upload response body missing`);
|
||||
if (uploadBody.respHead?.retCode && uploadBody.respHead.retCode !== 0) {
|
||||
throw new Error(uploadBody.respHead.message ?? `${label} upload failed`);
|
||||
@@ -186,11 +190,13 @@ export async function runNtv2Upload(params: NtV2UploadParams): Promise<any> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!target) continue;
|
||||
|
||||
const extend = buildHighwayExtend(
|
||||
uKey,
|
||||
upload.msgInfo,
|
||||
target.ipv4s ?? [],
|
||||
sub.sha1 as any, // sha1 may be Uint8Array | Uint8Array[]; the helper accepts both
|
||||
sub.sha1,
|
||||
sub.subFileIndex ?? 0,
|
||||
);
|
||||
log.debug('%s OIDB requires bytes, PUT %d bytes (sub=%s)', label, sub.bytes.length, String(sub.source));
|
||||
@@ -218,16 +224,16 @@ export async function runNtv2Upload(params: NtV2UploadParams): Promise<any> {
|
||||
* pic alone unless the server populates it.
|
||||
*/
|
||||
export function finalizeMediaMsgInfo(
|
||||
upload: any,
|
||||
upload: NTV2UploadRespBody,
|
||||
defaultPic?: { bizType: number; textSummary: string },
|
||||
): Uint8Array {
|
||||
if (!upload?.msgInfo) throw new Error('upload response missing msgInfo');
|
||||
|
||||
const msgInfoBody = (upload.msgInfo.msgInfoBody ?? []).map((b: any) => ({
|
||||
const msgInfoBody = (upload.msgInfo.msgInfoBody ?? []).map((b: HighwayMsgInfoBody) => ({
|
||||
index: b.index, picture: b.picture, fileExist: b.fileExist, hashSum: b.hashSum,
|
||||
}));
|
||||
|
||||
const extBizInfo: any = {};
|
||||
const extBizInfo: NonNullable<EncodableMediaMsgInfo['extBizInfo']> = {};
|
||||
if (upload.msgInfo.extBizInfo?.pic) {
|
||||
extBizInfo.pic = { ...upload.msgInfo.extBizInfo.pic };
|
||||
if (defaultPic) {
|
||||
|
||||
@@ -23,7 +23,7 @@ export namespace FetchFriendListPage {
|
||||
export type Deps = OidbSender;
|
||||
|
||||
export const serialize = (_ctx: Deps, p: Params): OidbFriendListRequest => {
|
||||
const body: any = {
|
||||
const body: OidbFriendListRequest = {
|
||||
friendCount: 300,
|
||||
field4: 0,
|
||||
field6: 1,
|
||||
|
||||
@@ -25,7 +25,7 @@ export namespace FetchGroupMemberListPage {
|
||||
export type Deps = OidbSender;
|
||||
|
||||
export const serialize = (_ctx: Deps, p: Params): OidbGroupMemberListRequest => {
|
||||
const body: any = {
|
||||
const body: OidbGroupMemberListRequest = {
|
||||
groupUin: p.groupId,
|
||||
field2: 5,
|
||||
field3: 2,
|
||||
|
||||
@@ -41,7 +41,7 @@ export namespace GetStrangerStatus {
|
||||
export const deserialize = (_ctx: Deps, body: OidbStrangerStatusResp): StrangerStatus | null => {
|
||||
const raw = body.data?.status?.value;
|
||||
if (raw === undefined || raw === null) return null;
|
||||
const extBig = typeof raw === 'bigint' ? raw : BigInt(raw as any);
|
||||
const extBig = typeof raw === 'bigint' ? raw : BigInt(raw);
|
||||
if (extBig <= 10n) {
|
||||
return { status: Number(extBig) * 10, ext_status: 0 };
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export namespace GetGroupPttUrl {
|
||||
client: { agentType: 2 },
|
||||
},
|
||||
download: {
|
||||
node: normalizeMediaNode(p.node) as any,
|
||||
node: normalizeMediaNode(p.node),
|
||||
download: { video: { busiType: 0, sceneType: 0 } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ export namespace GetGroupVideoUrl {
|
||||
client: { agentType: 2 },
|
||||
},
|
||||
download: {
|
||||
node: normalizeMediaNode(p.node) as any,
|
||||
node: normalizeMediaNode(p.node),
|
||||
download: { video: { busiType: 0, sceneType: 0 } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ export namespace GetPrivatePttUrl {
|
||||
client: { agentType: 2 },
|
||||
},
|
||||
download: {
|
||||
node: normalizeMediaNode(p.node) as any,
|
||||
node: normalizeMediaNode(p.node),
|
||||
download: { video: { busiType: 0, sceneType: 0 } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ export namespace GetPrivateVideoUrl {
|
||||
client: { agentType: 2 },
|
||||
},
|
||||
download: {
|
||||
node: normalizeMediaNode(p.node) as any,
|
||||
node: normalizeMediaNode(p.node),
|
||||
download: { video: { busiType: 0, sceneType: 0 } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// differ. The response is parsed into a `domain + urlPath +
|
||||
// rKeyParam` triple → composed into `https://<domain><path><rKey>`.
|
||||
|
||||
import type { NTV2RichMediaResp } from '@snowluma/proto-defs/oidb-actions/media';
|
||||
import type { NTV2IndexNode, NTV2RichMediaResp } from '@snowluma/proto-defs/oidb-actions/media';
|
||||
import { ensureRetCodeZero } from '../shared';
|
||||
|
||||
export function parseNtv2DownloadUrl(body: NTV2RichMediaResp): string {
|
||||
@@ -24,7 +24,7 @@ export function parseNtv2DownloadUrl(body: NTV2RichMediaResp): string {
|
||||
/** Normalize a MediaIndexNode-shaped object into the wire-fields the
|
||||
* NTV2RichMedia `download.node` slot expects: `info{...}` /
|
||||
* `fileUuid` / `storeId` / `uploadTime` / `ttl` / `subType`. */
|
||||
export function normalizeMediaNode(node: NtMediaIndex): Record<string, unknown> {
|
||||
export function normalizeMediaNode(node: NtMediaIndex): NTV2IndexNode {
|
||||
const fileUuid = typeof node.fileUuid === 'string' ? node.fileUuid : '';
|
||||
if (!fileUuid) throw new Error('media node fileUuid is required');
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Oidb0x112eReq, Oidb0x112eResp } from '@snowluma/proto-defs/oidb-ac
|
||||
import { invokeOidb, type OidbSender } from '../../oidb-service';
|
||||
|
||||
export interface InlineButtonClickResult {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
result: number;
|
||||
errMsg: string;
|
||||
status: 0;
|
||||
|
||||
@@ -10,15 +10,18 @@ import { invokeOidb, type OidbSender } from '../../oidb-service';
|
||||
import type { BridgeContext } from '../../bridge-context';
|
||||
|
||||
export interface LikeInfo {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
uid: string;
|
||||
time: number;
|
||||
favoriteInfo: {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
total_count: number;
|
||||
last_time: number;
|
||||
today_count: number;
|
||||
userInfos: never[];
|
||||
};
|
||||
voteInfo: {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
total_count: number;
|
||||
new_count: number;
|
||||
new_nearby_count: number;
|
||||
|
||||
@@ -13,6 +13,10 @@ import type { Oidb0xe17Req, Oidb0xe17Resp } from '@snowluma/proto-defs/oidb-acti
|
||||
import { invokeOidb, type OidbSender } from '../../oidb-service';
|
||||
import type { BridgeContext } from '../../bridge-context';
|
||||
|
||||
export interface UnidirectionalFriendEntry {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
}
|
||||
|
||||
export namespace GetUnidirectionalFriendList {
|
||||
export const command = 0xE17;
|
||||
export const subCommand = 0;
|
||||
@@ -35,9 +39,9 @@ export namespace GetUnidirectionalFriendList {
|
||||
return { jsonBody: JSON.stringify(reqObj) };
|
||||
};
|
||||
|
||||
export const deserialize = (_ctx: Deps, body: Oidb0xe17Resp): unknown[] => {
|
||||
export const deserialize = (_ctx: Deps, body: Oidb0xe17Resp): UnidirectionalFriendEntry[] => {
|
||||
if (!body || !body.jsonBody) throw new Error('get unidirectional friend list empty');
|
||||
const parsed = JSON.parse(body.jsonBody);
|
||||
const parsed = JSON.parse(body.jsonBody) as { rpt_block_list?: UnidirectionalFriendEntry[] };
|
||||
return parsed.rpt_block_list || [];
|
||||
};
|
||||
|
||||
@@ -47,6 +51,6 @@ export namespace GetUnidirectionalFriendList {
|
||||
export const decode = (bytes: Uint8Array): OidbBase<Oidb0xe17Resp> =>
|
||||
protobuf_decode<OidbBase<Oidb0xe17Resp>>(bytes);
|
||||
|
||||
export const invoke = (deps: Deps): Promise<unknown[]> =>
|
||||
export const invoke = (deps: Deps): Promise<UnidirectionalFriendEntry[]> =>
|
||||
invokeOidb(deps, GetUnidirectionalFriendList, {});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createHash } from 'crypto';
|
||||
import { statSync, createReadStream, writeFileSync, unlinkSync } from 'fs';
|
||||
import { basename, join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import type { JsonValue } from '@snowluma/common/json';
|
||||
|
||||
// 群相册信息
|
||||
export interface GroupAlbumInfo {
|
||||
@@ -10,13 +11,13 @@ export interface GroupAlbumInfo {
|
||||
name: string;
|
||||
picNum: number;
|
||||
createTime: number;
|
||||
[key: string]: any;
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
|
||||
// 获取群相册列表返回
|
||||
export interface GroupAlbumListRet {
|
||||
album: GroupAlbumInfo[];
|
||||
[key: string]: any;
|
||||
[key: string]: JsonValue | GroupAlbumInfo[];
|
||||
}
|
||||
|
||||
// 相册媒体信息
|
||||
@@ -24,7 +25,7 @@ export interface AlbumMediaInfo {
|
||||
photoId: string;
|
||||
url: string;
|
||||
uploadTime: number;
|
||||
[key: string]: any;
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,7 +151,7 @@ async function uploadAlbumSlice(
|
||||
session: string,
|
||||
filePath: string,
|
||||
// 移除 offset 和 chunkSize 参数,改为在内部控制
|
||||
imgMd5: string,
|
||||
_imgMd5: string,
|
||||
skey: string,
|
||||
pskey: string,
|
||||
uin: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RequestUtil, cookieToString, getBknFromCookie } from './request-util';
|
||||
import { createLogger } from '@snowluma/common/logger';
|
||||
import type { JsonObject } from '@snowluma/common/json';
|
||||
|
||||
const log = createLogger('Bridge.Web');
|
||||
|
||||
@@ -8,10 +9,10 @@ export interface GroupEssenceMsgRet {
|
||||
retcode: number;
|
||||
data: {
|
||||
is_end: boolean;
|
||||
msg_list: any[]; // 具体结构视需要补充
|
||||
[key: string]: any;
|
||||
msg_list: JsonObject[]; // 具体结构视需要补充
|
||||
[key: string]: JsonObject[] | boolean | string | number | null;
|
||||
};
|
||||
[key: string]: any;
|
||||
[key: string]: JsonObject | JsonObject[] | boolean | string | number | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,26 @@ import { createLogger } from '@snowluma/common/logger';
|
||||
|
||||
const log = createLogger('Bridge.Web');
|
||||
|
||||
export interface WebHonorItem {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
user_id: number | string | null;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface RawHonorItem {
|
||||
uin?: number | string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
interface HonorInitialState {
|
||||
talkativeList?: RawHonorItem[];
|
||||
actorList?: RawHonorItem[];
|
||||
}
|
||||
|
||||
export enum WebHonorType {
|
||||
TALKATIVE = 'talkative',
|
||||
PERFORMER = 'performer',
|
||||
@@ -12,8 +32,8 @@ export enum WebHonorType {
|
||||
}
|
||||
|
||||
|
||||
async function fetchHonorData(cookieObject: Record<string, string>, groupCode: string, type: number) {
|
||||
let resJson;
|
||||
async function fetchHonorData(cookieObject: Record<string, string>, groupCode: string, type: number): Promise<RawHonorItem[] | undefined> {
|
||||
let resJson: HonorInitialState | undefined;
|
||||
try {
|
||||
const res = await RequestUtil.HttpGetText(
|
||||
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
||||
@@ -26,7 +46,7 @@ async function fetchHonorData(cookieObject: Record<string, string>, groupCode: s
|
||||
);
|
||||
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
||||
if (match?.[1]) {
|
||||
resJson = JSON.parse(match[1].trim());
|
||||
resJson = JSON.parse(match[1].trim()) as HonorInitialState;
|
||||
}
|
||||
return type === 1 ? resJson?.talkativeList : resJson?.actorList;
|
||||
} catch (e) {
|
||||
@@ -34,16 +54,16 @@ async function fetchHonorData(cookieObject: Record<string, string>, groupCode: s
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHonorListWebAPI(cookieObject: Record<string, string>, groupCode: string, type: number) {
|
||||
export async function getHonorListWebAPI(cookieObject: Record<string, string>, groupCode: string, type: number): Promise<WebHonorItem[]> {
|
||||
try {
|
||||
const data = await fetchHonorData(cookieObject, groupCode, type);
|
||||
if (!data) return [];
|
||||
|
||||
return data.map((item: any) => ({
|
||||
user_id: item?.uin,
|
||||
nickname: item?.name,
|
||||
avatar: item?.avatar,
|
||||
description: item?.desc,
|
||||
return data.map((item) => ({
|
||||
user_id: item?.uin ?? null,
|
||||
nickname: item?.name ?? '',
|
||||
avatar: item?.avatar ?? '',
|
||||
description: item?.desc ?? '',
|
||||
}));
|
||||
} catch (e) {
|
||||
log.warn('getHonorListWebAPI failed (group=%s type=%d): %s',
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { RequestUtil, cookieToString } from './request-util';
|
||||
import https from 'node:https';
|
||||
import { createLogger } from '@snowluma/common/logger';
|
||||
import type { JsonValue } from '@snowluma/common/json';
|
||||
|
||||
const log = createLogger('Bridge.Web');
|
||||
|
||||
export interface SetNoticeRetSuccess {
|
||||
ec?: number;
|
||||
em?: string;
|
||||
[key: string]: any;
|
||||
[key: string]: JsonValue | undefined;
|
||||
}
|
||||
|
||||
export interface UploadImageRetSuccess {
|
||||
ec?: number;
|
||||
id?: string;
|
||||
[key: string]: any;
|
||||
[key: string]: JsonValue | undefined;
|
||||
}
|
||||
|
||||
export interface WebApiGroupNoticeFeed {
|
||||
@@ -24,16 +25,16 @@ export interface WebApiGroupNoticeFeed {
|
||||
text: string;
|
||||
pics?: Array<{ id: string; w: number; h: number }>;
|
||||
};
|
||||
settings: any;
|
||||
settings: JsonValue;
|
||||
read_num: number;
|
||||
[key: string]: any;
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
|
||||
export interface WebApiGroupNoticeRet {
|
||||
ec: number;
|
||||
em?: string;
|
||||
feeds?: Record<string, WebApiGroupNoticeFeed>;
|
||||
[key: string]: any;
|
||||
[key: string]: JsonValue | Record<string, WebApiGroupNoticeFeed> | undefined;
|
||||
}
|
||||
|
||||
export function calculateBkn(key: string): string {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import https from 'node:https';
|
||||
import http from 'node:http';
|
||||
|
||||
type RequestBody = string | Buffer | Uint8Array | Record<string, unknown> | undefined;
|
||||
|
||||
export class RequestUtil {
|
||||
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string; }> {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
@@ -50,7 +52,7 @@ export class RequestUtil {
|
||||
});
|
||||
}
|
||||
|
||||
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
|
||||
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: RequestBody, headers: {
|
||||
[key: string]: string;
|
||||
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true, maxRedirects: number = 5): Promise<T> {
|
||||
const option = new URL(url);
|
||||
@@ -91,8 +93,8 @@ export class RequestUtil {
|
||||
} else {
|
||||
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
||||
}
|
||||
} catch (parseError: any) {
|
||||
reject(new Error(parseError.message));
|
||||
} catch (parseError: unknown) {
|
||||
reject(new Error(parseError instanceof Error ? parseError.message : String(parseError)));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -105,7 +107,7 @@ export class RequestUtil {
|
||||
});
|
||||
}
|
||||
|
||||
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string; } = {}) {
|
||||
static async HttpGetText(url: string, method: string = 'GET', data?: RequestBody, headers: { [key: string]: string; } = {}) {
|
||||
return this.HttpGetJson<string>(url, method, data, headers, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/**
|
||||
* Runtime fallback functions.
|
||||
* If the Vite plugin is active, calls to protobuf_encode/decode are replaced
|
||||
* at compile-time with generated type-specific functions.
|
||||
* If NOT transformed, these fallbacks throw to alert the developer.
|
||||
*/
|
||||
import type { MessageRegistry } from './ast/types.js';
|
||||
import { buildDependencyRegistry } from './ast/dependency-graph.js';
|
||||
import type { MessageRegistry } from './ast/types.js';
|
||||
import { generateCode } from './codegen/generator.js';
|
||||
import {
|
||||
normalizeRuntimeMapPath,
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface MessageChainSender {
|
||||
const SINGLE_USE_TYPES = new Set(['reply']);
|
||||
|
||||
export class MessageChain<THasReply extends boolean = false> implements MessageChainLike {
|
||||
private readonly replyStateBrand?: THasReply;
|
||||
declare private readonly replyStateBrand?: THasReply;
|
||||
|
||||
private constructor(
|
||||
private readonly items: AnyMessageSegment[] = [],
|
||||
|
||||
@@ -73,16 +73,12 @@ function loadAddon(): unknown {
|
||||
type BetterSqlite3Type = typeof BetterSqlite3;
|
||||
type DatabaseInstance = InstanceType<BetterSqlite3Type>;
|
||||
type DatabaseOptions = ConstructorParameters<BetterSqlite3Type>[1];
|
||||
type BetterSqlite3Constructor = new (filename?: string | Buffer, options?: DatabaseOptions) => DatabaseInstance;
|
||||
|
||||
const Database = function Database(filename?: string | Buffer, options?: DatabaseOptions): DatabaseInstance {
|
||||
const opts = { ...(options ?? {}), nativeBinding: loadAddon() } as DatabaseOptions;
|
||||
// The `as any` shrug here is unavoidable — better-sqlite3's
|
||||
// overload set declares `new Database(...)` returns the class type
|
||||
// but its default export is a callable that *also* returns it via
|
||||
// an internal `new.target == null` branch. TS can't reconcile the
|
||||
// two without `as any`.
|
||||
|
||||
return new (BetterSqlite3 as any)(filename, opts);
|
||||
// better-sqlite3 的默认导出同时支持函数调用和 new 调用;这里只需要构造器形态。
|
||||
return new (BetterSqlite3 as unknown as BetterSqlite3Constructor)(filename, opts);
|
||||
} as unknown as BetterSqlite3Type;
|
||||
|
||||
export default Database;
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
"ES2022"
|
||||
],
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
Reference in New Issue
Block a user