Skip to content

Commit 69e379d

Browse files
committed
feat: docker deploy
1 parent bec832f commit 69e379d

18 files changed

+228
-79
lines changed

.dockerignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.git
3+
.gitignore
4+
*.md
5+
dist

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@ yarn-error.log*
4545
# Vitepress
4646
/docs/.vitepress/cache
4747
/docs/.vitepress/dist
48+
49+
# Env
50+
.env
51+
.env.dev
52+
.env.prod

Dockerfile

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM node:18.20-alpine AS builder
2+
WORKDIR /app
3+
4+
COPY . .
5+
ENV DOC_PARSE_BASE_URL=/doc-parse
6+
ENV CORS_FETCH_BASE_URL=/cors
7+
RUN npm install -g pnpm
8+
RUN pnpm install && pnpm build -m pwa
9+
10+
FROM python:3.12.7-slim
11+
WORKDIR /app
12+
13+
COPY src-backend/ .
14+
COPY --from=builder /app/dist ./app/static
15+
RUN pip install --no-cache-dir -r requirements.txt
16+
17+
EXPOSE 8000
18+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

src-backend/app.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from fastapi import FastAPI, HTTPException, Response, UploadFile, Form, File
2+
from pydantic import BaseModel
3+
import aiohttp
4+
from typing import Optional, Dict, Any
5+
import os
6+
from fastapi.staticfiles import StaticFiles
7+
from llama_parse import LlamaParse
8+
9+
10+
app = FastAPI()
11+
app.mount("/", StaticFiles(directory="app/static", html=True), name="static")
12+
13+
ALLOWED_PREFIXES = os.getenv('ALLOWED_PREFIXES', '').split(',')
14+
15+
class ProxyRequest(BaseModel):
16+
method: str
17+
url: str
18+
headers: Optional[Dict[str, str]] = None
19+
body: Optional[Any] = None
20+
21+
@app.post('/cors/proxy')
22+
async def proxy(request: ProxyRequest):
23+
if not any(request.url.startswith(prefix) for prefix in ALLOWED_PREFIXES):
24+
raise HTTPException(status_code=403, detail='URL not allowed')
25+
26+
kwargs = {
27+
'method': request.method,
28+
'url': request.url,
29+
'headers': request.headers or {}
30+
}
31+
32+
if request.body is not None:
33+
if isinstance(request.body, (dict, list)):
34+
kwargs['json'] = request.body
35+
else:
36+
kwargs['data'] = request.body
37+
38+
async with aiohttp.ClientSession() as session:
39+
try:
40+
async with session.request(**kwargs) as response:
41+
content = await response.read()
42+
43+
return Response(content=content, status_code=response.status)
44+
except Exception as e:
45+
raise HTTPException(status_code=500, detail=str(e))
46+
47+
@app.post('/doc-parse/parse')
48+
async def parse_document(
49+
file: UploadFile = File(...),
50+
language: Optional[str] = Form(default='en'),
51+
target_pages: Optional[str] = Form(default=None)
52+
):
53+
parser = LlamaParse(
54+
result_type='markdown',
55+
language=language,
56+
target_pages=target_pages
57+
)
58+
59+
file_content = await file.read()
60+
61+
try:
62+
documents = await parser.aload_data(
63+
file_content,
64+
{'file_name': file.filename}
65+
)
66+
67+
return {
68+
'success': True,
69+
'content': [{'text': doc.text, 'meta': doc.metadata} for doc in documents]
70+
}
71+
72+
except Exception as e:
73+
return {
74+
'success': False,
75+
'error': str(e)
76+
}
77+
78+
finally:
79+
await file.close()

src-backend/requirements.txt

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fastapi==0.115.5
2+
uvicorn==0.32.1
3+
aiohttp==3.11.7
4+
python-multipart==0.0.17
5+
llama-index-core==0.12.1
6+
llama-parse==0.5.15

