add first commit

This commit is contained in:
2026-07-21 15:40:49 +08:00
commit b478b3d3b5
45 changed files with 8080 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
VITE_API_BASE_URL = "http://localhost:8000"
+1
View File
@@ -0,0 +1 @@
VITE_API_BASE_URL = "/"
+39
View File
@@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"semi": false,
"singleQuote": true
}
+42
View File
@@ -0,0 +1,42 @@
# chongming-web
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
+10
View File
@@ -0,0 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}
+62
View File
@@ -0,0 +1,62 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import { GlobalComponents } from 'vue'
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElFooter: typeof import('element-plus/es')['ElFooter']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElTag: typeof import('element-plus/es')['ElTag']
ElUpload: typeof import('element-plus/es')['ElUpload']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
}
}
// For TSX support
declare global {
const ElAlert: typeof import('element-plus/es')['ElAlert']
const ElButton: typeof import('element-plus/es')['ElButton']
const ElCard: typeof import('element-plus/es')['ElCard']
const ElContainer: typeof import('element-plus/es')['ElContainer']
const ElDivider: typeof import('element-plus/es')['ElDivider']
const ElFooter: typeof import('element-plus/es')['ElFooter']
const ElForm: typeof import('element-plus/es')['ElForm']
const ElFormItem: typeof import('element-plus/es')['ElFormItem']
const ElHeader: typeof import('element-plus/es')['ElHeader']
const ElIcon: typeof import('element-plus/es')['ElIcon']
const ElInput: typeof import('element-plus/es')['ElInput']
const ElMain: typeof import('element-plus/es')['ElMain']
const ElMenu: typeof import('element-plus/es')['ElMenu']
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
const ElTag: typeof import('element-plus/es')['ElTag']
const ElUpload: typeof import('element-plus/es')['ElUpload']
const HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
const TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
const WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
}
Vendored
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+4398
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "chongming-web",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"format": "oxfmt src/",
"openapi": "npx @hey-api/openapi-ts --input http://127.0.0.1:8000/openapi.json --output ./src/api/generated/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"element-plus": "^2.14.3",
"pinia": "^3.0.4",
"vue": "^3.5.38",
"vue-router": "^5.1.0"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.96.1",
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.13.2",
"@vitejs/plugin-vue": "^6.0.7",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^9.0.2",
"oxfmt": "^0.54.0",
"typescript": "~6.0.0",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^32.1.0",
"vite": "^8.0.16",
"vite-plugin-vue-devtools": "^8.1.2",
"vue-tsc": "^3.3.5"
},
"engines": {
"node": "^22.18.0 || >=24.12.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+87
View File
@@ -0,0 +1,87 @@
<template>
<el-container class="app-container">
<el-header class="app-header">
<div class="header-left">
<el-icon :size="28" color="#409EFF"><Cpu /></el-icon>
<span class="app-title">Chongming 平台</span>
</div>
<el-menu
:default-active="$route.path"
mode="horizontal"
router
class="header-menu"
background-color="transparent"
text-color="#333"
active-text-color="#409EFF"
>
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon> 豆包测试
</el-menu-item>
<el-menu-item index="/about">
<el-icon><InfoFilled /></el-icon> 关于
</el-menu-item>
</el-menu>
</el-header>
<el-main class="app-main">
<router-view />
</el-main>
<el-footer class="app-footer" height="50px">
© 2026 Chongming · 基于 FastAPI + Vue 3 + Element Plus
</el-footer>
</el-container>
</template>
<script setup lang="ts">
import { Cpu, HomeFilled, InfoFilled } from '@element-plus/icons-vue'
</script>
<style scoped>
.app-container {
min-height: 100vh;
background: #f5f7fa;
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 0 24px;
height: 64px;
border-bottom: 1px solid #e4e7ed;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.app-title {
font-size: 20px;
font-weight: 600;
color: #2c3e50;
}
.header-menu {
border-bottom: none;
flex: 1;
justify-content: flex-end;
}
.header-menu .el-menu-item {
height: 64px;
line-height: 64px;
}
.app-main {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.app-footer {
text-align: center;
color: #909399;
font-size: 14px;
border-top: 1px solid #e4e7ed;
background: #ffffff;
}
</style>
+16
View File
@@ -0,0 +1,16 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://127.0.0.1:8000' }));
+298
View File
@@ -0,0 +1,298 @@
// This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from '../core/utils.gen';
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils.gen';
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
const beforeRequest = async <
TData = unknown,
TResponseStyle extends 'data' | 'fields' = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
>(
options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>,
) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined as string | undefined,
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
}
if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined;
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}
const resolvedOpts = opts as typeof opts &
ResolvedRequestOptions<TResponseStyle, ThrowOnError, Url>;
const url = buildUrl(resolvedOpts);
return { opts: resolvedOpts, url };
};
const request: Client['request'] = async (options) => {
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
body: getValidRequestBody(opts),
};
let request = new Request(url, requestInit);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response: Response;
try {
response = await _fetch(request);
} catch (error) {
// Handle fetch exceptions (AbortError, network errors, etc.)
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, undefined as any, request, opts)) as unknown;
}
}
finalError = finalError || ({} as unknown);
if (opts.throwOnError) {
throw finalError;
}
// Return error response
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
request,
response: undefined as any,
};
}
for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
let emptyData: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'text':
emptyData = await response[parseAs]();
break;
case 'formData':
emptyData = new FormData();
break;
case 'stream':
emptyData = response.body;
break;
case 'json':
default:
emptyData = {};
break;
}
return opts.responseStyle === 'data'
? emptyData
: {
data: emptyData,
...result,
};
}
let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'text':
data = await response[parseAs]();
break;
case 'json': {
// Some servers return 200 with no Content-Length and empty body.
// response.json() would throw; read as text and parse if non-empty.
const text = await response.text();
data = text ? JSON.parse(text) : {};
break;
}
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === 'data'
? data
: {
data,
...result,
};
}
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError);
} catch {
// noop
}
const error = jsonError ?? textError;
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
};
};
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record<string, string>,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
return request;
},
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
url,
});
};
const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options });
return {
buildUrl: _buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
getConfig,
head: makeMethodFn('HEAD'),
interceptors,
options: makeMethodFn('OPTIONS'),
patch: makeMethodFn('PATCH'),
post: makeMethodFn('POST'),
put: makeMethodFn('PUT'),
request,
setConfig,
sse: {
connect: makeSseFn('CONNECT'),
delete: makeSseFn('DELETE'),
get: makeSseFn('GET'),
head: makeSseFn('HEAD'),
options: makeSseFn('OPTIONS'),
patch: makeSseFn('PATCH'),
post: makeSseFn('POST'),
put: makeSseFn('PUT'),
trace: makeSseFn('TRACE'),
},
trace: makeMethodFn('TRACE'),
} as Client;
};
+25
View File
@@ -0,0 +1,25 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from '../core/auth.gen';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
RequestOptions,
RequestResult,
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from './types.gen';
export { createConfig, mergeHeaders } from './utils.gen';
+215
View File
@@ -0,0 +1,215 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from '../core/auth.gen';
import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from '../core/serverSentEvents.gen';
import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen';
import type { Middleware } from './utils.gen';
export type ResponseStyle = 'data' | 'fields';
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T['baseUrl'];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all.
*
* @default 'auto'
*/
parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text';
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T['throwOnError'];
}
export interface RequestOptions<
TData = unknown,
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
>
extends
Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}>,
Pick<
ServerSentEventsOptions<TData>,
| 'onRequest'
| 'onSseError'
| 'onSseEvent'
| 'sseDefaultRetryDelay'
| 'sseMaxRetryAttempts'
| 'sseMaxRetryDelay'
> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export interface ResolvedRequestOptions<
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
headers: Headers;
serializedBody?: string;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = ThrowOnError extends true
? Promise<
TResponseStyle extends 'data'
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends 'data'
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
: (
| {
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type SseFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<never, TResponseStyle, ThrowOnError>, 'method'>,
) => Promise<ServerSentEventsResult<TData, TError>>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: TData & Options<TData>,
) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
};
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponse = unknown,
TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url'
> &
([TData] extends [never] ? unknown : Omit<TData, 'url'>);
+316
View File
@@ -0,0 +1,316 @@
// This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from '../core/auth.gen';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
import { jsonBodySerializer } from '../core/bodySerializer.gen';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer.gen';
import { getUrl } from '../core/utils.gen';
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
export const createQuerySerializer = <T = unknown>({
parameters = {},
...args
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === 'object') {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) {
continue;
}
const options = parameters[name] || args;
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: 'form',
value,
...options.array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: 'deepObject',
value: value as Record<string, unknown>,
...options.object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved: options.allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join('&');
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return 'stream';
}
const cleanContent = contentType.split(';')[0]?.trim();
if (!cleanContent) {
return;
}
if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) {
return 'json';
}
if (cleanContent === 'multipart/form-data') {
return 'formData';
}
if (
['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type))
) {
return 'blob';
}
if (cleanContent.startsWith('text/')) {
return 'text';
}
return;
};
const checkForExistence = (
options: Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
},
name?: string,
): boolean => {
if (!name) {
return false;
}
if (
options.headers.has(name) ||
options.query?.[name] ||
options.headers.get('Cookie')?.includes(`${name}=`)
) {
return true;
}
return false;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}) => {
for (const auth of security) {
if (checkForExistence(options, auth.name)) {
continue;
}
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? 'Authorization';
switch (auth.in) {
case 'query':
if (!options.query) {
options.query = {};
}
options.query[name] = token;
break;
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
default:
options.headers.set(name, token);
break;
}
}
};
export const buildUrl: Client['buildUrl'] = (options) =>
getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === 'function'
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith('/')) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
const headersEntries = (headers: Headers): Array<[string, string]> => {
const entries: Array<[string, string]> = [];
headers.forEach((value, key) => {
entries.push([key, value]);
});
return entries;
};
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header) {
continue;
}
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e., their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === 'object' ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
fns: Array<Interceptor | null> = [];
clear(): void {
this.fns = [];
}
eject(id: number | Interceptor): void {
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = null;
}
}
exists(id: number | Interceptor): boolean {
const index = this.getInterceptorIndex(id);
return Boolean(this.fns[index]);
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === 'number') {
return this.fns[id] ? id : -1;
}
return this.fns.indexOf(id);
}
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = fn;
return id;
}
return false;
}
use(fn: Interceptor): number {
this.fns.push(fn);
return this.fns.length - 1;
}
}
export interface Middleware<Req, Res, Err, Options> {
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
request: Interceptors<ReqInterceptor<Req, Options>>;
response: Interceptors<ResInterceptor<Res, Req, Options>>;
}
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
Req,
Res,
Err,
Options
> => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: 'form',
},
object: {
explode: true,
style: 'deepObject',
},
});
const defaultHeaders = {
'Content-Type': 'application/json',
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: 'auto',
querySerializer: defaultQuerySerializer,
...override,
});
+41
View File
@@ -0,0 +1,41 @@
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: 'header' | 'query' | 'cookie';
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: 'basic' | 'bearer';
type: 'apiKey' | 'http';
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token = typeof callback === 'function' ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === 'bearer') {
return `Bearer ${token}`;
}
if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`;
}
return token;
};
@@ -0,0 +1,82 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen';
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: unknown) => unknown;
type QuerySerializerOptionsObject = {
allowReserved?: boolean;
array?: Partial<SerializerOptions<ArrayStyle>>;
object?: Partial<SerializerOptions<ObjectStyle>>;
};
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
/**
* Per-parameter serialization overrides. When provided, these settings
* override the global array/object settings for specific parameter names.
*/
parameters?: Record<string, QuerySerializerOptionsObject>;
};
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
} else if (value instanceof Date) {
data.append(key, value.toISOString());
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: (body: unknown): FormData => {
const data = new FormData();
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: (body: unknown): string =>
JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: (body: unknown): string => {
const data = new URLSearchParams();
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};
+169
View File
@@ -0,0 +1,169 @@
// This file is auto-generated by @hey-api/openapi-ts
type Slot = 'body' | 'headers' | 'path' | 'query';
export type Field =
| {
in: Exclude<Slot, 'body'>;
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`.
*/
map?: string;
}
| {
in: Extract<Slot, 'body'>;
/**
* Key isn't required for bodies.
*/
key?: string;
map?: string;
}
| {
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If `in` is omitted, `map` aliases `key` to the transport layer.
*/
map: Slot;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: 'body',
$headers_: 'headers',
$path_: 'path',
$query_: 'query',
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
| {
in: Slot;
map?: string;
}
| {
in?: never;
map: Slot;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ('in' in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if ('key' in config) {
map.set(config.key, {
map: config.map,
});
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ('in' in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
if (field.in) {
(params[field.in] as Record<string, unknown>)[name] = arg;
}
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
if (field.in) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
params[field.map] = value;
}
} else {
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
} else if ('allowExtra' in config && config.allowExtra) {
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};
@@ -0,0 +1,171 @@
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'form':
return ',';
case 'pipeDelimited':
return '|';
case 'spaceDelimited':
return '%20';
default:
return ',';
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case 'simple':
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'object') {
throw new Error(
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)];
});
const joinedValues = values.join(',');
switch (style) {
case 'form':
return `${name}=${joinedValues}`;
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === 'deepObject' ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
};
@@ -0,0 +1,117 @@
// This file is auto-generated by @hey-api/openapi-ts
/**
* JSON-friendly union that mirrors what Pinia Colada can hash.
*/
export type JsonValue =
| null
| string
| number
| boolean
| JsonValue[]
| { [key: string]: JsonValue };
/**
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
*/
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
return value;
};
/**
* Safely stringifies a value and parses it back into a JsonValue.
*/
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
try {
const json = JSON.stringify(input, queryKeyJsonReplacer);
if (json === undefined) {
return undefined;
}
return JSON.parse(json) as JsonValue;
} catch {
return undefined;
}
};
/**
* Detects plain objects (including objects with a null prototype).
*/
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (value === null || typeof value !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(value as object);
return prototype === Object.prototype || prototype === null;
};
/**
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
*/
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
const result: Record<string, JsonValue> = {};
for (const [key, value] of entries) {
const existing = result[key];
if (existing === undefined) {
result[key] = value;
continue;
}
if (Array.isArray(existing)) {
(existing as string[]).push(value);
} else {
result[key] = [existing, value];
}
}
return result;
};
/**
* Normalizes any accepted value into a JSON-friendly shape for query keys.
*/
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
if (value === null) {
return null;
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
if (Array.isArray(value)) {
return stringifyToJsonValue(value);
}
if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) {
return serializeSearchParams(value);
}
if (isPlainObject(value)) {
return stringifyToJsonValue(value);
}
return undefined;
};
@@ -0,0 +1,242 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from './types.gen';
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> &
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch;
/**
* Implementing clients can call request interceptors inside this hook.
*/
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param error The error that occurred.
*/
onSseError?: (error: unknown) => void;
/**
* Callback invoked when an event is streamed from the server.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param event Event streamed from the server.
* @returns Nothing (void).
*/
onSseEvent?: (event: StreamEvent<TData>) => void;
serializedBody?: RequestInit['body'];
/**
* Default retry delay in milliseconds.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 3000
*/
sseDefaultRetryDelay?: number;
/**
* Maximum number of retry attempts before giving up.
*/
sseMaxRetryAttempts?: number;
/**
* Maximum retry delay in milliseconds.
*
* Applies only when exponential backoff is used.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 30000
*/
sseMaxRetryDelay?: number;
/**
* Optional sleep function for retry backoff.
*
* Defaults to using `setTimeout`.
*/
sseSleepFn?: (ms: number) => Promise<void>;
url: string;
};
export interface StreamEvent<TData = unknown> {
data: TData;
event?: string;
id?: string;
retry?: number;
}
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
TNext
>;
};
export function createSseClient<TData = unknown>({
onRequest,
onSseError,
onSseEvent,
responseTransformer,
responseValidator,
sseDefaultRetryDelay,
sseMaxRetryAttempts,
sseMaxRetryDelay,
sseSleepFn,
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> {
let lastEventId: string | undefined;
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
let attempt = 0;
const signal = options.signal ?? new AbortController().signal;
while (true) {
if (signal.aborted) break;
attempt++;
const headers =
options.headers instanceof Headers
? options.headers
: new Headers(options.headers as Record<string, string> | undefined);
if (lastEventId !== undefined) {
headers.set('Last-Event-ID', lastEventId);
}
try {
const requestInit: RequestInit = {
redirect: 'follow',
...options,
body: options.serializedBody,
headers,
signal,
};
let request = new Request(url, requestInit);
if (onRequest) {
request = await onRequest(url, requestInit);
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = options.fetch ?? globalThis.fetch;
const response = await _fetch(request);
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
if (!response.body) throw new Error('No body in SSE response');
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
const abortHandler = () => {
try {
reader.cancel();
} catch {
// noop
}
};
signal.addEventListener('abort', abortHandler);
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
for (const chunk of chunks) {
const lines = chunk.split('\n');
const dataLines: Array<string> = [];
let eventName: string | undefined;
for (const line of lines) {
if (line.startsWith('data:')) {
dataLines.push(line.replace(/^data:\s*/, ''));
} else if (line.startsWith('event:')) {
eventName = line.replace(/^event:\s*/, '');
} else if (line.startsWith('id:')) {
lastEventId = line.replace(/^id:\s*/, '');
} else if (line.startsWith('retry:')) {
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10);
if (!Number.isNaN(parsed)) {
retryDelay = parsed;
}
}
}
let data: unknown;
let parsedJson = false;
if (dataLines.length) {
const rawData = dataLines.join('\n');
try {
data = JSON.parse(rawData);
parsedJson = true;
} catch {
data = rawData;
}
}
if (parsedJson) {
if (responseValidator) {
await responseValidator(data);
}
if (responseTransformer) {
data = await responseTransformer(data);
}
}
onSseEvent?.({
data,
event: eventName,
id: lastEventId,
retry: retryDelay,
});
if (dataLines.length) {
yield data as any;
}
}
}
} finally {
signal.removeEventListener('abort', abortHandler);
reader.releaseLock();
}
break; // exit loop on normal completion
} catch (error) {
// connection failed or aborted; retry after delay
onSseError?.(error);
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
await sleep(backoff);
}
}
};
const stream = createStream();
return { stream };
}
+104
View File
@@ -0,0 +1,104 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from './auth.gen';
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen';
export type HttpMethod =
| 'connect'
| 'delete'
| 'get'
| 'head'
| 'options'
| 'patch'
| 'post'
| 'put'
| 'trace';
export type Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
SseFn = never,
> = {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
getConfig: () => Config;
request: RequestFn;
setConfig: (config: Config) => Config;
} & {
[K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit['headers']
| Record<
string,
string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?: Uppercase<HttpMethod>;
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function validating request data. This is useful if you want to ensure
* the request conforms to the desired shape, so it can be safely sent to
* the server.
*/
requestValidator?: (data: unknown) => Promise<unknown>;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g., converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? true
: [T] extends [never | undefined]
? [undefined] extends [T]
? false
: true
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
};
+140
View File
@@ -0,0 +1,140 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from './pathSerializer.gen';
export interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
continue;
}
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export function getValidRequestBody(options: {
body?: unknown;
bodySerializer?: BodySerializer | null;
serializedBody?: unknown;
}) {
const hasBody = options.body !== undefined;
const isSerializedBody = hasBody && options.bodySerializer;
if (isSerializedBody) {
if ('serializedBody' in options) {
const hasSerializedBody =
options.serializedBody !== undefined && options.serializedBody !== '';
return hasSerializedBody ? options.serializedBody : null;
}
// not all clients implement a serializedBody property (i.e., client-axios)
return options.body !== '' ? options.body : null;
}
// plain/text body
if (hasBody) {
return options.body;
}
// no body was provided
return undefined;
}
+4
View File
@@ -0,0 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts
export { downloadFileApiV1StorageDownloadFileKeyGet, endpointApiV1CalcAddGet, endpointApiV1CalcAddPost, endpointApiV1CalcDivideGet, endpointApiV1CalcMultiplyGet, endpointApiV1CalcSubtractGet, endpointApiV1DoubaoAnalyzePost, type Options, rootGet, simpleEndpointApiV1CalcCountGet, uploadFileApiV1StorageUploadPost } from './sdk.gen';
export type { BodyUploadFileApiV1StorageUploadPost, ClientOptions, DoubaoDoubaoAnalyzePostRequest, DoubaoDoubaoAnalyzePostResponse, DownloadFileApiV1StorageDownloadFileKeyGetData, DownloadFileApiV1StorageDownloadFileKeyGetError, DownloadFileApiV1StorageDownloadFileKeyGetErrors, DownloadFileApiV1StorageDownloadFileKeyGetResponses, EndpointApiV1CalcAddGetData, EndpointApiV1CalcAddGetError, EndpointApiV1CalcAddGetErrors, EndpointApiV1CalcAddGetResponse, EndpointApiV1CalcAddGetResponses, EndpointApiV1CalcAddPostData, EndpointApiV1CalcAddPostError, EndpointApiV1CalcAddPostErrors, EndpointApiV1CalcAddPostResponse, EndpointApiV1CalcAddPostResponses, EndpointApiV1CalcDivideGetData, EndpointApiV1CalcDivideGetError, EndpointApiV1CalcDivideGetErrors, EndpointApiV1CalcDivideGetResponse, EndpointApiV1CalcDivideGetResponses, EndpointApiV1CalcMultiplyGetData, EndpointApiV1CalcMultiplyGetError, EndpointApiV1CalcMultiplyGetErrors, EndpointApiV1CalcMultiplyGetResponse, EndpointApiV1CalcMultiplyGetResponses, EndpointApiV1CalcSubtractGetData, EndpointApiV1CalcSubtractGetError, EndpointApiV1CalcSubtractGetErrors, EndpointApiV1CalcSubtractGetResponse, EndpointApiV1CalcSubtractGetResponses, EndpointApiV1DoubaoAnalyzePostData, EndpointApiV1DoubaoAnalyzePostError, EndpointApiV1DoubaoAnalyzePostErrors, EndpointApiV1DoubaoAnalyzePostResponse, EndpointApiV1DoubaoAnalyzePostResponses, ExampleCalcAddGetRequest, ExampleCalcAddGetResponse, ExampleCalcAddPostRequest, ExampleCalcAddPostResponse, ExampleCalcCountGetResponse, ExampleCalcDivideGetRequest, ExampleCalcDivideGetResponse, ExampleCalcMultiplyGetRequest, ExampleCalcMultiplyGetResponse, ExampleCalcSubtractGetRequest, ExampleCalcSubtractGetResponse, HttpValidationError, RootGetData, RootGetResponses, SimpleEndpointApiV1CalcCountGetData, SimpleEndpointApiV1CalcCountGetResponse, SimpleEndpointApiV1CalcCountGetResponses, UploadFileApiV1StorageUploadPostData, UploadFileApiV1StorageUploadPostError, UploadFileApiV1StorageUploadPostErrors, UploadFileApiV1StorageUploadPostResponses, ValidationError } from './types.gen';
+112
View File
@@ -0,0 +1,112 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client';
import { client } from './client.gen';
import type { DownloadFileApiV1StorageDownloadFileKeyGetData, DownloadFileApiV1StorageDownloadFileKeyGetErrors, DownloadFileApiV1StorageDownloadFileKeyGetResponses, EndpointApiV1CalcAddGetData, EndpointApiV1CalcAddGetErrors, EndpointApiV1CalcAddGetResponses, EndpointApiV1CalcAddPostData, EndpointApiV1CalcAddPostErrors, EndpointApiV1CalcAddPostResponses, EndpointApiV1CalcDivideGetData, EndpointApiV1CalcDivideGetErrors, EndpointApiV1CalcDivideGetResponses, EndpointApiV1CalcMultiplyGetData, EndpointApiV1CalcMultiplyGetErrors, EndpointApiV1CalcMultiplyGetResponses, EndpointApiV1CalcSubtractGetData, EndpointApiV1CalcSubtractGetErrors, EndpointApiV1CalcSubtractGetResponses, EndpointApiV1DoubaoAnalyzePostData, EndpointApiV1DoubaoAnalyzePostErrors, EndpointApiV1DoubaoAnalyzePostResponses, RootGetData, RootGetResponses, SimpleEndpointApiV1CalcCountGetData, SimpleEndpointApiV1CalcCountGetResponses, UploadFileApiV1StorageUploadPostData, UploadFileApiV1StorageUploadPostErrors, UploadFileApiV1StorageUploadPostResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
/**
* You can pass arbitrary values through the `meta` object. This can be
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record<string, unknown>;
};
/**
* 根路径
*
* 根路径端点,返回应用的基本信息。
*
* 返回:
* dict: 包含应用名称、版本、文档地址和健康检查地址的字典
*/
export const rootGet = <ThrowOnError extends boolean = false>(options?: Options<RootGetData, ThrowOnError>) => (options?.client ?? client).get<RootGetResponses, unknown, ThrowOnError>({ url: '/', ...options });
/**
* 上传文件
*
* 后端代理上传,直接写入 nanio
*/
export const uploadFileApiV1StorageUploadPost = <ThrowOnError extends boolean = false>(options: Options<UploadFileApiV1StorageUploadPostData, ThrowOnError>) => (options.client ?? client).post<UploadFileApiV1StorageUploadPostResponses, UploadFileApiV1StorageUploadPostErrors, ThrowOnError>({
...formDataBodySerializer,
url: '/api/v1/storage/upload',
...options,
headers: {
'Content-Type': null,
...options.headers
}
});
/**
* Download File
*/
export const downloadFileApiV1StorageDownloadFileKeyGet = <ThrowOnError extends boolean = false>(options: Options<DownloadFileApiV1StorageDownloadFileKeyGetData, ThrowOnError>) => (options.client ?? client).get<DownloadFileApiV1StorageDownloadFileKeyGetResponses, DownloadFileApiV1StorageDownloadFileKeyGetErrors, ThrowOnError>({ url: '/api/v1/storage/download/{file_key}', ...options });
/**
* 分析图片内容
*
* 发送图片(base64或路径)到豆包视觉模型,返回描述
*/
export const endpointApiV1DoubaoAnalyzePost = <ThrowOnError extends boolean = false>(options: Options<EndpointApiV1DoubaoAnalyzePostData, ThrowOnError>) => (options.client ?? client).post<EndpointApiV1DoubaoAnalyzePostResponses, EndpointApiV1DoubaoAnalyzePostErrors, ThrowOnError>({
url: '/api/v1/doubao/analyze',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Add two numbers
*
* Add two numbers via GET query params
*/
export const endpointApiV1CalcAddGet = <ThrowOnError extends boolean = false>(options: Options<EndpointApiV1CalcAddGetData, ThrowOnError>) => (options.client ?? client).get<EndpointApiV1CalcAddGetResponses, EndpointApiV1CalcAddGetErrors, ThrowOnError>({ url: '/api/v1/calc/add', ...options });
/**
* Add two numbers (POST)
*
* Add two numbers via POST JSON body
*/
export const endpointApiV1CalcAddPost = <ThrowOnError extends boolean = false>(options: Options<EndpointApiV1CalcAddPostData, ThrowOnError>) => (options.client ?? client).post<EndpointApiV1CalcAddPostResponses, EndpointApiV1CalcAddPostErrors, ThrowOnError>({
url: '/api/v1/calc/add',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get visit count and increment
*
* Increments a counter and returns current value
*/
export const simpleEndpointApiV1CalcCountGet = <ThrowOnError extends boolean = false>(options?: Options<SimpleEndpointApiV1CalcCountGetData, ThrowOnError>) => (options?.client ?? client).get<SimpleEndpointApiV1CalcCountGetResponses, unknown, ThrowOnError>({ url: '/api/v1/calc/count', ...options });
/**
* Divide two numbers
*
* Divide two numbers via GET query params (a / b). Returns error if b == 0.
*/
export const endpointApiV1CalcDivideGet = <ThrowOnError extends boolean = false>(options: Options<EndpointApiV1CalcDivideGetData, ThrowOnError>) => (options.client ?? client).get<EndpointApiV1CalcDivideGetResponses, EndpointApiV1CalcDivideGetErrors, ThrowOnError>({ url: '/api/v1/calc/divide', ...options });
/**
* Multiply two numbers
*
* Multiply two numbers via GET query params (a * b)
*/
export const endpointApiV1CalcMultiplyGet = <ThrowOnError extends boolean = false>(options: Options<EndpointApiV1CalcMultiplyGetData, ThrowOnError>) => (options.client ?? client).get<EndpointApiV1CalcMultiplyGetResponses, EndpointApiV1CalcMultiplyGetErrors, ThrowOnError>({ url: '/api/v1/calc/multiply', ...options });
/**
* Subtract two numbers
*
* Subtract two numbers via GET query params (a - b)
*/
export const endpointApiV1CalcSubtractGet = <ThrowOnError extends boolean = false>(options: Options<EndpointApiV1CalcSubtractGetData, ThrowOnError>) => (options.client ?? client).get<EndpointApiV1CalcSubtractGetResponses, EndpointApiV1CalcSubtractGetErrors, ThrowOnError>({ url: '/api/v1/calc/subtract', ...options });
+530
View File
@@ -0,0 +1,530 @@
// This file is auto-generated by @hey-api/openapi-ts
export type ClientOptions = {
baseUrl: 'http://127.0.0.1:8000' | (string & {});
};
/**
* Body_upload_file_api_v1_storage_upload_post
*/
export type BodyUploadFileApiV1StorageUploadPost = {
/**
* File
*/
file: Blob | File;
/**
* Worker Type
*/
worker_type: string;
};
/**
* HTTPValidationError
*/
export type HttpValidationError = {
/**
* Detail
*/
detail?: Array<ValidationError>;
};
/**
* ValidationError
*/
export type ValidationError = {
/**
* Location
*/
loc: Array<string | number>;
/**
* Message
*/
msg: string;
/**
* Error Type
*/
type: string;
/**
* Input
*/
input?: unknown;
/**
* Context
*/
ctx?: {
[key: string]: unknown;
};
};
/**
* doubao_doubao_analyze_POST_Request
*/
export type DoubaoDoubaoAnalyzePostRequest = {
/**
* Image Base64
*/
image_base64: string;
/**
* Prompt
*/
prompt: string;
};
/**
* doubao_doubao_analyze_POST_Response
*/
export type DoubaoDoubaoAnalyzePostResponse = {
/**
* Result
*/
result: string;
/**
* Error
*/
error?: string;
};
/**
* example_calc_add_GET_Response
*/
export type ExampleCalcAddGetResponse = {
/**
* Result
*/
result: number;
/**
* Operation
*/
operation: string;
/**
* Timestamp
*/
timestamp?: number;
};
/**
* example_calc_add_POST_Request
*/
export type ExampleCalcAddPostRequest = {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
/**
* example_calc_add_POST_Response
*/
export type ExampleCalcAddPostResponse = {
/**
* Result
*/
result: number;
/**
* Operation
*/
operation: string;
/**
* Timestamp
*/
timestamp?: number;
};
/**
* example_calc_count_GET_Response
*/
export type ExampleCalcCountGetResponse = {
/**
* Name
*/
name: string;
/**
* Value
*/
value: number;
};
/**
* example_calc_divide_GET_Response
*/
export type ExampleCalcDivideGetResponse = {
/**
* Result
*/
result?: number;
/**
* Operation
*/
operation: string;
/**
* Timestamp
*/
timestamp?: number;
/**
* Error
*/
error?: string;
};
/**
* example_calc_multiply_GET_Response
*/
export type ExampleCalcMultiplyGetResponse = {
/**
* Result
*/
result: number;
/**
* Operation
*/
operation: string;
/**
* Timestamp
*/
timestamp?: number;
};
/**
* example_calc_subtract_GET_Response
*/
export type ExampleCalcSubtractGetResponse = {
/**
* Result
*/
result: number;
/**
* Operation
*/
operation: string;
/**
* Timestamp
*/
timestamp?: number;
};
/**
* example_calc_add_GET_Request
*/
export type ExampleCalcAddGetRequest = {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
/**
* example_calc_divide_GET_Request
*/
export type ExampleCalcDivideGetRequest = {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
/**
* example_calc_multiply_GET_Request
*/
export type ExampleCalcMultiplyGetRequest = {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
/**
* example_calc_subtract_GET_Request
*/
export type ExampleCalcSubtractGetRequest = {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
export type RootGetData = {
body?: never;
path?: never;
query?: never;
url: '/';
};
export type RootGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type UploadFileApiV1StorageUploadPostData = {
body: BodyUploadFileApiV1StorageUploadPost;
path?: never;
query?: never;
url: '/api/v1/storage/upload';
};
export type UploadFileApiV1StorageUploadPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UploadFileApiV1StorageUploadPostError = UploadFileApiV1StorageUploadPostErrors[keyof UploadFileApiV1StorageUploadPostErrors];
export type UploadFileApiV1StorageUploadPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type DownloadFileApiV1StorageDownloadFileKeyGetData = {
body?: never;
path: {
/**
* File Key
*/
file_key: string;
};
query?: never;
url: '/api/v1/storage/download/{file_key}';
};
export type DownloadFileApiV1StorageDownloadFileKeyGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DownloadFileApiV1StorageDownloadFileKeyGetError = DownloadFileApiV1StorageDownloadFileKeyGetErrors[keyof DownloadFileApiV1StorageDownloadFileKeyGetErrors];
export type DownloadFileApiV1StorageDownloadFileKeyGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type EndpointApiV1DoubaoAnalyzePostData = {
body: DoubaoDoubaoAnalyzePostRequest;
path?: never;
query?: never;
url: '/api/v1/doubao/analyze';
};
export type EndpointApiV1DoubaoAnalyzePostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type EndpointApiV1DoubaoAnalyzePostError = EndpointApiV1DoubaoAnalyzePostErrors[keyof EndpointApiV1DoubaoAnalyzePostErrors];
export type EndpointApiV1DoubaoAnalyzePostResponses = {
/**
* Successful Response
*/
200: DoubaoDoubaoAnalyzePostResponse;
};
export type EndpointApiV1DoubaoAnalyzePostResponse = EndpointApiV1DoubaoAnalyzePostResponses[keyof EndpointApiV1DoubaoAnalyzePostResponses];
export type EndpointApiV1CalcAddGetData = {
body?: never;
path?: never;
query: {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
url: '/api/v1/calc/add';
};
export type EndpointApiV1CalcAddGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type EndpointApiV1CalcAddGetError = EndpointApiV1CalcAddGetErrors[keyof EndpointApiV1CalcAddGetErrors];
export type EndpointApiV1CalcAddGetResponses = {
/**
* Successful Response
*/
200: ExampleCalcAddGetResponse;
};
export type EndpointApiV1CalcAddGetResponse = EndpointApiV1CalcAddGetResponses[keyof EndpointApiV1CalcAddGetResponses];
export type EndpointApiV1CalcAddPostData = {
body: ExampleCalcAddPostRequest;
path?: never;
query?: never;
url: '/api/v1/calc/add';
};
export type EndpointApiV1CalcAddPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type EndpointApiV1CalcAddPostError = EndpointApiV1CalcAddPostErrors[keyof EndpointApiV1CalcAddPostErrors];
export type EndpointApiV1CalcAddPostResponses = {
/**
* Successful Response
*/
200: ExampleCalcAddPostResponse;
};
export type EndpointApiV1CalcAddPostResponse = EndpointApiV1CalcAddPostResponses[keyof EndpointApiV1CalcAddPostResponses];
export type SimpleEndpointApiV1CalcCountGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/calc/count';
};
export type SimpleEndpointApiV1CalcCountGetResponses = {
/**
* Successful Response
*/
200: ExampleCalcCountGetResponse;
};
export type SimpleEndpointApiV1CalcCountGetResponse = SimpleEndpointApiV1CalcCountGetResponses[keyof SimpleEndpointApiV1CalcCountGetResponses];
export type EndpointApiV1CalcDivideGetData = {
body?: never;
path?: never;
query: {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
url: '/api/v1/calc/divide';
};
export type EndpointApiV1CalcDivideGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type EndpointApiV1CalcDivideGetError = EndpointApiV1CalcDivideGetErrors[keyof EndpointApiV1CalcDivideGetErrors];
export type EndpointApiV1CalcDivideGetResponses = {
/**
* Successful Response
*/
200: ExampleCalcDivideGetResponse;
};
export type EndpointApiV1CalcDivideGetResponse = EndpointApiV1CalcDivideGetResponses[keyof EndpointApiV1CalcDivideGetResponses];
export type EndpointApiV1CalcMultiplyGetData = {
body?: never;
path?: never;
query: {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
url: '/api/v1/calc/multiply';
};
export type EndpointApiV1CalcMultiplyGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type EndpointApiV1CalcMultiplyGetError = EndpointApiV1CalcMultiplyGetErrors[keyof EndpointApiV1CalcMultiplyGetErrors];
export type EndpointApiV1CalcMultiplyGetResponses = {
/**
* Successful Response
*/
200: ExampleCalcMultiplyGetResponse;
};
export type EndpointApiV1CalcMultiplyGetResponse = EndpointApiV1CalcMultiplyGetResponses[keyof EndpointApiV1CalcMultiplyGetResponses];
export type EndpointApiV1CalcSubtractGetData = {
body?: never;
path?: never;
query: {
/**
* A
*/
a: number;
/**
* B
*/
b: number;
};
url: '/api/v1/calc/subtract';
};
export type EndpointApiV1CalcSubtractGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type EndpointApiV1CalcSubtractGetError = EndpointApiV1CalcSubtractGetErrors[keyof EndpointApiV1CalcSubtractGetErrors];
export type EndpointApiV1CalcSubtractGetResponses = {
/**
* Successful Response
*/
200: ExampleCalcSubtractGetResponse;
};
export type EndpointApiV1CalcSubtractGetResponse = EndpointApiV1CalcSubtractGetResponses[keyof EndpointApiV1CalcSubtractGetResponses];
+86
View File
@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

+35
View File
@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
+41
View File
@@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vue’s
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
>Vue - Official</a
>. If you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>
+87
View File
@@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>
+26
View File
@@ -0,0 +1,26 @@
import './assets/main.css'
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { client } from "./api/generated/client.gen"
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
if (API_BASE_URL) {
client.setConfig({
baseUrl: API_BASE_URL
})
}
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
+23
View File
@@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import DoubaoView from '../views/DoubaoView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'doubao',
component: DoubaoView,
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
],
})
export default router
+12
View File
@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
+16
View File
@@ -0,0 +1,16 @@
<template>
<div class="about">
<h1>关于页面</h1>
<p>这里是后续扩展的示例页面。</p>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
+265
View File
@@ -0,0 +1,265 @@
<template>
<div class="doubao-container">
<el-card class="main-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon :size="24"><Cpu /></el-icon>
<span style="font-weight: 600; font-size: 18px; margin-left: 8px"
>豆包视觉理解测试</span
>
</div>
</template>
<el-form label-position="top">
<el-form-item label="提示词 (prompt)">
<el-input
v-model="prompt"
type="textarea"
:rows="3"
placeholder="请输入对图片的提问(可选)"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<el-upload
class="upload-area"
drag
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
accept="image/*"
>
<div v-if="!imagePreview" class="upload-placeholder">
<el-icon :size="48" class="el-icon--upload"><Upload /></el-icon>
<div class="el-upload__text">
拖拽图片到此处,或 <em>点击选择</em>
</div>
<div class="el-upload__tip">支持 JPG / PNG / WebP</div>
</div>
<div v-else class="preview-wrapper">
<img :src="imagePreview" class="preview-image" />
<div class="preview-actions">
<el-button
type="danger"
size="small"
circle
@click.stop="clearImage"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
</el-upload>
<div v-if="fileInfo" style="margin: 12px 0">
<el-tag type="info"
><el-icon><Document /></el-icon> {{ fileInfo }}</el-tag
>
</div>
<div class="action-bar">
<el-button
type="primary"
size="large"
:loading="loading"
:disabled="!imageBase64"
@click="analyze"
>
{{ loading ? "分析中..." : "🚀 分析图片" }}
</el-button>
<el-button v-if="resultText" size="large" @click="copyResult">
<el-icon><CopyDocument /></el-icon> 复制结果
</el-button>
</div>
<div v-if="statusText" style="margin-top: 16px">
<el-alert
:title="statusText"
:type="statusType"
:closable="false"
show-icon
/>
</div>
<div v-if="resultText" style="margin-top: 20px">
<el-divider content-position="left">分析结果</el-divider>
<div class="result-content" v-html="resultHtml"></div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { ElMessage } from "element-plus";
import {
Cpu,
Upload,
Close,
Document,
CopyDocument,
} from "@element-plus/icons-vue";
// 导入生成的 SDK 函数及类型
import { endpointApiV1DoubaoAnalyzePost } from "@/api/generated/sdk.gen";
import type { EndpointApiV1DoubaoAnalyzePostData } from "@/api/generated/types.gen";
const prompt = ref("描述这张图片的内容");
const imageBase64 = ref("");
const imagePreview = ref("");
const loading = ref(false);
const statusText = ref("");
const resultText = ref("");
const fileInfo = ref("");
const statusType = computed(() => {
if (statusText.value.includes("✅")) return "success";
if (statusText.value.includes("❌")) return "error";
return "info";
});
const resultHtml = computed(() => resultText.value.replace(/\n/g, "<br>"));
const handleFileChange = (file: any) => {
const raw = file.raw || file;
if (!raw) return;
if (!raw.type.startsWith("image/")) {
ElMessage.warning("请选择图片文件");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = e.target?.result as string;
imageBase64.value = data;
imagePreview.value = data;
fileInfo.value = `${raw.name} (${(raw.size / 1024).toFixed(1)} KB)`;
statusText.value = "";
resultText.value = "";
};
reader.readAsDataURL(raw);
};
const clearImage = () => {
imageBase64.value = "";
imagePreview.value = "";
fileInfo.value = "";
statusText.value = "";
resultText.value = "";
};
const analyze = async () => {
if (!imageBase64.value) {
ElMessage.warning("请先选择一张图片");
return;
}
// 构造请求参数
const requestData: EndpointApiV1DoubaoAnalyzePostData = {
body: {
image_base64: imageBase64.value,
prompt: prompt.value.trim() || "描述这张图片的内容",
},
url: "/api/v1/doubao/analyze",
};
loading.value = true;
statusText.value = "⏳ 正在分析中...";
resultText.value = "";
try {
// 调用生成的 API 函数 (默认 throwOnError = false)
const response = await endpointApiV1DoubaoAnalyzePost(requestData);
// 检查 HTTP 错误
if (response.error) {
const errMsg = response.error || "请求失败";
resultText.value = `❌ 错误: ${errMsg}`;
statusText.value = "❌ 分析失败";
return;
}
// 获取业务数据
const data = response.data;
if (data?.error) {
resultText.value = `❌ 错误: ${data.error}`;
statusText.value = "❌ 分析失败";
} else {
resultText.value = data?.result || "(无返回内容)";
statusText.value = "✅ 分析完成";
}
} catch (err: any) {
// 网络错误或其他异常
resultText.value = `❌ 请求异常: ${err.message}`;
statusText.value = "❌ 请求失败";
} finally {
loading.value = false;
}
};
const copyResult = () => {
const text = resultText.value.replace(/<[^>]+>/g, "");
navigator.clipboard
.writeText(text)
.then(() => {
ElMessage.success("已复制");
})
.catch(() => {
ElMessage.error("复制失败");
});
};
</script>
<style scoped>
.doubao-container {
max-width: 700px;
margin: 0 auto;
}
.main-card {
border-radius: 12px;
}
.card-header {
display: flex;
align-items: center;
}
.upload-area {
width: 100%;
}
.upload-placeholder {
padding: 30px 0;
text-align: center;
}
.preview-wrapper {
position: relative;
display: inline-block;
width: 100%;
}
.preview-image {
max-width: 100%;
max-height: 400px;
border-radius: 8px;
border: 1px solid #dcdfe6;
}
.preview-actions {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
}
.action-bar {
margin-top: 16px;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.result-content {
background: #f5f7fa;
padding: 16px;
border-radius: 8px;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.8;
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>
+24
View File
@@ -0,0 +1,24 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
// Extra safety for array and object lookups, but may have false positives.
"noUncheckedIndexedAccess": true,
// Path mapping for cleaner imports.
"paths": {
"@/*": [
"./src/*"
]
},
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
+26
View File
@@ -0,0 +1,26 @@
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping.
// Bundler mode provides a smoother developer experience.
"module": "preserve",
"moduleResolution": "bundler",
// Include Node.js types and avoid accidentally including other `@types/*` packages.
"types": [
"node"
],
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
"noEmit": true,
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
}
}
+40
View File
@@ -0,0 +1,40 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
vueJsx(),
vueDevTools(),
],
base: '/static/',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
},
},
})