Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
*.g.ts

### IntelliJ IDEA ###
.idea/
Expand Down
57 changes: 37 additions & 20 deletions common/src/generator/typescript/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,41 +65,42 @@ export function generateTypeScriptStaticSpec(ir: IR): string {
formatBlockDocComment(derivedType.description, derivedType.since).forEach((line) => {
l(line);
});
l(`export interface ${normalizeDerivedStructName(struct.name, derivedType.tagValue)} {`);
l(` /** 数据类型区分字段,表示自身为${derivedType.description} */`);
l(` ${struct.tagFieldName}: '${derivedType.tagValue}';`);
struct.baseFields.forEach((field) => {
formatBlockDocComment(field.description, field.since, ' ').forEach((line) => {
l(line);
});
l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`);
});

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) => {
formatBlockDocComment(field.description, field.since, ' ').forEach((line) => {
l(line);
});
l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`);
l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`);
});
l('}');
l(' }');
} else {
// ref type
l(
`export type ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data = ${derivedType.refStructName};`,
);
}
// add type aliases for Event
if (struct.name === 'Event') {
formatBlockDocComment(derivedType.description, derivedType.since).forEach((line) => {
l(line);
});
l(
`export type ${normalizeDerivedStructName(struct.name, derivedType.tagValue)} = ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data;`,
);
l(` /** 数据内容 */`);
l(` data: ${derivedType.refStructName};`);
}
l('}');
l();
});
formatBlockDocComment(struct.description, struct.since).forEach((line) => {
l(line);
});
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 ? ';' : ''}`,
);
});
}
}
Expand Down Expand Up @@ -165,6 +166,22 @@ export function generateTypeScriptStaticSpec(ir: IR): string {
});
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(` response: ${typeNames.outputName};`);
l(' };');
});
});
l('}');
l();

return writer.toString();
}
83 changes: 44 additions & 39 deletions common/src/generator/typescript/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,19 @@ 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']);

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('转发消息内容'); },",
],
]);

Expand Down Expand Up @@ -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<typeof ${structName}>;`);
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<typeof ${structName}>;`,
` ${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<typeof ${normalizeDerivedStructName(struct.name, derived.tagValue)}>;`,
);
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<typeof ${normalizeDerivedStructName(struct.name, derived.tagValue)}>;`,
);
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({');
Expand Down Expand Up @@ -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();
}
10 changes: 9 additions & 1 deletion docs/app/raw/json-schema/schema.json/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
),
),
);
}
10 changes: 9 additions & 1 deletion docs/app/raw/openapi/openapi.json/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
),
),
);
}
2 changes: 0 additions & 2 deletions docs/content/awesome.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,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)

### 原始文件与实用资源

Expand Down
1 change: 0 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"dependencies": {
"@saltify/milky-common": "workspace:*",
"@saltify/milky-protocol": "workspace:*",
"@saltify/milky-types": "workspace:*",
"next": "^16.2.4",
"nextra": "^4.6.1",
"nextra-theme-docs": "^4.6.1",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 .",
Expand Down
13 changes: 0 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion protocol/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@saltify/milky-protocol",
"type": "module",
"version": "1.2.2",
"version": "1.3.0-rc.1",
"description": "Milky protocol definition in form of IR",
"main": "src/index.ts",
"publishConfig": {
Expand Down
4 changes: 4 additions & 0 deletions protocol/src/ir/api/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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')
Expand Down
10 changes: 9 additions & 1 deletion protocol/src/ir/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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 })
]);

Expand All @@ -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')
Expand Down
2 changes: 0 additions & 2 deletions types/.gitignore

This file was deleted.

Loading