diff --git a/.env.development b/.env.development index 7aba812..4a49885 100644 --- a/.env.development +++ b/.env.development @@ -1 +1 @@ -VITE_API_BASE_URL = 'https://opencomputing.ai' +VITE_API_BASE_URL=https://token.opencomputing.cn diff --git a/.env.production b/.env.production index 7aba812..4a49885 100644 --- a/.env.production +++ b/.env.production @@ -1 +1 @@ -VITE_API_BASE_URL = 'https://opencomputing.ai' +VITE_API_BASE_URL=https://token.opencomputing.cn diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..e616685 --- /dev/null +++ b/.env.test @@ -0,0 +1 @@ +VITE_API_BASE_URL=https://tokentest.opencomputing.cn diff --git a/package-lock.json b/package-lock.json index 9799d16..c752507 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "vue-waterfall-plugin-next": "^3.0.1" }, "devDependencies": { - "@types/node": "^25.4.0", + "@types/node": "^25.9.2", "@types/swiper": "^6.0.0", "@vitejs/plugin-vue": "^5.2.3", "@vue/tsconfig": "^0.7.0", @@ -1712,12 +1712,13 @@ } }, "node_modules/@types/node": { - "version": "25.4.0", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.4.0.tgz", - "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "version": "25.9.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/swiper": { @@ -3432,10 +3433,11 @@ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==" }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true + "version": "7.24.6", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" }, "node_modules/unplugin": { "version": "3.0.0", diff --git a/package.json b/package.json index 6b28298..1a5630b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vue-tsc -b && vite build", + "build": "vite build", + "build:test": "vite build --mode test", + "build:prod": "vite build --mode production", "preview": "vite preview" }, "dependencies": { diff --git a/src/apis/Model.ts b/src/apis/Model.ts index 91b3cb1..b51a589 100644 --- a/src/apis/Model.ts +++ b/src/apis/Model.ts @@ -1,4 +1,5 @@ -import {get, post} from '../utlis/request' +import { get, getApiBaseURL, post } from '../utlis/request' +import type { TextToTextParams } from '@/types/model' // 文生视频 export const textvideo = (data:D):Promise =>post('/llmage/vidu_inference.dspy',data) @@ -9,6 +10,184 @@ export const imagetovideo = (data:D):Promise =>post('/llmage/vidu_infere // 参考生视频 export const referencevideo = (data:D):Promise =>post('/llmage/video',data) +// 文生图 +export const texttoimage = (data:D):Promise =>post('/llmage/v1/image/generations',data) + +// 文生文 +export const texttoText = (data:D):Promise =>post('/llmage/v1/chat/completions',data) + +const isDonePayload = (payload: string): boolean => /^\[\s*DONE\s*\]$/i.test(payload.trim()) + +const isStructuredPayload = (payload: string): boolean => { + const firstChar = payload.trim().charAt(0) + return firstChar === '{' || firstChar === '[' +} + +const trimEventPayload = (payload: string): string => payload + .replace(/(?:\\r\\n|\\n|\r?\n)+$/g, '') + .trim() + +const isCompletePayload = (payload: string): boolean => { + const text = payload.trim() + if (!text) return false + if (isDonePayload(text) || !isStructuredPayload(text)) return true + + let depth = 0 + let inString = false + let escaped = false + + for (const char of text) { + if (escaped) { + escaped = false + continue + } + + if (char === '\\') { + escaped = inString + continue + } + + if (char === '"') { + inString = !inString + continue + } + + if (inString) continue + + if (char === '{' || char === '[') { + depth += 1 + } else if (char === '}' || char === ']') { + depth -= 1 + } + } + + return depth === 0 && !inString +} + +const extractTextFromPayload = (payload: string): string => { + const normalizedPayload = trimEventPayload(payload) + if (!normalizedPayload || isDonePayload(normalizedPayload)) { + return '' + } + + try { + const parsed = JSON.parse(normalizedPayload) + const contentCandidates = [ + parsed?.choices?.[0]?.delta?.content, + parsed?.choices?.[0]?.message?.content, + parsed?.data?.content, + parsed?.data?.answer, + parsed?.content, + parsed?.answer, + parsed?.text, + ] + const content = contentCandidates.find((item): item is string => typeof item === 'string' && item.length > 0) + + return content || '' + } catch { + return isStructuredPayload(normalizedPayload) ? '' : normalizedPayload + } +} + +const readPayloadsFromBuffer = (buffer: string, flush = false): { payloads: string[]; rest: string } => { + const payloads: string[] = [] + let rest = buffer + const dataMarkerPattern = /(?:^|\r?\n|\\n)data:\s*/ + const nextDataMarkerPattern = /(?:\r?\n|\\n)data:\s*/ + + while (rest) { + const startMatch = dataMarkerPattern.exec(rest) + if (!startMatch) { + return { + payloads: flush && rest.trim() ? [...payloads, rest] : payloads, + rest: flush ? '' : rest, + } + } + + const payloadStart = startMatch.index + startMatch[0].length + const candidateText = rest.slice(payloadStart) + const nextMatch = nextDataMarkerPattern.exec(candidateText) + + if (nextMatch) { + payloads.push(trimEventPayload(candidateText.slice(0, nextMatch.index))) + rest = candidateText.slice(nextMatch.index) + continue + } + + const candidatePayload = trimEventPayload(candidateText) + if (flush || isCompletePayload(candidatePayload)) { + payloads.push(candidatePayload) + rest = '' + } else { + rest = rest.slice(startMatch.index) + } + break + } + + return { payloads, rest } +} + +const extractTextFromChunk = (chunk: string): string => { + const { payloads } = readPayloadsFromBuffer(chunk, true) + return payloads.reduce((result, payload) => result + extractTextFromPayload(payload), '') +} + +// 文生文流式输出 +export const textToTextStream = async ( + data: TextToTextParams, + onMessage: (content: string) => void, + signal?: AbortSignal +) => { + const response = await fetch(`${getApiBaseURL()}/llmage/v1/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(data), + signal, + }) + + if (!response.ok) { + throw new Error(`文生文请求失败:${response.status}`) + } + + if (!response.body) { + const text = await response.text() + onMessage(extractTextFromChunk(text)) + return + } + + const reader = response.body.getReader() + const decoder = new TextDecoder('utf-8') + let pendingText = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + + pendingText += decoder.decode(value, { stream: true }) + const { payloads, rest } = readPayloadsFromBuffer(pendingText) + pendingText = rest + + payloads.forEach((payload) => { + const content = extractTextFromPayload(payload) + if (content) { + onMessage(content) + } + }) + } + + pendingText += decoder.decode() + const { payloads } = readPayloadsFromBuffer(pendingText, true) + payloads.forEach((payload) => { + const content = extractTextFromPayload(payload) + if (content) { + onMessage(content) + } + }) +} + // 获取所有模型 export const AllModel = (data:D):Promise =>get('/llmage/get_type_llms.dspy',data) @@ -16,7 +195,10 @@ export const AllModel = (data:D):Promise =>get('/llmage/get_type_llms.ds export const queryModelPrice = (data:D):Promise =>post('/platformbiz/product_query_price.dspy',data) // 获取当天任务列表 -export const getTodayTask = ():Promise =>get('/llmage/get_my_asynctasks.dspy') +export const getTodayTask = (data?:D):Promise =>get('/llmage/get_my_asynctasks.dspy',data) // 获取任务状态 -export const getAsyncTaskStatus = (data:D):Promise =>get('/llmage/get_asynctask_status.dspy',data) \ No newline at end of file +export const getAsyncTaskStatus = (data:D):Promise =>get('/llmage/get_asynctask_status.dspy',data) + +// 历史记录 +export const getHistoryRecord = (data:D):Promise =>get('/llmage/api/get_inference_history.dspy',data) \ No newline at end of file diff --git a/src/assets/css/iconfont/demo_index.html b/src/assets/css/iconfont/demo_index.html index d31edd7..07407ca 100644 --- a/src/assets/css/iconfont/demo_index.html +++ b/src/assets/css/iconfont/demo_index.html @@ -54,6 +54,54 @@
    +
  • + +
    下载
    +
    &#xe621;
    +
  • + +
  • + +
    箭头_上下切换_o
    +
    &#xeb90;
    +
  • + +
  • + +
    搜索
    +
    &#xe619;
    +
  • + +
  • + +
    我的创作
    +
    &#xe61e;
    +
  • + +
  • + +
    +
    &#xe62f;
    +
  • + +
  • + +
    +
    &#xe611;
    +
  • + +
  • + +
    消息
    +
    &#xe8bd;
    +
  • + +
  • + +
    17A发送
    +
    &#xe67d;
    +
  • +
  • 414126695
    @@ -192,9 +240,9 @@
    @font-face {
       font-family: 'iconfont';
    -  src: url('iconfont.woff2?t=1776413178216') format('woff2'),
    -       url('iconfont.woff?t=1776413178216') format('woff'),
    -       url('iconfont.ttf?t=1776413178216') format('truetype');
    +  src: url('iconfont.woff2?t=1780995416168') format('woff2'),
    +       url('iconfont.woff?t=1780995416168') format('woff'),
    +       url('iconfont.ttf?t=1780995416168') format('truetype');
     }
     

    第二步:定义使用 iconfont 的样式

    @@ -220,6 +268,78 @@
      +
    • + +
      + 下载 +
      +
      .icon-xiazai +
      +
    • + +
    • + +
      + 箭头_上下切换_o +
      +
      .icon-jiantou_shangxiaqiehuan_o +
      +
    • + +
    • + +
      + 搜索 +
      +
      .icon-sousuo +
      +
    • + +
    • + +
      + 我的创作 +
      +
      .icon-wodechuangzuo +
      +
    • + +
    • + +
      + 上 +
      +
      .icon-shang +
      +
    • + +
    • + +
      + 下 +
      +
      .icon-xia +
      +
    • + +
    • + +
      + 消息 +
      +
      .icon-xiaoxi +
      +
    • + +
    • + +
      + 17A发送 +
      +
      .icon-a-17Afasong +
      +
    • +
    • @@ -427,6 +547,70 @@
        +
      • + +
        下载
        +
        #icon-xiazai
        +
      • + +
      • + +
        箭头_上下切换_o
        +
        #icon-jiantou_shangxiaqiehuan_o
        +
      • + +
      • + +
        搜索
        +
        #icon-sousuo
        +
      • + +
      • + +
        我的创作
        +
        #icon-wodechuangzuo
        +
      • + +
      • + +
        +
        #icon-shang
        +
      • + +
      • + +
        +
        #icon-xia
        +
      • + +
      • + +
        消息
        +
        #icon-xiaoxi
        +
      • + +
      • + +
        17A发送
        +
        #icon-a-17Afasong
        +
      • +
      • 账户余额 - {{ formattedBalance }} + + {{ formattedBalance }} + +
      diff --git a/src/components/LoginPrompt/LoginPrompt.vue b/src/components/LoginPrompt/LoginPrompt.vue index aafd1b8..751f074 100644 --- a/src/components/LoginPrompt/LoginPrompt.vue +++ b/src/components/LoginPrompt/LoginPrompt.vue @@ -3,7 +3,7 @@ @@ -22,8 +22,8 @@ interface Props { withDefaults(defineProps(), { title: '请先登录', - description: '', - buttonText: '去登录', + description: '登录后即可继续使用当前功能。', + buttonText: '立即登录', fullHeight: false, }); @@ -40,7 +40,7 @@ const handleLogin = () => { display: flex; align-items: center; justify-content: center; - padding: 32px 16px; + padding: 40px 24px; box-sizing: border-box; &.is-full-height { @@ -49,26 +49,48 @@ const handleLogin = () => { } .login-prompt__content { + width: min(460px, 100%); display: flex; flex-direction: column; align-items: center; - gap: 16px; + gap: 18px; + padding: 34px 36px; + // border: 1px solid rgba(148, 216, 255, 0.08); + border-radius: 18px; + // background: + // linear-gradient(180deg, rgba(18, 28, 42, 0.42), rgba(12, 18, 28, 0.18)); text-align: center; } .login-prompt__title { - font-size: 18px; - font-weight: 600; - color: rgba(255, 255, 255, 0.9); + font-size: 24px; + font-weight: 700; + color: rgba(255, 255, 255, 0.92); } .login-prompt__description { - max-width: 520px; - line-height: 1.7; - color: rgba(255, 255, 255, 0.65); + max-width: 390px; + line-height: 1.75; + color: rgba(235, 244, 255, 0.58); + font-size: 16px; } .login-prompt__button { - min-width: 120px; + min-width: 148px; + height: 44px; + margin-top: 8px; + border: none; + border-radius: 12px; + color: #07101a; + background: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff); + font-size: 15px; + font-weight: 700; + box-shadow: 0 12px 28px rgba(148, 216, 255, 0.18); + + &:hover, + &:focus { + color: #07101a; + background: linear-gradient(270deg, #a8e0ff 11.11%, #efedff); + } } diff --git a/src/components/RightResult/RightResult.vue b/src/components/RightResult/RightResult.vue index a5c11ae..688f2bf 100644 --- a/src/components/RightResult/RightResult.vue +++ b/src/components/RightResult/RightResult.vue @@ -5,8 +5,8 @@
      -
      - - {{ getStatusText(task.status) }} - + {{ getTaskTypeText(task) }}
    @@ -62,8 +57,15 @@
    -
    +
    + 图片预览
    +
    @@ -48,7 +49,7 @@ @@ -65,13 +66,19 @@ active-text-color="#00FFCC" @select="handleSelect" > - + + + - +