package main // 模板与 fun v1.3.7 核心库的字节级一致移植(gen 产物保持不变) type templateGo struct{} func (ctx templateGo) genDefaultServiceTemplate() string { return `package api import ( "bufio" "bytes" "encoding/json" "fmt" "net/http" "strings" ) // Result 统一响应结构 type Result[T any] struct { Code *uint16 Data *T Msg *string Status uint8 } func (r Result[T]) Error() string { if r.Msg != nil { return *r.Msg } if r.Code != nil { return fmt.Sprintf("code=%d", *r.Code) } return "api: unknown error" } // Void 用于无数据返回的方法 type Void = struct{} // RequestInterceptor 请求前拦截器:可鉴权、加签、改 dto;返回 error 则直接失败 type RequestInterceptor func(serviceName string, methodName string, dto any) error // ResponseInterceptor 响应后拦截器:可记录日志、埋点、解密;返回 error 则转为失败响应 type ResponseInterceptor func(serviceName string, methodName string, result Result[any]) error // Client 内联 HTTP 客户端,不依赖外部 funclient 包 type Client struct { url string client *http.Client state map[string]string requestInterceptors []RequestInterceptor responseInterceptors []ResponseInterceptor } // NewClient 创建客户端 func NewClient(url string) (*Client, error) { return &Client{ url: strings.TrimRight(url, "/"), client: &http.Client{}, }, nil } // SetHttpClient 替换底层 http.Client func (c *Client) SetHttpClient(client *http.Client) { c.client = client } // AddRequestInterceptor 注册请求前拦截器 func (c *Client) AddRequestInterceptor(i RequestInterceptor) { c.requestInterceptors = append(c.requestInterceptors, i) } // AddResponseInterceptor 注册响应后拦截器 func (c *Client) AddResponseInterceptor(i ResponseInterceptor) { c.responseInterceptors = append(c.responseInterceptors, i) } // SetState 设置随每个请求携带的状态(如 token),服务端 Guard 可读取 func (c *Client) SetState(state map[string]string) { c.state = state } // Request 发起普通调用 func Request[T any](c *Client, serviceName string, methodName string, dto ...any) Result[T] { payload := newPayload(serviceName, methodName, dto, c.state) b, err := json.Marshal(payload) if err != nil { return Result[T]{Status: 2, Msg: ptr(err.Error())} } for _, i := range c.requestInterceptors { var dtoVal any if len(dto) > 0 { dtoVal = dto[0] } if err := i(serviceName, methodName, dtoVal); err != nil { return Result[T]{Status: 2, Msg: ptr(err.Error())} } } req, err := http.NewRequest(http.MethodPost, c.url+"/cell", bytes.NewReader(b)) if err != nil { return Result[T]{Status: 2, Msg: ptr(err.Error())} } req.Header.Set("Content-Type", "application/json") resp, err := c.client.Do(req) if err != nil { return Result[T]{Status: 2, Msg: ptr(err.Error())} } defer resp.Body.Close() var out Result[T] if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return Result[T]{Status: 2, Msg: ptr(err.Error())} } var anyData *any if out.Data != nil { v := any(*out.Data) anyData = &v } anyResult := Result[any]{Code: out.Code, Data: anyData, Msg: out.Msg, Status: out.Status} for _, i := range c.responseInterceptors { if err := i(serviceName, methodName, anyResult); err != nil { return Result[T]{Status: 2, Msg: ptr(err.Error())} } } return out } // Stream 发起流式调用,通过 NDJSON 行逐个推送消息(Streamable HTTP) func Stream[T any](c *Client, serviceName string, methodName string, dto ...any) (<-chan T, error) { payload := newPayload(serviceName, methodName, dto, c.state) b, err := json.Marshal(payload) if err != nil { return nil, err } for _, i := range c.requestInterceptors { var dtoVal any if len(dto) > 0 { dtoVal = dto[0] } if err := i(serviceName, methodName, dtoVal); err != nil { return nil, err } } req, err := http.NewRequest(http.MethodPost, c.url+"/cell", bytes.NewReader(b)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") resp, err := c.client.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, fmt.Errorf("api: unexpected status %d", resp.StatusCode) } anyResult := Result[any]{Status: 0} for _, i := range c.responseInterceptors { if err := i(serviceName, methodName, anyResult); err != nil { resp.Body.Close() return nil, err } } ch := make(chan T) go func() { defer resp.Body.Close() defer close(ch) scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } var msg T if err := json.Unmarshal([]byte(line), &msg); err != nil { continue } ch <- msg } }() return ch, nil } func newPayload(serviceName string, methodName string, dto []any, state map[string]string) map[string]any { payload := map[string]any{ "serviceName": serviceName, "methodName": methodName, } if len(dto) > 0 { payload["data"] = dto[0] } if len(state) > 0 { payload["state"] = state } return payload } func ptr(s string) *string { return &s } type Api struct { {{- range .GenServiceList}} {{.ServiceName}} *{{.ServiceName}} {{- end}} *Client } func CreateApi(url string) (Api, error) { apiClient, err := NewClient(url) return Api{ {{- range .GenServiceList}} {{.ServiceName}}: New{{.ServiceName}}(apiClient), {{- end}} Client: apiClient, }, err }` } func (ctx templateGo) genServiceTemplate() string { return `package api type {{.ServiceName}} struct { *Client } func New{{.ServiceName}}(client *Client) *{{.ServiceName}} { return &{{.ServiceName}}{ Client: client, } } {{- $serviceName := .ServiceName }} {{- range .GenMethodTypeList}} {{if .IsStream }}func (ctx *{{$serviceName}}) {{.MethodName}}({{.DtoText}}) (<-chan {{.GenericTypeText}}, error) { return Stream[{{.GenericTypeText}}](ctx.Client, "{{$serviceName}}", "{{.MethodName}}"{{.ArgsText}}) }{{else}}func (ctx *{{$serviceName}}) {{.MethodName}}({{.DtoText}}) {{.ReturnValueText}} { return Request[{{.GenericTypeText}}](ctx.Client, "{{$serviceName}}", "{{.MethodName}}"{{.ArgsText}}) }{{end}} {{- end}}` } func (ctx templateGo) genStructTemplate() string { return `package api type {{.Name}} struct{ {{- range .GenClassFieldType}} {{.Name}} {{.Type}} {{.Tag}} {{- end}} }` } func (ctx templateGo) genEnumTemplate() string { return `package api type {{.Name}} uint8 {{$enumName := .Name}} const ( {{- range $index, $element := .Names}} {{$element}}{{if eq $index 0}} {{$enumName}} = iota{{end}} {{- end}} ) func ({{.Name}}) Values() []{{.Name}} { return []{{.Name}}{ {{- range $index, $element := .Names}} {{$element}}, {{- end}} } } {{if .DisplayNames}} func ({{.Name}}) DisplayNames() []string { return []string{ {{- range $index, $element := .DisplayNames}} "{{$element}}", {{- end}} } } {{end}}` } type templateTs struct{} func (ctx templateTs) genClientTemplate() string { return `// 大整数安全 JSON:超过 Number.MAX_SAFE_INTEGER(2^53-1)的整数字面量 // 解析为 BigInt,序列化时 BigInt 还原为数字字面量——雪花 ID 等不再丢精度。 // 自包含实现,不引入运行时依赖;如需换 json-bigint,替换下面两个函数即可 const BIGINT_PREFIX = "\u0000fun-bigint:"; function reviveBigint(value: any): any { if (typeof value === "string" && value.startsWith(BIGINT_PREFIX) && /^-?\d+$/.test(value.slice(BIGINT_PREFIX.length))) { return BigInt(value.slice(BIGINT_PREFIX.length)); } if (Array.isArray(value)) return value.map(reviveBigint); if (value !== null && typeof value === "object") { for (const key of Object.keys(value)) value[key] = reviveBigint(value[key]); return value; } return value; } function parseLossless(text: string): any { // 先把超出安全范围的大整数包成带哨兵的字符串(正则的字符串分支优先, // 字符串内容里的数字不受影响),JSON.parse 后再还原为 BigInt const guarded = text.replace( /"(?:[^"\\]|\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}))*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, token => { if (token.startsWith('"')) return token; if (!/[.eE]/.test(token) && !Number.isSafeInteger(Number(token))) { // 哨兵必须以转义形式 \\u0000 写入文本(JSON 字符串禁止裸控制字符), // JSON.parse 解码后即为 BIGINT_PREFIX(真实 NUL 开头) return '"\\u0000fun-bigint:' + token + '"'; } return token; } ); return reviveBigint(JSON.parse(guarded)); } function stringifyLossless(value: any): string | undefined { const raw = JSON.stringify(value, (_key, v) => typeof v === "bigint" ? BIGINT_PREFIX + v.toString() : v ); if (raw === undefined) return undefined; // 哨兵字符串在序列化文本中形如 "\u0000fun-bigint:123"(NUL 被转义),去引号还原为数字字面量 return raw.replace(/"\\u0000fun-bigint:(-?\d+)"/g, "$1"); } export type result = { code?: number; data?: T; msg?: string; status: number; }; export type resultStatus = 0 | 1 | 2 | 4 | 5; // 0 success; 1 framework/client protocol error; 2 business error; 4 external request error; 5 external timeout export type RequestOptions = { signal?: AbortSignal; state?: Record; }; export type StreamOptions = { signal?: AbortSignal; state?: Record; /** * 原生断线重连:流因传输失败(status 4/5)结束时按指数退避自动重连。 * dto 传工厂函数时每次重连重新求值,用于刷新续传游标。 * 未传时关闭(默认),与 request 语义一致。 */ retry?: StreamRetryOptions | false; /** 空闲看门狗:超过该毫秒数未收到任何字节(含心跳空行)即判定流死亡并走重连。0 关闭。 */ idleTimeoutMs?: number; }; export type StreamRetryOptions = { /** 最大重连次数,默认 Infinity(直到 signal 取消或成功)。 */ maxAttempts?: number; /** 首次重连延迟(毫秒),默认 500。 */ baseDelayMs?: number; /** 退避上限(毫秒),默认 10000。实际延迟为指数退避 × 0.75~1.25 抖动。 */ maxDelayMs?: number; }; export type RequestInterceptor = ( serviceName: string, methodName: string, state: Record, dto?: any ) => Promise | void; export type ResponseContext = { readonly requestState: Readonly>; readonly response?: Response; }; export type ResponseInterceptor = ( serviceName: string, methodName: string, result: result ) => Promise | void> | result | void; export type ContextResponseInterceptor = ( serviceName: string, methodName: string, result: result, context: ResponseContext ) => Promise | void> | result | void; function messageOf(error: unknown): string { if (error instanceof Error && error.message) return error.message; if (typeof error === "string" && error) return error; return "unknown error"; } function failure(status: resultStatus, msg: string): result { return { status, msg }; } function isTimeout(error: unknown, signal?: AbortSignal): boolean { const errorName = error !== null && typeof error === "object" ? (error as { name?: unknown }).name : undefined; const reason = signal?.reason; const reasonName = reason !== null && typeof reason === "object" ? (reason as { name?: unknown }).name : undefined; return errorName === "TimeoutError" || reasonName === "TimeoutError"; } function requestFailure(error: unknown, signal: AbortSignal | undefined, stream: boolean): result { if (isTimeout(error, signal)) { return failure(5, stream ? "Stream timed out" : "Request timed out"); } if (signal?.aborted === true) { return failure(4, stream ? "Stream aborted" : "Request aborted"); } const kind = stream ? "External stream request" : "External request"; return failure(4, ` + "`${kind} failed: ${messageOf(error)}`" + `); } function isResult(value: unknown): value is result { return value !== null && typeof value === "object" && typeof (value as { status?: unknown }).status === "number"; } function mediaType(response: Response): string { return (response.headers.get("content-type") || "").split(";", 1)[0].trim().toLowerCase(); } function excerpt(text: string, limit = 180): string { const value = text.replace(/\s+/g, " ").trim(); return value.length <= limit ? value : ` + "`${value.slice(0, limit)}...`" + `; } function externalFailure(response: Response, detail?: string): result { const timeout = response.status === 408 || response.status === 504; const statusText = response.statusText || (timeout ? "timeout" : "request failed"); const suffix = detail ? ` + "`: ${detail}`" + ` : ""; return failure(timeout ? 5 : 4, ` + "`HTTP ${response.status} ${statusText}${suffix}`" + `); } function responseReadFailure( error: unknown, response: Response, signal: AbortSignal | undefined, stream: boolean ): result { if (isTimeout(error, signal)) { return failure(5, stream ? "Stream timed out" : "Request timed out"); } if (signal?.aborted === true) { return failure(4, stream ? "Stream aborted" : "Request aborted"); } const kind = stream ? "Stream" : "Response body"; return response.ok ? failure(4, ` + "`${kind} failed: ${messageOf(error)}`" + `) : externalFailure(response, ` + "`response body failed: ${messageOf(error)}`" + `); } function parseResult(response: Response, text: string): result { const body = text.trim(); if (!body) { return response.ok ? failure(1, "Empty response body") : externalFailure(response); } let value: unknown; try { value = parseLossless(body); } catch { if (!response.ok) return externalFailure(response, excerpt(body)); const type = mediaType(response); if (type === "text/html" || /^\s*(?: = {}; private requestInterceptors: RequestInterceptor[] = []; private responseInterceptors: ContextResponseInterceptor[] = []; constructor(url: string) { this.url = url.replace(/\/+$/, ""); } setState(state: Record) { this.state = state; } addRequestInterceptor(interceptor: RequestInterceptor) { this.requestInterceptors.push(interceptor); } addResponseInterceptor(interceptor: ResponseInterceptor): void; addResponseInterceptor(interceptor: ContextResponseInterceptor): void; addResponseInterceptor(interceptor: ResponseInterceptor | ContextResponseInterceptor) { this.responseInterceptors.push(interceptor as ContextResponseInterceptor); } private async interceptResponse( serviceName: string, methodName: string, initial: result, requestState: Readonly>, response?: Response ): Promise> { let current = initial; const context: ResponseContext = Object.freeze( response === undefined ? { requestState } : { requestState, response } ); for (const interceptor of this.responseInterceptors) { try { const replaced = await interceptor(serviceName, methodName, current, context); if (replaced) current = replaced; } catch (error) { current = failure(1, ` + "`Response interceptor failed: ${messageOf(error)}`" + `); } } return current; } private async interceptRequest( serviceName: string, methodName: string, state: Record, dto: any ): Promise { for (const interceptor of this.requestInterceptors) { await interceptor(serviceName, methodName, state, dto); } } private snapshotState(state: Record): Readonly> { return Object.freeze({ ...state }); } async request( serviceName: string, methodName: string, dto?: any, options?: RequestOptions ): Promise> { let state: Record; try { state = { ...this.state, ...options?.state }; } catch (error) { return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Could not prepare request state: ${messageOf(error)}`" + `), Object.freeze({}) ) as result; } try { await this.interceptRequest(serviceName, methodName, state, dto); } catch (error) { let requestState: Readonly>; try { requestState = this.snapshotState(state); } catch (snapshotError) { return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Could not snapshot request state: ${messageOf(snapshotError)}`" + `), Object.freeze({}) ) as result; } return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Request interceptor failed: ${messageOf(error)}`" + `), requestState ) as result; } let requestState: Readonly>; try { requestState = this.snapshotState(state); } catch (error) { return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Could not snapshot request state: ${messageOf(error)}`" + `), Object.freeze({}) ) as result; } let body: string; try { const serialized = stringifyLossless({ serviceName, methodName, data: dto, ...(Object.keys(requestState).length ? { state: requestState } : {}), }); if (serialized === undefined) throw new Error("serialization produced no output"); body = serialized; } catch (error) { return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Could not serialize request: ${messageOf(error)}`" + `), requestState ) as result; } let output: result; let response: Response | undefined; try { response = await fetch(` + "`${this.url}/cell`" + `, { method: "POST", headers: { "Content-Type": "application/json" }, body, signal: options?.signal, }); try { output = parseResult(response, await response.text()); } catch (error) { output = responseReadFailure(error, response, options?.signal, false); } } catch (error) { output = requestFailure(error, options?.signal, false); } return await this.interceptResponse(serviceName, methodName, output, requestState, response) as result; } async stream( serviceName: string, methodName: string, dto: any | undefined | (() => any | undefined), onMessage: (data: T) => unknown, options?: StreamOptions ): Promise> { // dto 工厂:重连时重新求值,用于刷新续传游标(如 afterId) const getDto = typeof dto === "function" ? (dto as () => any | undefined) : () => dto; // 重连默认关闭(与 request 语义一致);传 retry: {...} 显式开启 const retry = options?.retry ? { maxAttempts: Infinity, baseDelayMs: 500, maxDelayMs: 10000, ...options.retry, } : null; for (let attempt = 0; ; attempt++) { const r = await this.streamOnce(serviceName, methodName, getDto(), onMessage, options); // 0 成功、1 协议错误、2 业务错误:终结态,不重连 if (r.status !== 4 && r.status !== 5) return r; if (!retry) return r; if (options?.signal?.aborted) return r; if (attempt + 1 > retry.maxAttempts) return r; const delay = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** attempt) * (0.75 + Math.random() * 0.5); // 退避等待:AbortSignal.timeout 免定时器 API,且后台标签页照常走时 await new Promise((resolve) => { if (options?.signal?.aborted) { resolve(); return; } AbortSignal.timeout(delay).addEventListener("abort", () => resolve(), { once: true }); options?.signal?.addEventListener("abort", () => resolve(), { once: true }); }); if (options?.signal?.aborted) return r; } } private async streamOnce( serviceName: string, methodName: string, dto: any | undefined, onMessage: (data: T) => unknown, options?: StreamOptions ): Promise> { let state: Record; try { state = { ...this.state, ...options?.state }; } catch (error) { return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Could not prepare request state: ${messageOf(error)}`" + `), Object.freeze({}) ); } try { await this.interceptRequest(serviceName, methodName, state, dto); } catch (error) { let requestState: Readonly>; try { requestState = this.snapshotState(state); } catch (snapshotError) { return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Could not snapshot request state: ${messageOf(snapshotError)}`" + `), Object.freeze({}) ); } return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Request interceptor failed: ${messageOf(error)}`" + `), requestState ); } let requestState: Readonly>; try { requestState = this.snapshotState(state); } catch (error) { return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Could not snapshot request state: ${messageOf(error)}`" + `), Object.freeze({}) ); } let body: string; try { const serialized = stringifyLossless({ serviceName, methodName, data: dto, ...(Object.keys(requestState).length ? { state: requestState } : {}), }); if (serialized === undefined) throw new Error("serialization produced no output"); body = serialized; } catch (error) { return await this.interceptResponse( serviceName, methodName, failure(1, ` + "`Could not serialize request: ${messageOf(error)}`" + `), requestState ); } let response: Response; try { response = await fetch(` + "`${this.url}/cell`" + `, { method: "POST", headers: { "Content-Type": "application/json" }, body, signal: options?.signal, }); } catch (error) { return await this.interceptResponse( serviceName, methodName, requestFailure(error, options?.signal, true), requestState ); } if (!response.ok) { let text: string; try { text = await response.text(); } catch (error) { return await this.interceptResponse( serviceName, methodName, responseReadFailure(error, response, options?.signal, true), requestState, response ); } return await this.interceptResponse( serviceName, methodName, parseResult(response, text), requestState, response ); } if (mediaType(response) !== "application/x-ndjson") { let text: string; try { text = await response.text(); } catch (error) { return await this.interceptResponse( serviceName, methodName, responseReadFailure(error, response, options?.signal, true), requestState, response ); } const rpcResult = parseResult(response, text); return await this.interceptResponse( serviceName, methodName, rpcResult.status === 0 ? failure(1, "Expected application/x-ndjson response") : rpcResult, requestState, response ); } if (!response.body) { return await this.interceptResponse( serviceName, methodName, { status: 0 }, requestState, response ); } let reader: ReadableStreamDefaultReader; try { reader = response.body.getReader(); } catch (error) { return await this.interceptResponse( serviceName, methodName, responseReadFailure(error, response, options?.signal, true), requestState, response ); } const decoder = new TextDecoder("utf-8", { fatal: true }); let buffer = ""; let lineNumber = 0; let failed: result | undefined; let cause: unknown; const emitLine = async (line: string) => { const payload = line.replace(/\r$/, "").trim(); if (!payload) return; let data: T; try { data = parseLossless(payload) as T; } catch (error) { failed = failure(1, ` + "`Invalid NDJSON at line ${lineNumber}: ${excerpt(payload)}`" + `); cause = error; return; } try { await onMessage(data); } catch (error) { failed = failure(1, ` + "`Stream callback failed: ${messageOf(error)}`" + `); cause = error; } }; try { // 空闲看门狗:idleTimeoutMs 内未收到任何字节(含心跳空行)判定流死亡, // 抛出哨兵错误 → status 5 → 交给上层重连。服务端 25s 心跳时建议 35000。 const STREAM_IDLE = Symbol("stream-idle"); for (;;) { let part: ReadableStreamReadResult; try { if (options?.idleTimeoutMs) { const idlePromise = new Promise((_, reject) => { AbortSignal.timeout(options.idleTimeoutMs!).addEventListener( "abort", () => reject(STREAM_IDLE), { once: true } ); }); idlePromise.catch(() => {}); // read 先返回时防 unhandled rejection part = await Promise.race([reader.read(), idlePromise]); } else { part = await reader.read(); } } catch (error) { cause = error; if (error === STREAM_IDLE) { failed = failure(5, "Stream idle timeout"); } else if (isTimeout(error, options?.signal)) { failed = failure(5, "Stream timed out"); } else if (options?.signal?.aborted === true) { failed = failure(4, "Stream aborted"); } else { failed = failure(4, ` + "`Stream read failed: ${messageOf(error)}`" + `); } break; } if (part.done) break; try { buffer += decoder.decode(part.value, { stream: true }); } catch (error) { failed = failure(1, ` + "`Invalid UTF-8 stream data: ${messageOf(error)}`" + `); cause = error; break; } for (;;) { const newline = buffer.indexOf("\n"); if (newline < 0) break; const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); lineNumber++; await emitLine(line); if (failed) break; } if (failed) break; } if (!failed) { try { buffer += decoder.decode(); } catch (error) { failed = failure(1, ` + "`Invalid UTF-8 stream data: ${messageOf(error)}`" + `); cause = error; } } if (!failed && buffer.length > 0) { lineNumber++; await emitLine(buffer); } } finally { if (failed) { try { await reader.cancel(cause); } catch { // The reader may already be closed by the runtime. } } try { reader.releaseLock(); } catch { // The reader may already be errored or released. } } return await this.interceptResponse( serviceName, methodName, failed || { status: 0 }, requestState, response ); } }` } func (ctx templateTs) genDefaultServiceTemplate() string { return `import { Client } from "./client"; {{- range .GenServiceList}} import {{.ServiceName}} from "./{{.ServiceName}}"; {{- end}} export class defaultApi extends Client { constructor(url: string) { super(url); } {{- range .GenServiceList}} public {{.ServiceName}}: {{.ServiceName}} = new {{.ServiceName}}(this); {{- end}} } export default class api { static create(url: string): defaultApi { return new defaultApi(url); } }` } func (ctx templateTs) genServiceTemplate() string { return `import { Client{{if .IsIncludeRequest}}, type result, type RequestOptions{{end}}{{if .IsIncludeStream}}{{if not .IsIncludeRequest}}, type result{{end}}, type StreamOptions{{end}} } from "./client"; {{- range .GenImport}} import type {{.Name}} from "./{{.Name}}"; {{- end}} export default class {{.ServiceName}} { private client: Client; constructor(client: Client) { this.client = client; } {{- $serviceName := .ServiceName }} {{- range .GenMethodTypeList}} {{if .IsStream }}async {{.MethodName}}({{if .DtoText}}dto: {{.DtoText}} | (() => {{.DtoText}}), {{end}}onMessage: (data: {{.GenericTypeText}}) => unknown, options?: StreamOptions): Promise> { return await this.client.stream<{{.GenericTypeText}}>("{{$serviceName}}", "{{.MethodName}}", {{if .DtoText}}dto{{else}}undefined{{end}}, onMessage, options) }{{else}}async {{.MethodName}}({{if .DtoText}}{{.DtoText}}, {{end}}options?: RequestOptions): Promise<{{.ReturnValueText}}> { return await this.client.request<{{.GenericTypeText}}>("{{$serviceName}}", "{{.MethodName}}", {{if .DtoText}}dto{{else}}undefined{{end}}, options) }{{end}} {{- end}} }` } func (ctx templateTs) genStructTemplate() string { return `{{- range .GenImport}}import type {{.Name}} from "./{{.Name}}";{{"\n"}}{{- end}}export default interface {{.Name}} { {{- range .GenClassFieldType}} {{.Name}}:{{.Type}} {{- end}} }` } func (ctx templateTs) genEnumTemplate() string { return `enum {{.Name}} { {{- range $index, $element := .Names}} {{$element}}, {{- end}} }{{ $enumName := .Name }} function values(): {{.Name}}[] { return [ {{- range $index, $element := .Names}} {{$enumName}}.{{$element}}, {{- end}} ] } {{if .DisplayNames}} function displayNames(): string[] { return [ {{- range $index, $element := .DisplayNames}} "{{$element}}", {{- end}} ] } {{end}} export default {{.Name}}` }