├── .dockerignore ├── .env.example ├── .github └── workflows │ └── docker-image.yml ├── .gitignore ├── Controller ├── liff │ ├── __init__.py │ ├── front_end.py │ ├── knowledge_base.py │ ├── knowledge_base_file.py │ ├── llm_model.py │ └── upload_file.py └── line │ ├── __init__.py │ └── line.py ├── Dockerfile ├── LICENSE ├── Middleware.py ├── Model ├── BaseModel.py ├── ChatHistory.py ├── KnowledgeBase.py ├── KnowledgeBaseFile.py ├── LlmModel.py ├── Setting.py ├── UploadedFiles.py ├── UserSelectKnowledgeBace.py └── __init__.py ├── README.md ├── Service ├── LineFunction.py ├── __init__.py ├── embedding.py ├── llm.py └── upload_file.py ├── View ├── .gitignore ├── .npmrc ├── README.md ├── babel.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── data │ │ │ ├── CityCountyData.json │ │ │ ├── workFeature.json │ │ │ ├── workMoney.json │ │ │ └── workTime.json │ │ ├── icon │ │ │ ├── csv.svg │ │ │ ├── doc.svg │ │ │ ├── pdf.svg │ │ │ ├── txt.svg │ │ │ └── upload.svg │ │ └── logo.png │ ├── components │ │ ├── basicSetting.vue │ │ ├── bottomTab.vue │ │ ├── fileDisplayCards │ │ │ ├── fileDisplayCard.vue │ │ │ └── fileSelectCard.vue │ │ ├── fileManager.vue │ │ ├── fileSelectWindow.vue │ │ ├── knowledgeBase.vue │ │ ├── knowledgeBaseSetting.vue │ │ ├── searchBar.vue │ │ ├── switchButton.vue │ │ ├── uploadWindow.vue │ │ └── whiteBackground.vue │ ├── index.css │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ └── index.js │ └── views │ │ └── HomeView.vue ├── tailwind.config.js └── vue.config.js ├── config ├── database_backup.db └── line_reply_template.py ├── db ├── files │ └── uploaded_files.txt └── vector_db │ └── vector_db.txt ├── dependencies.py ├── main.py └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | # uploaded files 2 | db/files/* 3 | !/db/files/uploaded_files.txt 4 | 5 | # db 6 | db/*.db 7 | db/*.db-journal 8 | *.sqlite3 9 | *.bin 10 | db/vetor_db/* 11 | !/db/vector_db/vector_db.txt 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # node modules 19 | View/node_modules/ 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | share/python-wheels/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | MANIFEST 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .nox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *.cover 64 | *.py,cover 65 | .hypothesis/ 66 | .pytest_cache/ 67 | cover/ 68 | 69 | # Translations 70 | *.mo 71 | *.pot 72 | 73 | # Django stuff: 74 | *.log 75 | local_settings.py 76 | db.sqlite3 77 | db.sqlite3-journal 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | .pybuilder/ 91 | target/ 92 | 93 | # Jupyter Notebook 94 | .ipynb_checkpoints 95 | 96 | # IPython 97 | profile_default/ 98 | ipython_config.py 99 | 100 | # pyenv 101 | # For a library or package, you might want to ignore these files since the code is 102 | # intended to run in multiple environments; otherwise, check them in: 103 | # .python-version 104 | 105 | # pipenv 106 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 107 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 108 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 109 | # install all needed dependencies. 110 | #Pipfile.lock 111 | 112 | # poetry 113 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 114 | # This is especially recommended for binary packages to ensure reproducibility, and is more 115 | # commonly ignored for libraries. 116 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 117 | #poetry.lock 118 | 119 | # pdm 120 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 121 | #pdm.lock 122 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 123 | # in version control. 124 | # https://pdm.fming.dev/#use-with-ide 125 | .pdm.toml 126 | 127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 128 | __pypackages__/ 129 | 130 | # Celery stuff 131 | celerybeat-schedule 132 | celerybeat.pid 133 | 134 | # SageMath parsed files 135 | *.sage.py 136 | 137 | # Environments 138 | .env 139 | .venv 140 | env/ 141 | venv/ 142 | ENV/ 143 | env.bak/ 144 | venv.bak/ 145 | 146 | # Spyder project settings 147 | .spyderproject 148 | .spyproject 149 | 150 | # Rope project settings 151 | .ropeproject 152 | 153 | # mkdocs documentation 154 | /site 155 | 156 | # mypy 157 | .mypy_cache/ 158 | .dmypy.json 159 | dmypy.json 160 | 161 | # Pyre type checker 162 | .pyre/ 163 | 164 | # pytype static type analyzer 165 | .pytype/ 166 | 167 | # Cython debug symbols 168 | cython_debug/ 169 | 170 | # PyCharm 171 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 172 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 173 | # and can be added to the global gitignore or merged into this file. For a more nuclear 174 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 175 | #.idea/ 176 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MODEL_NAME=llama3-8b-8192|gpt-3.5-turbo|gpt-4-1106-preview 2 | BASE_URL=https://api.groq.com/openai/v1|https://api.openai.com/v1|https://api.openai.com/v1 3 | API_KEY=API_KEY_1|API_KEY_2|API_KEY_3 4 | 5 | MAX_CHAT_HISTORY=5 6 | EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2" 7 | EMBEDDING_DEVICE="cpu" 8 | 9 | LINE_CHANNEL_ACCESS_TOKEN=YOUR_LINE_CHANNEL_ACCESS_TOKEN 10 | LINE_CHANNEL_SECRET=YOUR_LINE_CHANNEL_SECRET 11 | LINE_LIFF_ID=YOUR_LINE_LIFF_ID 12 | LINE_LOGIN_CHANNEL_ID=YOUR_LINE_LOGIN_CLIENT_ID 13 | 14 | FILE_MAX_SIZE=5MB 15 | SPACE_PER_USER=200MB 16 | 17 | ALLOW_FILE_TYPE=pdf,csv,txt 18 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Get current time 14 | uses: josStorer/get-current-time@v2 15 | id: current-time 16 | with: 17 | format: YYYYMMDD-HH 18 | utcOffset: "+08:00" 19 | - 20 | name: Checkout 21 | uses: actions/checkout@v4 22 | - 23 | name: Login to Docker Hub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | - 29 | name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | - 32 | name: Build and push 33 | uses: docker/build-push-action@v5 34 | with: 35 | context: . 36 | file: ./Dockerfile 37 | push: true 38 | tags: | 39 | ${{ secrets.DOCKERHUB_USERNAME }}/chatpdf-linebot:latest 40 | ${{ secrets.DOCKERHUB_USERNAME }}/chatpdf-linebot:${{ steps.current-time.outputs.formattedTime }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # uploaded files 2 | db/files/* 3 | !/db/files/uploaded_files.txt 4 | 5 | # db 6 | db/*.db 7 | db/*.db-journal 8 | *.sqlite3 9 | *.bin 10 | db/vetor_db/* 11 | !/db/vector_db/vector_db.txt 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | cover/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | .pybuilder/ 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | # For a library or package, you might want to ignore these files since the code is 99 | # intended to run in multiple environments; otherwise, check them in: 100 | # .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # poetry 110 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 111 | # This is especially recommended for binary packages to ensure reproducibility, and is more 112 | # commonly ignored for libraries. 113 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 114 | #poetry.lock 115 | 116 | # pdm 117 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 118 | #pdm.lock 119 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 120 | # in version control. 121 | # https://pdm.fming.dev/#use-with-ide 122 | .pdm.toml 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | #.idea/ 173 | -------------------------------------------------------------------------------- /Controller/liff/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/Controller/liff/__init__.py -------------------------------------------------------------------------------- /Controller/liff/front_end.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from fastapi.responses import FileResponse 3 | 4 | from Model.Setting import setting 5 | 6 | import os 7 | import requests 8 | import json 9 | 10 | router = APIRouter() 11 | 12 | @router.get("/liff") 13 | async def get_index(): 14 | return FileResponse('View/dist/index.html') 15 | 16 | @router.get("/liff/{path:path}") 17 | async def get_static_files_or_404(path): 18 | # try open file for path 19 | file_path = os.path.join("View/dist",path) 20 | if os.path.isfile(file_path): 21 | return FileResponse(file_path) 22 | return FileResponse('View/dist/index.html') 23 | 24 | # @router.post("/line_id") 25 | # async def session_line_id(request: Request): 26 | # try: 27 | # line_id = request.json()["line_id"] 28 | # request.session["line_id"] = line_id 29 | # return {"message": "OK"} 30 | # except: 31 | # return {"message": "error"} 32 | 33 | @router.get("/liffid") 34 | async def get_liffid(): 35 | return {"liff_id": setting.LINE_LIFF_ID} 36 | 37 | @router.get("/line_id/{line_id}") 38 | async def session_line_id(request: Request, line_id: str): 39 | 40 | try: 41 | url = "https://api.line.me/oauth2/v2.1/verify" 42 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 43 | data = { 44 | "id_token": line_id, 45 | "client_id": setting.LINE_LOGIN_CHANNEL_ID 46 | } 47 | 48 | response = requests.post(url, headers=headers, data=data) 49 | # print(response.sub) 50 | if response.status_code == 200: 51 | temp = json.loads(response.text) 52 | # print(temp) 53 | request.session["line_id"] = temp['sub'] 54 | else: 55 | request.session["line_id"] = None 56 | 57 | return {"message": "OK"} 58 | except: 59 | return {"message": "error"} 60 | -------------------------------------------------------------------------------- /Controller/liff/knowledge_base.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, Depends, HTTPException 2 | from fastapi.responses import FileResponse, JSONResponse 3 | 4 | from Model.KnowledgeBase import knowledgeBase 5 | 6 | from pydantic import BaseModel 7 | from dependencies import verify_line_id 8 | 9 | import os 10 | 11 | router = APIRouter(dependencies=[Depends(verify_line_id)]) 12 | 13 | @router.get("/api/knowledge_base") 14 | async def get_knowledge_base(request: Request): 15 | try: 16 | line_id = request.session.get("line_id") 17 | list = knowledgeBase.get_list(line_id) 18 | response = [] 19 | for i in list: 20 | response.append({ 21 | "id": i[0], 22 | "name": i[2], 23 | "model": i[3], 24 | "temperature": i[4], 25 | "score_threshold": i[5], 26 | "search_item_limit": i[6], 27 | }) 28 | return JSONResponse({"status":"success","data":response}) 29 | except Exception as e: 30 | print(e) 31 | return {"status": "error"} 32 | 33 | @router.get("/api/knowledge_base/{id}") 34 | async def get_knowledge_base_setting(request: Request, id: int): 35 | try: 36 | line_id = request.session.get("line_id") 37 | response = knowledgeBase.get_setting(id, line_id) 38 | response = { 39 | "id": response['id'], 40 | "name": response['name'], 41 | "model": response['model'], 42 | "temperature": response['temperature'], 43 | "score_threshold": response['score_threshold'], 44 | "search_item_limit": response['search_item_limit'], 45 | } 46 | return JSONResponse({"status":"success","data":response}) 47 | except Exception as e: 48 | print(e) 49 | raise HTTPException(status_code=400, detail="unable to access") 50 | 51 | class CreateKnowledgeBaseData(BaseModel): 52 | name: str 53 | model: int 54 | temperature: float 55 | score_threshold: float 56 | search_item_limit: int 57 | 58 | @router.post("/api/knowledge_base") 59 | async def create_knowledge_base(request: Request, postdata: CreateKnowledgeBaseData): 60 | try: 61 | line_id = request.session.get("line_id") 62 | data = postdata.dict() 63 | temperature = 1 if abs(data['temperature']) > 1 else abs(data['temperature']) 64 | score_threshold = 1 if abs(data['score_threshold']) > 1 else abs(data['score_threshold']) 65 | search_item_limit = 1 if abs(data['search_item_limit']) < 1 else abs(data['search_item_limit']) 66 | 67 | knowledgeBase.saveData((None, line_id, data['name'], int(data['model']), temperature, score_threshold, search_item_limit, )) 68 | return {"status": "success"} 69 | except Exception as e: 70 | print(e) 71 | return {"status": "error"} 72 | 73 | class KnowledgeBaseData(BaseModel): 74 | id: int 75 | name: str 76 | model: int 77 | temperature: float 78 | score_threshold: float 79 | search_item_limit: int 80 | 81 | @router.put("/api/knowledge_base/{id}") 82 | async def update_knowledge_base(request: Request, id: int, postdata: KnowledgeBaseData): 83 | line_id = request.session.get("line_id") 84 | data = postdata.dict() 85 | data['temperature'] = 1 if abs(data['temperature']) > 1 else abs(data['temperature']) 86 | data['score_threshold'] = 1 if abs(data['score_threshold']) > 1 else abs(data['score_threshold']) 87 | data['search_item_limit'] = 1 if abs(data['search_item_limit']) < 1 else abs(data['search_item_limit']) 88 | try: 89 | knowledgeBase.updateData(id, line_id, data) 90 | except Exception as e: 91 | print(e) 92 | return {"status": "error"} 93 | return {"status": "success"} 94 | 95 | @router.delete("/api/knowledge_base/{id}") 96 | async def delete_knowledge_base(request: Request, id: int): 97 | line_id = request.session.get("line_id") 98 | try: 99 | knowledgeBase.deleteData(id, line_id) 100 | except Exception as e: 101 | print(e) 102 | return {"status": "error"} 103 | return {"status": "success"} 104 | 105 | -------------------------------------------------------------------------------- /Controller/liff/knowledge_base_file.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, Body, Depends 2 | from fastapi.responses import FileResponse, JSONResponse 3 | 4 | from Model.KnowledgeBaseFile import knowledgeBaseFile 5 | from pydantic import BaseModel 6 | from Service.embedding import re_embedding 7 | 8 | from dependencies import verify_line_id 9 | 10 | import os 11 | 12 | router = APIRouter(dependencies=[Depends(verify_line_id)]) 13 | 14 | @router.get("/api/knowledge_base_setting/{id}") 15 | async def get_knowledge_base_file(request: Request, id: int): 16 | try: 17 | line_id = request.session.get("line_id") 18 | list = knowledgeBaseFile.get_all_files(line_id, id) 19 | response = [] 20 | for i in list: 21 | response.append({ 22 | "id": i[0], 23 | "name": i[1], 24 | "active": i[3], 25 | "filetype": i[2].split(".")[-1] 26 | }) 27 | return JSONResponse({"status": "success", "data": response}) 28 | except Exception as e: 29 | print(e) 30 | return {"status": "error"} 31 | 32 | @router.post("/api/knowledge_base_setting/{id}") 33 | async def add_file_to_knowledge_base(request: Request, id: int, file_id: int = Body(..., embed=True)): 34 | try: 35 | line_id = request.session.get("line_id") 36 | knowledgeBaseFile.add_file_to_knowledge_base(line_id, id, file_id) 37 | await re_embedding(line_id, id) 38 | return {"status": "success"} 39 | except Exception as e: 40 | print(e) 41 | return {"status": "error"} 42 | 43 | @router.put("/api/knowledge_base_setting/{id}") 44 | async def update_knowledge_base_file(request: Request, id: int, file_id: int = Body(..., embed=True), active: bool = Body(..., embed=True)): 45 | try: 46 | line_id = request.session.get("line_id") 47 | knowledgeBaseFile.setActive(line_id, id, file_id, active) 48 | await re_embedding(line_id, id) 49 | except Exception as e: 50 | print(e) 51 | return {"status": "error"} 52 | return {"status": "success"} 53 | 54 | @router.delete("/api/knowledge_base_setting/{id}") 55 | async def delete_knowledge_base_file(request: Request, id: int, file_id: int = Body(..., embed=True)): 56 | try: 57 | line_id = request.session.get("line_id") 58 | knowledgeBaseFile.delete_file_from_knowledge_base(id, file_id, line_id) 59 | await re_embedding(line_id, id) 60 | except Exception as e: 61 | print(e) 62 | return {"status": "error"} 63 | return {"status": "success"} 64 | -------------------------------------------------------------------------------- /Controller/liff/llm_model.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, Body 2 | from fastapi.responses import FileResponse 3 | 4 | from Model.Setting import setting 5 | 6 | import os 7 | 8 | router = APIRouter() 9 | 10 | @router.get("/api/model") 11 | async def get_all_llm_models(request: Request): 12 | try: 13 | line_id = request.session.get("line_id") 14 | list = setting.MODEL_NAME 15 | response = [] 16 | for i, item in enumerate(list): 17 | response.append({ 18 | "id": i, 19 | "name": item, 20 | }) 21 | return {"status": "success", "data": response} 22 | except Exception as e: 23 | print(e) 24 | return {"status": "error"} 25 | -------------------------------------------------------------------------------- /Controller/liff/upload_file.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, File, Form, UploadFile, Depends, HTTPException 2 | from fastapi.responses import FileResponse 3 | 4 | from Model.UploadedFiles import uploadedFiles 5 | 6 | from dependencies import verify_line_id 7 | 8 | from Service.upload_file import upload_file, delete_file 9 | 10 | import os 11 | 12 | router = APIRouter(dependencies=[Depends(verify_line_id)]) 13 | 14 | @router.get("/api/upload") 15 | async def get_all_files_of_this_user(request: Request): 16 | line_id = request.session.get("line_id") 17 | list = uploadedFiles.get_all_files_list(line_id) 18 | response = [] 19 | for i in list: 20 | response.append({ 21 | "id": i[0], 22 | "name": i[2], 23 | "filetype": i[3].split(".")[-1] 24 | }) 25 | return {"status": "success", "data": response} 26 | 27 | @router.get("/api/upload/{id}") 28 | async def get_file_data(request: Request, id: int): 29 | line_id = request.session.get("line_id") 30 | list = uploadedFiles.get_all_files_list(line_id) 31 | response = [] 32 | for i in list: 33 | response.append({ 34 | "id": i[0], 35 | "name": i[2], 36 | "filetype": i[3].split(".")[-1] 37 | }) 38 | return {"status": "success", "data": response} 39 | 40 | 41 | @router.post("/api/upload") 42 | async def upload_file_to_server(request: Request, file: UploadFile = File(...)): 43 | try: 44 | line_id = request.session.get("line_id") 45 | # file_id = await uploadedFiles.uploaded_file(line_id, file) 46 | file_id = await upload_file(line_id, file) 47 | return {"status": "success", "file_id": file_id} 48 | except Exception as e: 49 | raise HTTPException(status_code=400, detail=str(e)) 50 | 51 | # @router.put("/api/upload/{id}") 52 | # async def update_knowledge_base(request: Request, id: int): 53 | # return FileResponse('public/index.html') 54 | 55 | @router.delete("/api/upload/{id}") 56 | async def delete_file_from_server(request: Request, id: int): 57 | try: 58 | line_id = request.session.get("line_id") 59 | delete_file(id, line_id) 60 | except Exception as e: 61 | print(e) 62 | return {"status": "error"} 63 | return {"status": "success"} 64 | 65 | -------------------------------------------------------------------------------- /Controller/line/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/Controller/line/__init__.py -------------------------------------------------------------------------------- /Controller/line/line.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, HTTPException, APIRouter 2 | 3 | from linebot import LineBotApi, WebhookHandler 4 | 5 | from linebot.exceptions import InvalidSignatureError 6 | from linebot.models import MessageEvent, TextMessage, TextSendMessage, QuickReply, QuickReplyButton, MessageAction, FlexSendMessage 7 | 8 | import os 9 | import json 10 | 11 | from Model.Setting import setting 12 | from Model.UserSelectKnowledgeBace import userSelectKnowledgeBace 13 | from Model.ChatHistory import chatHistory 14 | 15 | from Service.llm import chat_llm 16 | import Service.LineFunction as LineFunction 17 | 18 | line_router = APIRouter() 19 | line_bot_api = LineBotApi(setting.LINE_CHANNEL_ACCESS_TOKEN) 20 | handler = WebhookHandler(setting.LINE_CHANNEL_SECRET) 21 | 22 | 23 | @line_router.post("/callback") 24 | async def callback(request: Request): 25 | signature = request.headers["X-Line-Signature"] 26 | body = await request.body() 27 | try: 28 | handler.handle(body.decode(), signature) 29 | except InvalidSignatureError: 30 | raise HTTPException(status_code=400, detail="Missing Parameters") 31 | return "OK" 32 | 33 | @handler.add(MessageEvent, message=TextMessage) 34 | def handling_message(event): 35 | 36 | 37 | if isinstance(event.message, TextMessage): 38 | 39 | 40 | user_message = event.message.text 41 | line_id = event.source.user_id 42 | QuickReplyButtons = [ 43 | QuickReplyButton( 44 | action=MessageAction(label="繼續",text="繼續") 45 | ), 46 | QuickReplyButton( 47 | action=MessageAction(label="清除對話",text="/clear") 48 | ) 49 | ] 50 | 51 | reply_msg = TextSendMessage( 52 | text="" 53 | ) 54 | 55 | if user_message[0] == "/": 56 | func, args = parseMessage(user_message) 57 | if func == "select": 58 | try: 59 | userSelectKnowledgeBace.changeSelected(line_id, args) 60 | reply_msg = TextSendMessage( 61 | text="已切換知識庫" 62 | ) 63 | except Exception as e: 64 | print(e) 65 | reply_msg = TextSendMessage( 66 | text="error" 67 | ) 68 | elif func == "clear": 69 | try: 70 | chatHistory.clear(line_id) 71 | reply_msg = TextSendMessage( 72 | text="對話紀錄已清除" 73 | ) 74 | except Exception as e: 75 | print(e) 76 | reply_msg = TextSendMessage( 77 | text="error" 78 | ) 79 | elif func == "info": 80 | reply_msg = FlexSendMessage( 81 | alt_text='資訊版面', 82 | contents=json.loads(LineFunction.replay_info(line_id)) 83 | ) 84 | elif func == "help": 85 | reply_msg = FlexSendMessage( 86 | alt_text='幫助', 87 | contents=json.loads(LineFunction.replay_help()) 88 | ) 89 | elif func == "about": 90 | reply_msg = FlexSendMessage( 91 | alt_text='關於作者', 92 | contents=json.loads(LineFunction.replay_about_me()) 93 | ) 94 | else: 95 | reply_text = "請輸入正確的指令" 96 | else: 97 | reply_text = chat_llm(user_message, line_id) 98 | reply_msg = TextSendMessage( 99 | text=reply_text, 100 | quick_reply=QuickReply( 101 | items=QuickReplyButtons 102 | ) 103 | ) 104 | 105 | line_bot_api.reply_message( 106 | event.reply_token, 107 | reply_msg 108 | ) 109 | 110 | 111 | def parseMessage(user_message): 112 | temp = user_message[1:].split(" ") 113 | func = temp[0] 114 | 115 | if len(temp) == 1: 116 | return func, '' 117 | 118 | args = temp[1] 119 | return func, args -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用 Node.js 來構建 Vue.js 網頁 2 | FROM node:20 AS build 3 | 4 | # 設置工作目錄 5 | WORKDIR /app 6 | 7 | # 複製前端代碼 8 | COPY View ./View 9 | 10 | # 進入前端目錄並安裝依賴和構建 11 | WORKDIR /app/View 12 | RUN npm install && npm run build 13 | 14 | # 使用 Python 作為基礎映像 15 | FROM python:3.10.12-slim 16 | 17 | # 設置工作目錄 18 | WORKDIR /app 19 | 20 | # 設置時區 21 | ENV TZ=Asia/Taipei 22 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 23 | 24 | # 設置 IN_DOCKER 環境變量 25 | ENV IN_DOCKER=1 26 | 27 | # 複製後端代碼和構建後的前端文件 28 | COPY . . 29 | COPY --from=build /app/View/dist /app/View/dist 30 | 31 | # Install Python requirements 32 | RUN ls && \ 33 | cp .env.example .env && \ 34 | pip install --no-cache-dir -r requirements.txt 35 | 36 | # Expose port 37 | EXPOSE 8000 38 | 39 | # Run the application 40 | CMD ["python", "main.py"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, Response 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | 4 | class NoIndexMiddleware(BaseHTTPMiddleware): 5 | async def dispatch(self, request: Request, call_next): 6 | response = await call_next(request) 7 | response.headers["X-Robots-Tag"] = "noindex" 8 | return response 9 | -------------------------------------------------------------------------------- /Model/BaseModel.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | class BaseModel: 4 | def __init__(self): 5 | self.filed = [] 6 | self.con = self.get_db_connection() 7 | self.table = "" 8 | 9 | def get_db_connection(self): 10 | import sqlite3 11 | if os.path.exists('./db/database.db'): 12 | return sqlite3.connect('./db/database.db') 13 | else: 14 | shutil.copy('./config/database_backup.db', './db/database.db') 15 | return sqlite3.connect('./db/database.db') 16 | 17 | def sql_query(self, query, parameters=None, get_lastrow_id = False): 18 | cursor = self.con.cursor() 19 | cursor.execute(query, parameters) 20 | self.con.commit() 21 | if get_lastrow_id: 22 | return cursor.lastrowid 23 | return cursor.fetchall() 24 | 25 | def sql_query_many(self, query, parameters=None): 26 | cursor = self.con.cursor() 27 | cursor.executemany(query, parameters) 28 | self.con.commit() 29 | return cursor.fetchall() 30 | 31 | 32 | def saveData(self, data): 33 | cursor = self.con.cursor() 34 | columns = ', '.join(self.filed) 35 | values = ', '.join(['?'] * len(self.filed)) 36 | query = f"INSERT INTO {self.table} ({columns}) VALUES ({values})" 37 | cursor.execute(query, data) 38 | self.con.commit() 39 | 40 | def getData(self, conditions=None, limit=None, order=None): 41 | cursor = self.con.cursor() 42 | query = f"SELECT * FROM {self.table}" 43 | if conditions: 44 | query += " WHERE " + " AND ".join(conditions) 45 | 46 | if limit: 47 | query += " LIMIT " + limit 48 | 49 | if order: 50 | query += " ORDER BY " + order 51 | 52 | cursor.execute(query) 53 | return cursor.fetchall() 54 | -------------------------------------------------------------------------------- /Model/ChatHistory.py: -------------------------------------------------------------------------------- 1 | from Model.BaseModel import BaseModel 2 | from Model.Setting import setting 3 | 4 | class ChatHistory(BaseModel): 5 | def __init__(self): 6 | super().__init__() 7 | self.filed = ['time', 'line_id', 'user_message', 'bot_message'] 8 | self.table = 'chat_history' 9 | 10 | def clear(self, line_id): 11 | return self.sql_query('DELETE FROM chat_history WHERE line_id =?', (line_id,)) 12 | 13 | def get_history(self, line_id): 14 | return self.sql_query('SELECT user_message, bot_message FROM chat_history WHERE line_id =? ORDER BY time DESC LIMIT ?', (line_id, setting.MAX_CHAT_HISTORY,)) 15 | 16 | chatHistory = ChatHistory() -------------------------------------------------------------------------------- /Model/KnowledgeBase.py: -------------------------------------------------------------------------------- 1 | from Model.BaseModel import BaseModel 2 | from Model.Setting import Setting 3 | setting = Setting() 4 | class KnowledgeBase(BaseModel): 5 | def __init__(self): 6 | super().__init__() 7 | self.filed = ['id', 'line_id', 'name', 'model','temperature', 'score_threshold', 'search_item_limit'] 8 | self.table = 'knowledge_base' 9 | 10 | def get_setting(self, id, line_id): 11 | result = self.sql_query(f"SELECT * FROM {self.table} WHERE id = ? AND line_id = ?", (id, line_id,))[0] 12 | 13 | return { 14 | "id": result[0], 15 | "name": result[2], 16 | "base_url": setting.BASE_URL[result[3]], 17 | "api_key": setting.API_KEY[result[3]], 18 | # "model": setting.MODEL_NAME[result[3]], 19 | "model": result[3], 20 | "temperature": result[4], 21 | "score_threshold": result[5], 22 | "search_item_limit": result[6] 23 | } 24 | 25 | def get_list(self, line_id): 26 | result = self.sql_query(f"SELECT * FROM {self.table} WHERE line_id = ?", (line_id,)) 27 | return result 28 | 29 | def updateData(self, id, line_id, data: dict): 30 | self.sql_query(f"UPDATE {self.table} SET id=?, name=?, model=?, temperature=?, score_threshold=?, search_item_limit=? WHERE id = ? AND line_id = ?", 31 | (data['id'], data['name'], data['model'], data['temperature'], data['score_threshold'], data['search_item_limit'], id, line_id,)) 32 | 33 | def deleteData(self, id, line_id): 34 | self.sql_query(f"DELETE FROM {self.table} WHERE id = ? AND line_id = ?", (id, line_id,)) 35 | 36 | knowledgeBase = KnowledgeBase() -------------------------------------------------------------------------------- /Model/KnowledgeBaseFile.py: -------------------------------------------------------------------------------- 1 | from Model.BaseModel import BaseModel 2 | 3 | class KnowledgeBaseFile(BaseModel): 4 | def __init__(self): 5 | super().__init__() 6 | self.filed = ['knowledge_base_id', 'file_id', 'active'] 7 | self.table = 'knowledge_base_file' 8 | 9 | def get_all_files(self, line_id, knowledge_base_id): 10 | return self.sql_query(f'SELECT f.id, f.file_name, f.file_path, kbf.active FROM {self.table} AS kbf INNER JOIN knowledge_base AS kb ON kb.id=kbf.knowledge_base_id INNER JOIN uploaded_files AS f ON f.id=kbf.file_id WHERE knowledge_base_id = ? AND kb.line_id = ?', (knowledge_base_id, line_id, )) 11 | 12 | def add_file_to_knowledge_base(self, line_id, knowledge_base_id, file_id): 13 | # values = ', '.join(['?'] * len(file_ids)) 14 | 15 | return self.sql_query(f""" 16 | INSERT INTO knowledge_base_file (knowledge_base_id, file_id) 17 | SELECT kb.id, uf.id 18 | FROM knowledge_base kb 19 | CROSS JOIN uploaded_files uf 20 | WHERE kb.id = ? 21 | AND kb.line_id = ? 22 | AND uf.line_id = ? 23 | AND uf.id IN (?) 24 | """, (knowledge_base_id, line_id, line_id, file_id,)) 25 | 26 | def setActive(self, line_id, knowledge_base_id, file_id, active): 27 | return self.sql_query(f""" 28 | UPDATE {self.table} SET active =? 29 | WHERE knowledge_base_id=(SELECT id FROM knowledge_base WHERE id=? AND line_id=?) 30 | AND file_id=(SELECT id FROM uploaded_files WHERE id=? AND line_id=?) 31 | """, (active, knowledge_base_id, line_id, file_id, line_id)) 32 | 33 | def delete_file_from_knowledge_base(self, knowledge_base_id, file_id, line_id): 34 | return self.sql_query(f""" 35 | DELETE FROM {self.table} 36 | WHERE knowledge_base_id = (SELECT id FROM knowledge_base WHERE id = ? AND line_id = ?) 37 | AND file_id = (SELECT id FROM uploaded_files WHERE id = ? AND line_id = ?) 38 | """, (knowledge_base_id, line_id, file_id, line_id)) 39 | 40 | knowledgeBaseFile = KnowledgeBaseFile() -------------------------------------------------------------------------------- /Model/LlmModel.py: -------------------------------------------------------------------------------- 1 | from Model.BaseModel import BaseModel 2 | 3 | class LlmModel(BaseModel): 4 | def __init__(self): 5 | super().__init__() 6 | self.filed = ['id', 'name', 'base_url', 'token'] 7 | self.table = 'llm_model' 8 | 9 | def get_all_models(self): 10 | return self.sql_query(f"SELECT id, name from {self.table}") -------------------------------------------------------------------------------- /Model/Setting.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | if not os.getenv("IN_DOCKER", ""): 5 | load_dotenv(override=True) 6 | 7 | class Setting: 8 | def __init__(self): 9 | self.LINE_CHANNEL_ACCESS_TOKEN = os.getenv("LINE_CHANNEL_ACCESS_TOKEN", "YOUR_LINE_CHANNEL_ACCESS_TOKEN") 10 | self.LINE_CHANNEL_SECRET = os.getenv("LINE_CHANNEL_SECRET", "YOUR_LINE_CHANNEL_SECRET") 11 | self.LINE_LIFF_ID = os.environ.get("LINE_LIFF_ID") or os.getenv("LINE_LIFF_ID", "YOUR_LINE_LIFF_ID") 12 | self.LINE_LOGIN_CHANNEL_ID = os.getenv("LINE_LOGIN_CHANNEL_ID", "YOUR_LINE_LOGIN_CLIENT_ID") 13 | 14 | self.EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2") 15 | self.EMBEDDING_DEVICE = os.getenv("EMBEDDING_DEVICE", "cpu") 16 | 17 | self.MODEL_NAME = os.getenv("MODEL_NAME", "llama3-8b-8192|gpt-3.5-turbo|gpt-4-1106-preview").split("|") 18 | self.BASE_URL = os.getenv("BASE_URL", "https://api.groq.com/openai/v1|https://api.openai.com/v1|https://api.openai.com/v1").split("|") 19 | self.API_KEY = os.getenv("API_KEY", "API_KEY_1|API_KEY_2|API_KEY_3").split("|") 20 | 21 | self.FILE_MAX_SIZE = self.space_conversion(os.getenv("FILE_MAX_SIZE", "5MB")) 22 | self.SPACE_PER_USER = self.space_conversion(os.getenv("SPACE_PER_USER", "200MB")) 23 | 24 | self.ALLOW_FILE_TYPE = os.getenv("ALLOW_FILE_TYPE", "pdf,csv,txt").split(",") 25 | 26 | self.MAX_CHAT_HISTORY = int(os.getenv("MAX_CHAT_HISTORY", "5")) 27 | self.VERSION = os.getenv("VERSION", "1.3") 28 | 29 | 30 | def space_conversion(self, space): 31 | space = space.upper() 32 | space = space.replace("B", "") 33 | if 'K' in space: 34 | space = float(space.replace("K", "")) 35 | return int(space * 1024) 36 | 37 | if 'M' in space: 38 | space = float(space.replace("M", "")) 39 | return int(space * 1024 * 1024) 40 | 41 | if 'G' in space: 42 | space = float(space.replace("G", "")) 43 | return int(space * 1024 * 1024 * 1024) 44 | 45 | if 'T' in space: 46 | space = float(space.replace("T", "")) 47 | return int(space * 1024 * 1024 * 1024 * 1024) 48 | 49 | return 0 50 | 51 | def byte_to_kb_or_mb(self, byte): 52 | i = 0 53 | units = ['B', 'KB', 'MB', 'GB', 'TB'] 54 | for i, unit in enumerate(units): 55 | if byte < 1024 ** (i + 1) or i == len(units) - 1: 56 | return f"{byte / (1024 ** i):.1f} {unit}" 57 | 58 | setting = Setting() -------------------------------------------------------------------------------- /Model/UploadedFiles.py: -------------------------------------------------------------------------------- 1 | from Model.BaseModel import BaseModel 2 | import time 3 | import random 4 | import datetime 5 | import os 6 | from fastapi import UploadFile 7 | import uuid 8 | from Model.Setting import setting 9 | from fastapi import HTTPException 10 | 11 | class UploadedFiles(BaseModel): 12 | def __init__(self): 13 | super().__init__() 14 | self.filed = ['id', 'line_id', 'file_name', 'file_path'] 15 | self.table = 'uploaded_files' 16 | 17 | def get_file(self, id, line_id): 18 | return self.sql_query('SELECT file_name, file_path FROM uploaded_files WHERE id =? AND line_id =?', (id, line_id, )) 19 | 20 | def get_file_amount(self, line_id): 21 | return self.sql_query('SELECT COUNT(id) FROM uploaded_files WHERE line_id =?', (line_id, ))[0][0] 22 | 23 | def get_all_files_list(self, line_id): 24 | return self.sql_query('SELECT * FROM uploaded_files WHERE line_id = ?', (line_id, )) 25 | 26 | async def upload_file(self, line_id, filename, filepath): 27 | try: 28 | return self.sql_query('INSERT INTO uploaded_files (id, line_id, file_name, file_path) VALUES (null, ?, ?, ?)', (line_id, filename, filepath, ), True) 29 | except Exception as e: 30 | raise {"error": str(e)} 31 | 32 | def delete_file(self, file_id, line_id): 33 | return self.sql_query('DELETE FROM uploaded_files WHERE id =? AND line_id =?', (file_id, line_id, )) 34 | 35 | 36 | uploadedFiles = UploadedFiles() -------------------------------------------------------------------------------- /Model/UserSelectKnowledgeBace.py: -------------------------------------------------------------------------------- 1 | from Model.BaseModel import BaseModel 2 | 3 | class UserSelectKnowledgeBace(BaseModel): 4 | def __init__(self): 5 | super().__init__() 6 | self.filed = ['line_id', 'knowledge_base_id'] 7 | self.table = 'user_select_knowledge_base' 8 | 9 | def getData(self, user_id): 10 | return self.sql_query(f"SELECT * FROM {self.table} WHERE line_id=?", (user_id, )) 11 | 12 | def changeSelected(self, line_id: str, knowledge_base_id: int): 13 | if len(self.getData(line_id)) == 0: 14 | self.sql_query(f"INSERT INTO {self.table} (line_id, knowledge_base_id) VALUES (?,?)", (line_id, knowledge_base_id, )) 15 | else: 16 | self.sql_query(f"UPDATE {self.table} SET knowledge_base_id=? WHERE line_id=?", (knowledge_base_id, line_id, )) 17 | 18 | userSelectKnowledgeBace = UserSelectKnowledgeBace() -------------------------------------------------------------------------------- /Model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/Model/__init__.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Line PDF 問答機器人 2 | 3 | 這是一個使用 LangChain、FastAPI 和 Vue 構建的 Line Bot,可以透過 Line Liff 網頁上傳 PDF 文件並回答相關問題。 4 | 5 | ## 範例 6 | 7 | | ![Screenshot_20240514-132200](https://github.com/ADT109119/ChatPDF-LineBot/assets/106337749/996fea6c-3ae8-4d9a-baff-7ada3860b4f9) | ![Screenshot_20240514-131836](https://github.com/ADT109119/ChatPDF-LineBot/assets/106337749/0cb06999-e8c3-4779-8fe2-c6f87a7a370c) |Demo 連結(每次更新時刪檔) ![image](https://github.com/ADT109119/ChatPDF-LineBot/assets/106337749/c1860b26-3371-4b2d-935f-cc4823286092) https://lin.ee/QajGJOY| 8 | |------|------|------| 9 | 10 | ## 說明文件 11 | 12 | * [中文](https://adt109119.github.io/ChatPDF-LineBot-Docs/) 13 | * [English](https://adt109119.github.io/ChatPDF-LineBot-Docs/en/) (使用 ChatGPT 翻譯) 14 | 15 | ## 功能 16 | 17 | 目前支援以下幾種功能: 18 | - [x] 上傳 PDF 文件 19 | - [x] 自然語言處理,從 PDF 中提取相關內容回答問題 20 | - [x] 自動 Embedding 文件 21 | - [x] 使用 Line Bot 進行互動 22 | 23 | 尚待完成: 24 | - [ ] 顯示用戶使用量 25 | - [ ] 文件預覽 26 | - [ ] 功能列表 27 | 28 | ## 技術 29 | 30 | - **LangChain**: 用於構建問答系統和文本處理 31 | - **FastAPI**: 用於構建 Web API 服務 32 | - **Vue.js**: 用於構建用戶介面 33 | - **Line Bot SDK**: 用於與 Line 平台集成 34 | 35 | ## Hugging Face 一鍵部署 36 | 37 | 本專案可以快速部署在 Hugging Face 上 38 | 39 | 支援 CloudFlare Tunnel 自訂網址 40 | 41 | 但要注意若無購買 Hugging Face 的永久儲存空間 42 | 43 | 每次更新或更改資料時檔案都會遺失 44 | 45 | [![](https://huggingface.co/datasets/huggingface/badges/resolve/main/deploy-on-spaces-lg-dark.svg)](https://huggingface.co/spaces/ADT109119/ChatPDF-LineBot?duplicate=true) 46 | 47 | ## 安裝 48 | > 使用 Docker 的人可直接跳至第 5 步 49 | 50 | ### 1. 複製存儲庫: 51 | ``` 52 | git clone https://github.com/ADT109119/ChatPDF-LineBot-Docs.git 53 | ``` 54 | 55 | ### 2. 安裝伺服器依賴: 56 | ``` 57 | cd ChatPDF-LineBot 58 | pip install -r requirements.txt 59 | ``` 60 | 可考慮在虛擬環境中執行,這裡提供 Python 內建的虛擬環境指令: 61 | ``` 62 | cd ChatPDF-LineBot 63 | python -m venv .\venv 64 | .\venv\Scripts\activate 65 | pip install -r requirements.txt 66 | ``` 67 | 68 | ### 3. 安裝 Node 依賴: 69 | ``` 70 | cd ChatPDF-LineBot/View 71 | npn install 72 | ``` 73 | 74 | ### 4. Build 前端網頁 75 | ``` 76 | cd View 77 | npm install 78 | npm run build 79 | ``` 80 | 81 | ### 5. 登入 LINE 平台 82 | * 創建 Line Bot。 83 | * 新增一個提供者(Provider),例如「My Provider」。 84 | * 在「My Provider」新增一個類型為「Messaging API」的頻道(Channel),例如「My AI Assistant」。 85 | * 進到「My AI Assistant」頻道頁面,點選「Messaging API」頁籤,生成一個頻道的 channel access token。 86 | * 創建 LIFF 網頁。 87 | * 在與「Messaging API」同一個 `Provider` 內(例如:「My Provider」)新增一個類型為「Line Login」的頻道(Channel),例如「My LIFF Page」。 88 | * 進到「My LIFF Page」頻道頁面,點選「LIFF」頁籤,創建一個新的 LIFF 網頁。 89 | * `Endpoint URL` 填入 `https://<你的網域或IP地址>/liff`(可先隨便填後續再來改)。 90 | * `Scopes` 選項請勾選「chat_message.write」、「openid」。 91 | * 點選 `Add` 創建。 92 | 93 | ### 6. 登入 OpenAI、Groq 平台(或其他平台) 94 | * 生成一個 OpenAI 的 API key。 95 | * 也可以生成其他平台的API Key(例如 Groq),只是該平台的 API 必須與 OpenAI 兼容,且要記得在下一步改`BASE_URL` 96 | 97 | ### 7. 設定環境變數 98 | 99 | 伺服器或 Docker 可直接設定環境變數 100 | 101 | 自己電腦上跑的話可複製 `.env.example` 文件,並改名為 `.env`,接著設定 API Key、模型等參數。 102 | 103 | 以下為 `環境變數列表` `*` 代表必改項目 104 | 105 | #### 環境變數列表 106 | 107 | | 變數 | 說明 | 預設值 | 108 | |------|------|------| 109 | | `MODEL_NAME` | 用於問答的模型名稱,多個模型以`\|`分隔 | `llama3-8b-8192\|gpt-3.5-turbo\|gpt-4-1106-preview` | 110 | | `BASE_URL` | 對應模型的 API 基礎 URL,多個 URL 以`\|`分隔 | `https://api.groq.com/openai/v1\|https://api.openai.com/v1\|https://api.openai.com/v1` | 111 | | * `API_KEY` | 對應模型的 API 密鑰,多個密鑰以`\|`分隔 | `API_KEY_1\|API_KEY_2\|API_KEY_3` | 112 | | `MAX_CHAT_HISTORY` | 保留的最大聊天記錄數 | `5` | 113 | | `EMBEDDING_MODEL` | 用於文本嵌入的模型名稱(請填入HF模型路徑) | `sentence-transformers/all-MiniLM-L6-v2` | 114 | | `EMBEDDING_DEVICE` | 運行文本嵌入模型的設備(cpu或cuda,可用cuda:0、cuda:1選擇顯卡) | `cpu` | 115 | | * `LINE_CHANNEL_ACCESS_TOKEN` | Line Bot 的access token | `YOUR_LINE_CHANNEL_ACCESS_TOKEN` | 116 | | * `LINE_CHANNEL_SECRET` | Line Bot 的secret | `YOUR_LINE_CHANNEL_SECRET` | 117 | | * `LINE_LIFF_ID` | Line LIFF 網頁 ID | `YOUR_LINE_LIFF_ID` | 118 | | * `LINE_LOGIN_CHANNEL_ID` | Line LIFF 所在的 LINE Login Channel ID | `YOUR_LINE_LOGIN_CHANNEL_ID` | 119 | | `FILE_MAX_SIZE` | 允許上傳的最大文件大小 | `5MB` | 120 | | `SPACE_PER_USER` | 每個用戶可用的最大空間大小 | `200MB` | 121 | | `ALLOW_FILE_TYPE` | 允許上傳的文件類型,多個類型以`,`分隔 | `pdf,csv,txt` | 122 | 123 | 124 | ### 8. 運行伺服器 125 | 126 | #### 使用 Docker 127 | 128 | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/adt109119/chatpdf-linebot/latest) 129 | 130 | ``` 131 | docker run -d \ 132 | --name chatpdf-linebot \ 133 | -p 8000:8000 \ 134 | -v /local/file/store/path:/app/db 135 | -e LINE_CHANNEL_ACCESS_TOKEN=YOUR_LINE_CHANNEL_ACCESS_TOKEN \ 136 | -e LINE_CHANNEL_SECRET=YOUR_LINE_CHANNEL_SECRET \ 137 | -e LINE_LIFF_ID=YOUR_LINE_LIFF_ID \ 138 | -e LINE_LOGIN_CHANNEL_ID=YOUR_LINE_LOGIN_CHANNEL_ID \ 139 | -e MODEL_NAME=YOUR_MODELS \ 140 | -e BASE_URL=BASE_URLS \ 141 | -e API_KEY=API_KEYS \ 142 | adt109119/chatpdf-linebot 143 | ``` 144 | 145 | #### 本機直接運行 146 | 147 | 若 1. ~ 4. 步的安裝皆無問題,僅需直接執行以下指令便可開啟伺服器 148 | 149 | 預設使用 `PORT` `8000` 150 | ``` 151 | python .\main.py 152 | ``` 153 | 154 | ### 9. 回到 LINE 設定 155 | * Line Bot 設定 156 | * 進到「My AI Assistant」頻道頁面,點選「Messaging API」頁籤,設置「Webhook URL」,填入應用程式網址並加上「/callback」路徑,例如 `https://line.the-walking.fish.com/callback`,點選「Update」按鈕。 157 | * 點選「Verify」按鈕,驗證是否呼叫成功。 158 | * 將「Use webhook」功能開啟。 159 | * 將「Auto-reply messages」功能關閉。 160 | * 將「Greeting messages」功能關閉。 161 | 162 | * Line LIFF 網頁設定 163 | * 進到「LIFF」頁籤 164 | * 若原本`Endpoint URL` 隨便填的,現在請正式填入 LIFF 網頁網址,路徑為「/liff」,例如 `https://line.the-walking.fish.com/liff`。 165 | * 回到「LIFF」頁籤,複製「LIFF URL」 166 | 167 | * 圖文選單設定 168 | * 進到「LINE Official Account Manager」 169 | * 在側邊欄找到「圖文選單」 170 | * 點選「建立」 171 | * 名稱、版型等可照自己的喜好填寫 172 | * 在「動作」選擇「連結」,並填入上一部複製的 LIFF URL 173 | * 按「儲存」 174 | 175 | ## 貢獻 176 | 177 | 如果您有任何改進建議或錯誤修復,歡迎提交 Pull Request。 178 | 179 | 180 | 181 | 182 | 183 | ## 聯繫作者 184 | 185 | 你可以透過以下方式與我聯絡 186 | 187 | - 2.jerry32262686@gmail.com 188 | 189 | 190 | ## License 191 | This project is under the Apache 2.0 License. See [LICENSE](https://github.com/ADT109119/ChatPDF-LineBot/blob/main/LICENSE) for further details. 192 | 193 | -------------------------------------------------------------------------------- /Service/LineFunction.py: -------------------------------------------------------------------------------- 1 | import config.line_reply_template as config 2 | from Model.Setting import setting 3 | from Model.UploadedFiles import uploadedFiles 4 | 5 | import time 6 | 7 | from Service.upload_file import calc_total_size 8 | 9 | def replay_info(line_id): 10 | temp = config.INFO_TEMPLATE 11 | temp = temp.replace("{$VERSION}", setting.VERSION) 12 | temp = temp.replace("{$FILE_AMOUNT}", str(uploadedFiles.get_file_amount(line_id))) 13 | temp = temp.replace("{$FILE_SIZE_LIMIT}", setting.byte_to_kb_or_mb(setting.FILE_MAX_SIZE)) 14 | temp = temp.replace("{$USED_SPACE}", f"{setting.byte_to_kb_or_mb(calc_total_size(line_id))}/{setting.byte_to_kb_or_mb(setting.SPACE_PER_USER)}") 15 | temp = temp.replace("{$USED_SPACE_PERCENTAGE}", str(calc_total_size(line_id)/setting.SPACE_PER_USER * 100)+"%") 16 | temp = temp.replace("{$MAX_CHAT_HISTORY}", str(setting.MAX_CHAT_HISTORY)) 17 | return temp 18 | 19 | def replay_help(): 20 | t = time.time() 21 | tt = time.localtime(t) 22 | temp = config.HELP_TEMPLATE 23 | temp = temp.replace("{$TIME}", f"{tt.tm_year}{tt.tm_mon}{tt.tm_mday}") 24 | return temp 25 | 26 | def replay_about_me(): 27 | return config.ABOUT_ME -------------------------------------------------------------------------------- /Service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/Service/__init__.py -------------------------------------------------------------------------------- /Service/embedding.py: -------------------------------------------------------------------------------- 1 | from Model.Setting import Setting 2 | setting = Setting() 3 | from langchain.embeddings import HuggingFaceEmbeddings 4 | model_name = setting.EMBEDDING_MODEL 5 | model_kwargs = {'device': setting.EMBEDDING_DEVICE} 6 | embedding = HuggingFaceEmbeddings(model_name=model_name, 7 | model_kwargs=model_kwargs) 8 | 9 | import asyncio 10 | from typing import Dict 11 | from datetime import datetime, timedelta 12 | import threading 13 | 14 | # 存儲每個id的重新嵌入任務和最後呼叫時間 15 | re_embedding_tasks: Dict[int, asyncio.Task] = {} 16 | last_call_times: Dict[int, datetime] = {} 17 | 18 | async def re_embedding(line_id: str, id: int, delay: int = 5): 19 | """ 20 | Re-embedding function with a delay for a specific id. 21 | 22 | Args: 23 | id (int): The id for which the re-embedding process is being performed. 24 | delay (int, optional): The delay in seconds before executing the re-embedding process. Defaults to 5. 25 | """ 26 | global re_embedding_tasks, last_call_times 27 | 28 | # 獲取當前時間 29 | now = datetime.now() 30 | 31 | # 檢查是否已經有任務在執行,以及是否在延遲時間內被再次呼叫 32 | if id in re_embedding_tasks and (now - last_call_times[id]) < timedelta(seconds=delay): 33 | # 取消之前的任務 34 | re_embedding_tasks[id].cancel() 35 | 36 | # 更新最後呼叫時間 37 | last_call_times[id] = now 38 | 39 | # 定義實際的重新嵌入協程 40 | async def re_embedding_coroutine(): 41 | # 等待指定的延遲時間 42 | await asyncio.sleep(delay) 43 | 44 | # 檢查在延遲時間內是否有重複呼叫 45 | if id in re_embedding_tasks and (datetime.now() - last_call_times[id]) < timedelta(seconds=delay): 46 | # 如果有重複呼叫,則重新計時 47 | return 48 | 49 | # 執行重新生成向量資料庫的過程 50 | files = knowledgeBaseFile.get_all_files(line_id, id) 51 | print(len(files)) 52 | if len(files) > 0: 53 | # 建立一個子執行緒 54 | t = threading.Thread(target = generate_vectordb, args = (line_id, id, files)) 55 | # 執行該子執行緒 56 | t.start() 57 | 58 | # 從字典中移除任務和最後呼叫時間 59 | del re_embedding_tasks[id] 60 | del last_call_times[id] 61 | 62 | # 創建新的任務 63 | re_embedding_tasks[id] = asyncio.create_task(re_embedding_coroutine()) 64 | 65 | 66 | from langchain.document_loaders import PyMuPDFLoader, TextLoader 67 | import glob 68 | from langchain_community.document_loaders.csv_loader import CSVLoader 69 | from langchain.text_splitter import RecursiveCharacterTextSplitter 70 | from langchain.vectorstores import Chroma 71 | import os 72 | from Model.KnowledgeBaseFile import knowledgeBaseFile 73 | 74 | def generate_vectordb(line_id, id, files): 75 | datas = [] 76 | 77 | # files = knowledgeBaseFile.get_all_files(line_id, id) 78 | for i in files: 79 | path = i[2] 80 | active = i[3] 81 | if active == 0: 82 | continue 83 | 84 | if path.endswith(".pdf"): 85 | loader = PyMuPDFLoader(path) 86 | datas.extend(loader.load()) 87 | elif path.endswith(".csv"): 88 | loader = CSVLoader(file_path=path, encoding="utf-8") 89 | datas.extend(loader.load()) 90 | elif path.endswith(".txt"): 91 | loader = TextLoader(file_path=path, encoding="utf-8") 92 | datas.extend(loader.load()) 93 | 94 | if len(datas) == 0: 95 | return 96 | 97 | text_splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=5) 98 | all_splits = text_splitter.split_documents(datas) 99 | 100 | persist_directory = f"./db/vector_db/{line_id}" 101 | if not os.path.exists(persist_directory): 102 | try: 103 | os.makedirs(persist_directory) 104 | except: 105 | raise 106 | 107 | print("embedding... ...") 108 | db = Chroma( 109 | embedding_function=embedding, 110 | persist_directory=persist_directory, 111 | collection_name = f"{line_id}_{id}", 112 | collection_metadata={"hnsw:space": "cosine"} 113 | ) 114 | db.delete_collection() 115 | 116 | Chroma.from_documents( 117 | documents=all_splits, 118 | embedding=embedding, 119 | persist_directory=persist_directory, 120 | collection_name = f"{line_id}_{id}", 121 | collection_metadata={"hnsw:space": "cosine"}) 122 | print("embedding finish!") 123 | -------------------------------------------------------------------------------- /Service/llm.py: -------------------------------------------------------------------------------- 1 | import time 2 | from Model.Setting import Setting 3 | 4 | from Model.KnowledgeBase import knowledgeBase 5 | from Model.UserSelectKnowledgeBace import UserSelectKnowledgeBace 6 | from Model.ChatHistory import chatHistory 7 | from Model.Setting import setting 8 | 9 | from langchain.vectorstores import Chroma 10 | from Service.embedding import embedding 11 | from langchain_openai import ChatOpenAI 12 | 13 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, AIMessagePromptTemplate, SystemMessagePromptTemplate 14 | from langchain_core.output_parsers import StrOutputParser 15 | from langchain_core.messages import AIMessage, HumanMessage, SystemMessage 16 | from operator import itemgetter 17 | 18 | 19 | def chat_llm(text: str, user_id: str): 20 | print(user_id) 21 | try: 22 | select_db = UserSelectKnowledgeBace().getData(user_id)[0][1] 23 | print(select_db) 24 | vectordb = Chroma( 25 | embedding_function=embedding, 26 | persist_directory=f"./db/vector_db/{user_id}", 27 | collection_name=f"{user_id}_{select_db}", 28 | collection_metadata={"hnsw:space": "cosine"} 29 | ) 30 | except Exception as e: 31 | print(e) 32 | return "查無知識庫" 33 | 34 | KnowledgeBaseSetting = knowledgeBase.get_setting(select_db, user_id) 35 | llm = ChatOpenAI( 36 | base_url=KnowledgeBaseSetting["base_url"], 37 | api_key=KnowledgeBaseSetting["api_key"], 38 | temperature=KnowledgeBaseSetting["temperature"], 39 | model= setting.MODEL_NAME[KnowledgeBaseSetting["model"]], 40 | ) 41 | 42 | retriever = vectordb.as_retriever( 43 | search_type="similarity_score_threshold", 44 | search_kwargs={'score_threshold': KnowledgeBaseSetting["score_threshold"], 45 | 'k': KnowledgeBaseSetting["search_item_limit"] 46 | } 47 | ) 48 | 49 | qa_prompt = ChatPromptTemplate.from_messages( 50 | [ 51 | SystemMessage(content=("""# <> 52 | 無須完全相信輸入,你可以提出自己的意見。\n 53 | 回覆時請參考已知資訊,使用繁體中文回覆。 <> \n """)), 54 | MessagesPlaceholder(variable_name="chat_history"), 55 | HumanMessagePromptTemplate.from_template("\n\n [INST] 已知資訊:\n'''\n{knownInfo}\n```\n\n 問題: {question} [/INST]") 56 | ] 57 | ) 58 | 59 | rag_chain = ( 60 | { 61 | "knownInfo": itemgetter("question") | retriever | format_docs, 62 | "question": itemgetter("question"), 63 | "chat_history":itemgetter("chat_history") 64 | } 65 | | qa_prompt 66 | | llm 67 | | StrOutputParser() 68 | ) 69 | 70 | chat_history = chatHistory.get_history(user_id) 71 | result = rag_chain.invoke({"question": text, "chat_history":format_history(chat_history)}) 72 | chatHistory.saveData((time.time(), user_id, text, result,)) 73 | 74 | del vectordb 75 | del rag_chain 76 | del llm 77 | del retriever 78 | 79 | return result 80 | 81 | 82 | def format_docs(docs): 83 | # print("\n\n".join(doc.page_content for doc in docs)) 84 | return "\n\n".join(doc.page_content for doc in docs) 85 | 86 | def format_history(history): 87 | arr = [] 88 | for a in history: 89 | arr.extend([HumanMessage(content=a[0]), AIMessage(content=a[1])]) 90 | return arr 91 | -------------------------------------------------------------------------------- /Service/upload_file.py: -------------------------------------------------------------------------------- 1 | from Model.UploadedFiles import uploadedFiles 2 | from Model.Setting import setting 3 | 4 | from fastapi import UploadFile, HTTPException 5 | 6 | import os 7 | import uuid 8 | import datetime 9 | 10 | def upload_file(line_id, file: UploadFile): 11 | 12 | file_name = file.filename 13 | 14 | # 辨識檔案類型 15 | if file_name.split(".")[-1] not in setting.ALLOW_FILE_TYPE: 16 | raise HTTPException(status_code=400, detail="unsupport file type") 17 | 18 | today = datetime.date.today() 19 | unique_id = str(uuid.uuid4()) 20 | 21 | file_path = f'./db/files/{today.year}/{today.month}/{today.day}/{unique_id}_{file_name}' 22 | 23 | # 獲取目錄路徑 24 | dir_path = os.path.dirname(file_path) 25 | 26 | # 如果目錄不存在,則創建它 27 | if not os.path.exists(dir_path): 28 | try: 29 | os.makedirs(dir_path) 30 | except OSError as e: 31 | if e.errno != os.errno.EEXIST: 32 | raise # 如果不是文件已存在的錯誤,則重新引發異常 33 | 34 | try: 35 | with open(file_path, 'wb') as fd: 36 | content = file.file.read() 37 | if len(content) > setting.FILE_MAX_SIZE: 38 | raise Exception("file size too large") 39 | 40 | # 檢查空間是否足夠 41 | total_size = calc_total_size(line_id) 42 | # file_list = uploadedFiles.get_all_files_list(line_id) 43 | # for f in file_list: 44 | # total_size += os.stat(f[3]).st_size 45 | # print(total_size) 46 | if total_size+len(content) > setting.SPACE_PER_USER: 47 | raise Exception("space limit reached") 48 | 49 | fd.write(content) 50 | except Exception as e: 51 | raise HTTPException(status_code=400, detail=str(e)) 52 | 53 | try: 54 | # 寫入資料庫 55 | return uploadedFiles.upload_file(line_id, file_name, file_path) 56 | except Exception as e: 57 | print(e) 58 | raise HTTPException(status_code=400, detail="error") 59 | # return self.sql_query('INSERT INTO uploaded_files (id, line_id, file_name, file_path) VALUES (null, ?, ?, ?)', (line_id, file_name, file_path, ), True) 60 | 61 | 62 | def delete_file(file_id, line_id): 63 | try: 64 | # 獲取檔案路徑 65 | _, file_path = uploadedFiles.get_file(file_id, line_id)[0] 66 | except Exception as e: 67 | raise HTTPException(status_code=400, detail="file does not exist") 68 | 69 | try: 70 | # 刪除檔案 71 | if os.path.exists(file_path): 72 | os.remove(file_path) 73 | # 刪除資料庫中的資料 74 | uploadedFiles.delete_file(file_id, line_id) 75 | except Exception as e: 76 | print(e) 77 | raise HTTPException(status_code=400, detail="error") 78 | 79 | def calc_total_size(line_id): 80 | total_size = 0 81 | file_list = uploadedFiles.get_all_files_list(line_id) 82 | for f in file_list: 83 | total_size += os.stat(f[3]).st_size 84 | 85 | return total_size 86 | -------------------------------------------------------------------------------- /View/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /View/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /View/README.md: -------------------------------------------------------------------------------- 1 | # ChatPDF-LineBot-Liff 2 | 3 | ## Project setup 4 | ``` 5 | pnpm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | pnpm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | pnpm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | pnpm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /View/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /View/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /View/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatpdf-linebot", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@line/liff": "^2.23.2", 12 | "body-parser": "^1.20.3", 13 | "core-js": "^3.8.3", 14 | "sweetalert2": "^11.10.8", 15 | "vue": "^3.2.13", 16 | "vue-router": "^4.0.3", 17 | "vuex": "^4.0.0" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.12.16", 21 | "@babel/eslint-parser": "^7.12.16", 22 | "@vue/cli-plugin-babel": "~5.0.0", 23 | "@vue/cli-plugin-eslint": "~5.0.0", 24 | "@vue/cli-plugin-router": "~5.0.0", 25 | "@vue/cli-plugin-vuex": "~5.0.0", 26 | "@vue/cli-service": "~5.0.0", 27 | "autoprefixer": "^10.4.19", 28 | "eslint": "^7.32.0", 29 | "eslint-plugin-vue": "^8.0.3", 30 | "postcss": "^8.4.38", 31 | "tailwindcss": "^3.4.3" 32 | }, 33 | "eslintConfig": { 34 | "root": true, 35 | "env": { 36 | "node": true 37 | }, 38 | "extends": [ 39 | "plugin:vue/vue3-essential", 40 | "eslint:recommended" 41 | ], 42 | "parserOptions": { 43 | "parser": "@babel/eslint-parser" 44 | }, 45 | "rules": {} 46 | }, 47 | "browserslist": [ 48 | "> 1%", 49 | "last 2 versions", 50 | "not dead", 51 | "not ie 11" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /View/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /View/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/View/public/favicon.ico -------------------------------------------------------------------------------- /View/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /View/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /View/src/assets/data/CityCountyData.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "CityName": "臺北市", 4 | "CityEngName": "Taipei City", 5 | "AreaList": [ 6 | { 7 | "ZipCode": "100", 8 | "AreaName": "中正區", 9 | "AreaEngName": "Zhongzheng Dist." 10 | }, 11 | { 12 | "ZipCode": "103", 13 | "AreaName": "大同區", 14 | "AreaEngName": "Datong Dist." 15 | }, 16 | { 17 | "ZipCode": "104", 18 | "AreaName": "中山區", 19 | "AreaEngName": "Zhongshan Dist." 20 | }, 21 | { 22 | "ZipCode": "105", 23 | "AreaName": "松山區", 24 | "AreaEngName": "Songshan Dist." 25 | }, 26 | { 27 | "ZipCode": "106", 28 | "AreaName": "大安區", 29 | "AreaEngName": "Da’an Dist." 30 | }, 31 | { 32 | "ZipCode": "108", 33 | "AreaName": "萬華區", 34 | "AreaEngName": "Wanhua Dist." 35 | }, 36 | { 37 | "ZipCode": "110", 38 | "AreaName": "信義區", 39 | "AreaEngName": "Xinyi Dist." 40 | }, 41 | { 42 | "ZipCode": "111", 43 | "AreaName": "士林區", 44 | "AreaEngName": "Shilin Dist." 45 | }, 46 | { 47 | "ZipCode": "112", 48 | "AreaName": "北投區", 49 | "AreaEngName": "Beitou Dist." 50 | }, 51 | { 52 | "ZipCode": "114", 53 | "AreaName": "內湖區", 54 | "AreaEngName": "Neihu Dist." 55 | }, 56 | { 57 | "ZipCode": "115", 58 | "AreaName": "南港區", 59 | "AreaEngName": "Nangang Dist." 60 | }, 61 | { 62 | "ZipCode": "116", 63 | "AreaName": "文山區", 64 | "AreaEngName": "Wenshan Dist." 65 | } 66 | ] 67 | }, 68 | { 69 | "CityName": "基隆市", 70 | "CityEngName": "Keelung City", 71 | "AreaList": [ 72 | { 73 | "ZipCode": "200", 74 | "AreaName": "仁愛區", 75 | "AreaEngName": "Ren’ai Dist." 76 | }, 77 | { 78 | "ZipCode": "201", 79 | "AreaName": "信義區", 80 | "AreaEngName": "Xinyi Dist." 81 | }, 82 | { 83 | "ZipCode": "202", 84 | "AreaName": "中正區", 85 | "AreaEngName": "Zhongzheng Dist." 86 | }, 87 | { 88 | "ZipCode": "203", 89 | "AreaName": "中山區", 90 | "AreaEngName": "Zhongshan Dist." 91 | }, 92 | { 93 | "ZipCode": "204", 94 | "AreaName": "安樂區", 95 | "AreaEngName": "Anle Dist." 96 | }, 97 | { 98 | "ZipCode": "205", 99 | "AreaName": "暖暖區", 100 | "AreaEngName": "Nuannuan Dist." 101 | }, 102 | { 103 | "ZipCode": "206", 104 | "AreaName": "七堵區", 105 | "AreaEngName": "Qidu Dist." 106 | } 107 | ] 108 | }, 109 | { 110 | "CityName": "新北市", 111 | "CityEngName": "New Taipei City", 112 | "AreaList": [ 113 | { 114 | "ZipCode": "207", 115 | "AreaName": "萬里區", 116 | "AreaEngName": "Wanli Dist." 117 | }, 118 | { 119 | "ZipCode": "208", 120 | "AreaName": "金山區", 121 | "AreaEngName": "Jinshan Dist." 122 | }, 123 | { 124 | "ZipCode": "220", 125 | "AreaName": "板橋區", 126 | "AreaEngName": "Banqiao Dist." 127 | }, 128 | { 129 | "ZipCode": "221", 130 | "AreaName": "汐止區", 131 | "AreaEngName": "Xizhi Dist." 132 | }, 133 | { 134 | "ZipCode": "222", 135 | "AreaName": "深坑區", 136 | "AreaEngName": "Shenkeng Dist." 137 | }, 138 | { 139 | "ZipCode": "223", 140 | "AreaName": "石碇區", 141 | "AreaEngName": "Shiding Dist." 142 | }, 143 | { 144 | "ZipCode": "224", 145 | "AreaName": "瑞芳區", 146 | "AreaEngName": "Ruifang Dist." 147 | }, 148 | { 149 | "ZipCode": "226", 150 | "AreaName": "平溪區", 151 | "AreaEngName": "Pingxi Dist." 152 | }, 153 | { 154 | "ZipCode": "227", 155 | "AreaName": "雙溪區", 156 | "AreaEngName": "Shuangxi Dist." 157 | }, 158 | { 159 | "ZipCode": "228", 160 | "AreaName": "貢寮區", 161 | "AreaEngName": "Gongliao Dist." 162 | }, 163 | { 164 | "ZipCode": "231", 165 | "AreaName": "新店區", 166 | "AreaEngName": "Xindian Dist." 167 | }, 168 | { 169 | "ZipCode": "232", 170 | "AreaName": "坪林區", 171 | "AreaEngName": "Pinglin Dist." 172 | }, 173 | { 174 | "ZipCode": "233", 175 | "AreaName": "烏來區", 176 | "AreaEngName": "Wulai Dist." 177 | }, 178 | { 179 | "ZipCode": "234", 180 | "AreaName": "永和區", 181 | "AreaEngName": "Yonghe Dist." 182 | }, 183 | { 184 | "ZipCode": "235", 185 | "AreaName": "中和區", 186 | "AreaEngName": "Zhonghe Dist." 187 | }, 188 | { 189 | "ZipCode": "236", 190 | "AreaName": "土城區", 191 | "AreaEngName": "Tucheng Dist." 192 | }, 193 | { 194 | "ZipCode": "237", 195 | "AreaName": "三峽區", 196 | "AreaEngName": "Sanxia Dist." 197 | }, 198 | { 199 | "ZipCode": "238", 200 | "AreaName": "樹林區", 201 | "AreaEngName": "Shulin Dist." 202 | }, 203 | { 204 | "ZipCode": "239", 205 | "AreaName": "鶯歌區", 206 | "AreaEngName": "Yingge Dist." 207 | }, 208 | { 209 | "ZipCode": "241", 210 | "AreaName": "三重區", 211 | "AreaEngName": "Sanchong Dist." 212 | }, 213 | { 214 | "ZipCode": "242", 215 | "AreaName": "新莊區", 216 | "AreaEngName": "Xinzhuang Dist." 217 | }, 218 | { 219 | "ZipCode": "243", 220 | "AreaName": "泰山區", 221 | "AreaEngName": "Taishan Dist." 222 | }, 223 | { 224 | "ZipCode": "244", 225 | "AreaName": "林口區", 226 | "AreaEngName": "Linkou Dist." 227 | }, 228 | { 229 | "ZipCode": "247", 230 | "AreaName": "蘆洲區", 231 | "AreaEngName": "Luzhou Dist." 232 | }, 233 | { 234 | "ZipCode": "248", 235 | "AreaName": "五股區", 236 | "AreaEngName": "Wugu Dist." 237 | }, 238 | { 239 | "ZipCode": "249", 240 | "AreaName": "八里區", 241 | "AreaEngName": "Bali Dist." 242 | }, 243 | { 244 | "ZipCode": "251", 245 | "AreaName": "淡水區", 246 | "AreaEngName": "Tamsui Dist." 247 | }, 248 | { 249 | "ZipCode": "252", 250 | "AreaName": "三芝區", 251 | "AreaEngName": "Sanzhi Dist." 252 | }, 253 | { 254 | "ZipCode": "253", 255 | "AreaName": "石門區", 256 | "AreaEngName": "Shimen Dist." 257 | } 258 | ] 259 | }, 260 | { 261 | "CityName": "連江縣", 262 | "CityEngName": "Lienchiang County", 263 | "AreaList": [ 264 | { 265 | "ZipCode": "209", 266 | "AreaName": "南竿鄉", 267 | "AreaEngName": "Nangan Township" 268 | }, 269 | { 270 | "ZipCode": "210", 271 | "AreaName": "北竿鄉", 272 | "AreaEngName": "Beigan Township" 273 | }, 274 | { 275 | "ZipCode": "211", 276 | "AreaName": "莒光鄉", 277 | "AreaEngName": "Juguang Township" 278 | }, 279 | { 280 | "ZipCode": "212", 281 | "AreaName": "東引鄉", 282 | "AreaEngName": "Dongyin Township" 283 | } 284 | ] 285 | }, 286 | { 287 | "CityName": "宜蘭縣", 288 | "CityEngName": "Yilan County", 289 | "AreaList": [ 290 | { 291 | "ZipCode": "260", 292 | "AreaName": "宜蘭市", 293 | "AreaEngName": "Yilan City" 294 | }, 295 | { 296 | "ZipCode": "263", 297 | "AreaName": "壯圍鄉", 298 | "AreaEngName": "Zhuangwei Township" 299 | }, 300 | { 301 | "ZipCode": "261", 302 | "AreaName": "頭城鎮", 303 | "AreaEngName": "Toucheng Township" 304 | }, 305 | { 306 | "ZipCode": "262", 307 | "AreaName": "礁溪鄉", 308 | "AreaEngName": "Jiaoxi Township" 309 | }, 310 | { 311 | "ZipCode": "264", 312 | "AreaName": "員山鄉", 313 | "AreaEngName": "Yuanshan Township" 314 | }, 315 | { 316 | "ZipCode": "265", 317 | "AreaName": "羅東鎮", 318 | "AreaEngName": "Luodong Township" 319 | }, 320 | { 321 | "ZipCode": "266", 322 | "AreaName": "三星鄉", 323 | "AreaEngName": "Sanxing Township" 324 | }, 325 | { 326 | "ZipCode": "267", 327 | "AreaName": "大同鄉", 328 | "AreaEngName": "Datong Township" 329 | }, 330 | { 331 | "ZipCode": "268", 332 | "AreaName": "五結鄉", 333 | "AreaEngName": "Wujie Township" 334 | }, 335 | { 336 | "ZipCode": "269", 337 | "AreaName": "冬山鄉", 338 | "AreaEngName": "Dongshan Township" 339 | }, 340 | { 341 | "ZipCode": "270", 342 | "AreaName": "蘇澳鎮", 343 | "AreaEngName": "Su’ao Township" 344 | }, 345 | { 346 | "ZipCode": "272", 347 | "AreaName": "南澳鄉", 348 | "AreaEngName": "Nan’ao Township" 349 | }, 350 | { 351 | "ZipCode": "290", 352 | "AreaName": "釣魚臺", 353 | "AreaEngName": "Diaoyutai" 354 | } 355 | ] 356 | }, 357 | { 358 | "CityName": "新竹市", 359 | "CityEngName": "Hsinchu City", 360 | "AreaList": [ 361 | { 362 | "ZipCode": "300", 363 | "AreaName": "東區", 364 | "AreaEngName": "East Dist." 365 | }, 366 | { 367 | "ZipCode": "300", 368 | "AreaName": "北區", 369 | "AreaEngName": "North Dist." 370 | }, 371 | { 372 | "ZipCode": "300", 373 | "AreaName": "香山區", 374 | "AreaEngName": "Xiangshan Dist." 375 | } 376 | ] 377 | }, 378 | { 379 | "CityName": "新竹縣", 380 | "CityEngName": "Hsinchu County", 381 | "AreaList": [ 382 | { 383 | "ZipCode": "308", 384 | "AreaName": "寶山鄉", 385 | "AreaEngName": "Baoshan Township" 386 | }, 387 | { 388 | "ZipCode": "302", 389 | "AreaName": "竹北市", 390 | "AreaEngName": "Zhubei City" 391 | }, 392 | { 393 | "ZipCode": "303", 394 | "AreaName": "湖口鄉", 395 | "AreaEngName": "Hukou Township" 396 | }, 397 | { 398 | "ZipCode": "304", 399 | "AreaName": "新豐鄉", 400 | "AreaEngName": "Xinfeng Township" 401 | }, 402 | { 403 | "ZipCode": "305", 404 | "AreaName": "新埔鎮", 405 | "AreaEngName": "Xinpu Township" 406 | }, 407 | { 408 | "ZipCode": "306", 409 | "AreaName": "關西鎮", 410 | "AreaEngName": "Guanxi Township" 411 | }, 412 | { 413 | "ZipCode": "307", 414 | "AreaName": "芎林鄉", 415 | "AreaEngName": "Qionglin Township" 416 | }, 417 | { 418 | "ZipCode": "310", 419 | "AreaName": "竹東鎮", 420 | "AreaEngName": "Zhudong Township" 421 | }, 422 | { 423 | "ZipCode": "311", 424 | "AreaName": "五峰鄉", 425 | "AreaEngName": "Wufeng Township" 426 | }, 427 | { 428 | "ZipCode": "312", 429 | "AreaName": "橫山鄉", 430 | "AreaEngName": "Hengshan Township" 431 | }, 432 | { 433 | "ZipCode": "313", 434 | "AreaName": "尖石鄉", 435 | "AreaEngName": "Jianshi Township" 436 | }, 437 | { 438 | "ZipCode": "314", 439 | "AreaName": "北埔鄉", 440 | "AreaEngName": "Beipu Township" 441 | }, 442 | { 443 | "ZipCode": "315", 444 | "AreaName": "峨眉鄉", 445 | "AreaEngName": "Emei Township" 446 | } 447 | ] 448 | }, 449 | { 450 | "CityName": "桃園市", 451 | "CityEngName": "Taoyuan City", 452 | "AreaList": [ 453 | { 454 | "ZipCode": "320", 455 | "AreaName": "中壢區", 456 | "AreaEngName": "Zhongli Dist." 457 | }, 458 | { 459 | "ZipCode": "324", 460 | "AreaName": "平鎮區", 461 | "AreaEngName": "Pingzhen Dist." 462 | }, 463 | { 464 | "ZipCode": "325", 465 | "AreaName": "龍潭區", 466 | "AreaEngName": "Longtan Dist." 467 | }, 468 | { 469 | "ZipCode": "326", 470 | "AreaName": "楊梅區", 471 | "AreaEngName": "Yangmei Dist." 472 | }, 473 | { 474 | "ZipCode": "327", 475 | "AreaName": "新屋區", 476 | "AreaEngName": "Xinwu Dist." 477 | }, 478 | { 479 | "ZipCode": "328", 480 | "AreaName": "觀音區", 481 | "AreaEngName": "Guanyin Dist." 482 | }, 483 | { 484 | "ZipCode": "330", 485 | "AreaName": "桃園區", 486 | "AreaEngName": "Taoyuan Dist." 487 | }, 488 | { 489 | "ZipCode": "333", 490 | "AreaName": "龜山區", 491 | "AreaEngName": "Guishan Dist." 492 | }, 493 | { 494 | "ZipCode": "334", 495 | "AreaName": "八德區", 496 | "AreaEngName": "Bade Dist." 497 | }, 498 | { 499 | "ZipCode": "335", 500 | "AreaName": "大溪區", 501 | "AreaEngName": "Daxi Dist." 502 | }, 503 | { 504 | "ZipCode": "336", 505 | "AreaName": "復興區", 506 | "AreaEngName": "Fuxing Dist." 507 | }, 508 | { 509 | "ZipCode": "337", 510 | "AreaName": "大園區", 511 | "AreaEngName": "Dayuan Dist." 512 | }, 513 | { 514 | "ZipCode": "338", 515 | "AreaName": "蘆竹區", 516 | "AreaEngName": "Luzhu Dist." 517 | } 518 | ] 519 | }, 520 | { 521 | "CityName": "苗栗縣", 522 | "CityEngName": "Miaoli County", 523 | "AreaList": [ 524 | { 525 | "ZipCode": "350", 526 | "AreaName": "竹南鎮", 527 | "AreaEngName": "Zhunan Township" 528 | }, 529 | { 530 | "ZipCode": "351", 531 | "AreaName": "頭份市", 532 | "AreaEngName": "Toufen City" 533 | }, 534 | { 535 | "ZipCode": "352", 536 | "AreaName": "三灣鄉", 537 | "AreaEngName": "Sanwan Township" 538 | }, 539 | { 540 | "ZipCode": "353", 541 | "AreaName": "南庄鄉", 542 | "AreaEngName": "Nanzhuang Township" 543 | }, 544 | { 545 | "ZipCode": "354", 546 | "AreaName": "獅潭鄉", 547 | "AreaEngName": "Shitan Township" 548 | }, 549 | { 550 | "ZipCode": "356", 551 | "AreaName": "後龍鎮", 552 | "AreaEngName": "Houlong Township" 553 | }, 554 | { 555 | "ZipCode": "357", 556 | "AreaName": "通霄鎮", 557 | "AreaEngName": "Tongxiao Township" 558 | }, 559 | { 560 | "ZipCode": "358", 561 | "AreaName": "苑裡鎮", 562 | "AreaEngName": "Yuanli Township" 563 | }, 564 | { 565 | "ZipCode": "360", 566 | "AreaName": "苗栗市", 567 | "AreaEngName": "Miaoli City" 568 | }, 569 | { 570 | "ZipCode": "361", 571 | "AreaName": "造橋鄉", 572 | "AreaEngName": "Zaoqiao Township" 573 | }, 574 | { 575 | "ZipCode": "362", 576 | "AreaName": "頭屋鄉", 577 | "AreaEngName": "Touwu Township" 578 | }, 579 | { 580 | "ZipCode": "363", 581 | "AreaName": "公館鄉", 582 | "AreaEngName": "Gongguan Township" 583 | }, 584 | { 585 | "ZipCode": "364", 586 | "AreaName": "大湖鄉", 587 | "AreaEngName": "Dahu Township" 588 | }, 589 | { 590 | "ZipCode": "365", 591 | "AreaName": "泰安鄉", 592 | "AreaEngName": "Tai’an Township" 593 | }, 594 | { 595 | "ZipCode": "366", 596 | "AreaName": "銅鑼鄉", 597 | "AreaEngName": "Tongluo Township" 598 | }, 599 | { 600 | "ZipCode": "367", 601 | "AreaName": "三義鄉", 602 | "AreaEngName": "Sanyi Township" 603 | }, 604 | { 605 | "ZipCode": "368", 606 | "AreaName": "西湖鄉", 607 | "AreaEngName": "Xihu Township" 608 | }, 609 | { 610 | "ZipCode": "369", 611 | "AreaName": "卓蘭鎮", 612 | "AreaEngName": "Zhuolan Township" 613 | } 614 | ] 615 | }, 616 | { 617 | "CityName": "臺中市", 618 | "CityEngName": "Taichung City", 619 | "AreaList": [ 620 | { 621 | "ZipCode": "400", 622 | "AreaName": "中區", 623 | "AreaEngName": "Central Dist." 624 | }, 625 | { 626 | "ZipCode": "401", 627 | "AreaName": "東區", 628 | "AreaEngName": "East Dist." 629 | }, 630 | { 631 | "ZipCode": "402", 632 | "AreaName": "南區", 633 | "AreaEngName": "South Dist." 634 | }, 635 | { 636 | "ZipCode": "403", 637 | "AreaName": "西區", 638 | "AreaEngName": "West Dist." 639 | }, 640 | { 641 | "ZipCode": "404", 642 | "AreaName": "北區", 643 | "AreaEngName": "North Dist." 644 | }, 645 | { 646 | "ZipCode": "406", 647 | "AreaName": "北屯區", 648 | "AreaEngName": "Beitun Dist." 649 | }, 650 | { 651 | "ZipCode": "407", 652 | "AreaName": "西屯區", 653 | "AreaEngName": "Xitun Dist." 654 | }, 655 | { 656 | "ZipCode": "408", 657 | "AreaName": "南屯區", 658 | "AreaEngName": "Nantun Dist." 659 | }, 660 | { 661 | "ZipCode": "411", 662 | "AreaName": "太平區", 663 | "AreaEngName": "Taiping Dist." 664 | }, 665 | { 666 | "ZipCode": "412", 667 | "AreaName": "大里區", 668 | "AreaEngName": "Dali Dist." 669 | }, 670 | { 671 | "ZipCode": "413", 672 | "AreaName": "霧峰區", 673 | "AreaEngName": "Wufeng Dist." 674 | }, 675 | { 676 | "ZipCode": "414", 677 | "AreaName": "烏日區", 678 | "AreaEngName": "Wuri Dist." 679 | }, 680 | { 681 | "ZipCode": "420", 682 | "AreaName": "豐原區", 683 | "AreaEngName": "Fengyuan Dist." 684 | }, 685 | { 686 | "ZipCode": "421", 687 | "AreaName": "后里區", 688 | "AreaEngName": "Houli Dist." 689 | }, 690 | { 691 | "ZipCode": "422", 692 | "AreaName": "石岡區", 693 | "AreaEngName": "Shigang Dist." 694 | }, 695 | { 696 | "ZipCode": "423", 697 | "AreaName": "東勢區", 698 | "AreaEngName": "Dongshi Dist." 699 | }, 700 | { 701 | "ZipCode": "424", 702 | "AreaName": "和平區", 703 | "AreaEngName": "Heping Dist." 704 | }, 705 | { 706 | "ZipCode": "426", 707 | "AreaName": "新社區", 708 | "AreaEngName": "Xinshe Dist." 709 | }, 710 | { 711 | "ZipCode": "427", 712 | "AreaName": "潭子區", 713 | "AreaEngName": "Tanzi Dist." 714 | }, 715 | { 716 | "ZipCode": "428", 717 | "AreaName": "大雅區", 718 | "AreaEngName": "Daya Dist." 719 | }, 720 | { 721 | "ZipCode": "429", 722 | "AreaName": "神岡區", 723 | "AreaEngName": "Shengang Dist." 724 | }, 725 | { 726 | "ZipCode": "432", 727 | "AreaName": "大肚區", 728 | "AreaEngName": "Dadu Dist." 729 | }, 730 | { 731 | "ZipCode": "433", 732 | "AreaName": "沙鹿區", 733 | "AreaEngName": "Shalu Dist." 734 | }, 735 | { 736 | "ZipCode": "434", 737 | "AreaName": "龍井區", 738 | "AreaEngName": "Longjing Dist." 739 | }, 740 | { 741 | "ZipCode": "435", 742 | "AreaName": "梧棲區", 743 | "AreaEngName": "Wuqi Dist." 744 | }, 745 | { 746 | "ZipCode": "436", 747 | "AreaName": "清水區", 748 | "AreaEngName": "Qingshui Dist." 749 | }, 750 | { 751 | "ZipCode": "437", 752 | "AreaName": "大甲區", 753 | "AreaEngName": "Dajia Dist." 754 | }, 755 | { 756 | "ZipCode": "438", 757 | "AreaName": "外埔區", 758 | "AreaEngName": "Waipu Dist." 759 | }, 760 | { 761 | "ZipCode": "439", 762 | "AreaName": "大安區", 763 | "AreaEngName": "Da’an Dist." 764 | } 765 | ] 766 | }, 767 | { 768 | "CityName": "彰化縣", 769 | "CityEngName": "Changhua County", 770 | "AreaList": [ 771 | { 772 | "ZipCode": "500", 773 | "AreaName": "彰化市", 774 | "AreaEngName": "Changhua City" 775 | }, 776 | { 777 | "ZipCode": "502", 778 | "AreaName": "芬園鄉", 779 | "AreaEngName": "Fenyuan Township" 780 | }, 781 | { 782 | "ZipCode": "503", 783 | "AreaName": "花壇鄉", 784 | "AreaEngName": "Huatan Township" 785 | }, 786 | { 787 | "ZipCode": "504", 788 | "AreaName": "秀水鄉", 789 | "AreaEngName": "Xiushui Township" 790 | }, 791 | { 792 | "ZipCode": "505", 793 | "AreaName": "鹿港鎮", 794 | "AreaEngName": "Lukang Township" 795 | }, 796 | { 797 | "ZipCode": "506", 798 | "AreaName": "福興鄉", 799 | "AreaEngName": "Fuxing Township" 800 | }, 801 | { 802 | "ZipCode": "507", 803 | "AreaName": "線西鄉", 804 | "AreaEngName": "Xianxi Township" 805 | }, 806 | { 807 | "ZipCode": "508", 808 | "AreaName": "和美鎮", 809 | "AreaEngName": "Hemei Township" 810 | }, 811 | { 812 | "ZipCode": "509", 813 | "AreaName": "伸港鄉", 814 | "AreaEngName": "Shengang Township" 815 | }, 816 | { 817 | "ZipCode": "510", 818 | "AreaName": "員林市", 819 | "AreaEngName": "Yuanlin City" 820 | }, 821 | { 822 | "ZipCode": "511", 823 | "AreaName": "社頭鄉", 824 | "AreaEngName": "Shetou Township" 825 | }, 826 | { 827 | "ZipCode": "512", 828 | "AreaName": "永靖鄉", 829 | "AreaEngName": "Yongjing Township" 830 | }, 831 | { 832 | "ZipCode": "513", 833 | "AreaName": "埔心鄉", 834 | "AreaEngName": "Puxin Township" 835 | }, 836 | { 837 | "ZipCode": "514", 838 | "AreaName": "溪湖鎮", 839 | "AreaEngName": "Xihu Township" 840 | }, 841 | { 842 | "ZipCode": "515", 843 | "AreaName": "大村鄉", 844 | "AreaEngName": "Dacun Township" 845 | }, 846 | { 847 | "ZipCode": "516", 848 | "AreaName": "埔鹽鄉", 849 | "AreaEngName": "Puyan Township" 850 | }, 851 | { 852 | "ZipCode": "520", 853 | "AreaName": "田中鎮", 854 | "AreaEngName": "Tianzhong Township" 855 | }, 856 | { 857 | "ZipCode": "521", 858 | "AreaName": "北斗鎮", 859 | "AreaEngName": "Beidou Township" 860 | }, 861 | { 862 | "ZipCode": "522", 863 | "AreaName": "田尾鄉", 864 | "AreaEngName": "Tianwei Township" 865 | }, 866 | { 867 | "ZipCode": "523", 868 | "AreaName": "埤頭鄉", 869 | "AreaEngName": "Pitou Township" 870 | }, 871 | { 872 | "ZipCode": "524", 873 | "AreaName": "溪州鄉", 874 | "AreaEngName": "Xizhou Township" 875 | }, 876 | { 877 | "ZipCode": "525", 878 | "AreaName": "竹塘鄉", 879 | "AreaEngName": "Zhutang Township" 880 | }, 881 | { 882 | "ZipCode": "526", 883 | "AreaName": "二林鎮", 884 | "AreaEngName": "Erlin Township" 885 | }, 886 | { 887 | "ZipCode": "527", 888 | "AreaName": "大城鄉", 889 | "AreaEngName": "Dacheng Township" 890 | }, 891 | { 892 | "ZipCode": "528", 893 | "AreaName": "芳苑鄉", 894 | "AreaEngName": "Fangyuan Township" 895 | }, 896 | { 897 | "ZipCode": "530", 898 | "AreaName": "二水鄉", 899 | "AreaEngName": "Ershui Township" 900 | } 901 | ] 902 | }, 903 | { 904 | "CityName": "南投縣", 905 | "CityEngName": "Nantou County", 906 | "AreaList": [ 907 | { 908 | "ZipCode": "540", 909 | "AreaName": "南投市", 910 | "AreaEngName": "Nantou City" 911 | }, 912 | { 913 | "ZipCode": "541", 914 | "AreaName": "中寮鄉", 915 | "AreaEngName": "Zhongliao Township" 916 | }, 917 | { 918 | "ZipCode": "542", 919 | "AreaName": "草屯鎮", 920 | "AreaEngName": "Caotun Township" 921 | }, 922 | { 923 | "ZipCode": "544", 924 | "AreaName": "國姓鄉", 925 | "AreaEngName": "Guoxing Township" 926 | }, 927 | { 928 | "ZipCode": "545", 929 | "AreaName": "埔里鎮", 930 | "AreaEngName": "Puli Township" 931 | }, 932 | { 933 | "ZipCode": "546", 934 | "AreaName": "仁愛鄉", 935 | "AreaEngName": "Ren’ai Township" 936 | }, 937 | { 938 | "ZipCode": "551", 939 | "AreaName": "名間鄉", 940 | "AreaEngName": "Mingjian Township" 941 | }, 942 | { 943 | "ZipCode": "552", 944 | "AreaName": "集集鎮", 945 | "AreaEngName": "Jiji Township" 946 | }, 947 | { 948 | "ZipCode": "553", 949 | "AreaName": "水里鄉", 950 | "AreaEngName": "Shuili Township" 951 | }, 952 | { 953 | "ZipCode": "555", 954 | "AreaName": "魚池鄉", 955 | "AreaEngName": "Yuchi Township" 956 | }, 957 | { 958 | "ZipCode": "556", 959 | "AreaName": "信義鄉", 960 | "AreaEngName": "Xinyi Township" 961 | }, 962 | { 963 | "ZipCode": "557", 964 | "AreaName": "竹山鎮", 965 | "AreaEngName": "Zhushan Township" 966 | }, 967 | { 968 | "ZipCode": "558", 969 | "AreaName": "鹿谷鄉", 970 | "AreaEngName": "Lugu Township" 971 | } 972 | ] 973 | }, 974 | { 975 | "CityName": "嘉義市", 976 | "CityEngName": "Chiayi City", 977 | "AreaList": [ 978 | { 979 | "ZipCode": "600", 980 | "AreaName": "西區", 981 | "AreaEngName": "West Dist." 982 | }, 983 | { 984 | "ZipCode": "600", 985 | "AreaName": "東區", 986 | "AreaEngName": "East Dist." 987 | } 988 | ] 989 | }, 990 | { 991 | "CityName": "嘉義縣", 992 | "CityEngName": "Chiayi County", 993 | "AreaList": [ 994 | { 995 | "ZipCode": "602", 996 | "AreaName": "番路鄉", 997 | "AreaEngName": "Fanlu Township" 998 | }, 999 | { 1000 | "ZipCode": "603", 1001 | "AreaName": "梅山鄉", 1002 | "AreaEngName": "Meishan Township" 1003 | }, 1004 | { 1005 | "ZipCode": "604", 1006 | "AreaName": "竹崎鄉", 1007 | "AreaEngName": "Zhuqi Township" 1008 | }, 1009 | { 1010 | "ZipCode": "605", 1011 | "AreaName": "阿里山鄉", 1012 | "AreaEngName": "Alishan Township" 1013 | }, 1014 | { 1015 | "ZipCode": "606", 1016 | "AreaName": "中埔鄉", 1017 | "AreaEngName": "Zhongpu Township" 1018 | }, 1019 | { 1020 | "ZipCode": "607", 1021 | "AreaName": "大埔鄉", 1022 | "AreaEngName": "Dapu Township" 1023 | }, 1024 | { 1025 | "ZipCode": "608", 1026 | "AreaName": "水上鄉", 1027 | "AreaEngName": "Shuishang Township" 1028 | }, 1029 | { 1030 | "ZipCode": "611", 1031 | "AreaName": "鹿草鄉", 1032 | "AreaEngName": "Lucao Township" 1033 | }, 1034 | { 1035 | "ZipCode": "612", 1036 | "AreaName": "太保市", 1037 | "AreaEngName": "Taibao City" 1038 | }, 1039 | { 1040 | "ZipCode": "613", 1041 | "AreaName": "朴子市", 1042 | "AreaEngName": "Puzi City" 1043 | }, 1044 | { 1045 | "ZipCode": "614", 1046 | "AreaName": "東石鄉", 1047 | "AreaEngName": "Dongshi Township" 1048 | }, 1049 | { 1050 | "ZipCode": "615", 1051 | "AreaName": "六腳鄉", 1052 | "AreaEngName": "Liujiao Township" 1053 | }, 1054 | { 1055 | "ZipCode": "616", 1056 | "AreaName": "新港鄉", 1057 | "AreaEngName": "Xingang Township" 1058 | }, 1059 | { 1060 | "ZipCode": "621", 1061 | "AreaName": "民雄鄉", 1062 | "AreaEngName": "Minxiong Township" 1063 | }, 1064 | { 1065 | "ZipCode": "622", 1066 | "AreaName": "大林鎮", 1067 | "AreaEngName": "Dalin Township" 1068 | }, 1069 | { 1070 | "ZipCode": "623", 1071 | "AreaName": "溪口鄉", 1072 | "AreaEngName": "Xikou Township" 1073 | }, 1074 | { 1075 | "ZipCode": "624", 1076 | "AreaName": "義竹鄉", 1077 | "AreaEngName": "Yizhu Township" 1078 | }, 1079 | { 1080 | "ZipCode": "625", 1081 | "AreaName": "布袋鎮", 1082 | "AreaEngName": "Budai Township" 1083 | } 1084 | ] 1085 | }, 1086 | { 1087 | "CityName": "雲林縣", 1088 | "CityEngName": "Yunlin County", 1089 | "AreaList": [ 1090 | { 1091 | "ZipCode": "630", 1092 | "AreaName": "斗南鎮", 1093 | "AreaEngName": "Dounan Township" 1094 | }, 1095 | { 1096 | "ZipCode": "631", 1097 | "AreaName": "大埤鄉", 1098 | "AreaEngName": "Dapi Township" 1099 | }, 1100 | { 1101 | "ZipCode": "632", 1102 | "AreaName": "虎尾鎮", 1103 | "AreaEngName": "Huwei Township" 1104 | }, 1105 | { 1106 | "ZipCode": "633", 1107 | "AreaName": "土庫鎮", 1108 | "AreaEngName": "Tuku Township" 1109 | }, 1110 | { 1111 | "ZipCode": "634", 1112 | "AreaName": "褒忠鄉", 1113 | "AreaEngName": "Baozhong Township" 1114 | }, 1115 | { 1116 | "ZipCode": "635", 1117 | "AreaName": "東勢鄉", 1118 | "AreaEngName": "Dongshi Township" 1119 | }, 1120 | { 1121 | "ZipCode": "636", 1122 | "AreaName": "臺西鄉", 1123 | "AreaEngName": "Taixi Township" 1124 | }, 1125 | { 1126 | "ZipCode": "637", 1127 | "AreaName": "崙背鄉", 1128 | "AreaEngName": "Lunbei Township" 1129 | }, 1130 | { 1131 | "ZipCode": "638", 1132 | "AreaName": "麥寮鄉", 1133 | "AreaEngName": "Mailiao Township" 1134 | }, 1135 | { 1136 | "ZipCode": "640", 1137 | "AreaName": "斗六市", 1138 | "AreaEngName": "Douliu City" 1139 | }, 1140 | { 1141 | "ZipCode": "643", 1142 | "AreaName": "林內鄉", 1143 | "AreaEngName": "Linnei Township" 1144 | }, 1145 | { 1146 | "ZipCode": "646", 1147 | "AreaName": "古坑鄉", 1148 | "AreaEngName": "Gukeng Township" 1149 | }, 1150 | { 1151 | "ZipCode": "647", 1152 | "AreaName": "莿桐鄉", 1153 | "AreaEngName": "Citong Township" 1154 | }, 1155 | { 1156 | "ZipCode": "648", 1157 | "AreaName": "西螺鎮", 1158 | "AreaEngName": "Xiluo Township" 1159 | }, 1160 | { 1161 | "ZipCode": "649", 1162 | "AreaName": "二崙鄉", 1163 | "AreaEngName": "Erlun Township" 1164 | }, 1165 | { 1166 | "ZipCode": "651", 1167 | "AreaName": "北港鎮", 1168 | "AreaEngName": "Beigang Township" 1169 | }, 1170 | { 1171 | "ZipCode": "652", 1172 | "AreaName": "水林鄉", 1173 | "AreaEngName": "Shuilin Township" 1174 | }, 1175 | { 1176 | "ZipCode": "653", 1177 | "AreaName": "口湖鄉", 1178 | "AreaEngName": "Kouhu Township" 1179 | }, 1180 | { 1181 | "ZipCode": "654", 1182 | "AreaName": "四湖鄉", 1183 | "AreaEngName": "Sihu Township" 1184 | }, 1185 | { 1186 | "ZipCode": "655", 1187 | "AreaName": "元長鄉", 1188 | "AreaEngName": "Yuanchang Township" 1189 | } 1190 | ] 1191 | }, 1192 | { 1193 | "CityName": "臺南市", 1194 | "CityEngName": "Tainan City", 1195 | "AreaList": [ 1196 | { 1197 | "ZipCode": "700", 1198 | "AreaName": "中西區", 1199 | "AreaEngName": "West Central Dist." 1200 | }, 1201 | { 1202 | "ZipCode": "701", 1203 | "AreaName": "東區", 1204 | "AreaEngName": "East Dist." 1205 | }, 1206 | { 1207 | "ZipCode": "702", 1208 | "AreaName": "南區", 1209 | "AreaEngName": "South Dist." 1210 | }, 1211 | { 1212 | "ZipCode": "704", 1213 | "AreaName": "北區", 1214 | "AreaEngName": "North Dist." 1215 | }, 1216 | { 1217 | "ZipCode": "708", 1218 | "AreaName": "安平區", 1219 | "AreaEngName": "Anping Dist." 1220 | }, 1221 | { 1222 | "ZipCode": "709", 1223 | "AreaName": "安南區", 1224 | "AreaEngName": "Annan Dist." 1225 | }, 1226 | { 1227 | "ZipCode": "710", 1228 | "AreaName": "永康區", 1229 | "AreaEngName": "Yongkang Dist." 1230 | }, 1231 | { 1232 | "ZipCode": "711", 1233 | "AreaName": "歸仁區", 1234 | "AreaEngName": "Guiren Dist." 1235 | }, 1236 | { 1237 | "ZipCode": "712", 1238 | "AreaName": "新化區", 1239 | "AreaEngName": "Xinhua Dist." 1240 | }, 1241 | { 1242 | "ZipCode": "713", 1243 | "AreaName": "左鎮區", 1244 | "AreaEngName": "Zuozhen Dist." 1245 | }, 1246 | { 1247 | "ZipCode": "714", 1248 | "AreaName": "玉井區", 1249 | "AreaEngName": "Yujing Dist." 1250 | }, 1251 | { 1252 | "ZipCode": "715", 1253 | "AreaName": "楠西區", 1254 | "AreaEngName": "Nanxi Dist." 1255 | }, 1256 | { 1257 | "ZipCode": "716", 1258 | "AreaName": "南化區", 1259 | "AreaEngName": "Nanhua Dist." 1260 | }, 1261 | { 1262 | "ZipCode": "717", 1263 | "AreaName": "仁德區", 1264 | "AreaEngName": "Rende Dist." 1265 | }, 1266 | { 1267 | "ZipCode": "718", 1268 | "AreaName": "關廟區", 1269 | "AreaEngName": "Guanmiao Dist." 1270 | }, 1271 | { 1272 | "ZipCode": "719", 1273 | "AreaName": "龍崎區", 1274 | "AreaEngName": "Longqi Dist." 1275 | }, 1276 | { 1277 | "ZipCode": "720", 1278 | "AreaName": "官田區", 1279 | "AreaEngName": "Guantian Dist." 1280 | }, 1281 | { 1282 | "ZipCode": "721", 1283 | "AreaName": "麻豆區", 1284 | "AreaEngName": "Madou Dist." 1285 | }, 1286 | { 1287 | "ZipCode": "722", 1288 | "AreaName": "佳里區", 1289 | "AreaEngName": "Jiali Dist." 1290 | }, 1291 | { 1292 | "ZipCode": "723", 1293 | "AreaName": "西港區", 1294 | "AreaEngName": "Xigang Dist." 1295 | }, 1296 | { 1297 | "ZipCode": "724", 1298 | "AreaName": "七股區", 1299 | "AreaEngName": "Qigu Dist." 1300 | }, 1301 | { 1302 | "ZipCode": "725", 1303 | "AreaName": "將軍區", 1304 | "AreaEngName": "Jiangjun Dist." 1305 | }, 1306 | { 1307 | "ZipCode": "726", 1308 | "AreaName": "學甲區", 1309 | "AreaEngName": "Xuejia Dist." 1310 | }, 1311 | { 1312 | "ZipCode": "727", 1313 | "AreaName": "北門區", 1314 | "AreaEngName": "Beimen Dist." 1315 | }, 1316 | { 1317 | "ZipCode": "730", 1318 | "AreaName": "新營區", 1319 | "AreaEngName": "Xinying Dist." 1320 | }, 1321 | { 1322 | "ZipCode": "731", 1323 | "AreaName": "後壁區", 1324 | "AreaEngName": "Houbi Dist." 1325 | }, 1326 | { 1327 | "ZipCode": "732", 1328 | "AreaName": "白河區", 1329 | "AreaEngName": "Baihe Dist." 1330 | }, 1331 | { 1332 | "ZipCode": "733", 1333 | "AreaName": "東山區", 1334 | "AreaEngName": "Dongshan Dist." 1335 | }, 1336 | { 1337 | "ZipCode": "734", 1338 | "AreaName": "六甲區", 1339 | "AreaEngName": "Liujia Dist." 1340 | }, 1341 | { 1342 | "ZipCode": "735", 1343 | "AreaName": "下營區", 1344 | "AreaEngName": "Xiaying Dist." 1345 | }, 1346 | { 1347 | "ZipCode": "736", 1348 | "AreaName": "柳營區", 1349 | "AreaEngName": "Liuying Dist." 1350 | }, 1351 | { 1352 | "ZipCode": "737", 1353 | "AreaName": "鹽水區", 1354 | "AreaEngName": "Yanshui Dist." 1355 | }, 1356 | { 1357 | "ZipCode": "741", 1358 | "AreaName": "善化區", 1359 | "AreaEngName": "Shanhua Dist." 1360 | }, 1361 | { 1362 | "ZipCode": "744", 1363 | "AreaName": "新市區", 1364 | "AreaEngName": "Xinshi Dist." 1365 | }, 1366 | { 1367 | "ZipCode": "742", 1368 | "AreaName": "大內區", 1369 | "AreaEngName": "Danei Dist." 1370 | }, 1371 | { 1372 | "ZipCode": "743", 1373 | "AreaName": "山上區", 1374 | "AreaEngName": "Shanshang Dist." 1375 | }, 1376 | { 1377 | "ZipCode": "745", 1378 | "AreaName": "安定區", 1379 | "AreaEngName": "Anding Dist." 1380 | } 1381 | ] 1382 | }, 1383 | { 1384 | "CityName": "高雄市", 1385 | "CityEngName": "Kaohsiung City", 1386 | "AreaList": [ 1387 | { 1388 | "ZipCode": "800", 1389 | "AreaName": "新興區", 1390 | "AreaEngName": "Xinxing Dist." 1391 | }, 1392 | { 1393 | "ZipCode": "801", 1394 | "AreaName": "前金區", 1395 | "AreaEngName": "Qianjin Dist." 1396 | }, 1397 | { 1398 | "ZipCode": "802", 1399 | "AreaName": "苓雅區", 1400 | "AreaEngName": "Lingya Dist." 1401 | }, 1402 | { 1403 | "ZipCode": "803", 1404 | "AreaName": "鹽埕區", 1405 | "AreaEngName": "Yancheng Dist." 1406 | }, 1407 | { 1408 | "ZipCode": "804", 1409 | "AreaName": "鼓山區", 1410 | "AreaEngName": "Gushan Dist." 1411 | }, 1412 | { 1413 | "ZipCode": "805", 1414 | "AreaName": "旗津區", 1415 | "AreaEngName": "Qijin Dist." 1416 | }, 1417 | { 1418 | "ZipCode": "806", 1419 | "AreaName": "前鎮區", 1420 | "AreaEngName": "Qianzhen Dist." 1421 | }, 1422 | { 1423 | "ZipCode": "807", 1424 | "AreaName": "三民區", 1425 | "AreaEngName": "Sanmin Dist." 1426 | }, 1427 | { 1428 | "ZipCode": "811", 1429 | "AreaName": "楠梓區", 1430 | "AreaEngName": "Nanzi Dist." 1431 | }, 1432 | { 1433 | "ZipCode": "812", 1434 | "AreaName": "小港區", 1435 | "AreaEngName": "Xiaogang Dist." 1436 | }, 1437 | { 1438 | "ZipCode": "813", 1439 | "AreaName": "左營區", 1440 | "AreaEngName": "Zuoying Dist." 1441 | }, 1442 | { 1443 | "ZipCode": "814", 1444 | "AreaName": "仁武區", 1445 | "AreaEngName": "Renwu Dist." 1446 | }, 1447 | { 1448 | "ZipCode": "815", 1449 | "AreaName": "大社區", 1450 | "AreaEngName": "Dashe Dist." 1451 | }, 1452 | { 1453 | "ZipCode": "817", 1454 | "AreaName": "東沙群島", 1455 | "AreaEngName": "Dongsha Islands" 1456 | }, 1457 | { 1458 | "ZipCode": "819", 1459 | "AreaName": "南沙群島", 1460 | "AreaEngName": "Nansha Islands" 1461 | }, 1462 | { 1463 | "ZipCode": "820", 1464 | "AreaName": "岡山區", 1465 | "AreaEngName": "Gangshan Dist." 1466 | }, 1467 | { 1468 | "ZipCode": "821", 1469 | "AreaName": "路竹區", 1470 | "AreaEngName": "Luzhu Dist." 1471 | }, 1472 | { 1473 | "ZipCode": "822", 1474 | "AreaName": "阿蓮區", 1475 | "AreaEngName": "Alian Dist." 1476 | }, 1477 | { 1478 | "ZipCode": "823", 1479 | "AreaName": "田寮區", 1480 | "AreaEngName": "Tianliao Dist." 1481 | }, 1482 | { 1483 | "ZipCode": "824", 1484 | "AreaName": "燕巢區", 1485 | "AreaEngName": "Yanchao Dist." 1486 | }, 1487 | { 1488 | "ZipCode": "825", 1489 | "AreaName": "橋頭區", 1490 | "AreaEngName": "Qiaotou Dist." 1491 | }, 1492 | { 1493 | "ZipCode": "826", 1494 | "AreaName": "梓官區", 1495 | "AreaEngName": "Ziguan Dist." 1496 | }, 1497 | { 1498 | "ZipCode": "827", 1499 | "AreaName": "彌陀區", 1500 | "AreaEngName": "Mituo Dist." 1501 | }, 1502 | { 1503 | "ZipCode": "828", 1504 | "AreaName": "永安區", 1505 | "AreaEngName": "Yong’an Dist." 1506 | }, 1507 | { 1508 | "ZipCode": "829", 1509 | "AreaName": "湖內區", 1510 | "AreaEngName": "Hunei Dist." 1511 | }, 1512 | { 1513 | "ZipCode": "830", 1514 | "AreaName": "鳳山區", 1515 | "AreaEngName": "Fengshan Dist." 1516 | }, 1517 | { 1518 | "ZipCode": "831", 1519 | "AreaName": "大寮區", 1520 | "AreaEngName": "Daliao Dist." 1521 | }, 1522 | { 1523 | "ZipCode": "832", 1524 | "AreaName": "林園區", 1525 | "AreaEngName": "Linyuan Dist." 1526 | }, 1527 | { 1528 | "ZipCode": "833", 1529 | "AreaName": "鳥松區", 1530 | "AreaEngName": "Niaosong Dist." 1531 | }, 1532 | { 1533 | "ZipCode": "840", 1534 | "AreaName": "大樹區", 1535 | "AreaEngName": "Dashu Dist." 1536 | }, 1537 | { 1538 | "ZipCode": "842", 1539 | "AreaName": "旗山區", 1540 | "AreaEngName": "Qishan Dist." 1541 | }, 1542 | { 1543 | "ZipCode": "843", 1544 | "AreaName": "美濃區", 1545 | "AreaEngName": "Meinong Dist." 1546 | }, 1547 | { 1548 | "ZipCode": "844", 1549 | "AreaName": "六龜區", 1550 | "AreaEngName": "Liugui Dist." 1551 | }, 1552 | { 1553 | "ZipCode": "845", 1554 | "AreaName": "內門區", 1555 | "AreaEngName": "Neimen Dist." 1556 | }, 1557 | { 1558 | "ZipCode": "846", 1559 | "AreaName": "杉林區", 1560 | "AreaEngName": "Shanlin Dist." 1561 | }, 1562 | { 1563 | "ZipCode": "847", 1564 | "AreaName": "甲仙區", 1565 | "AreaEngName": "Jiaxian Dist." 1566 | }, 1567 | { 1568 | "ZipCode": "848", 1569 | "AreaName": "桃源區", 1570 | "AreaEngName": "Taoyuan Dist." 1571 | }, 1572 | { 1573 | "ZipCode": "849", 1574 | "AreaName": "那瑪夏區", 1575 | "AreaEngName": "Namaxia Dist." 1576 | }, 1577 | { 1578 | "ZipCode": "851", 1579 | "AreaName": "茂林區", 1580 | "AreaEngName": "Maolin Dist." 1581 | }, 1582 | { 1583 | "ZipCode": "852", 1584 | "AreaName": "茄萣區", 1585 | "AreaEngName": "Qieding Dist." 1586 | } 1587 | ] 1588 | }, 1589 | { 1590 | "CityName": "澎湖縣", 1591 | "CityEngName": "Penghu County", 1592 | "AreaList": [ 1593 | { 1594 | "ZipCode": "880", 1595 | "AreaName": "馬公市", 1596 | "AreaEngName": "Magong City" 1597 | }, 1598 | { 1599 | "ZipCode": "881", 1600 | "AreaName": "西嶼鄉", 1601 | "AreaEngName": "Xiyu Township" 1602 | }, 1603 | { 1604 | "ZipCode": "882", 1605 | "AreaName": "望安鄉", 1606 | "AreaEngName": "Wang’an Township" 1607 | }, 1608 | { 1609 | "ZipCode": "883", 1610 | "AreaName": "七美鄉", 1611 | "AreaEngName": "Qimei Township" 1612 | }, 1613 | { 1614 | "ZipCode": "884", 1615 | "AreaName": "白沙鄉", 1616 | "AreaEngName": "Baisha Township" 1617 | }, 1618 | { 1619 | "ZipCode": "885", 1620 | "AreaName": "湖西鄉", 1621 | "AreaEngName": "Huxi Township" 1622 | } 1623 | ] 1624 | }, 1625 | { 1626 | "CityName": "金門縣", 1627 | "CityEngName": "Kinmen County", 1628 | "AreaList": [ 1629 | { 1630 | "ZipCode": "890", 1631 | "AreaName": "金沙鎮", 1632 | "AreaEngName": "Jinsha Township" 1633 | }, 1634 | { 1635 | "ZipCode": "891", 1636 | "AreaName": "金湖鎮", 1637 | "AreaEngName": "Jinhu Township" 1638 | }, 1639 | { 1640 | "ZipCode": "892", 1641 | "AreaName": "金寧鄉", 1642 | "AreaEngName": "Jinning Township" 1643 | }, 1644 | { 1645 | "ZipCode": "893", 1646 | "AreaName": "金城鎮", 1647 | "AreaEngName": "Jincheng Township" 1648 | }, 1649 | { 1650 | "ZipCode": "894", 1651 | "AreaName": "烈嶼鄉", 1652 | "AreaEngName": "Lieyu Township" 1653 | }, 1654 | { 1655 | "ZipCode": "896", 1656 | "AreaName": "烏坵鄉", 1657 | "AreaEngName": "Wuqiu Township" 1658 | } 1659 | ] 1660 | }, 1661 | { 1662 | "CityName": "屏東縣", 1663 | "CityEngName": "Pingtung County", 1664 | "AreaList": [ 1665 | { 1666 | "ZipCode": "900", 1667 | "AreaName": "屏東市", 1668 | "AreaEngName": "Pingtung City" 1669 | }, 1670 | { 1671 | "ZipCode": "901", 1672 | "AreaName": "三地門鄉", 1673 | "AreaEngName": "Sandimen Township" 1674 | }, 1675 | { 1676 | "ZipCode": "902", 1677 | "AreaName": "霧臺鄉", 1678 | "AreaEngName": "Wutai Township" 1679 | }, 1680 | { 1681 | "ZipCode": "903", 1682 | "AreaName": "瑪家鄉", 1683 | "AreaEngName": "Majia Township" 1684 | }, 1685 | { 1686 | "ZipCode": "904", 1687 | "AreaName": "九如鄉", 1688 | "AreaEngName": "Jiuru Township" 1689 | }, 1690 | { 1691 | "ZipCode": "905", 1692 | "AreaName": "里港鄉", 1693 | "AreaEngName": "Ligang Township" 1694 | }, 1695 | { 1696 | "ZipCode": "906", 1697 | "AreaName": "高樹鄉", 1698 | "AreaEngName": "Gaoshu Township" 1699 | }, 1700 | { 1701 | "ZipCode": "907", 1702 | "AreaName": "鹽埔鄉", 1703 | "AreaEngName": "Yanpu Township" 1704 | }, 1705 | { 1706 | "ZipCode": "908", 1707 | "AreaName": "長治鄉", 1708 | "AreaEngName": "Changzhi Township" 1709 | }, 1710 | { 1711 | "ZipCode": "909", 1712 | "AreaName": "麟洛鄉", 1713 | "AreaEngName": "Linluo Township" 1714 | }, 1715 | { 1716 | "ZipCode": "911", 1717 | "AreaName": "竹田鄉", 1718 | "AreaEngName": "Zhutian Township" 1719 | }, 1720 | { 1721 | "ZipCode": "912", 1722 | "AreaName": "內埔鄉", 1723 | "AreaEngName": "Neipu Township" 1724 | }, 1725 | { 1726 | "ZipCode": "913", 1727 | "AreaName": "萬丹鄉", 1728 | "AreaEngName": "Wandan Township" 1729 | }, 1730 | { 1731 | "ZipCode": "920", 1732 | "AreaName": "潮州鎮", 1733 | "AreaEngName": "Chaozhou Township" 1734 | }, 1735 | { 1736 | "ZipCode": "921", 1737 | "AreaName": "泰武鄉", 1738 | "AreaEngName": "Taiwu Township" 1739 | }, 1740 | { 1741 | "ZipCode": "922", 1742 | "AreaName": "來義鄉", 1743 | "AreaEngName": "Laiyi Township" 1744 | }, 1745 | { 1746 | "ZipCode": "923", 1747 | "AreaName": "萬巒鄉", 1748 | "AreaEngName": "Wanluan Township" 1749 | }, 1750 | { 1751 | "ZipCode": "924", 1752 | "AreaName": "崁頂鄉", 1753 | "AreaEngName": "Kanding Township" 1754 | }, 1755 | { 1756 | "ZipCode": "925", 1757 | "AreaName": "新埤鄉", 1758 | "AreaEngName": "Xinpi Township" 1759 | }, 1760 | { 1761 | "ZipCode": "926", 1762 | "AreaName": "南州鄉", 1763 | "AreaEngName": "Nanzhou Township" 1764 | }, 1765 | { 1766 | "ZipCode": "927", 1767 | "AreaName": "林邊鄉", 1768 | "AreaEngName": "Linbian Township" 1769 | }, 1770 | { 1771 | "ZipCode": "928", 1772 | "AreaName": "東港鎮", 1773 | "AreaEngName": "Donggang Township" 1774 | }, 1775 | { 1776 | "ZipCode": "929", 1777 | "AreaName": "琉球鄉", 1778 | "AreaEngName": "Liuqiu Township" 1779 | }, 1780 | { 1781 | "ZipCode": "931", 1782 | "AreaName": "佳冬鄉", 1783 | "AreaEngName": "Jiadong Township" 1784 | }, 1785 | { 1786 | "ZipCode": "932", 1787 | "AreaName": "新園鄉", 1788 | "AreaEngName": "Xinyuan Township" 1789 | }, 1790 | { 1791 | "ZipCode": "940", 1792 | "AreaName": "枋寮鄉", 1793 | "AreaEngName": "Fangliao Township" 1794 | }, 1795 | { 1796 | "ZipCode": "941", 1797 | "AreaName": "枋山鄉", 1798 | "AreaEngName": "Fangshan Township" 1799 | }, 1800 | { 1801 | "ZipCode": "942", 1802 | "AreaName": "春日鄉", 1803 | "AreaEngName": "Chunri Township" 1804 | }, 1805 | { 1806 | "ZipCode": "943", 1807 | "AreaName": "獅子鄉", 1808 | "AreaEngName": "Shizi Township" 1809 | }, 1810 | { 1811 | "ZipCode": "944", 1812 | "AreaName": "車城鄉", 1813 | "AreaEngName": "Checheng Township" 1814 | }, 1815 | { 1816 | "ZipCode": "945", 1817 | "AreaName": "牡丹鄉", 1818 | "AreaEngName": "Mudan Township" 1819 | }, 1820 | { 1821 | "ZipCode": "946", 1822 | "AreaName": "恆春鎮", 1823 | "AreaEngName": "Hengchun Township" 1824 | }, 1825 | { 1826 | "ZipCode": "947", 1827 | "AreaName": "滿州鄉", 1828 | "AreaEngName": "Manzhou Township" 1829 | } 1830 | ] 1831 | }, 1832 | { 1833 | "CityName": "臺東縣", 1834 | "CityEngName": "Taitung County", 1835 | "AreaList": [ 1836 | { 1837 | "ZipCode": "950", 1838 | "AreaName": "臺東市", 1839 | "AreaEngName": "Taitung City" 1840 | }, 1841 | { 1842 | "ZipCode": "951", 1843 | "AreaName": "綠島鄉", 1844 | "AreaEngName": "Ludao Township" 1845 | }, 1846 | { 1847 | "ZipCode": "952", 1848 | "AreaName": "蘭嶼鄉", 1849 | "AreaEngName": "Lanyu Township" 1850 | }, 1851 | { 1852 | "ZipCode": "953", 1853 | "AreaName": "延平鄉", 1854 | "AreaEngName": "Yanping Township" 1855 | }, 1856 | { 1857 | "ZipCode": "954", 1858 | "AreaName": "卑南鄉", 1859 | "AreaEngName": "Beinan Township" 1860 | }, 1861 | { 1862 | "ZipCode": "955", 1863 | "AreaName": "鹿野鄉", 1864 | "AreaEngName": "Luye Township" 1865 | }, 1866 | { 1867 | "ZipCode": "956", 1868 | "AreaName": "關山鎮", 1869 | "AreaEngName": "Guanshan Township" 1870 | }, 1871 | { 1872 | "ZipCode": "957", 1873 | "AreaName": "海端鄉", 1874 | "AreaEngName": "Haiduan Township" 1875 | }, 1876 | { 1877 | "ZipCode": "958", 1878 | "AreaName": "池上鄉", 1879 | "AreaEngName": "Chishang Township" 1880 | }, 1881 | { 1882 | "ZipCode": "959", 1883 | "AreaName": "東河鄉", 1884 | "AreaEngName": "Donghe Township" 1885 | }, 1886 | { 1887 | "ZipCode": "961", 1888 | "AreaName": "成功鎮", 1889 | "AreaEngName": "Chenggong Township" 1890 | }, 1891 | { 1892 | "ZipCode": "962", 1893 | "AreaName": "長濱鄉", 1894 | "AreaEngName": "Changbin Township" 1895 | }, 1896 | { 1897 | "ZipCode": "963", 1898 | "AreaName": "太麻里鄉", 1899 | "AreaEngName": "Taimali Township" 1900 | }, 1901 | { 1902 | "ZipCode": "964", 1903 | "AreaName": "金峰鄉", 1904 | "AreaEngName": "Jinfeng Township" 1905 | }, 1906 | { 1907 | "ZipCode": "965", 1908 | "AreaName": "大武鄉", 1909 | "AreaEngName": "Dawu Township" 1910 | }, 1911 | { 1912 | "ZipCode": "966", 1913 | "AreaName": "達仁鄉", 1914 | "AreaEngName": "Daren Township" 1915 | } 1916 | ] 1917 | }, 1918 | { 1919 | "CityName": "花蓮縣", 1920 | "CityEngName": "Hualien County", 1921 | "AreaList": [ 1922 | { 1923 | "ZipCode": "970", 1924 | "AreaName": "花蓮市", 1925 | "AreaEngName": "Hualien City" 1926 | }, 1927 | { 1928 | "ZipCode": "971", 1929 | "AreaName": "新城鄉", 1930 | "AreaEngName": "Xincheng Township" 1931 | }, 1932 | { 1933 | "ZipCode": "972", 1934 | "AreaName": "秀林鄉", 1935 | "AreaEngName": "Xiulin Township" 1936 | }, 1937 | { 1938 | "ZipCode": "973", 1939 | "AreaName": "吉安鄉", 1940 | "AreaEngName": "Ji’an Township" 1941 | }, 1942 | { 1943 | "ZipCode": "974", 1944 | "AreaName": "壽豐鄉", 1945 | "AreaEngName": "Shoufeng Township" 1946 | }, 1947 | { 1948 | "ZipCode": "975", 1949 | "AreaName": "鳳林鎮", 1950 | "AreaEngName": "Fenglin Township" 1951 | }, 1952 | { 1953 | "ZipCode": "976", 1954 | "AreaName": "光復鄉", 1955 | "AreaEngName": "Guangfu Township" 1956 | }, 1957 | { 1958 | "ZipCode": "977", 1959 | "AreaName": "豐濱鄉", 1960 | "AreaEngName": "Fengbin Township" 1961 | }, 1962 | { 1963 | "ZipCode": "978", 1964 | "AreaName": "瑞穗鄉", 1965 | "AreaEngName": "Ruisui Township" 1966 | }, 1967 | { 1968 | "ZipCode": "979", 1969 | "AreaName": "萬榮鄉", 1970 | "AreaEngName": "Wanrong Township" 1971 | }, 1972 | { 1973 | "ZipCode": "981", 1974 | "AreaName": "玉里鎮", 1975 | "AreaEngName": "Yuli Township" 1976 | }, 1977 | { 1978 | "ZipCode": "982", 1979 | "AreaName": "卓溪鄉", 1980 | "AreaEngName": "Zhuoxi Township" 1981 | }, 1982 | { 1983 | "ZipCode": "983", 1984 | "AreaName": "富里鄉", 1985 | "AreaEngName": "Fuli Township" 1986 | } 1987 | ] 1988 | } 1989 | ] 1990 | -------------------------------------------------------------------------------- /View/src/assets/data/workFeature.json: -------------------------------------------------------------------------------- 1 | [ 2 | "全職", "短期兼職", "長期兼職", "實習", "寒暑假工讀", "複委託", "遠端工作" 3 | ] -------------------------------------------------------------------------------- /View/src/assets/data/workMoney.json: -------------------------------------------------------------------------------- 1 | [ 2 | "", "時薪", "日新", "月薪", "面議" 3 | ] -------------------------------------------------------------------------------- /View/src/assets/data/workTime.json: -------------------------------------------------------------------------------- 1 | [ 2 | "日班", "中班", "晚班", "大夜班", "假日班", "其他時段" 3 | ] -------------------------------------------------------------------------------- /View/src/assets/icon/csv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /View/src/assets/icon/doc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 23 | 28 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /View/src/assets/icon/pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 30 | 31 | 32 | 36 | 40 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /View/src/assets/icon/txt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /View/src/assets/icon/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /View/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/View/src/assets/logo.png -------------------------------------------------------------------------------- /View/src/components/basicSetting.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 207 | 208 | 258 | -------------------------------------------------------------------------------- /View/src/components/bottomTab.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | -------------------------------------------------------------------------------- /View/src/components/fileDisplayCards/fileDisplayCard.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /View/src/components/fileDisplayCards/fileSelectCard.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /View/src/components/fileManager.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 51 | 52 | -------------------------------------------------------------------------------- /View/src/components/fileSelectWindow.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 69 | 70 | -------------------------------------------------------------------------------- /View/src/components/knowledgeBase.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /View/src/components/knowledgeBaseSetting.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 115 | 116 | -------------------------------------------------------------------------------- /View/src/components/searchBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 86 | -------------------------------------------------------------------------------- /View/src/components/switchButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /View/src/components/uploadWindow.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 147 | 148 | -------------------------------------------------------------------------------- /View/src/components/whiteBackground.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 47 | 48 | -------------------------------------------------------------------------------- /View/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /View/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import './index.css' 6 | 7 | createApp(App).use(store).use(router).mount('#app') 8 | -------------------------------------------------------------------------------- /View/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import HomeView from '../views/HomeView.vue' 3 | 4 | const routes = [ 5 | { 6 | path: '/liff', 7 | name: 'home', 8 | component: HomeView 9 | } 10 | ] 11 | 12 | const router = createRouter({ 13 | history: createWebHistory(), 14 | routes 15 | }) 16 | 17 | export default router 18 | -------------------------------------------------------------------------------- /View/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | export default createStore({ 4 | state: { 5 | files:[], 6 | basicSetting:{ 7 | "id": 0, 8 | "name":"新建知識庫", 9 | "model":0, 10 | "temperature": 0.3, 11 | "score_threshold": 0.5, 12 | "search_item_limit": 4 13 | }, 14 | searchHistory: localStorage.getItem('searchHistory')!=null?JSON.parse(localStorage.getItem('searchHistory')):[], 15 | models: [], 16 | knowledgebase: [] 17 | }, 18 | getters: { 19 | getData(state){ 20 | return state.nowData; 21 | }, 22 | getHistory(state){ 23 | return state.searchHistory; 24 | }, 25 | getSetting(state){ 26 | return state.basicSetting; 27 | }, 28 | models(state){ 29 | return state.models; 30 | }, 31 | knowledgebase(state){ 32 | return state.knowledgebase; 33 | }, 34 | files(state){ 35 | return state.files; 36 | }, 37 | }, 38 | mutations: { 39 | setData(state, val){ 40 | state.nowData = val; 41 | }, 42 | addHistory(state){ 43 | state.searchHistory.unshift(JSON.parse(JSON.stringify(state.nowData))); 44 | localStorage.setItem('searchHistory', JSON.stringify(state.searchHistory)) 45 | 46 | state.searchHistory.forEach((item, i)=> i>=10?state.searchHistory.pop():true ) 47 | }, 48 | setSetting(state, val){ 49 | state.basicSetting = val; 50 | }, 51 | setModels(state, val){ 52 | state.models = val; 53 | }, 54 | setKnowledgebase(state, val){ 55 | state.knowledgebase = val; 56 | }, 57 | setFiles(state, val){ 58 | state.files = val; 59 | }, 60 | }, 61 | actions: { 62 | setData({ commit }, data){ 63 | commit("setData", data) 64 | }, 65 | addHistory({ commit }){ 66 | commit("addHistory") 67 | }, 68 | setSetting({ commit }, data){ 69 | commit("setSetting", data) 70 | }, 71 | setModels({ commit }, data){ 72 | commit("setModels", data) 73 | }, 74 | setKnowledgebase({ commit }, data){ 75 | commit("setKnowledgebase", data) 76 | }, 77 | setFiles({ commit }, data){ 78 | commit("setFiles", data) 79 | } 80 | }, 81 | modules: { 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /View/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 37 | -------------------------------------------------------------------------------- /View/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [], 4 | purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 5 | 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } 11 | 12 | -------------------------------------------------------------------------------- /View/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | module.exports = defineConfig({ 3 | transpileDependencies: true, 4 | publicPath: process.env.NODE_ENV === 'production'?"./":"./", 5 | }) 6 | -------------------------------------------------------------------------------- /config/database_backup.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/config/database_backup.db -------------------------------------------------------------------------------- /config/line_reply_template.py: -------------------------------------------------------------------------------- 1 | HELP_TEMPLATE=""" 2 | { 3 | "type": "bubble", 4 | "hero": { 5 | "type": "image", 6 | "url": "https://opengraph.githubassets.com/{$TIME}/ADT109119/ChatPDF-LineBot", 7 | "size": "full", 8 | "aspectRatio": "20:13", 9 | "aspectMode": "cover" 10 | }, 11 | "body": { 12 | "type": "box", 13 | "layout": "vertical", 14 | "contents": [ 15 | { 16 | "type": "text", 17 | "text": "幫助選單", 18 | "weight": "bold", 19 | "size": "xl" 20 | } 21 | ] 22 | }, 23 | "footer": { 24 | "type": "box", 25 | "layout": "vertical", 26 | "spacing": "sm", 27 | "contents": [ 28 | { 29 | "type": "button", 30 | "style": "link", 31 | "height": "sm", 32 | "action": { 33 | "type": "message", 34 | "label": "資訊面板", 35 | "text": "/info" 36 | } 37 | }, 38 | { 39 | "type": "button", 40 | "style": "link", 41 | "height": "sm", 42 | "action": { 43 | "type": "uri", 44 | "label": "本專案GitHub Repo", 45 | "uri": "https://github.com/ADT109119/ChatPDF-LineBot" 46 | } 47 | }, 48 | { 49 | "type": "button", 50 | "style": "link", 51 | "height": "sm", 52 | "action": { 53 | "type": "uri", 54 | "label": "使用文件", 55 | "uri": "https://adt109119.github.io/ChatPDF-LineBot-Docs/" 56 | } 57 | }, 58 | { 59 | "type": "button", 60 | "style": "link", 61 | "height": "sm", 62 | "action": { 63 | "type": "message", 64 | "label": "關於作者", 65 | "text": "/about" 66 | } 67 | }, 68 | { 69 | "type": "box", 70 | "layout": "vertical", 71 | "contents": [], 72 | "margin": "sm" 73 | } 74 | ], 75 | "flex": 0 76 | } 77 | } 78 | """ 79 | 80 | INFO_TEMPLATE = """ 81 | { 82 | "type": "bubble", 83 | "hero": { 84 | "type": "box", 85 | "layout": "vertical", 86 | "contents": [ 87 | { 88 | "type": "text", 89 | "text": "ChatPDF-LineBot ver {$VERSION}", 90 | "position": "absolute", 91 | "offsetTop": "10px", 92 | "offsetStart": "18px", 93 | "color": "#aaaaaa", 94 | "weight": "regular" 95 | }, 96 | { 97 | "type": "text", 98 | "text": "資訊面板", 99 | "size": "3xl", 100 | "weight": "bold" 101 | }, 102 | { 103 | "type": "text", 104 | "text": "by ADT109119", 105 | "size": "sm", 106 | "color": "#aaaaaa", 107 | "offsetTop": "-6px", 108 | "offsetStart": "2px" 109 | }, 110 | { 111 | "type": "separator" 112 | } 113 | ], 114 | "paddingTop": "30px", 115 | "paddingBottom": "6px", 116 | "paddingStart": "16px" 117 | }, 118 | "body": { 119 | "type": "box", 120 | "layout": "vertical", 121 | "contents": [ 122 | { 123 | "type": "box", 124 | "layout": "vertical", 125 | "margin": "none", 126 | "spacing": "sm", 127 | "contents": [ 128 | { 129 | "type": "box", 130 | "layout": "baseline", 131 | "spacing": "sm", 132 | "contents": [ 133 | { 134 | "type": "text", 135 | "text": "檔案數量", 136 | "color": "#aaaaaa", 137 | "size": "sm", 138 | "flex": 5 139 | }, 140 | { 141 | "type": "text", 142 | "text": "{$FILE_AMOUNT}", 143 | "wrap": true, 144 | "color": "#666666", 145 | "size": "sm", 146 | "flex": 3, 147 | "align": "end" 148 | } 149 | ] 150 | }, 151 | { 152 | "type": "box", 153 | "layout": "baseline", 154 | "spacing": "sm", 155 | "contents": [ 156 | { 157 | "type": "text", 158 | "text": "單檔大小限制", 159 | "color": "#aaaaaa", 160 | "size": "sm", 161 | "flex": 5 162 | }, 163 | { 164 | "type": "text", 165 | "text": "{$FILE_SIZE_LIMIT}", 166 | "wrap": true, 167 | "color": "#666666", 168 | "size": "sm", 169 | "flex": 3, 170 | "align": "end" 171 | } 172 | ] 173 | }, 174 | { 175 | "type": "box", 176 | "layout": "baseline", 177 | "spacing": "sm", 178 | "contents": [ 179 | { 180 | "type": "text", 181 | "text": "已使用空間", 182 | "color": "#aaaaaa", 183 | "size": "sm", 184 | "flex": 5 185 | }, 186 | { 187 | "type": "text", 188 | "wrap": true, 189 | "color": "#666666", 190 | "size": "sm", 191 | "flex": 5, 192 | "align": "end", 193 | "text": "{$USED_SPACE}" 194 | } 195 | ] 196 | }, 197 | { 198 | "type": "box", 199 | "layout": "vertical", 200 | "contents": [ 201 | { 202 | "type": "box", 203 | "layout": "vertical", 204 | "contents": [ 205 | { 206 | "type": "filler" 207 | } 208 | ], 209 | "backgroundColor": "#00a0ff", 210 | "width": "{$USED_SPACE_PERCENTAGE}", 211 | "height": "5px", 212 | "flex": 10 213 | } 214 | ], 215 | "height": "5px", 216 | "margin": "md", 217 | "backgroundColor": "#0060ff20" 218 | }, 219 | { 220 | "type": "box", 221 | "layout": "baseline", 222 | "spacing": "sm", 223 | "contents": [ 224 | { 225 | "type": "text", 226 | "text": "對話歷史上限", 227 | "color": "#aaaaaa", 228 | "size": "sm", 229 | "flex": 5 230 | }, 231 | { 232 | "type": "text", 233 | "text": "{$MAX_CHAT_HISTORY} 則對話", 234 | "wrap": true, 235 | "color": "#666666", 236 | "size": "sm", 237 | "flex": 3, 238 | "align": "end" 239 | } 240 | ], 241 | "margin": "md" 242 | } 243 | ] 244 | } 245 | ] 246 | }, 247 | "footer": { 248 | "type": "box", 249 | "layout": "vertical", 250 | "spacing": "sm", 251 | "contents": [ 252 | { 253 | "type": "button", 254 | "style": "link", 255 | "height": "sm", 256 | "action": { 257 | "type": "message", 258 | "label": "開啟幫助選單", 259 | "text": "/help" 260 | } 261 | }, 262 | { 263 | "type": "box", 264 | "layout": "vertical", 265 | "contents": [], 266 | "margin": "sm" 267 | } 268 | ], 269 | "flex": 0 270 | } 271 | } 272 | """ 273 | 274 | ABOUT_ME = """ 275 | { 276 | "type": "bubble", 277 | "body": { 278 | "type": "box", 279 | "layout": "vertical", 280 | "contents": [ 281 | { 282 | "type": "box", 283 | "layout": "horizontal", 284 | "contents": [ 285 | { 286 | "type": "box", 287 | "layout": "vertical", 288 | "contents": [ 289 | { 290 | "type": "image", 291 | "url": "https://the-walking-fish.com/img/avatar_1_hu9a5663c003f5c52746527244f7342efc_867962_300x0_resize_q75_h2_box_3.webp", 292 | "aspectMode": "cover", 293 | "size": "full" 294 | } 295 | ], 296 | "cornerRadius": "100px", 297 | "width": "72px", 298 | "height": "72px" 299 | }, 300 | { 301 | "type": "box", 302 | "layout": "vertical", 303 | "contents": [ 304 | { 305 | "type": "text", 306 | "contents": [ 307 | { 308 | "type": "span", 309 | "text": "The Walking Fish 步行魚", 310 | "weight": "bold", 311 | "color": "#000000" 312 | }, 313 | { 314 | "type": "span", 315 | "text": " " 316 | }, 317 | { 318 | "type": "span", 319 | "text": "一個剛大學畢業的新鮮人,常在YouTube發一些實用的程式、專案分享,並會動手製做一些開源專案" 320 | } 321 | ], 322 | "size": "sm", 323 | "wrap": true 324 | }, 325 | { 326 | "type": "box", 327 | "layout": "baseline", 328 | "contents": [ 329 | { 330 | "type": "icon", 331 | "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png" 332 | }, 333 | { 334 | "type": "text", 335 | "text": "12k Subscribers", 336 | "size": "sm", 337 | "color": "#bcbcbc" 338 | } 339 | ], 340 | "spacing": "sm", 341 | "margin": "md" 342 | } 343 | ] 344 | } 345 | ], 346 | "spacing": "xl", 347 | "paddingAll": "20px" 348 | }, 349 | { 350 | "type": "separator" 351 | } 352 | ], 353 | "paddingAll": "0px" 354 | }, 355 | "footer": { 356 | "type": "box", 357 | "layout": "vertical", 358 | "contents": [ 359 | { 360 | "type": "button", 361 | "action": { 362 | "type": "uri", 363 | "label": "YouTube頻道", 364 | "uri": "https://www.youtube.com/@the_walking_fish" 365 | } 366 | }, 367 | { 368 | "type": "button", 369 | "action": { 370 | "type": "uri", 371 | "label": "GitHub", 372 | "uri": "https://github.com/ADT109119" 373 | } 374 | }, 375 | { 376 | "type": "button", 377 | "action": { 378 | "type": "uri", 379 | "label": "頻道網站", 380 | "uri": "https://the-walking-fish.com/" 381 | } 382 | } 383 | ] 384 | } 385 | } 386 | """ -------------------------------------------------------------------------------- /db/files/uploaded_files.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/db/files/uploaded_files.txt -------------------------------------------------------------------------------- /db/vector_db/vector_db.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/db/vector_db/vector_db.txt -------------------------------------------------------------------------------- /dependencies.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, HTTPException 2 | 3 | async def verify_line_id(request: Request): 4 | line_id = request.session.get("line_id") 5 | if not line_id: 6 | raise HTTPException(status_code=401, detail="line id invalid") 7 | return True 8 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, HTTPException, APIRouter 2 | from starlette.middleware.sessions import SessionMiddleware 3 | from Middleware import NoIndexMiddleware 4 | 5 | from Controller.line.line import line_router 6 | from Controller.liff import knowledge_base_file, knowledge_base, upload_file, front_end, llm_model 7 | 8 | import uvicorn 9 | 10 | app = FastAPI() 11 | app.add_middleware(SessionMiddleware, secret_key="SECRET_KEY") 12 | app.add_middleware(NoIndexMiddleware) 13 | 14 | app.include_router(knowledge_base_file.router) 15 | app.include_router(knowledge_base.router) 16 | app.include_router(upload_file.router) 17 | app.include_router(front_end.router) 18 | app.include_router(llm_model.router) 19 | app.include_router(line_router) 20 | 21 | if __name__ == "__main__": 22 | uvicorn.run(app, host="0.0.0.0", port=8000) 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADT109119/ChatPDF-LineBot/25594ee1964c3492e6ee3df1047b82b4de7fe16a/requirements.txt --------------------------------------------------------------------------------