src/components/MessageItem.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ const contents = computed(() => props.message.contents.map(x => {
242242
if (x.type === 'assistant-message' || x.type === 'user-message') {
243243
return {
244244
...x,
245-
text: sourceCodeMode.value ? wrapCode(x.text, 'markdown') : x.text
245+
text: sourceCodeMode.value ? wrapCode(x.text, 'markdown', 5) : x.text
246246
}
247247
}
248248
// Vue 3.4 computed is lazy. Force it to trigger.

src/composables/first-visit.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useQuasar } from 'quasar'
2+
import { DexieDBURL, LitellmBaseURL } from 'src/utils/config'
23
import { db } from 'src/utils/db'
34
import { localData } from 'src/utils/local-data'
45
import { dialogOptions } from 'src/utils/values'
@@ -15,13 +16,17 @@ export function useFirstVisit() {
1516
return
1617
}
1718
if (!localData.visited) {
19+
const serviceAvailable = !!(DexieDBURL && LitellmBaseURL)
20+
const message = serviceAvailable
21+
? 'AIaW 是全功能、轻量级、可拓展的 AI 客户端。<br><br>为了使用 AI 模型,你需要<b>配置服务商(API)</b>或者<b>登录</b>。<br>登录之后,还可以使用跨设备实时云同步功能。'
22+
: 'AIaW 是全功能、轻量级、可拓展的 AI 客户端。<br><br>为了使用 AI 模型,你需要<b>配置服务商(API)</b>。'
1823
$q.dialog({
1924
title: '欢迎使用 AI as Workspace',
20-
message: 'AIaW 是全功能、轻量级、可拓展的 AI 客户端。<br><br>为了使用 AI 模型,你需要<b>配置服务商(API)</b>或者<b>登录</b>。<br>登录之后,还可以使用跨设备实时云同步功能。',
25+
message,
2126
html: true,
2227
cancel: '配置服务商',
23-
persistent: true,
24-
ok: '登录',
28+
persistent: serviceAvailable,
29+
ok: serviceAvailable ? '登录' : false,
2530
...dialogOptions
2631
}).onCancel(() => {
2732
router.push('/settings')

src/composables/login-dialogs.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { useObservable } from '@vueuse/rxjs'
33
import { db } from 'src/utils/db'
44
import { watch } from 'vue'
55
import { dialogOptions } from 'src/utils/values'
6+
import { DexieDBURL } from 'src/utils/config'
67

78
export function useLoginDialogs() {
9+
if (!DexieDBURL) return
810
const userInteraction = useObservable(db.cloud.userInteraction)
911
const user = useObservable(db.cloud.currentUser)
1012
const $q = useQuasar()

src/composables/model.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { useUserPerfsStore } from 'src/stores/user-perfs'
33
import { Model, Provider } from 'src/utils/types'
44
import { useObservable } from '@vueuse/rxjs'
55
import { db } from 'src/utils/db'
6-
import { LitellmBaseURL } from 'src/utils/config'
6+
import { DexieDBURL, LitellmBaseURL } from 'src/utils/config'
77
import { ProviderTypes } from 'src/utils/values'
88

99
export function useModel(provider: Ref<Provider>, model: Ref<Model>) {
1010
const sdkModel = ref(null)
1111
const _model = ref<Model>(null)
1212
const { perfs } = useUserPerfsStore()
13-
const user = useObservable(db.cloud.currentUser)
13+
const user = DexieDBURL ? useObservable(db.cloud.currentUser) : null
1414
watchEffect(() => {
1515
_model.value = model.value || perfs.model
1616
if (!_model.value) {
@@ -21,7 +21,7 @@ export function useModel(provider: Ref<Provider>, model: Ref<Model>) {
2121
let _provider: Provider = null
2222
if (provider.value) _provider = provider.value
2323
else if (perfs.provider) _provider = perfs.provider
24-
else if (user.value.isLoggedIn) {
24+
else if (user?.value.isLoggedIn) {
2525
_provider = { type: 'openai', settings: { apiKey: user.value.data.apiKey, baseURL: LitellmBaseURL, compatibility: 'strict' } }
2626
} else {
2727
sdkModel.value = null

src/layouts/MainLayout.vue

+12
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,19 @@
8383
text-on-sur-var
8484
>
8585
<account-btn
86+
v-if="DexieDBURL"
8687
flat
8788
/>
89+
<q-btn
90+
v-else
91+
flat
92+
dense
93+
round
94+
icon="sym_o_book_2"
95+
title="使用指南"
96+
href="https://docs.aiaw.app/usage/"
97+
target="_blank"
98+
/>
8899
<q-space />
89100
<dark-switch-btn />
90101
<q-btn
@@ -140,6 +151,7 @@ import { useRoute, useRouter } from 'vue-router'
140151
import AccountBtn from 'src/components/AccountBtn.vue'
141152
import DarkSwitchBtn from 'src/components/DarkSwitchBtn.vue'
142153
import MenuItem from 'src/components/MenuItem.vue'
154+
import { DexieDBURL } from 'src/utils/config'
143155
144156
defineOptions({
145157
name: 'MainLayout'

src/pages/AccountPage.vue

+56-50
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<q-item>
4343
<q-item-section>
4444
<q-item-label caption>
45-
跨设备实时云同步服务,能够同步工作区、对话、助手、设置、插件等所有数据。价格为{{ SyncServicePrice }}元/月
45+
跨设备实时云同步服务,能够同步工作区、对话、助手、设置、插件等所有数据。<span v-if="SyncServicePrice">价格为{{ SyncServicePrice }}元/月</span>
4646
</q-item-label>
4747
</q-item-section>
4848
</q-item>
@@ -67,6 +67,7 @@
6767
</q-item-section>
6868
<q-item-section side>
6969
<q-btn
70+
v-if="BudgetBaseURL"
7071
unelevated
7172
label="订阅"
7273
bg-pri-c
@@ -94,55 +95,58 @@
9495
/>
9596
</q-item-section>
9697
</q-item>
97-
<q-separator spaced />
98-
<q-item-label header>
99-
模型服务
100-
</q-item-label>
101-
<q-item>
102-
<q-item-section>
103-
<q-item-label
104-
caption
105-
important:lh="1.5em"
106-
>
107-
一站式地使用不同服务商的各种先进模型,包括 gpt-4o、claude-3-5-sonnet、o1-mini 等,无需配置。额度随用随充,永久有效。按照官方API原价扣费(按USD/CNY=7计算)。<router-link
108-
to="/model-pricing"
109-
pri-link
98+
<template v-if="LitellmBaseURL">
99+
<q-separator spaced />
100+
<q-item-label header>
101+
模型服务
102+
</q-item-label>
103+
<q-item>
104+
<q-item-section>
105+
<q-item-label
106+
caption
107+
important:lh="1.5em"
110108
>
111-
模型价格
112-
</router-link>
113-
</q-item-label>
114-
</q-item-section>
115-
</q-item>
116-
<q-item>
117-
<q-item-section>
118-
<q-item-label>
119-
状态
120-
</q-item-label>
121-
<q-item-label caption>
122-
{{ !perfs.provider && user.isLoggedIn ? '正在使用(作为全局默认服务商)' : '未使用(已配置全局自定义服务商)' }}
123-
</q-item-label>
124-
</q-item-section>
125-
</q-item>
126-
<q-item>
127-
<q-item-section>
128-
<q-item-label>
129-
剩余额度
130-
</q-item-label>
131-
<q-item-label caption>
132-
<span v-if="llmBalance">¥{{ llmBalance }}</span>
133-
<span v-else>-</span>
134-
</q-item-label>
135-
</q-item-section>
136-
<q-item-section side>
137-
<q-btn
138-
unelevated
139-
bg-pri-c
140-
text-on-pri-c
141-
label="充值"
142-
@click="topupDialog"
143-
/>
144-
</q-item-section>
145-
</q-item>
109+
一站式地使用不同服务商的各种先进模型,包括 gpt-4o、claude-3-5-sonnet、o1-mini 等,无需配置。额度随用随充,永久有效。按照官方API原价扣费(按USD/CNY=7计算)。<router-link
110+
to="/model-pricing"
111+
pri-link
112+
>
113+
模型价格
114+
</router-link>
115+
</q-item-label>
116+
</q-item-section>
117+
</q-item>
118+
<q-item>
119+
<q-item-section>
120+
<q-item-label>
121+
状态
122+
</q-item-label>
123+
<q-item-label caption>
124+
{{ !perfs.provider && user.isLoggedIn ? '正在使用(作为全局默认服务商)' : '未使用(已配置全局自定义服务商)' }}
125+
</q-item-label>
126+
</q-item-section>
127+
</q-item>
128+
<q-item>
129+
<q-item-section>
130+
<q-item-label>
131+
剩余额度
132+
</q-item-label>
133+
<q-item-label caption>
134+
<span v-if="llmBalance">¥{{ llmBalance }}</span>
135+
<span v-else>-</span>
136+
</q-item-label>
137+
</q-item-section>
138+
<q-item-section side>
139+
<q-btn
140+
v-if="BudgetBaseURL"
141+
unelevated
142+
bg-pri-c
143+
text-on-pri-c
144+
label="充值"
145+
@click="topupDialog"
146+
/>
147+
</q-item-section>
148+
</q-item>
149+
</template>
146150
<template v-if="user.data.orderHistory?.length">
147151
<q-separator spaced />
148152
<q-item-label header>
@@ -162,6 +166,7 @@
162166
<q-item-label
163167
caption
164168
p="x-4 y-2"
169+
v-if="BudgetBaseURL"
165170
>
166171
若订单遇到异常,请联系开发者,Email:<a
167172
href="mailto:[email protected]"
@@ -195,7 +200,7 @@ import { useObservable } from '@vueuse/rxjs'
195200
import { db } from 'src/utils/db'
196201
import { useQuasar } from 'quasar'
197202
import SubscribeDialog from 'src/components/SubscribeDialog.vue'
198-
import { LitellmBaseURL, SyncServicePrice, UsdToCnyRate } from 'src/utils/config'
203+
import { BudgetBaseURL, LitellmBaseURL, SyncServicePrice, UsdToCnyRate } from 'src/utils/config'
199204
import TopupDialog from 'src/components/TopupDialog.vue'
200205
import { useRouter } from 'vue-router'
201206
import PayDialog from 'src/components/PayDialog.vue'
@@ -255,6 +260,7 @@ async function logout() {
255260
256261
const llmBalance = ref(null)
257262
async function refreshLlmBalance() {
263+
if (!LitellmBaseURL) return
258264
const resp = await fetch(`${LitellmBaseURL}/key/info`, {
259265
method: 'GET',
260266
headers: {

src/pages/SettingsPage.vue

+3-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
caption
5151
p="x-4 y-2"
5252
text-on-sur-var
53-
v-if="!perfs.provider && user.isLoggedIn"
53+
v-if="!perfs.provider && user?.isLoggedIn && LitellmBaseURL"
5454
>
5555
当前未配置自定义服务商,将默认使用我们提供的模型服务。详见<router-link
5656
pri-link
@@ -275,6 +275,7 @@ import { db } from 'src/utils/db'
275275
import ProviderInputItems from 'src/components/ProviderInputItems.vue'
276276
import { useLocateId } from 'src/composables/locate-id'
277277
import { pageFhStyle } from 'src/utils/functions'
278+
import { DexieDBURL, LitellmBaseURL } from 'src/utils/config'
278279
279280
const uiStateStore = useUiStateStore()
280281
const { perfs, restore } = useUserPerfsStore()
@@ -308,7 +309,7 @@ const providerLink = computed(() => {
308309
const provider = encodeURIComponent(JSON.stringify(perfs.provider))
309310
return `${location.origin}/set-provider?provider=${provider}`
310311
})
311-
const user = useObservable(db.cloud.currentUser)
312+
const user = DexieDBURL ? useObservable(db.cloud.currentUser) : null
312313
const { filteredOptions, filterFn } = useFilterOptions(modelOptions)
313314
314315
useLocateId(ref(true))

0 commit comments

Comments
 (0)