From 77a64c02f2eb2fec628ffd62718d4f2dfbede2b9 Mon Sep 17 00:00:00 2001 From: March7qwq Date: Fri, 24 Jul 2026 05:00:29 +0800 Subject: [PATCH] refactor: manage runtime dependencies with Effect layers --- src/env.ts | 37 ++++++++++++++++---------- src/index.ts | 73 +++++++++++++++++++++++++++++++++++---------------- src/layers.ts | 35 ++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 src/layers.ts diff --git a/src/env.ts b/src/env.ts index ca28217..ca67dc2 100644 --- a/src/env.ts +++ b/src/env.ts @@ -3,29 +3,38 @@ export interface EnvVar { value: string } -export const getEnvVars = (names: readonly string[]) => +export interface RuntimeEnv { + readonly env: (name: string) => string | undefined + readonly main: string + readonly execPath: string +} + +export const runtimeEnv: RuntimeEnv = { + env: name => Bun.env[name], + main: Bun.main, + execPath: process.execPath, +} + +export const getEnvVars = (runtime: RuntimeEnv) => (names: readonly string[]) => names.map(name => { - const rawValue = Bun.env[name] + const rawValue = runtime.env(name) const value = rawValue === undefined ? "" : rawValue const envVar = { name, value } return envVar }) -export const getExePathOrSrcDir = () => - getIsExeFile() ? getRawExecPath() : getRawMainDir() - -export const getWorkingDir = () => - getIsExeFile() ? getRawExecDir() : getRawMainDir() - -export const getIsExeFile = () => getRawMainPath().startsWith("/$bunfs/root/") - -const getRawMainDir = () => dropPathLastN(2)(getRawMainPath()) +export const getExePathOrSrcDir = (runtime: RuntimeEnv) => + getIsExeFile(runtime) ? runtime.execPath : getRawMainDir(runtime.main) -const getRawMainPath = () => Bun.main +export const getWorkingDir = (runtime: RuntimeEnv) => + getIsExeFile(runtime) + ? dropPathLastN(1)(runtime.execPath) + : getRawMainDir(runtime.main) -const getRawExecDir = () => dropPathLastN(1)(getRawExecPath()) +export const getIsExeFile = (runtime: RuntimeEnv) => + runtime.main.startsWith("/$bunfs/root/") -const getRawExecPath = () => process.execPath +const getRawMainDir = (main: string) => dropPathLastN(2)(main) const dropPathLastN = (n: number) => (path: string) => path.split("/").slice(0, -n).join("/") diff --git a/src/index.ts b/src/index.ts index 826e4a2..4ac1e44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { getIsExeFile, getWorkingDir, } from "./env" +import { AppLive, Files, Http, Runtime } from "./layers" import { buildDummyAnswer, buildShellFunc, @@ -60,12 +61,17 @@ Usage: - pipe( - buildShellFunc(getIsExeFile())(getExePathOrSrcDir())( - args.at(1) ?? "qwq", - )(text2ShellType(args.at(0))), - Console.log, - ) + Effect.gen(function* () { + const runtime = yield* Runtime + yield* Effect.sync(() => + pipe( + buildShellFunc(getIsExeFile(runtime))( + getExePathOrSrcDir(runtime), + )(args.at(1) ?? "qwq")(text2ShellType(args.at(0))), + Console.log, + ), + ) + }) const checkCmdExist = (args: string[]) => pipe( @@ -85,7 +91,8 @@ const extractCommand = (args: string[]) => const ask = (args: string[]) => Effect.gen(function* () { const config = yield* getConfig() - const envVars = getEnvVars(config.env_access.env_vars) + const runtime = yield* Runtime + const envVars = getEnvVars(runtime)(config.env_access.env_vars) const shell = pipe(args.at(0), text2ShellName) const question = pipe(args.slice(1), argsToText) return yield* config.debug @@ -134,20 +141,28 @@ const askAi = }) const getConfig = () => - Effect.succeed(`${getWorkingDir()}/config.yaml`).pipe( - Effect.flatMap(readFile), - Effect.flatMap(yaml2Data), - Effect.flatMap(Schema.decodeUnknown(ConfigS)), - Effect.mapError( - e => - `配置文件有误,请查阅文档重新配置: + Effect.gen(function* () { + const runtime = yield* Runtime + return yield* Effect.succeed( + `${getWorkingDir(runtime)}/config.yaml`, + ).pipe( + Effect.flatMap(readFile), + Effect.flatMap(yaml2Data), + Effect.flatMap(Schema.decodeUnknown(ConfigS)), + Effect.mapError( + e => + `配置文件有误,请查阅文档重新配置: ${typeof e === "string" ? e : ParseResult.TreeFormatter.formatErrorSync(e)}`, - ), - ) + ), + ) + }) const getCache = () => Effect.gen(function* () { - const cacheFile = fileFromPath(`${getWorkingDir()}/cache.hxqa`) + const runtime = yield* Runtime + const cacheFile = yield* fileFromPath( + `${getWorkingDir(runtime)}/cache.hxqa`, + ) const exists = yield* fileExist(cacheFile) if (!exists) { yield* writeFile(cacheFile)("") @@ -165,7 +180,10 @@ const getCache = () => const updateCache = (length: number) => (msgs: Message[]) => Effect.gen(function* () { if (msgs.length <= 0) return - const cacheFile = fileFromPath(`${getWorkingDir()}/cache.hxqa`) + const runtime = yield* Runtime + const cacheFile = yield* fileFromPath( + `${getWorkingDir(runtime)}/cache.hxqa`, + ) if (msgs.at(-1)!.content === qwqMetaTermMsg) { yield* writeFile(cacheFile)("") return @@ -298,7 +316,8 @@ const splitCmd = (ans: string) => ? ans.split(qwqCmdBeginId).at(-1)!.split(qwqCmdEndId).at(0)!.trim() : "" -const fileFromPath = (path: string) => Bun.file(path) +const fileFromPath = (path: string) => + Effect.map(Files, files => files.file(path)) const fileExist = (file: Bun.BunFile) => Effect.promise(() => file.exists()) @@ -313,10 +332,10 @@ const fileText = (file: Bun.BunFile) => ), ) -const readFile = (path: string) => pipe(path, fileFromPath, fileText) +const readFile = (path: string) => Effect.flatMap(fileFromPath(path), fileText) const writeFile = (pathOrFile: string | Bun.BunFile) => (data: string) => - Effect.promise(() => Bun.write(pathOrFile, data)) + Effect.flatMap(Files, files => files.write(pathOrFile, data)) const msgs2Hxqa = (msgs: Message[]) => Effect.succeed(msgs).pipe( @@ -335,7 +354,16 @@ const hxqa2Msgs = (hxqa: string) => // fetch 错误无法被 try 捕获。 const makeRequest = (url: string) => (data: Request) => - Effect.promise(() => fetch(url, data as unknown as Record)) + Effect.flatMap(Http, http => + http.request(url, { + method: data.method, + headers: { + Authorization: data.headers.Authorization, + "Content-Type": data.headers["Content-Type"], + }, + body: data.body, + }), + ) const responseText = (res: Response) => Effect.promise(() => res.text()) @@ -371,5 +399,6 @@ const data2Json = (data: unknown) => JSON.stringify(data) if (import.meta.main) main(Bun.argv.slice(2)).pipe( Effect.catchAll(e => Console.log("出现了错误:\n", e)), + Effect.provide(AppLive), Effect.runFork, ) diff --git a/src/layers.ts b/src/layers.ts new file mode 100644 index 0000000..e44db56 --- /dev/null +++ b/src/layers.ts @@ -0,0 +1,35 @@ +import { Context, Effect, Layer } from "effect" +import { type RuntimeEnv, runtimeEnv } from "./env" + +export interface FileSystem { + readonly file: (path: string) => Bun.BunFile + readonly write: ( + pathOrFile: string | Bun.BunFile, + data: string, + ) => Effect.Effect +} + +export interface HttpClient { + readonly request: ( + url: string, + data: RequestInit, + ) => Effect.Effect +} + +export class Runtime extends Context.Tag("qwq/Runtime")< + Runtime, + RuntimeEnv +>() {} +export class Files extends Context.Tag("qwq/Files")() {} +export class Http extends Context.Tag("qwq/Http")() {} + +export const RuntimeLive = Layer.succeed(Runtime, runtimeEnv) +export const FilesLive = Layer.succeed(Files, { + file: (path: string) => Bun.file(path), + write: (pathOrFile, data) => + Effect.promise(() => Bun.write(pathOrFile, data)), +}) +export const HttpLive = Layer.succeed(Http, { + request: (url, data) => Effect.promise(() => fetch(url, data)), +}) +export const AppLive = Layer.mergeAll(RuntimeLive, FilesLive, HttpLive)