mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(server-v2): expose /openapi.json via @fastify/swagger
- register @fastify/swagger before routes and serve GET /openapi.json - add v2-specific openapi transform for multipart upload, binary downloads, and the fs-action/question oneOf dispatchers - project the session-action dispatcher into archive only (v2 registers a subset of v1 routes) - reuse protocol wire schemas, no inline re-declaration
This commit is contained in:
parent
299e6d6bd8
commit
ce0e4f2589
5 changed files with 514 additions and 1 deletions
|
|
@ -21,6 +21,7 @@
|
|||
"@moonshot-ai/agent-core-v2": "workspace:^",
|
||||
"@moonshot-ai/protocol": "workspace:^",
|
||||
"@fastify/multipart": "^10.0.0",
|
||||
"@fastify/swagger": "^9.7.0",
|
||||
"fastify": "^5.1.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"pino": "^9.5.0",
|
||||
|
|
|
|||
341
packages/server-v2/src/openapi/transforms.ts
Normal file
341
packages/server-v2/src/openapi/transforms.ts
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
/**
|
||||
* `openapi` transforms — post-process the `@fastify/swagger` document so the
|
||||
* routes that swagger cannot describe from static schemas (multipart upload,
|
||||
* binary download, and the `{tail}` dispatchers) are represented accurately.
|
||||
*
|
||||
* Tailored to the route subset `server-v2` actually registers. Notably, the
|
||||
* session-action dispatcher only serves `::archive` in v2, so this module
|
||||
* projects `/sessions/{tail}` into a single `/sessions/{session_id}:archive`
|
||||
* operation — unlike v1, which clones it into fork / compact / undo. Wire
|
||||
* schemas are re-used from `@moonshot-ai/protocol`; none are re-declared here.
|
||||
*/
|
||||
|
||||
import {
|
||||
archiveSessionResponseSchema,
|
||||
fsDiffRequestSchema,
|
||||
fsDiffResponseSchema,
|
||||
fsGitStatusRequestSchema,
|
||||
fsGitStatusResponseSchema,
|
||||
fsGrepRequestSchema,
|
||||
fsGrepResponseSchema,
|
||||
fsListManyRequestSchema,
|
||||
fsListManyResponseSchema,
|
||||
fsListRequestSchema,
|
||||
fsListResponseSchema,
|
||||
fsMkdirRequestSchema,
|
||||
fsMkdirResponseSchema,
|
||||
fsOpenInRequestSchema,
|
||||
fsOpenInResponseSchema,
|
||||
fsOpenRequestSchema,
|
||||
fsOpenResponseSchema,
|
||||
fsReadRequestSchema,
|
||||
fsReadResponseSchema,
|
||||
fsRevealRequestSchema,
|
||||
fsRevealResponseSchema,
|
||||
fsSearchRequestSchema,
|
||||
fsSearchResponseSchema,
|
||||
fsStatManyRequestSchema,
|
||||
fsStatManyResponseSchema,
|
||||
fsStatRequestSchema,
|
||||
fsStatResponseSchema,
|
||||
questionDismissResultSchema,
|
||||
questionResolveRequestSchema,
|
||||
questionResolveResultSchema,
|
||||
} from '@moonshot-ai/protocol';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
openApiDocumentEnvelopeJsonSchema,
|
||||
openApiDocumentJsonSchema,
|
||||
} from '../middleware/schema';
|
||||
|
||||
const binarySchema = {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
} as const;
|
||||
|
||||
const fileUploadMultipartSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: binarySchema,
|
||||
name: { type: 'string' },
|
||||
expires_in_sec: { type: 'number', minimum: 0 },
|
||||
},
|
||||
required: ['file'],
|
||||
} as const;
|
||||
|
||||
const errorEnvelopeSchema = openApiDocumentEnvelopeJsonSchema(z.null());
|
||||
|
||||
const fsActionRequestSchema = {
|
||||
oneOf: [
|
||||
openApiDocumentJsonSchema(fsListRequestSchema),
|
||||
openApiDocumentJsonSchema(fsReadRequestSchema),
|
||||
openApiDocumentJsonSchema(fsListManyRequestSchema),
|
||||
openApiDocumentJsonSchema(fsStatRequestSchema),
|
||||
openApiDocumentJsonSchema(fsStatManyRequestSchema),
|
||||
openApiDocumentJsonSchema(fsMkdirRequestSchema),
|
||||
openApiDocumentJsonSchema(fsSearchRequestSchema),
|
||||
openApiDocumentJsonSchema(fsGrepRequestSchema),
|
||||
openApiDocumentJsonSchema(fsGitStatusRequestSchema),
|
||||
openApiDocumentJsonSchema(fsDiffRequestSchema),
|
||||
openApiDocumentJsonSchema(fsOpenRequestSchema),
|
||||
openApiDocumentJsonSchema(fsOpenInRequestSchema),
|
||||
openApiDocumentJsonSchema(fsRevealRequestSchema),
|
||||
],
|
||||
} as const;
|
||||
|
||||
const fsActionResponseSchema = {
|
||||
oneOf: [
|
||||
openApiDocumentEnvelopeJsonSchema(fsListResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsReadResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsListManyResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsStatResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsStatManyResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsMkdirResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsSearchResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsGrepResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsGitStatusResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsDiffResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsOpenResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsOpenInResponseSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(fsRevealResponseSchema),
|
||||
],
|
||||
} as const;
|
||||
|
||||
const questionResponseSchema = {
|
||||
oneOf: [
|
||||
openApiDocumentEnvelopeJsonSchema(questionResolveResultSchema),
|
||||
openApiDocumentEnvelopeJsonSchema(questionDismissResultSchema),
|
||||
],
|
||||
} as const;
|
||||
|
||||
export function transformOpenApiDocument(
|
||||
document: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const paths = asRecord(document['paths']);
|
||||
if (paths === undefined) return document;
|
||||
|
||||
patchFileUpload(paths);
|
||||
patchFileDownload(paths);
|
||||
patchSessionAction(paths);
|
||||
patchFsAction(paths);
|
||||
patchFsDownload(paths);
|
||||
patchQuestionResolveOrDismiss(paths);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
function patchFileUpload(paths: Record<string, unknown>): void {
|
||||
const operation = getOperation(paths, '/api/v1/files', 'post');
|
||||
if (operation === undefined) return;
|
||||
|
||||
operation['requestBody'] = {
|
||||
required: true,
|
||||
content: {
|
||||
'multipart/form-data': {
|
||||
schema: fileUploadMultipartSchema,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function patchFileDownload(paths: Record<string, unknown>): void {
|
||||
const operation = getOperation(paths, '/api/v1/files/{file_id}', 'get');
|
||||
if (operation === undefined) return;
|
||||
|
||||
setResponse(operation, '200', {
|
||||
description: 'Binary file download',
|
||||
headers: {
|
||||
'content-disposition': headerString(),
|
||||
'content-length': headerInteger(),
|
||||
etag: headerString(),
|
||||
},
|
||||
content: {
|
||||
'application/octet-stream': {
|
||||
schema: binarySchema,
|
||||
},
|
||||
},
|
||||
});
|
||||
setResponse(operation, '404', {
|
||||
description: 'File not found',
|
||||
content: jsonContent(errorEnvelopeSchema),
|
||||
});
|
||||
}
|
||||
|
||||
function patchSessionAction(paths: Record<string, unknown>): void {
|
||||
const internalPath = '/api/v1/sessions/{tail}';
|
||||
const pathItem = asRecord(paths[internalPath]);
|
||||
const operation = asRecord(pathItem?.['post']);
|
||||
if (pathItem === undefined || operation === undefined) return;
|
||||
|
||||
const cloned = cloneRecord(pathItem);
|
||||
replacePathParamName(cloned, 'tail', 'session_id');
|
||||
const clonedOperation = asRecord(cloned['post']);
|
||||
if (clonedOperation !== undefined) {
|
||||
clonedOperation['operationId'] = 'runSessionArchiveAction';
|
||||
setResponse(clonedOperation, '200', {
|
||||
description: 'Session archive response',
|
||||
content: jsonContent(openApiDocumentEnvelopeJsonSchema(archiveSessionResponseSchema)),
|
||||
});
|
||||
}
|
||||
paths['/api/v1/sessions/{session_id}:archive'] = cloned;
|
||||
delete paths[internalPath];
|
||||
}
|
||||
|
||||
function patchFsAction(paths: Record<string, unknown>): void {
|
||||
const operation = getOperation(paths, '/api/v1/sessions/{session_id}/{tail}', 'post');
|
||||
if (operation === undefined) return;
|
||||
|
||||
operation['description'] = appendDescription(
|
||||
operation['description'],
|
||||
'The request and response schemas depend on the `fs:<action>` path tail and are represented as OpenAPI `oneOf` unions.',
|
||||
);
|
||||
operation['requestBody'] = {
|
||||
required: true,
|
||||
content: jsonContent(fsActionRequestSchema),
|
||||
};
|
||||
setResponse(operation, '200', {
|
||||
description: 'Filesystem action response',
|
||||
content: jsonContent(fsActionResponseSchema),
|
||||
});
|
||||
}
|
||||
|
||||
function patchFsDownload(paths: Record<string, unknown>): void {
|
||||
const operation = getOperation(paths, '/api/v1/sessions/{session_id}/fs/{*}', 'get');
|
||||
if (operation === undefined) return;
|
||||
|
||||
setResponse(operation, '200', {
|
||||
description: 'Binary workspace file download',
|
||||
headers: {
|
||||
'content-disposition': headerString(),
|
||||
'content-length': headerInteger(),
|
||||
etag: headerString(),
|
||||
'last-modified': headerString(),
|
||||
},
|
||||
content: {
|
||||
'application/octet-stream': {
|
||||
schema: binarySchema,
|
||||
},
|
||||
},
|
||||
});
|
||||
setResponse(operation, '206', {
|
||||
description: 'Partial binary workspace file download',
|
||||
headers: {
|
||||
'content-disposition': headerString(),
|
||||
'content-length': headerInteger(),
|
||||
'content-range': headerString(),
|
||||
etag: headerString(),
|
||||
'last-modified': headerString(),
|
||||
},
|
||||
content: {
|
||||
'application/octet-stream': {
|
||||
schema: binarySchema,
|
||||
},
|
||||
},
|
||||
});
|
||||
setResponse(operation, '304', {
|
||||
description: 'Not modified',
|
||||
headers: {
|
||||
etag: headerString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function patchQuestionResolveOrDismiss(paths: Record<string, unknown>): void {
|
||||
const operation = getOperation(paths, '/api/v1/sessions/{session_id}/questions/{tail}', 'post');
|
||||
if (operation === undefined) return;
|
||||
|
||||
operation['description'] = appendDescription(
|
||||
operation['description'],
|
||||
'Resolve uses the question response body; `:dismiss` sends an empty body.',
|
||||
);
|
||||
operation['requestBody'] = {
|
||||
required: false,
|
||||
content: jsonContent(openApiDocumentJsonSchema(questionResolveRequestSchema)),
|
||||
};
|
||||
setResponse(operation, '200', {
|
||||
description: 'Question resolved or dismissed',
|
||||
content: jsonContent(questionResponseSchema),
|
||||
});
|
||||
}
|
||||
|
||||
function getOperation(
|
||||
paths: Record<string, unknown>,
|
||||
path: string,
|
||||
method: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
const pathItem = asRecord(paths[path]);
|
||||
if (pathItem === undefined) return undefined;
|
||||
return asRecord(pathItem[method]);
|
||||
}
|
||||
|
||||
function setResponse(
|
||||
operation: Record<string, unknown>,
|
||||
statusCode: string,
|
||||
response: Record<string, unknown>,
|
||||
): void {
|
||||
const responses = asRecord(operation['responses']) ?? {};
|
||||
responses[statusCode] = response;
|
||||
operation['responses'] = responses;
|
||||
}
|
||||
|
||||
function jsonContent(schema: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
'application/json': {
|
||||
schema,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function headerString(): Record<string, unknown> {
|
||||
return {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function headerInteger(): Record<string, unknown> {
|
||||
return {
|
||||
schema: {
|
||||
type: 'integer',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function appendDescription(existing: unknown, extra: string): string {
|
||||
if (typeof existing !== 'string' || existing.length === 0) return extra;
|
||||
return `${existing} ${extra}`;
|
||||
}
|
||||
|
||||
function replacePathParamName(
|
||||
container: Record<string, unknown>,
|
||||
from: string,
|
||||
to: string,
|
||||
): void {
|
||||
const params = container['parameters'];
|
||||
if (Array.isArray(params)) {
|
||||
for (const param of params) {
|
||||
const record = asRecord(param);
|
||||
if (record?.['in'] === 'path' && record['name'] === from) {
|
||||
record['name'] = to;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
|
||||
const operation = asRecord(container[method]);
|
||||
if (operation !== undefined) {
|
||||
replacePathParamName(operation, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
if (typeof value !== 'object' || value === null) return undefined;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
|
||||
import { installErrorHandler } from './error-handler';
|
||||
import { transformOpenApiDocument } from './openapi/transforms';
|
||||
import { resolveRequestId } from './request-id';
|
||||
import { registerApiV1Routes } from './routes/registerApiV1Routes';
|
||||
// Registers the real `node-pty` `ITerminalBackend`, overriding the
|
||||
|
|
@ -92,8 +93,51 @@ export async function startServer(opts: ServerStartOptions = {}): Promise<Runnin
|
|||
core.dispose();
|
||||
};
|
||||
|
||||
const serverVersion = getServerVersion();
|
||||
|
||||
async function registerOpenApi(): Promise<void> {
|
||||
const { default: swagger } = await import('@fastify/swagger');
|
||||
await app.register(swagger, {
|
||||
openapi: {
|
||||
info: {
|
||||
title: 'Kimi Code Server API',
|
||||
description:
|
||||
'REST API for the Kimi Code local server. All JSON responses are wrapped in a uniform envelope `{ code, msg, data, request_id }`.',
|
||||
version: serverVersion,
|
||||
},
|
||||
tags: [
|
||||
{ name: 'meta', description: 'Server metadata' },
|
||||
{ name: 'auth', description: 'Auth readiness & login state' },
|
||||
{ name: 'models', description: 'Configured model aliases' },
|
||||
{ name: 'providers', description: 'Configured providers' },
|
||||
{ name: 'sessions', description: 'Session lifecycle' },
|
||||
{ name: 'workspaces', description: 'Workspace registry + folder picker' },
|
||||
{ name: 'messages', description: 'Message history' },
|
||||
{ name: 'prompts', description: 'Prompt submission & abort' },
|
||||
{ name: 'approvals', description: 'Approval resolution' },
|
||||
{ name: 'questions', description: 'Question resolution & dismiss' },
|
||||
{ name: 'tools', description: 'Tool & MCP server management' },
|
||||
{ name: 'tasks', description: 'Background tasks' },
|
||||
{ name: 'terminals', description: 'PTY terminal sessions' },
|
||||
{ name: 'fs', description: 'Filesystem operations' },
|
||||
{ name: 'files', description: 'File upload & download' },
|
||||
],
|
||||
},
|
||||
transformObject: (documentObject) => {
|
||||
if (!('openapiObject' in documentObject)) {
|
||||
return documentObject.swaggerObject;
|
||||
}
|
||||
return transformOpenApiDocument(documentObject.openapiObject as Record<string, unknown>);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// `@fastify/swagger` collects route schemas via an `onRoute` hook, so it must
|
||||
// be registered before any routes it should document.
|
||||
await registerOpenApi();
|
||||
|
||||
await registerApiV1Routes(app, core, {
|
||||
serverVersion: getServerVersion(),
|
||||
serverVersion,
|
||||
debugEndpoints: opts.debugEndpoints,
|
||||
onShutdown: () => {
|
||||
void close();
|
||||
|
|
@ -103,6 +147,11 @@ export async function startServer(opts: ServerStartOptions = {}): Promise<Runnin
|
|||
registerRpcRoutes(app, core, { token: opts.rpcToken });
|
||||
registerWs(app, core, { token: opts.rpcToken });
|
||||
|
||||
app.get('/openapi.json', async (_req, reply) => {
|
||||
const openApiDocument = (app as unknown as { swagger(): unknown }).swagger();
|
||||
return reply.type('application/json').send(openApiDocument);
|
||||
});
|
||||
|
||||
const host = opts.host ?? DEFAULT_HOST;
|
||||
const port = opts.port ?? DEFAULT_PORT;
|
||||
await app.listen({ host, port });
|
||||
|
|
|
|||
119
packages/server-v2/test/openapi.test.ts
Normal file
119
packages/server-v2/test/openapi.test.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* OpenAPI smoke test for server-v2.
|
||||
*
|
||||
* Boots the server, fetches `/openapi.json`, and asserts that `@fastify/swagger`
|
||||
* is wired and that the v2-specific post-processing transforms ran (as opposed
|
||||
* to a verbatim copy of v1's transforms, which would fabricate endpoints v2
|
||||
* does not register).
|
||||
*/
|
||||
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { type RunningServer, startServer } from '../src/start';
|
||||
|
||||
describe('server-v2 OpenAPI', () => {
|
||||
let server: RunningServer | undefined;
|
||||
let home: string | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
if (server !== undefined) {
|
||||
await server.close();
|
||||
server = undefined;
|
||||
}
|
||||
if (home !== undefined) {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
home = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchOpenApi(): Promise<Record<string, unknown>> {
|
||||
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-openapi-'));
|
||||
server = await startServer({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
homeDir: home,
|
||||
logLevel: 'silent',
|
||||
});
|
||||
const res = await fetch(`http://127.0.0.1:${server.port}/openapi.json`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toContain('application/json');
|
||||
return (await res.json()) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
it('returns a valid OpenAPI 3 document', async () => {
|
||||
const doc = await fetchOpenApi();
|
||||
|
||||
expect(doc['openapi']).toMatch(/^3\.\d+\.\d+$/);
|
||||
const info = asRecord(doc['info']);
|
||||
expect(info['title']).toBe('Kimi Code Server API');
|
||||
expect(typeof info['version']).toBe('string');
|
||||
});
|
||||
|
||||
it('covers the core /api/v1 routes v2 registers', async () => {
|
||||
const doc = await fetchOpenApi();
|
||||
const paths = asRecord(doc['paths']);
|
||||
|
||||
expect(paths['/api/v1/healthz']).toBeDefined();
|
||||
expect(paths['/api/v1/meta']).toBeDefined();
|
||||
expect(paths['/api/v1/sessions']).toBeDefined();
|
||||
expect(paths['/api/v1/files']).toBeDefined();
|
||||
expect(paths['/api/v1/sessions/{session_id}/fs/{*}']).toBeDefined();
|
||||
});
|
||||
|
||||
it('projects the session-action dispatcher into archive only', async () => {
|
||||
const doc = await fetchOpenApi();
|
||||
const paths = asRecord(doc['paths']);
|
||||
|
||||
// v2 only registers ::archive — the generic `{tail}` path must be gone and
|
||||
// the v1-only actions must not be fabricated.
|
||||
expect(paths['/api/v1/sessions/{tail}']).toBeUndefined();
|
||||
expect(paths['/api/v1/sessions/{session_id}:archive']).toBeDefined();
|
||||
expect(paths['/api/v1/sessions/{session_id}:fork']).toBeUndefined();
|
||||
expect(paths['/api/v1/sessions/{session_id}:undo']).toBeUndefined();
|
||||
|
||||
const archiveOp = operation(doc, '/api/v1/sessions/{session_id}:archive', 'post');
|
||||
expect(archiveOp['operationId']).toBe('runSessionArchiveAction');
|
||||
const params = archiveOp['parameters'] as Array<Record<string, unknown>>;
|
||||
expect(params.some((p) => p['in'] === 'path' && p['name'] === 'session_id')).toBe(true);
|
||||
expect(params.some((p) => p['name'] === 'tail')).toBe(false);
|
||||
});
|
||||
|
||||
it('describes the file upload as multipart/form-data', async () => {
|
||||
const doc = await fetchOpenApi();
|
||||
const uploadOp = operation(doc, '/api/v1/files', 'post');
|
||||
const requestBody = asRecord(uploadOp['requestBody']);
|
||||
const content = asRecord(requestBody['content']);
|
||||
expect(content['multipart/form-data']).toBeDefined();
|
||||
});
|
||||
|
||||
it('represents the fs-action dispatcher as a oneOf union', async () => {
|
||||
const doc = await fetchOpenApi();
|
||||
const fsActionOp = operation(doc, '/api/v1/sessions/{session_id}/{tail}', 'post');
|
||||
const requestBody = asRecord(fsActionOp['requestBody']);
|
||||
const content = asRecord(requestBody['content']);
|
||||
const json = asRecord(content['application/json']);
|
||||
const schema = asRecord(json['schema']);
|
||||
expect(Array.isArray(schema['oneOf'])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
throw new Error('expected object');
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function operation(
|
||||
doc: Record<string, unknown>,
|
||||
path: string,
|
||||
method: string,
|
||||
): Record<string, unknown> {
|
||||
const paths = asRecord(doc['paths']);
|
||||
const pathItem = asRecord(paths[path]);
|
||||
return asRecord(pathItem[method]);
|
||||
}
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -675,6 +675,9 @@ importers:
|
|||
'@fastify/multipart':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
'@fastify/swagger':
|
||||
specifier: ^9.7.0
|
||||
version: 9.7.0
|
||||
'@moonshot-ai/agent-core-v2':
|
||||
specifier: workspace:^
|
||||
version: link:../agent-core-v2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue