diff --git a/.gitignore b/.gitignore index 4040fc8..c7875ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +*.g.ts ### IntelliJ IDEA ### .idea/ diff --git a/common/src/generator/typescript/shared.ts b/common/src/generator/typescript/shared.ts index 9c7aa54..2c5b737 100644 --- a/common/src/generator/typescript/shared.ts +++ b/common/src/generator/typescript/shared.ts @@ -1,6 +1,31 @@ import type { IRField } from '@saltify/milky-protocol'; -export function getTypeScriptTypeProjection(field: IRField): string { +export type TypeScriptTypeProjectionMode = 'input' | 'output'; + +export interface TypeScriptTypeProjectionOptions { + mode?: TypeScriptTypeProjectionMode; + getRefTypeName?: (name: string, mode: TypeScriptTypeProjectionMode) => string; +} + +function getDefaultRefTypeName(name: string, mode: TypeScriptTypeProjectionMode): string { + if (mode === 'input') { + return `${name}_ZodInput`; + } + return name; +} + +export function getTypeScriptTypeProjection(field: IRField, options: TypeScriptTypeProjectionOptions = {}): string { + const mode = options.mode ?? 'output'; + const getRefTypeName = options.getRefTypeName ?? getDefaultRefTypeName; + + if (field.isOptional) { + return `${getTypeScriptTypeProjection({ ...field, isOptional: false }, options)} | null | undefined`; + } + + if (mode === 'input' && field.defaultValue !== undefined) { + return `${getTypeScriptTypeProjection({ ...field, defaultValue: undefined }, options)} | null | undefined`; + } + if (field.fieldType === 'scalar') { if (field.scalarType === 'string') { if (field.isArray) { @@ -35,9 +60,9 @@ export function getTypeScriptTypeProjection(field: IRField): string { if (field.fieldType === 'ref') { if (field.isArray) { - return `${field.refStructName}[]`; + return `${getRefTypeName(field.refStructName, mode)}[]`; } else { - return field.refStructName; + return getRefTypeName(field.refStructName, mode); } } diff --git a/common/src/generator/typescript/static.ts b/common/src/generator/typescript/static.ts index 3e1d613..9679e3a 100644 --- a/common/src/generator/typescript/static.ts +++ b/common/src/generator/typescript/static.ts @@ -1,9 +1,35 @@ -import type { IR } from '@saltify/milky-protocol'; +import type { IR, IRField } from '@saltify/milky-protocol'; import { getApiTypeNames } from '../shared/ir'; import { normalizeDerivedStructName } from '../shared/naming'; import { createLineWriter, formatBlockDocComment } from '../shared/text'; -import { getTypeScriptTypeProjection } from './shared'; +import { getTypeScriptTypeProjection, type TypeScriptTypeProjectionMode } from './shared'; + +function getZodInputTypeName(name: string): string { + return `${name}_ZodInput`; +} + +function getTypeScriptFieldProjection(field: IRField, mode: TypeScriptTypeProjectionMode): string { + return getTypeScriptTypeProjection(field, { mode }); +} + +function isFieldOptionalInProjection(field: IRField, mode: TypeScriptTypeProjectionMode): boolean { + return field.isOptional || (mode === 'input' && field.defaultValue !== undefined); +} + +function renderField( + l: (line?: string) => void, + field: IRField, + mode: TypeScriptTypeProjectionMode, + indent: string = ' ', +) { + formatBlockDocComment(field.description, field.since, indent).forEach((line) => { + l(line); + }); + l( + `${indent}${field.name}${isFieldOptionalInProjection(field, mode) ? '?' : ''}: ${getTypeScriptFieldProjection(field, mode)};`, + ); +} export function generateTypeScriptStaticSpec(ir: IR): string { const writer = createLineWriter(); @@ -25,10 +51,16 @@ export function generateTypeScriptStaticSpec(ir: IR): string { }); l(`export interface ${struct.name} {`); struct.fields.forEach((field) => { - formatBlockDocComment(field.description, field.since, ' ').forEach((line) => { - l(line); - }); - l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`); + renderField(l, field, 'output'); + }); + l('}'); + l(); + formatBlockDocComment(struct.description, struct.since).forEach((line) => { + l(line); + }); + l(`export interface ${getZodInputTypeName(struct.name)} {`); + struct.fields.forEach((field) => { + renderField(l, field, 'input'); }); l('}'); } else { @@ -42,10 +74,20 @@ export function generateTypeScriptStaticSpec(ir: IR): string { l(` /** 数据类型区分字段,表示自身为${derivedStruct.description} */`); l(` ${struct.tagFieldName}: '${derivedStruct.tagValue}';`); derivedStruct.fields.forEach((field) => { - formatBlockDocComment(field.description, field.since, ' ').forEach((line) => { - l(line); - }); - l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`); + renderField(l, field, 'output'); + }); + l('}'); + l(); + formatBlockDocComment(derivedStruct.description, derivedStruct.since).forEach((line) => { + l(line); + }); + l( + `export interface ${getZodInputTypeName(normalizeDerivedStructName(struct.name, derivedStruct.tagValue))} {`, + ); + l(` /** 数据类型区分字段,表示自身为${derivedStruct.description} */`); + l(` ${struct.tagFieldName}: '${derivedStruct.tagValue}';`); + derivedStruct.fields.forEach((field) => { + renderField(l, field, 'input'); }); l('}'); l(); @@ -59,36 +101,67 @@ export function generateTypeScriptStaticSpec(ir: IR): string { ` | ${normalizeDerivedStructName(struct.name, derivedStruct.tagValue)}${index === struct.derivedStructs.length - 1 ? ';' : ''}`, ); }); + l(); + formatBlockDocComment(struct.description, struct.since).forEach((line) => { + l(line); + }); + l(`export type ${getZodInputTypeName(struct.name)} =`); + struct.derivedStructs.forEach((derivedStruct, index) => { + l( + ` | ${getZodInputTypeName(normalizeDerivedStructName(struct.name, derivedStruct.tagValue))}${index === struct.derivedStructs.length - 1 ? ';' : ''}`, + ); + }); } else { // with data property struct.derivedTypes.forEach((derivedType) => { + const derivedStructName = normalizeDerivedStructName(struct.name, derivedType.tagValue); formatBlockDocComment(derivedType.description, derivedType.since).forEach((line) => { l(line); }); + l(`export interface ${derivedStructName} {`); + l(` /** 数据类型区分字段,表示自身为${derivedType.description} */`); + l(` ${struct.tagFieldName}: '${derivedType.tagValue}';`); + struct.baseFields.forEach((field) => { + renderField(l, field, 'output'); + }); + if (derivedType.derivingType === 'struct') { - l(`export interface ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data {`); + l(` /** 数据内容 */`); + l(` data: {`); derivedType.fields.forEach((field) => { - formatBlockDocComment(field.description, field.since, ' ').forEach((line) => { - l(line); - }); - l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`); + renderField(l, field, 'output', ' '); }); - l('}'); + l(' }'); } else { // ref type - l( - `export type ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data = ${derivedType.refStructName};`, - ); + l(` /** 数据内容 */`); + l(` data: ${derivedType.refStructName};`); } - // add type aliases for Event - if (struct.name === 'Event') { - formatBlockDocComment(derivedType.description, derivedType.since).forEach((line) => { - l(line); + l('}'); + l(); + formatBlockDocComment(derivedType.description, derivedType.since).forEach((line) => { + l(line); + }); + l(`export interface ${getZodInputTypeName(derivedStructName)} {`); + l(` /** 数据类型区分字段,表示自身为${derivedType.description} */`); + l(` ${struct.tagFieldName}: '${derivedType.tagValue}';`); + struct.baseFields.forEach((field) => { + renderField(l, field, 'input'); + }); + + if (derivedType.derivingType === 'struct') { + l(` /** 数据内容 */`); + l(` data: {`); + derivedType.fields.forEach((field) => { + renderField(l, field, 'input', ' '); }); - l( - `export type ${normalizeDerivedStructName(struct.name, derivedType.tagValue)} = ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data;`, - ); + l(' }'); + } else { + // ref type + l(` /** 数据内容 */`); + l(` data: ${getZodInputTypeName(derivedType.refStructName)};`); } + l('}'); l(); }); formatBlockDocComment(struct.description, struct.since).forEach((line) => { @@ -96,10 +169,19 @@ export function generateTypeScriptStaticSpec(ir: IR): string { }); l(`export type ${struct.name} =`); struct.derivedTypes.forEach((derivedType, index) => { - l(' | {'); - l(` ${struct.tagFieldName}: '${derivedType.tagValue}';`); - l(` data: ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data;`); - l(` }${index === struct.derivedTypes.length - 1 ? ';' : ''}`); + l( + ` | ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}${index === struct.derivedTypes.length - 1 ? ';' : ''}`, + ); + }); + l(); + formatBlockDocComment(struct.description, struct.since).forEach((line) => { + l(line); + }); + l(`export type ${getZodInputTypeName(struct.name)} =`); + struct.derivedTypes.forEach((derivedType, index) => { + l( + ` | ${getZodInputTypeName(normalizeDerivedStructName(struct.name, derivedType.tagValue))}${index === struct.derivedTypes.length - 1 ? ';' : ''}`, + ); }); } } @@ -119,32 +201,52 @@ export function generateTypeScriptStaticSpec(ir: IR): string { if (api.requestFields !== undefined) { l(`export interface ${typeNames.inputName} {`); api.requestFields.forEach((field) => { - formatBlockDocComment(field.description, field.since, ' ').forEach((line) => { - l(line); - }); - l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`); + renderField(l, field, 'output'); }); l('}'); } else { l(`export type ${typeNames.inputName} = {};`); } l(); + formatBlockDocComment(`${api.description} API 请求参数`, api.since).forEach((line) => { + l(line); + }); + if (api.requestFields !== undefined) { + l(`export interface ${getZodInputTypeName(typeNames.inputName)} {`); + api.requestFields.forEach((field) => { + renderField(l, field, 'input'); + }); + l('}'); + } else { + l(`export type ${getZodInputTypeName(typeNames.inputName)} = {};`); + } + l(); formatBlockDocComment(`${api.description} API 响应数据`, api.since).forEach((line) => { l(line); }); if (api.responseFields !== undefined) { l(`export interface ${typeNames.outputName} {`); api.responseFields.forEach((field) => { - formatBlockDocComment(field.description, field.since, ' ').forEach((line) => { - l(line); - }); - l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`); + renderField(l, field, 'output'); }); l('}'); } else { l(`export type ${typeNames.outputName} = {};`); } l(); + formatBlockDocComment(`${api.description} API 响应数据`, api.since).forEach((line) => { + l(line); + }); + if (api.responseFields !== undefined) { + l(`export interface ${getZodInputTypeName(typeNames.outputName)} {`); + api.responseFields.forEach((field) => { + renderField(l, field, 'input'); + }); + l('}'); + } else { + l(`export type ${getZodInputTypeName(typeNames.outputName)} = {};`); + } + l(); }); }); l('export interface ApiCategories {'); @@ -158,13 +260,33 @@ export function generateTypeScriptStaticSpec(ir: IR): string { }); l(` ${api.endpoint}: {`); l(` request: ${typeNames.inputName};`); + l(` request_ZodInput: ${getZodInputTypeName(typeNames.inputName)};`); l(` response: ${typeNames.outputName};`); + l(` response_ZodInput: ${getZodInputTypeName(typeNames.outputName)};`); l(' };'); }); l(' };'); }); l('}'); l(); + l('export interface ApiEndpoints {'); + ir.apiCategories.forEach((category) => { + category.apis.forEach((api) => { + const typeNames = getApiTypeNames(api.endpoint); + formatBlockDocComment(api.description, api.since, ' ').forEach((line) => { + l(line); + }); + l(` /** ${api.description} */`); + l(` '${api.endpoint}': {`); + l(` request: ${typeNames.inputName};`); + l(` request_ZodInput: ${getZodInputTypeName(typeNames.inputName)};`); + l(` response: ${typeNames.outputName};`); + l(` response_ZodInput: ${getZodInputTypeName(typeNames.outputName)};`); + l(' };'); + }); + }); + l('}'); + l(); return writer.toString(); } diff --git a/common/src/generator/typescript/zod.ts b/common/src/generator/typescript/zod.ts index 7649b92..8c6c0cf 100644 --- a/common/src/generator/typescript/zod.ts +++ b/common/src/generator/typescript/zod.ts @@ -2,7 +2,7 @@ import type { IR, IRField } from '@saltify/milky-protocol'; import { getApiTypeNames } from '../shared/ir'; import { normalizeDerivedStructName, snakeCaseToPascalCase } from '../shared/naming'; -import { createLineWriter } from '../shared/text'; +import { createLineWriter, indentLines } from '../shared/text'; import { getTypeScriptTypeProjection } from './shared'; const applyDropBadElementArrayStructNames = new Set(['GroupNotification']); @@ -10,17 +10,11 @@ const applyDropBadElementArrayStructNames = new Set(['GroupNotification']); const specialReplacements = new Map([ [ 'IncomingReplySegmentData.segments', - [' get segments() {', " return z.array(z.lazy(() => IncomingSegment)).describe('回复消息内容');", ' },'].join( - '\n', - ), + " get segments() { return z.array(IncomingSegment).describe('回复消息内容'); },", ], [ 'OutgoingForwardSegmentData.messages', - [ - ' get messages() {', - " return z.array(z.lazy(() => OutgoingForwardedMessage)).describe('转发消息内容');", - ' },', - ].join('\n'), + " get messages() { return z.array(OutgoingForwardedMessage).describe('转发消息内容'); },", ], ]); @@ -162,46 +156,45 @@ export function generateTypeScriptZodSpec(ir: IR) { struct.derivedTypes.forEach((derived) => { const structName = `${normalizeDerivedStructName(struct.name, derived.tagValue)}Data`; if (derived.derivingType === 'struct') { - l( - `export const ${structName} = ${renderIRObject(ir, structName, derived.fields, derived.description, true)};`, - ); - l(`export type ${structName} = z.infer;`); - l(); - // add type aliases for Event - if (struct.name === 'Event') { - l(`export const ${normalizeDerivedStructName(struct.name, derived.tagValue)} = ${structName};`); + l(`export const ${normalizeDerivedStructName(struct.name, derived.tagValue)} = z.object({`); + l(` ${struct.tagFieldName}: z.literal('${derived.tagValue}'),`); + struct.baseFields.forEach((field) => { l( - `export type ${normalizeDerivedStructName(struct.name, derived.tagValue)} = z.infer;`, + ` ${field.name}: ${getZodTypeSpec(ir, field, { + dropBadElementArrayStructNames: applyDropBadElementArrayStructNames, + })}.describe('${field.description}'),`, ); - l(); - } - } - }); - l(`export const ${struct.name} = z.discriminatedUnion('${struct.tagFieldName}', [`); - struct.derivedTypes.forEach((derived, index) => { - l(' z.object({'); - l(` ${struct.tagFieldName}: z.literal('${derived.tagValue}'),`); - struct.baseFields.forEach((field) => { + }); l( - ` ${field.name}: ${getZodTypeSpec(ir, field, { - dropBadElementArrayStructNames: applyDropBadElementArrayStructNames, - })}.describe('${field.description}'),`, + ` data: ${indentLines(renderIRObject(ir, structName, derived.fields, derived.description, false), ' ').trimStart()},`, ); - }); - if (derived.derivingType === 'struct') { + l(`}).describe('${derived.description}');`); l( - ` data: ${normalizeDerivedStructName(struct.name, derived.tagValue)}Data.describe('${derived.description}'),`, + `export type ${normalizeDerivedStructName(struct.name, derived.tagValue)} = z.infer;`, ); - l(` }).describe('${derived.description}'),`); + l(); } else { - // ref type - l(` data: z.lazy(() => ${derived.refStructName}).describe('${derived.description}'),`); - l(` }).describe('${derived.description}'),`); - } - if (index !== struct.derivedTypes.length - 1) { + l(`export const ${normalizeDerivedStructName(struct.name, derived.tagValue)} = z.object({`); + l(` ${struct.tagFieldName}: z.literal('${derived.tagValue}'),`); + struct.baseFields.forEach((field) => { + l( + ` ${field.name}: ${getZodTypeSpec(ir, field, { + dropBadElementArrayStructNames: applyDropBadElementArrayStructNames, + })}.describe('${field.description}'),`, + ); + }); + l(` data: z.lazy(() => ${derived.refStructName}).describe('${derived.description}'),`); + l(`}).describe('${derived.description}');`); + l( + `export type ${normalizeDerivedStructName(struct.name, derived.tagValue)} = z.infer;`, + ); l(); } }); + l(`export const ${struct.name} = z.discriminatedUnion('${struct.tagFieldName}', [`); + struct.derivedTypes.forEach((derived) => { + l(` ${normalizeDerivedStructName(struct.name, derived.tagValue)},`); + }); } if (struct.name === 'IncomingSegment') { l(']).catch({'); @@ -271,6 +264,18 @@ export function generateTypeScriptZodSpec(ir: IR) { }); l('};'); l(); + l('export const zodApiEndpoints = {'); + ir.apiCategories.forEach((category) => { + category.apis.forEach((spec) => { + l(` ${spec.endpoint}: {`); + l(` description: '${spec.description}',`); + l(` requestSchema: ${spec.requestFields ? getApiTypeNames(spec.endpoint).inputName : 'null'},`); + l(` responseSchema: ${spec.responseFields ? getApiTypeNames(spec.endpoint).outputName : 'null'},`); + l(' },'); + }); + }); + l('};'); + l(); return writer.toString(); } diff --git a/docs/app/raw/json-schema/schema.json/route.ts b/docs/app/raw/json-schema/schema.json/route.ts index c376405..e45426e 100644 --- a/docs/app/raw/json-schema/schema.json/route.ts +++ b/docs/app/raw/json-schema/schema.json/route.ts @@ -4,5 +4,13 @@ import { ir } from '@saltify/milky-protocol'; export const dynamic = 'force-static'; export async function GET() { - return new Response(JSON.stringify(await generateJsonSchema(ir, await import('@saltify/milky-types')))); + return new Response( + JSON.stringify( + await generateJsonSchema( + ir, + // @ts-expect-error + await import('@/zod-types.g'), + ), + ), + ); } diff --git a/docs/app/raw/openapi/openapi.json/route.ts b/docs/app/raw/openapi/openapi.json/route.ts index 7f4d82c..809c26d 100644 --- a/docs/app/raw/openapi/openapi.json/route.ts +++ b/docs/app/raw/openapi/openapi.json/route.ts @@ -4,5 +4,13 @@ import { ir } from '@saltify/milky-protocol'; export const dynamic = 'force-static'; export async function GET() { - return new Response(JSON.stringify(await generateOpenApiSpec(ir, await import('@saltify/milky-types')))); + return new Response( + JSON.stringify( + await generateOpenApiSpec( + ir, + // @ts-expect-error + await import('@/zod-types.g'), + ), + ), + ); } diff --git a/docs/content/awesome.md b/docs/content/awesome.md index 3b316c7..ad55974 100644 --- a/docs/content/awesome.md +++ b/docs/content/awesome.md @@ -49,10 +49,8 @@ Milky 的协议内容以 Milky IR 的形式发布在 npm 包 [@saltify/milky-pro 以下是由 Milky 社区提供的类型定义包,可以在项目中直接引用。虽然没有直接提供实现或对接功能,但可以帮助开发者快速编写 Milky 协议的实现或 SDK。 -- **TypeScript** - [@saltify/milky-types](https://www.npmjs.com/package/@saltify/milky-types) (MIT) - **.NET** - [Milky.Net.Model](https://www.nuget.org/packages/Milky.Net.Model) (MIT) - **Rust** - [milky-types](https://crates.io/crates/milky-types) (MIT **or** Apache 2.0) -- **Kotlin** - [milky-kt-types](https://central.sonatype.com/artifact/org.ntqqrev/milky-kt-types) (MIT) ### 原始文件与实用资源 diff --git a/docs/package.json b/docs/package.json index ac2cf13..3c22a3d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,7 +11,6 @@ "dependencies": { "@saltify/milky-common": "workspace:*", "@saltify/milky-protocol": "workspace:*", - "@saltify/milky-types": "workspace:*", "next": "^16.2.7", "nextra": "^4.6.1", "nextra-theme-docs": "^4.6.1", diff --git a/generator/README.md b/generator/README.md index 6a5e15d..770b0a3 100644 --- a/generator/README.md +++ b/generator/README.md @@ -35,7 +35,7 @@ milkygen generate [--output ] [--version ] ```bash milkygen generate openapi milkygen generate json-schema -o ./dist/schema.json -milkygen generate typescript/zod -version latest --output ./generated/schema.ts +milkygen generate typescript/zod --version latest --output ./generated/schema.ts milkygen generate rust/serde --version 1.2.1 ``` diff --git a/package.json b/package.json index 116a902..28ba039 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "milky", "scripts": { - "prepare": "tsx generator/src/cli.ts generate typescript/zod -v local -o types/src/index.g.ts", + "prepare": "tsx generator/src/cli.ts generate typescript/zod -v local -o docs/zod-types.g.ts", "docs:dev": "pnpm --filter @saltify/milky-docs dev", "check": "biome check .", "check:fix": "biome check --write .", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e34e9a..0aac11d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: '@saltify/milky-protocol': specifier: workspace:* version: link:../protocol - '@saltify/milky-types': - specifier: workspace:* - version: link:../types next: specifier: ^16.2.7 version: 16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -93,16 +90,6 @@ importers: protocol: {} - types: - dependencies: - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@saltify/milky-protocol': - specifier: workspace:* - version: link:../protocol - packages: '@antfu/install-pkg@1.1.0': diff --git a/protocol/package.json b/protocol/package.json index 27af9e4..da9484e 100644 --- a/protocol/package.json +++ b/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@saltify/milky-protocol", "type": "module", - "version": "1.2.2", + "version": "1.3.0-rc.2", "description": "Milky protocol definition in form of IR", "main": "src/index.ts", "publishConfig": { diff --git a/protocol/src/ir/api/file.ts b/protocol/src/ir/api/file.ts index 6135d9d..84669d7 100644 --- a/protocol/src/ir/api/file.ts +++ b/protocol/src/ir/api/file.ts @@ -20,7 +20,8 @@ export const fileApiCategory: IRApiCategory = category('file', '文件 API', [ api('get_private_file_download_url', '获取私聊文件下载链接', [ scalarField('user_id', '好友 QQ 号', 'int64', { dataType: 'uin' }), scalarField('file_id', '文件 ID', 'string'), - scalarField('file_hash', '文件的 TriSHA1 哈希值', 'string') + scalarField('file_hash', '文件的 TriSHA1 哈希值', 'string'), + scalarField('is_self_send', '是否为自己发送的文件', 'bool', { defaultValue: false }) ], [ scalarField('download_url', '文件下载链接', 'string') ]), @@ -53,6 +54,10 @@ export const fileApiCategory: IRApiCategory = category('file', '文件 API', [ scalarField('group_id', '群号', 'int64', { dataType: 'uin' }), scalarField('file_id', '文件 ID', 'string') ]), + api('persist_group_file', '转存群文件为永久文件', [ + scalarField('group_id', '群号', 'int64', { dataType: 'uin' }), + scalarField('file_id', '文件 ID', 'string') + ], undefined, { since: '1.3' }), api('create_group_folder', '创建群文件夹', [ scalarField('group_id', '群号', 'int64', { dataType: 'uin' }), scalarField('folder_name', '文件夹名称', 'string') diff --git a/protocol/src/ir/api/system.ts b/protocol/src/ir/api/system.ts index 779e444..dce84c2 100644 --- a/protocol/src/ir/api/system.ts +++ b/protocol/src/ir/api/system.ts @@ -21,7 +21,7 @@ export const systemApiCategory: IRApiCategory = category('system', '系统 API', 'harmony', 'watch' ]), - scalarField('milky_version', '协议端实现的 Milky 协议版本,目前为 "1.2"', 'string') + scalarField('milky_version', '协议端实现的 Milky 协议版本,目前为 "1.3"', 'string') ]), api('get_user_profile', '获取用户个人信息', [ scalarField('user_id', '用户 QQ 号', 'int64', { dataType: 'uin' }) diff --git a/protocol/src/ir/common.ts b/protocol/src/ir/common.ts index 3abcd2d..bb3c88c 100644 --- a/protocol/src/ir/common.ts +++ b/protocol/src/ir/common.ts @@ -96,6 +96,10 @@ const Event = nestedUnion('Event', '事件', 'event_type', [ scalarField('user_id', '发生变更的用户 QQ 号', 'int64', { dataType: 'uin' }), scalarField('operator_id', '管理员 QQ 号,如果是管理员踢出', 'int64', { isOptional: true }) ]), + nestedUnionStructVariant('group_disband', '群解散事件', [ + scalarField('group_id', '群号', 'int64', { dataType: 'uin' }), + scalarField('operator_id', '操作者 QQ 号', 'int64', { dataType: 'uin' }) + ], { since: '1.3' }), nestedUnionStructVariant('group_name_change', '群名称变更事件', [ scalarField('group_id', '群号', 'int64', { dataType: 'uin' }), scalarField('new_group_name', '新的群名称', 'string'), @@ -373,12 +377,16 @@ const IncomingSegment = nestedUnion('IncomingSegment', '接收消息段', 'type' nestedUnionStructVariant('xml', 'XML 消息段', [ scalarField('service_id', '服务 ID', 'int32'), scalarField('xml_payload', 'XML 数据', 'string') + ]), + nestedUnionStructVariant('markdown', 'Markdown 消息段', [ + scalarField('content', 'Markdown 内容', 'string') ]) ]); const OutgoingForwardedMessage = struct('OutgoingForwardedMessage', '发送转发消息', [ scalarField('user_id', '发送者 QQ 号', 'int64', { dataType: 'uin' }), scalarField('sender_name', '发送者名称', 'string'), + scalarField('time', '消息 Unix 时间戳(秒)', 'int64', { isOptional: true, since: '1.3' }), refField('segments', '消息段列表', 'OutgoingSegment', { isArray: true }) ]); @@ -392,7 +400,7 @@ const OutgoingSegment = nestedUnion('OutgoingSegment', '发送消息段', 'type' nestedUnionStructVariant('mention_all', '提及全体消息段', []), nestedUnionStructVariant('face', '表情消息段', [ scalarField('face_id', '表情 ID', 'string'), - scalarField('is_large', '是否为超级表情', 'bool', { defaultValue: false }) + scalarField('is_large', '是否为超级表情', 'bool', { defaultValue: false, since: '1.1' }) ]), nestedUnionStructVariant('reply', '回复消息段', [ scalarField('message_seq', '被引用的消息序列号', 'int64') diff --git a/types/.gitignore b/types/.gitignore deleted file mode 100644 index 660bad3..0000000 --- a/types/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist/ -*.g.ts \ No newline at end of file diff --git a/types/README.md b/types/README.md deleted file mode 100644 index 374a23e..0000000 --- a/types/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# @saltify/milky-types - -这是 Milky 协议的 TypeScript 类型定义包,使用 [Zod](https://zod.dev/) 进行运行时类型验证。 - -## 安装 - -```bash -npm install @saltify/milky-types -``` - -## 使用方法 - -### 进行运行时类型验证 - -```typescript -import { FriendEntity } from '@saltify/milky-types'; - -const friend1 = FriendEntity.parse({ - user_id: 123456789, - nickname: 'Alice', - sex: 'female', - qid: 'Saltify', - remark: 'Best friend', - category: { - id: 1, - name: 'Close Friends', - }, -}); // 成功解析,friend1 的类型为 FriendEntity - -const friend2 = FriendEntity.parse({ - user_id: 'not a number', - nickname: 'Bob', -}); // 解析失败,抛出 ZodError 错误 -``` - -### 获取 TypeScript 类型定义 - -```typescript -import { FriendEntity } from '@saltify/milky-types'; - -const friend1: FriendEntity = { - user_id: 123456789, - nickname: 'Alice', - sex: 'female', - qid: 'Saltify', - remark: 'Best friend', - category: { - id: 1, - name: 'Close Friends', - }, -}; // friend1 的类型为 FriendEntity - -const friend2: FriendEntity = { - user_id: 'not a number', - nickname: 'Bob', -}; // TypeScript 编译错误,user_id 应为 number 类型,并且缺少 sex、qid、remark 和 category 字段 -``` diff --git a/types/package.json b/types/package.json deleted file mode 100644 index c35416d..0000000 --- a/types/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@saltify/milky-types", - "type": "module", - "version": "1.2.2", - "description": "Type definitions for Milky protocol", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.mjs", - "typings": "dist/index.d.mts" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsdown", - "prepack": "npm run build" - }, - "keywords": [], - "author": "SaltifyDev", - "repository": { - "type": "git", - "url": "git+https://github.com/SaltifyDev/milky.git" - }, - "license": "MIT", - "dependencies": { - "zod": "^4.3.6" - }, - "devDependencies": { - "@saltify/milky-protocol": "workspace:*" - } -} diff --git a/types/src/index.ts b/types/src/index.ts deleted file mode 100644 index bb14cd2..0000000 --- a/types/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './index.g'; diff --git a/types/tsconfig.json b/types/tsconfig.json deleted file mode 100644 index 5d27393..0000000 --- a/types/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "paths": { "@/*": ["./src/*"] }, - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": false, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "isolatedModules": true, - "declaration": true, - "strict": true, - "noFallthroughCasesInSwitch": true - } -} diff --git a/types/tsdown.config.ts b/types/tsdown.config.ts deleted file mode 100644 index 861aed5..0000000 --- a/types/tsdown.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'tsdown'; - -export default defineConfig({ - entry: 'src/index.ts', - format: 'esm', - dts: true, - clean: true, - target: false, -});