diff --git a/bun.lockb b/bun.lockb index 50c0079..7d0a184 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/internal/helper/src/env.ts b/internal/helper/src/env.ts index 6307ec6..799d3a6 100644 --- a/internal/helper/src/env.ts +++ b/internal/helper/src/env.ts @@ -2,12 +2,10 @@ const isProduction = process.env.NODE_ENV === 'production' const port = process.env.PORT || 5173 const base = process.env.BASE || '/' -const LOG_DIR = process.env.LOG_DIR || 'logs' export const Env = { isProduction, port: Number(port), - base, - LOG_DIR, + base } \ No newline at end of file diff --git a/internal/helper/src/path.ts b/internal/helper/src/path.ts index 41e8f4c..0a105e2 100644 --- a/internal/helper/src/path.ts +++ b/internal/helper/src/path.ts @@ -1,23 +1,18 @@ import path from "node:path" import fs from "node:fs/promises" -const isProduction = process.env.NODE_ENV === 'production' +export const isProduction = process.env.NODE_ENV === 'production' export function getPathByRoot(...argus: string[]) { return path.resolve(import.meta.dir, '../../..', ...argus) } -const templateHtml = isProduction - ? await fs.readFile(getPathByRoot('packages', 'client/index.html'), 'utf-8') - : '' - -export function getDevPathFromClient(...argus: string[]) { - return getPathByRoot('packages', 'client', ...argus) -} -export function getDevPathFromServer(...argus: string[]) { - return getPathByRoot('packages', 'server', ...argus) -} - -export function getProdPath(...argus: string[]) { - return getPathByRoot('dist', ...argus) -} +// 生产环境路径配置 +export const TemplateHtml = isProduction ? await fs.readFile('./client/index.html', 'utf-8') : "" +export const serverPublic = path.resolve("./public") +export const serverModules = path.resolve("./modules") +export const jobsDir = path.resolve("./jobs/jobs") +export const clientRoot = path.resolve("./client") +export const ssrManifest = path.resolve('./client/.vite/ssr-manifest.json') +export const entryServer = path.resolve('./server/entry-server.js') +export const logDir = path.resolve('./logs') diff --git a/package.json b/package.json index 1269c4e..5f8b1bf 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,10 @@ "scripts": { "postinstall": "node scripts/fix-type-router.js", "dev": "bun run --hot packages/server/src/booststap.ts", - "build": "bun run --filter client build", - "preview": "cross-env NODE_ENV=production bun run packages/server/src/booststap.ts", + "build:client": "bun run --filter client build", + "start": "cd dist && cross-env NODE_ENV=production bun run booststap.js", + "build:all": "tsup --config tsup.config.ts", + "build": "rimraf dist && bun run build:client && bun run build:all && node scripts/build.js", "tsc:booststap": "tsc packages/booststap/src/server.ts --outDir dist --module es2022 --target es2022 --lib es2022,dom --moduleResolution bundler --esModuleInterop --skipLibCheck --forceConsistentCasingInFileNames --noEmit false --incremental false", "tsc:server": "tsc packages/server/src/**/*.ts --outDir dist/server --module es2022 --target es2022 --lib es2022,dom --moduleResolution bundler --esModuleInterop --skipLibCheck --forceConsistentCasingInFileNames --noEmit false --incremental false" }, @@ -19,8 +21,11 @@ "client": "workspace:*", "core": "workspace:*", "cross-env": "^10.1.0", + "fast-glob": "^3.3.3", "helper": "workspace:*", + "rimraf": "^6.0.1", "server": "workspace:*", + "tsup": "^8.5.0", "unplugin-vue-components": "^29.1.0", "vite-plugin-devtools-json": "^1.0.0", "x": "workspace:*" diff --git a/packages/client/src/components/ChatBox/index.vue b/packages/client/src/components/ChatBox/index.vue index b8dc86f..f6b6c2b 100644 --- a/packages/client/src/components/ChatBox/index.vue +++ b/packages/client/src/components/ChatBox/index.vue @@ -11,8 +11,13 @@ const { scrollToBottom } = useScroll({ }); // import sseDataModule from "./_/sseData.ts"; - -const msgList = ref<{ role: "user" | "assistant" | "system", content: string, reasoning_content?: string }[]>([ +interface IMsg { + role: "user" | "assistant" | "system", + content: T extends "user" ? any : string, + reasoning_content?: string, + isHidden?: boolean +} +const msgList = ref([ { role: "system", content: PromptText @@ -26,7 +31,21 @@ enum STATUS { } const status = ref(STATUS.WAITING); -const { sendStream, getConfig, updateConfig } = useChat(); +const { sendStream, getConfig, updateConfig } = useChat(Chat.ModelProvider.OpenAI, { + // DeepSeek + // model: "deepseek-chat", + // apiKey: process.env.AI_APIKEY, + // baseUrl: "https://api.deepseek.com", + // temperature: 0.8, + + // siliconflow + model: "Qwen/Qwen3-8B", // 免费文本模型,可tools + // model: "deepseek-ai/deepseek-vl2", + apiKey: process.env.AI_APIKEY, + baseUrl: "https://api.siliconflow.cn/v1", + temperature: 0.8, +}); + const openaiConfig = reactive({ model: getConfig().model, apiKey: getConfig().apiKey, @@ -39,22 +58,15 @@ watch(openaiConfig, () => { }, { deep: true }) const inputEl = useTemplateRef("inputEl") -onMounted(() => { - inputMsg.value = "列举你能的事情,标记顺序,简洁回答,并提供友好的问候。" - handleSubmit() - inputEl.value?.focus() -}) -function handleSubmit() { - if(status.value === STATUS.SENDING) return +async function sendMsg(msg: any, isHidden?: boolean) { + const newMsg = `检查回答是否符合要求!!! + --- + ${msg}` msgList.value.push({ role: "user", - content: inputMsg.value, - }); - inputMsg.value = ""; - nextTick(() => { - scrollToBottom(); + content: newMsg, + isHidden: isHidden, }); - status.value = STATUS.SENDING; let contents = JSON.parse(JSON.stringify(unref(msgList))).map((v: any) => { return { @@ -67,23 +79,35 @@ function handleSubmit() { content: "", reasoning_content: "", }); - sendStream(contents as any, (msg: any) => { - msgList.value[msgList.value.length - 1].reasoning_content = msg.reasoning_content; - msgList.value[msgList.value.length - 1].content = msg.content; - if (msg.isComplete) { - status.value = STATUS.WAITING; + try { + await sendStream(contents as any, (msg: any) => { + msgList.value[msgList.value.length - 1].reasoning_content = msg.reasoning_content; + msgList.value[msgList.value.length - 1].content = msg.content; + if (msg.isComplete) { + status.value = STATUS.WAITING; + } + }); + } catch (error: any) { + try { + const text = await error.response.text() + msgList.value[msgList.value.length - 1].content = text; + } catch (err) { + msgList.value[msgList.value.length - 1].content = error.message; } + status.value = STATUS.WAITING; + } +} +onMounted(() => { + sendMsg("列举你能的事情,标记顺序,简洁回答。", true) + inputEl.value?.focus() +}) +function handleSubmit() { + if (status.value === STATUS.SENDING) return + sendMsg(inputMsg.value) + inputMsg.value = ""; + nextTick(() => { + scrollToBottom(); }); - - // let cursor = 0 - // const interval = setInterval(() => { - // msgList.value[msgList.value.length - 1].msg += sseDataModule[cursor].answer; - // cursor++; - // if (cursor >= sseDataModule.length) { - // clearInterval(interval); - // status.value = STATUS.WAITING; - // } - // }, 300); } function handleDelete(item: { role: string, content: string, reasoning_content?: string }, index: number) { @@ -110,17 +134,20 @@ function handleDelete(item: { role: string, content: string, reasoning_content?:
diff --git a/packages/client/src/components/ChatBox/prompt.txt b/packages/client/src/components/ChatBox/prompt.txt index e5fea4d..70e9ed1 100644 --- a/packages/client/src/components/ChatBox/prompt.txt +++ b/packages/client/src/components/ChatBox/prompt.txt @@ -1,6 +1,8 @@ +每次回答前必须遵循以下规则!!! + 知识库截断: 2024-06 -你是AI代理人, 由DeepSeek提供技术支持. 你为你的主人服务,为USER提供信息与服务。 +重要信息:你是AI代理人, 由Qwen/Qwen3-8B大模型提供技术支持. 你为你的主人服务,为USER提供信息与服务。 你可以帮主人代为处理一些任务。每次USER发送一条信息,你应该主动带入你的身份。 diff --git a/packages/client/src/composables/useChat/index.ts b/packages/client/src/composables/useChat/index.ts index 86f552f..545ca76 100644 --- a/packages/client/src/composables/useChat/index.ts +++ b/packages/client/src/composables/useChat/index.ts @@ -1,7 +1,7 @@ import { Chat } from "./Chat"; import { OpenAIModelConfig } from "./provider/Openai"; -export function useChat() { +export function useChat(provider: any, config: any) { if (import.meta.env.SSR) { return { sendStream: () => {}, @@ -18,14 +18,9 @@ export function useChat() { } const chat = new Chat(); let curProvider: ReturnType; - curProvider = chat.setProvider(Chat.ModelProvider.OpenAI, { - model: "deepseek-chat", - apiKey: __DEEPSEAK_APIKEY__, - baseUrl: "https://api.deepseek.com", - temperature: 0.8, - }); + curProvider = chat.setProvider(provider, config) function getConfig() { - return curProvider.getConfig(); + return curProvider!.getConfig(); } function updateConfig(config: OpenAIModelConfig) { chat.updateConfig({ diff --git a/packages/client/src/vue.d.ts b/packages/client/src/vue.d.ts index 6f344e0..d45375c 100644 --- a/packages/client/src/vue.d.ts +++ b/packages/client/src/vue.d.ts @@ -19,5 +19,9 @@ declare module '@vue/runtime-core' { } } declare global { - const __DEEPSEAK_APIKEY__: string + const process: { + env: { + AI_APIKEY: string + } + } } diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index ee243fa..42692d4 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -25,7 +25,7 @@ export default defineConfig({ }, }, define: { - __DEEPSEAK_APIKEY__: `"${process.env.DEEPSEAK_APIKEY}"`, + "process.env.AI_APIKEY": `"${process.env.AI_APIKEY}"`, }, build: { emptyOutDir: true, diff --git a/packages/core/src/SsrMiddleWare.ts b/packages/core/src/SsrMiddleWare.ts index 4be0786..8582769 100644 --- a/packages/core/src/SsrMiddleWare.ts +++ b/packages/core/src/SsrMiddleWare.ts @@ -1,5 +1,5 @@ import fs from 'node:fs/promises' -import { getPathByRoot } from "helper/path" +import { isProduction, TemplateHtml, getPathByRoot, clientRoot, ssrManifest, entryServer } from "helper/path" import { parseCookieHeader } from "helper/cookie" import { Env } from "helper/env" import { ViteDevServer } from 'vite' @@ -8,13 +8,8 @@ import type Koa from 'koa' import c2k from 'koa-connect' import { transformHtmlTemplate } from "unhead/server"; -const isProduction = Env.isProduction const base = Env.base -const templateHtml = isProduction - ? await fs.readFile(getPathByRoot('dist', 'client/index.html'), 'utf-8') - : '' - export async function SsrMiddleWare(app: Koa, options?: { onDevViteClose?: Function }) { let vite: ViteDevServer if (!isProduction) { @@ -36,7 +31,7 @@ export async function SsrMiddleWare(app: Koa, options?: { onDevViteClose?: Funct // Production mode: serve pre-built static assets. app.use(async (ctx, next) => { try { - await Send(ctx, ctx.path, { root: getPathByRoot('dist/client'), index: false }); + await Send(ctx, ctx.path, { root: clientRoot, index: false }); if (ctx.status === 404) { await next() } @@ -66,10 +61,10 @@ export async function SsrMiddleWare(app: Koa, options?: { onDevViteClose?: Funct manifest = {} render = (await vite.ssrLoadModule(getPathByRoot('packages', 'client/src/entry-server.ts'))).render } else { - manifest = await fs.readFile(getPathByRoot('dist', 'client/.vite/ssr-manifest.json'), 'utf-8') - template = templateHtml + manifest = await fs.readFile(ssrManifest, 'utf-8') + template = TemplateHtml // @ts-ignore - render = (await import(getPathByRoot('dist', 'server/entry-server.js'))).render + render = (await import(entryServer)).render } const cookies = parseCookieHeader(ctx.request.headers['cookie'] as string) diff --git a/packages/server/package.json b/packages/server/package.json index 6da9a5e..e996d54 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -13,12 +13,12 @@ "@types/koa": "^3.0.0", "@types/koa-bodyparser": "^4.3.12", "@types/koa-send": "^4.1.6", - "@types/path-is-absolute": "^1.0.2", - "jsonwebtoken": "^9.0.2" + "@types/path-is-absolute": "^1.0.2" }, "dependencies": { "@types/jsonwebtoken": "^9.0.10", "formidable": "^3.5.4", + "jsonwebtoken": "^9.0.2", "koa": "^3.0.1", "koa-bodyparser": "^4.4.1", "koa-connect": "^2.1.0", diff --git a/packages/server/src/jobs/index.ts b/packages/server/src/jobs/index.ts index 19c1c19..c4d3a2d 100644 --- a/packages/server/src/jobs/index.ts +++ b/packages/server/src/jobs/index.ts @@ -2,6 +2,8 @@ import fs from 'fs'; import path from 'path'; import scheduler from './scheduler'; import { TaskOptions } from 'node-cron'; +import { isProduction, jobsDir } from 'helper/path'; +import { logger } from '@/logger'; interface OneJob { id: string @@ -16,12 +18,12 @@ export function defineJob(job: OneJob) { return job; } -const jobsDir = path.join(__dirname, 'jobs'); +const _jobsDir = isProduction ? jobsDir : path.join(__dirname, 'jobs'); const jobModules: Record = {}; -fs.readdirSync(jobsDir).forEach(file => { - if (!file.endsWith('Job.ts')) return; - const jobModule = require(path.join(jobsDir, file)); +fs.readdirSync(_jobsDir).forEach(async file => { + if (!file.endsWith(isProduction ? 'Job.js' : 'Job.ts')) return; + const jobModule = await import(path.join(_jobsDir, file)); const job = jobModule.default || jobModule; if (job && job.id && job.cronTime && typeof job.task === 'function') { jobModules[job.id] = job; @@ -30,13 +32,15 @@ fs.readdirSync(jobsDir).forEach(file => { } }); +logger.info(`[Jobs] 加载了 ${Object.keys(jobModules).length} 个任务`); + function callHook(id: string, hookName: string) { const job = jobModules[id]; if (job && typeof job[hookName] === 'function') { try { job[hookName](); } catch (e) { - console.error(`[Job:${id}] ${hookName} 执行异常:`, e); + logger.error(`[Job:${id}] ${hookName} 执行异常:`, e); } } } diff --git a/packages/server/src/logger.ts b/packages/server/src/logger.ts index 6d7daea..d60b889 100644 --- a/packages/server/src/logger.ts +++ b/packages/server/src/logger.ts @@ -1,14 +1,12 @@ import log4js from "log4js"; -import { Env } from 'helper/env'; - -const { LOG_DIR } = Env; +import { logDir } from 'helper/path'; log4js.configure({ appenders: { all: { type: "file", - filename: `${LOG_DIR}/all.log`, + filename: `${logDir}/all.log`, maxLogSize: 102400, pattern: "-yyyy-MM-dd.log", alwaysIncludePattern: true, @@ -20,7 +18,7 @@ log4js.configure({ }, error: { type: "file", - filename: `${LOG_DIR}/error.log`, + filename: `${logDir}/error.log`, maxLogSize: 102400, pattern: "-yyyy-MM-dd.log", alwaysIncludePattern: true, @@ -32,7 +30,7 @@ log4js.configure({ }, jobs: { type: "file", - filename: `${LOG_DIR}/jobs.log`, + filename: `${logDir}/jobs.log`, maxLogSize: 102400, pattern: "-yyyy-MM-dd.log", alwaysIncludePattern: true, diff --git a/packages/server/src/middleware/install.ts b/packages/server/src/middleware/install.ts index 20d21a2..3c519b5 100644 --- a/packages/server/src/middleware/install.ts +++ b/packages/server/src/middleware/install.ts @@ -12,7 +12,7 @@ import { DefaultContext, Next, ParameterizedContext } from "koa" import { AuthMiddleware } from "./Auth" import Session from "./Session" import Send from "./Send" -import { getPathByRoot } from "helper/path" +import { getPathByRoot, isProduction, serverModules, serverPublic } from "helper/path" type App = typeof app @@ -31,7 +31,7 @@ export default async (app: App) => { }) - const publicPath = getPathByRoot("public") + const publicPath = isProduction ? serverPublic : getPathByRoot("public") app.use(async (ctx, next) => { if (!ctx.path.startsWith("/public")) return await next() if (ctx.method.toLowerCase() === "get") { @@ -65,7 +65,7 @@ export default async (app: App) => { app.use(bodyParser()) app.use( await Controller({ - root: path.resolve(__dirname, "../modules"), + root: isProduction ? serverModules : path.resolve(__dirname, "../modules"), handleBeforeEachRequest: (options: any) => { const { auth = true } = options || {} return async (ctx: ParameterizedContext, next: Next) => { diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..7ddeeb8 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// 获取当前文件目录 +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectRoot = join(__dirname, '..'); + +console.log('🚀 开始构建项目...'); + +try { + // 1. 读取package.json文件 + console.log('📋 读取package.json文件...'); + const rootPackageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8')); + const serverPackageJson = JSON.parse(readFileSync(join(projectRoot, 'packages/server/package.json'), 'utf8')); + + // 2. 合并依赖 + console.log('🔗 合并依赖...'); + const mergedDependencies = { + ...rootPackageJson.dependencies, + ...serverPackageJson.dependencies + }; + + // 3. 创建生产环境的package.json + const productionPackageJson = { + name: rootPackageJson.name, + type: 'module', + version: rootPackageJson.version || '1.0.0', + description: 'Koa SSR 生产环境应用', + main: 'booststap.js', + scripts: { + start: 'bun run booststap.js' + }, + dependencies: mergedDependencies, + engines: { + bun: '>=1.0.0' + }, + keywords: ['koa', 'ssr', 'vue', 'bun'], + author: '', + license: 'MIT' + }; + + // 4. 写入新的package.json到dist目录 + console.log('💾 生成生产环境package.json...'); + const distPackageJsonPath = join(projectRoot, 'dist', 'package.json'); + writeFileSync(distPackageJsonPath, JSON.stringify(productionPackageJson, null, 2)); + console.log('✅ package.json 已生成到 dist/package.json'); + + // 5. 显示合并后的依赖信息 + console.log('\n📊 依赖合并结果:'); + console.log(`- 根目录依赖: ${Object.keys(rootPackageJson.dependencies || {}).length} 个`); + console.log(`- Server依赖: ${Object.keys(serverPackageJson.dependencies || {}).length} 个`); + console.log(`- 合并后总依赖: ${Object.keys(mergedDependencies).length} 个`); + + console.log('\n🎉 构建脚本执行完成!'); + console.log('📁 生产环境文件已生成到 dist/ 目录'); + console.log('💡 可以使用以下命令启动生产环境:'); + console.log(' cd dist && bun install && bun run start'); + +} catch (error) { + console.error('❌ 构建过程中发生错误:', error.message); + process.exit(1); +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..9e57ea1 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "tsup"; +import pkg from "./package.json"; +import spkg from "./packages/server/package.json"; +import fg from "fast-glob" + +const jobsEntries = await fg(["packages/server/src/jobs/**/*.ts"], { }); +const modulesEntries = await fg(["packages/server/src/modules/**/*.ts"], { }); + + +export default defineConfig({ + entry: ["packages/server/src/booststap.ts", ...jobsEntries, ...modulesEntries], + format: "esm", + sourcemap: false, + clean: false, + cjsInterop: true, + external: [ + ...Object.keys(pkg.dependencies), + ...Object.keys(spkg.dependencies), + ] +}); diff --git a/tsup.jobs.config.ts b/tsup.jobs.config.ts new file mode 100644 index 0000000..8a1a0e0 --- /dev/null +++ b/tsup.jobs.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'tsup' +import pkg from "./package.json"; +import spkg from "./packages/server/package.json"; +import fg from "fast-glob" + +const entries = await fg(["packages/server/src/jobs/**/*.ts"], { }); + +export default defineConfig({ + entry: entries, + format: 'esm', + sourcemap: false, + clean: false, + outDir: "dist/jobs", + external: [ + ...Object.keys(pkg.dependencies), + ...Object.keys(spkg.dependencies), + ] +}) \ No newline at end of file diff --git a/tsup.modules.config.ts b/tsup.modules.config.ts new file mode 100644 index 0000000..76c6978 --- /dev/null +++ b/tsup.modules.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'tsup' +import pkg from "./package.json"; +import spkg from "./packages/server/package.json"; +import fg from "fast-glob" + +const entries = await fg(["packages/server/src/modules/**/*.ts"], { }); + +export default defineConfig({ + entry: entries, + format: 'esm', + sourcemap: false, + clean: false, + outDir: "dist/modules", + external: [ + ...Object.keys(pkg.dependencies), + ...Object.keys(spkg.dependencies), + ] +}) \ No newline at end of file