01- Dify部分介面分析

澄小兮發表於2024-10-25

services/acount_service.py

一、整體結構

這段 Python 程式碼實現了與使用者賬戶管理、租戶管理和註冊相關的功能,主要涉及到資料庫操作、密碼處理、令牌生成、郵件傳送等任務。它包含了多個類,分別是AccountServiceTenantServiceRegisterServiceCustomSignUpApi

二、AccountService 類

  1. 方法和功能
    • load_user(user_id):根據使用者 ID 從資料庫中載入使用者賬戶資訊,如果賬戶被封禁或關閉則丟擲異常。如果使用者有租戶關聯,則設定當前租戶 ID,並更新最後活躍時間。
    • get_account_jwt_token(account, exp):生成包含使用者資訊的 JWT 令牌,用於身份驗證。
    • authenticate(email, password):使用給定的郵箱和密碼進行賬戶認證,如果認證失敗則丟擲異常。
    • update_account_password(account, password, new_password):更新使用者賬戶密碼,先驗證舊密碼,再生成新的密碼鹽並加密新密碼後儲存到資料庫。
    • create_account(email, name, interface_language, password, interface_theme):建立新的使用者賬戶,可指定郵箱、使用者名稱、介面語言、密碼和介面主題等資訊,並根據語言設定時區。
    • link_account_integrate(provider, open_id, account):將使用者賬戶與第三方整合進行關聯,根據情況更新或建立關聯記錄。
    • close_account(account):關閉使用者賬戶,將賬戶狀態設定為已關閉。
    • update_account(account, **kwargs):根據傳入的關鍵字引數更新使用者賬戶的特定欄位。
    • update_last_login(account, ip_address):更新使用者賬戶的最後登入時間和 IP 地址。
    • login(account, ip_address):使用者登入操作,更新最後登入資訊,生成 JWT 令牌並存入 Redis 快取。
    • logout(account, token):使用者登出操作,從 Redis 快取中刪除登入令牌。
    • load_logged_in_account(account_id, token):根據使用者 ID 和令牌從快取中載入已登入的使用者賬戶資訊。
    • send_reset_password_email(cls, account):傳送重置密碼的郵件,使用令牌管理器生成令牌,並進行速率限制。
    • revoke_reset_password_token(cls, token):撤銷重置密碼的令牌。
    • get_reset_password_data(cls, token):獲取重置密碼的令牌資料。
  2. 靜態方法和類方法的使用
    • 該類中的大多數方法都是靜態方法,直接透過類名呼叫,方便在不同的地方複用這些方法。同時,還定義了一些類方法,如send_reset_password_emailrevoke_reset_password_tokenget_reset_password_data,這些方法可以訪問類的狀態(如速率限制器物件)。
  3. 密碼處理和安全措施
    • 使用secrets模組生成密碼鹽,增強密碼安全性。
    • 使用hash_password函式對密碼進行加密,並將加密後的密碼和密碼鹽以 base64 編碼的形式儲存到資料庫。
    • 在認證和更新密碼時,使用compare_password函式驗證密碼是否正確。
  4. 令牌和快取的使用
    • 透過 JWT 令牌進行使用者身份驗證,令牌中包含使用者 ID、過期時間、發行者等資訊。
    • 使用 Redis 快取儲存登入令牌,以便快速驗證使用者是否已登入。

三、TenantService 類

  1. 方法和功能
    • create_tenant(name):建立新的租戶,生成加密公鑰並儲存到資料庫。
    • create_owner_tenant_if_not_exist(account, name):如果使用者沒有關聯的租戶,則建立一個新的租戶,並將使用者設定為租戶的所有者。
    • create_tenant_member(tenant, account, role):在給定的租戶中建立一個新的成員,指定成員的角色。
    • get_join_tenants(account):獲取使用者加入的所有租戶列表。
    • get_current_tenant_by_account(account):獲取使用者當前的租戶資訊,並新增使用者在該租戶中的角色。
    • switch_tenant(account, tenant_id):切換使用者的當前租戶,更新租戶關聯記錄。
    • get_tenant_members(tenant):獲取租戶中的所有成員列表,併為每個成員新增其在租戶中的角色。
    • get_dataset_operator_members(tenant):獲取租戶中的資料集管理員成員列表,併為每個成員新增其在租戶中的角色。
    • has_roles(tenant, roles):檢查使用者在給定租戶中是否具有指定的角色之一。
    • get_user_role(account, tenant):獲取使用者在給定租戶中的角色。
    • get_tenant_count():獲取租戶的數量。
    • check_member_permission(tenant, operator, member, action):檢查使用者在租戶中對其他成員進行特定操作(如新增、刪除、更新)的許可權。
    • remove_member_from_tenant(tenant, account, operator):從租戶中刪除成員。
    • update_member_role(tenant, member, new_role, operator):更新租戶中成員的角色。
    • dissolve_tenant(tenant, operator):解散租戶,刪除租戶中的所有成員關聯記錄和租戶本身。
    • get_custom_config(tenant_id):獲取給定租戶的自定義配置。
  2. 租戶管理和許可權控制
    • 該類提供了一系列方法來管理租戶和租戶成員,包括建立租戶、新增成員、切換租戶、獲取成員列表等。
    • 透過角色和許可權檢查,確保使用者只能進行其有許可權的操作,如新增、刪除或更新租戶成員。

四、RegisterService 類

  1. 方法和功能
    • _get_invitation_token_key(token):生成邀請令牌的 Redis 鍵。
    • setup(email, name, password, ip_address):設定 Dify,包括註冊使用者賬戶、建立使用者的所有者租戶和儲存 Dify 設定資訊。如果設定過程中出現錯誤,則回滾所有操作。
    • register(email, name, password, open_id, provider, language, status):註冊使用者賬戶,可以選擇指定密碼、第三方整合資訊(如開放 ID 和提供商)、語言和賬戶狀態等。如果註冊過程中出現錯誤,則回滾事務並丟擲異常。
    • invite_new_member(tenant, email, language, role, inviter):邀請新成員加入租戶,根據情況註冊新使用者、建立租戶成員關聯併傳送邀請郵件。
    • generate_invite_token(tenant, account):生成邀請令牌,並將邀請資訊儲存到 Redis 快取中。
    • revoke_token(workspace_id, email, token):撤銷邀請令牌,可以根據工作區 ID、郵箱和令牌進行撤銷。
    • get_invitation_if_token_valid(workspace_id, email, token):檢查邀請令牌是否有效,如果有效則返回邀請資訊、使用者賬戶和租戶資訊。
    • _get_invitation_by_token(token, workspace_id, email):根據令牌、工作區 ID 和郵箱獲取邀請資訊。
  2. 註冊和邀請功能
    • 該類實現了使用者註冊和邀請新成員加入租戶的功能。使用者可以透過常規註冊方式或使用邀請令牌加入租戶。
    • 邀請令牌生成後,會將邀請資訊儲存到 Redis 快取中,並透過郵件傳送邀請連結。

五、CustomSignUpApi 類

  1. 方法和功能
    • custom_register(tenant, email, name, password, ip_address):自定義註冊方法,建立使用者賬戶、將使用者新增到給定租戶中,並儲存 Dify 設定資訊。
  2. 自定義註冊功能
    • 這個類提供了一種自定義的註冊方式,可以在特定的租戶中進行註冊。

六、資料庫操作和異常處理

  1. 資料庫操作
    • 程式碼中使用 SQLAlchemy 進行資料庫操作,透過db.session進行新增、查詢、更新和刪除等操作。
    • 使用filterjoin等方法構建複雜的資料庫查詢。
  2. 異常處理
    • 在各個方法中,對可能出現的異常進行了捕獲和處理,並根據情況丟擲特定的異常,以便在呼叫方進行更精確的錯誤處理。例如,在賬戶認證、註冊、邀請成員等操作中,如果出現問題,會丟擲相應的異常,如AccountLoginErrorAccountRegisterErrorAccountAlreadyInTenantError等。

Flask

Flask是一個非常小的PythonWeb框架,被稱為微型框架;只提供一個穩健的核心,其他功能全部是透過擴充套件實現的;意思就是我們可以根據專案的需要量身定製,也意味著需要學習箇中國擴充套件庫的使用。

1)安裝: pip install flask
2)組成:WSGI系統、除錯、路由
3)模板引擎:Jinja2(由Flask核心開發者人員開發)
4)使用到裝飾器:以@開頭的程式碼方法

基礎

1)路由route的建立

透過建立路由並關聯函式,實現一個基本的網頁:

from flask import Flask

# 用當前指令碼名稱例項化Flask物件,方便flask從該指令碼檔案中獲取需要的內容
app = Flask(__name__)

#程式例項需要知道每個url請求所對應的執行程式碼是誰。
#所以程式中必須要建立一個url請求地址到python執行函式的一個對映。
#處理url和檢視函式之間的關係的程式就是"路由",在Flask中,路由是透過@app.route裝飾器(以@開頭)來表示的
@app.route("/")
#url對映的函式,要傳參則在上述route(路由)中新增引數申明
def index():
    return "Hello World!"

# 直屬的第一個作為檢視函式被繫結,第二個就是普通函式
# 路由與檢視函式需要一一對應
# def not():
#     return "Not Hello World!"

# 啟動一個本地開發伺服器,啟用該網頁
app.run()

透過路由的methods指定url允許的請求格式:

from flask import Flask

app = Flask(__name__)

#methods引數用於指定允許的請求格式
#常規輸入url的訪問就是get方法
@app.route("/hello",methods=['GET','POST'])
def hello():
    return "Hello World!"
#注意路由路徑不要重名,對映的檢視函式也不要重名
@app.route("/hi",methods=['POST'])
def hi():
    return "Hi World!"

app.run()

透過路由在url內新增引數,其關聯的函式可以接收這個引數:

from flask import Flask

app = Flask(__name__)

# 可以在路徑內以/<引數名>的形式指定引數,預設接收到的引數型別是string

'''#######################
以下為框架自帶的轉換器,可以置於引數前將接收的引數轉化為對應型別
string 接受任何不包含斜槓的文字
int 接受正整數
float 接受正浮點數
path 接受包含斜槓的文字
########################'''

@app.route("/index/<int:id>",)
def index(id):
    if id == 1:
        return 'first'
    elif id == 2:
        return 'second'
    elif id == 3:
        return 'thrid'
    else:
        return 'hello world!'

if __name__=='__main__':
    app.run()

除了原有的轉換器,我們也可以自定義轉換器(pip install werkzeug):

from werkzeug.routing import BaseConverter #匯入轉換器的基類,用於繼承方法
from flask import Flask

app = Flask(__name__)

# 自定義轉換器類
class RegexConverter(BaseConverter):
    def __init__(self,url_map,regex):
        # 重寫父類定義方法
        super(RegexConverter,self).__init__(url_map)
        self.regex = regex

    def to_python(self, value):
        # 重寫父類方法,後續功能已經實現好了
        print('to_python方法被呼叫')
        return value

# 將自定義的轉換器類新增到flask應用中
# 具體過程是新增到Flask類下url_map屬性(一個Map類的例項)包含的轉換器字典屬性中
app.url_map.converters['re'] = RegexConverter
# 此處re後括號內的匹配語句,被自動傳給我們定義的轉換器中的regex屬性
# value值會與該語句匹配,匹配成功則傳達給url對映的檢視函式
@app.route("/index/<re('1\d{10}'):value>")
def index(value):
    print(value)
    return "Hello World!"

if __name__=='__main__':
    app.run(debug=True)

2)endpoint的作用

說明:每個app中都存在一個url_map,這個url_map中包含了url到endpoint的對映;
作用:當request請求傳來一個url的時候,會在url_map中先透過rule找到endpoint,然後再在view_functions中根據endpoint再找到對應的檢視函式view_func

from flask import Flask

app = Flask(__name__)

# endpoint預設為檢視函式的名稱
@app.route('/test')
def test():
    return 'test success!'
# 我們也可以在路由中修改endpoint(當檢視函式名稱很長時適用)
# 相當於為檢視函式起別名
@app.route('/hello',endpoint='our_set')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    print(app.view_functions)
    print(app.url_map)
    app.run()

介面IP

# 設定管理員賬戶IP
http://localhost/install
# 登入頁面
http://localhost/signin
# 首頁
http://localhost/apps

controllers/console/wraps.py

用於控制對 Flask 應用程式中特定檢視函式的訪問許可權
這些裝飾器主要用於根據使用者的賬戶狀態,應用程式版本以及計費功能的使用情況來限制或允許某些操作

import json
from functools import wraps

from flask import abort, request
from flask_login import current_user

from configs import dify_config
from controllers.console.workspace.error import AccountNotInitializedError
from services.feature_service import FeatureService
from services.operation_service import OperationService

"""
用於控制對 Flask 應用程式中特定檢視函式的訪問許可權
這些裝飾器主要用於根據使用者的賬戶狀態,應用程式版本以及計費功能的使用情況來限制或允許某些操作
"""

def account_initialization_required(view):
    """
    用於確保當前登入使用者的賬戶已經被初始化
    """
    @wraps(view)
    def decorated(*args, **kwargs):
        # check account initialization
        account = current_user

        # 如果為uninitialized就丟擲異常
        if account.status == "uninitialized":
            raise AccountNotInitializedError()

        return view(*args, **kwargs)

    return decorated

# only_edition_cloud和only_edition_self_hosted
# 分別用於限制只有在雲版和自我託管版的應用程式中才能訪問的檢視函式

def only_edition_cloud(view):
    @wraps(view)
    def decorated(*args, **kwargs):
        if dify_config.EDITION != "CLOUD":
            abort(404)

        return view(*args, **kwargs)

    return decorated


def only_edition_self_hosted(view):
    @wraps(view)
    def decorated(*args, **kwargs):
        if dify_config.EDITION != "SELF_HOSTED":
            abort(404)

        return view(*args, **kwargs)

    return decorated


# 用於檢查雲版應用程式中使用者的計費資源使用情況
# 根據請求的資源型別(如成員數,應用數量等),如果資源使用量達到了訂閱計劃的限制就返403
def cloud_edition_billing_resource_check(resource: str):
    def interceptor(view):
        @wraps(view)
        def decorated(*args, **kwargs):
            features = FeatureService.get_features(current_user.current_tenant_id)
            if features.billing.enabled:
                members = features.members
                apps = features.apps
                vector_space = features.vector_space
                documents_upload_quota = features.documents_upload_quota
                annotation_quota_limit = features.annotation_quota_limit
                if resource == "members" and 0 < members.limit <= members.size:
                    abort(403, "The number of members has reached the limit of your subscription.")
                elif resource == "apps" and 0 < apps.limit <= apps.size:
                    abort(403, "The number of apps has reached the limit of your subscription.")
                elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
                    abort(403, "The capacity of the vector space has reached the limit of your subscription.")
                elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
                    # The api of file upload is used in the multiple places,
                    # so we need to check the source of the request from datasets
                    source = request.args.get("source")
                    if source == "datasets":
                        abort(403, "The number of documents has reached the limit of your subscription.")
                    else:
                        return view(*args, **kwargs)
                elif resource == "workspace_custom" and not features.can_replace_logo:
                    abort(403, "The workspace custom feature has reached the limit of your subscription.")
                elif resource == "annotation" and 0 < annotation_quota_limit.limit < annotation_quota_limit.size:
                    abort(403, "The annotation quota has reached the limit of your subscription.")
                else:
                    return view(*args, **kwargs)

            return view(*args, **kwargs)

        return decorated

    return interceptor

# 這個裝飾器專門用於檢查知識相關的功能限制
# 如果使用者嘗試使用的是沙盒計劃,並試圖解鎖某些高階特性,則返回403提示使用者升級到付費計劃
def cloud_edition_billing_knowledge_limit_check(resource: str):
    def interceptor(view):
        @wraps(view)
        def decorated(*args, **kwargs):
            features = FeatureService.get_features(current_user.current_tenant_id)
            if features.billing.enabled:
                if resource == "add_segment":
                    if features.billing.subscription.plan == "sandbox":
                        abort(
                            403,
                            "To unlock this feature and elevate your Dify experience, please upgrade to a paid plan.",
                        )
                else:
                    return view(*args, **kwargs)

            return view(*args, **kwargs)

        return decorated

    return interceptor

# 這個裝飾器記錄使用者的UTM引數,通常用於跟蹤營銷活動的效果
# 並將這些資訊儲存到資料庫中,有助於分析使用者是如何找到並使用應用程式的
def cloud_utm_record(view):
    @wraps(view)
    def decorated(*args, **kwargs):
        try:
            features = FeatureService.get_features(current_user.current_tenant_id)

            if features.billing.enabled:
                utm_info = request.cookies.get("utm_info")

                if utm_info:
                    utm_info = json.loads(utm_info)
                    OperationService.record_utm(current_user.current_tenant_id, utm_info)
        except Exception as e:
            pass
        return view(*args, **kwargs)

    return decorated

controllers/console/setup.py

這段程式碼是為一個 Flask 應用程式定義了一個 RESTful API 資源(SetupApi),它處理應用程式的初始化設定。這個資源允許使用者透過 HTTP GET 和 POST 請求來檢查設定狀態或執行初始設定過程。

  1. SetupApi 類繼承了 flask_restfulResource 類,這意味著它是一個可以被新增到 Flask 應用路由中的 RESTful API 資源。
  2. get 方法用於獲取設定的狀態。如果應用程式是自我託管版本 (SELF_HOSTED) 並且已經設定了,則返回設定完成的時間戳;如果尚未開始設定,則返回 "not_started"。對於非自我託管版本,預設認為設定已完成。
  3. post 方法用於建立初始設定。首先檢查是否已經完成設定,並確認沒有其他租戶存在(意味著這是第一次設定)。然後驗證是否已經透過了初始驗證。使用 reqparse 解析傳入的 JSON 資料並驗證必要的欄位(郵箱、名稱和密碼)。最後,呼叫 RegisterService.setup 方法來完成設定,並返回成功資訊。
  4. setup_required 是一個裝飾器函式,用來確保在訪問某些需要應用已設定好的路由之前,應用程式已經被正確地初始化並且至少有一個租戶存在。
  5. get_setup_status 函式用於獲取應用程式的設定狀態。如果是自我託管版本,則查詢資料庫以確定設定狀態;否則,假定設定總是完成的。
  6. 在檔案的末尾,SetupApi 被新增到了 Flask 應用的 API 中,對映到了 /setup 路徑上。
  7. from .wraps import only_edition_self_hosted 表明還有另一個裝飾器用來限制某些功能僅在自我託管版本中可用。這在 post 方法中作為裝飾器使用。
  8. 這個程式碼片段還匯入了一些其他的模組和類,如 StrLen, email, get_remote_ip, valid_password 等,這些是用來驗證輸入資料的工具函式。
from functools import wraps

from flask import request
from flask_restful import Resource, reqparse

from configs import dify_config
from libs.helper import StrLen, email, get_remote_ip
from libs.password import valid_password
from models.model import DifySetup
from services.account_service import RegisterService, TenantService

from . import api
from .error import AlreadySetupError, NotInitValidateError, NotSetupError
from .init_validate import get_init_validate_status
from .wraps import only_edition_self_hosted

"""
SetupApi類繼承了flask_restful的Resource類
意味著是一個可以被新增到Flask應用路由中的RESTful API資源
"""

class SetupApi(Resource):
    """
    用於設定的狀態
    如果應用程式是自我託管版本(SELF_HOSTED)並且已經設定了,則返回設定完成的時間戳
    如果未開始設定,則返回not_started,對於非自我託管版本,預設設定已完成
    """
    def get(self):
        if dify_config.EDITION == "SELF_HOSTED":
            setup_status = get_setup_status()
            if setup_status:
                return {"step": "finished", "setup_at": setup_status.setup_at.isoformat()}
            return {"step": "not_started"}
        return {"step": "finished"}
    """
    用於建立初始設定
    首先檢查是否完成設定,並確認沒有其他租戶存在(意味著這是第一次設定)
    然後驗證是否已經透過了初始驗證
    """
    @only_edition_self_hosted
    def post(self):
        # is set up
        if get_setup_status():
            raise AlreadySetupError()

        # is tenant created
        tenant_count = TenantService.get_tenant_count()
        if tenant_count > 0:
            raise AlreadySetupError()

        if not get_init_validate_status():
            raise NotInitValidateError()

        """
        使用reqparse解析傳入的JSON資料並驗證必要欄位,郵箱,名稱密碼等
        """
        parser = reqparse.RequestParser()
        parser.add_argument("email", type=email, required=True, location="json")
        parser.add_argument("name", type=StrLen(30), required=True, location="json")
        parser.add_argument("password", type=valid_password, required=True, location="json")
        args = parser.parse_args()

        # setup
        # 最後呼叫RegisterService.setup方法來完成設定,並返回成功資訊
        RegisterService.setup(
            email=args["email"], name=args["name"], password=args["password"], ip_address=get_remote_ip(request)
        )

        return {"result": "success"}, 201


def setup_required(view):
    """
    一個裝飾器函式
    用來確保在訪問某些需要應用已設定好的路由之前,應用程式已經被正確的初始化並且至少有一個租戶存在
    """
    @wraps(view)
    def decorated(*args, **kwargs):
        # check setup
        if not get_init_validate_status():
            raise NotInitValidateError()

        elif not get_setup_status():
            raise NotSetupError()

        return view(*args, **kwargs)

    return decorated


def get_setup_status():
    """
    用於獲取應用程式的設定狀態
    如果是自我託管版本,則查詢資料庫以確定設定狀態;否則假定設定總是完成的
    """
    if dify_config.EDITION == "SELF_HOSTED":
        return DifySetup.query.first()
    else:
        return True


api.add_resource(SetupApi, "/setup")

SetupApi 類的關係

雖然這些裝飾器和 SetupApi 類都是用於控制對 Flask 應用的不同部分的訪問,但它們主要關注的是不同階段的功能。SetupApi 類專注於應用程式的初始設定過程,而這些裝飾器則更多地涉及到已經設定完畢並且有使用者登入之後的操作許可權控制。

例如,在初始設定完成後,使用者可能需要登入並初始化他們的賬戶。此時,account_initialization_required 裝飾器就會被用來確保使用者的賬戶已經完成初始化。另外,在使用者進行一些可能影響計費的操作時,如增加團隊成員或上傳文件,cloud_edition_billing_resource_check 裝飾器將檢查是否有足夠的資源配額。

總的來說,SetupApi 類和這些裝飾器都是為了保證應用程式的安全性和功能性,但是它們作用於不同的邏輯層面上。前者負責設定流程,後者則是在設定後管理訪問控制和資源限制。

Error: No such command 'db'

解決無效命令問題

flask db upgrade 是一個無效命令,通常這樣的命令是用於 Alembic 資料庫遷移工具。如果您正在使用 Alembic,那麼您需要先確保 Alembic 已經正確配置,並且在您的 Flask 專案中已經初始化了 Alembic。

1. 初始化 Alembic

如果尚未初始化 Alembic,您需要建立 Alembic 配置檔案 alembic.ini 並編寫相應的指令碼模板:

(dify-api-py3.10) PS D:\projects\dify\api> alembic init alembic

這將在專案根目錄下建立一個名為 alembic 的新目錄,幷包含 Alembic 需要的所有檔案。

2. 配置 Alembic

編輯 alembic.ini 檔案,確保資料庫 URL 和目標後設資料指向您的 Flask 應用:

[alembic]
script_location = api/alembic

sqlalchemy.url = sqlite:///path/to/your/database.db

# version location
version_locations = %r

3. 建立遷移指令碼

在您的 Flask 應用中執行一次遷移生成:

(dify-api-py3.10) PS D:\projects\dify\api> alembic revision --autogenerate -m "initial migration"

這將建立一個新的遷移指令碼。

4. 升級資料庫

然後使用 Alembic 升級資料庫:

(dify-api-py3.10) PS D:\projects\dify\api> alembic upgrade head

最終步驟

完成以上步驟後,您可以再次嘗試執行 Flask 應用:

(dify-api-py3.10) PS D:\projects\dify\api> flask run --host 0.0.0.0 --port=5001 --debug

如果您仍然遇到問題,請提供更多關於您的 app.py 檔案和其他相關檔案的具體內容,以便進一步診斷問題。

確定資料庫遷移工具

要確定您的專案是否使用 Alembic 作為資料庫遷移工具,您可以檢查專案的目錄結構以及是否存在特定的 Alembic 檔案和配置。

檢查 Alembic 是否存在的方法:

  1. 查詢 alembic.ini 檔案: Alembic 需要一個配置檔案 alembic.ini。這個檔案通常位於專案的根目錄或某個特定的子目錄內。開啟此檔案,檢視其中是否包含 Alembic 的配置資訊。

  2. 查詢 alembic 目錄: Alembic 還需要一個目錄來儲存版本指令碼,通常命名為 alembic。在這個目錄中應該有如下結構:

    alembic/
    ├── env.py
    ├── script.py.mako
    ├── versions/
    │   └── <version>_migration_name.py
    └── README
    
  3. 檢查專案依賴: 檢視您的虛擬環境的 requirements.txt 或者 Pipfile(如果您使用的是 Pipenv),看看是否包含 alembic 包。

  4. 檢視程式碼中的匯入: 檢查您的程式碼中是否有對 Alembic 的匯入,例如:

    from alembic import op
    
  5. 查詢 Alembic 命令的使用: 查詢專案文件或指令碼檔案中是否有使用 Alembic 命令的地方,例如:

    alembic revision --autogenerate -m "add_users_table"
    alembic upgrade head
    

如何確認 Alembic 是否已安裝:

您可以在虛擬環境中檢查 Alembic 是否已安裝:

(dify-api-py3.10) PS D:\projects\dify\api> pip list | findstr alembic

如果安裝了 Alembic,您將看到類似下面的輸出:

alembic                1.7.6

如果沒有安裝 Alembic,您可以安裝它:

(dify-api-py3.10) PS D:\projects\dify\api> pip install alembic

如果沒有 Alembic

如果您的專案中沒有找到上述任何跡象,則很可能您的專案不是使用 Alembic 進行資料庫遷移的。可能是使用其他工具,如 SQLAlchemy-migrate,或者是手動管理資料庫模式。

如何使用 Alembic

如果您決定使用 Alembic,您可以按照以下步驟初始化 Alembic:

(dify-api-py3.10) PS D:\projects\dify\api> alembic init alembic

這會在專案根目錄下建立一個名為 alembic 的目錄,包含 Alembic 需要的所有檔案。

然後編輯 alembic.ini 檔案中的資料庫連線字串,使其指向您的資料庫:

sqlalchemy.url = sqlite:///path/to/your/database.db

接著生成一個初始遷移:

(dify-api-py3.10) PS D:\projects\dify\api> alembic revision --autogenerate -m "initial migration"

最後升級資料庫到最新版本:

(dify-api-py3.10) PS D:\projects\dify\api> alembic upgrade head

如果您的專案確實使用了 Alembic,並且您仍然遇到問題,請提供更多關於您的專案配置的資訊,以便進一步診斷問題。

如果您使用的框架或工具整合了 Flask 和資料庫遷移的功能,那麼可能會使用特定的 Flask 外掛來進行資料庫遷移。最常用的 Flask 外掛之一是 Flask-Migrate,它結合了 SQLAlchemy 和 Alembic 來進行資料庫遷移。

如何確認是否使用 Flask-Migrate

  1. 查詢 migrations 目錄: Flask-Migrate 通常會在專案的某個位置建立一個 migrations 目錄,用於存放 Alembic 的版本指令碼。
  2. 查詢 migrations 檔案: 在 migrations 目錄中,您應該能找到以下檔案:
    • env.py
    • script.py.mako
    • versions 子目錄,裡面包含資料庫遷移指令碼。
    • app.py 或者類似的主檔案中,查詢是否有對 Flask-Migrate 的初始化程式碼。
  3. 查詢依賴項: 檢查您的 requirements.txtPipfile 檔案,看看是否包含 Flask-MigrateSQLAlchemy
  4. 查詢配置檔案: 檢視您的 Flask 應用配置檔案(如 app.py 或者 config.py),看看是否有關於 Flask-Migrate 的配置和初始化程式碼。

如何使用 Flask-Migrate

如果您確定使用的是 Flask-Migrate,那麼您應該能夠使用 Flask 命令來執行資料庫遷移。首先確保安裝了所需的庫:

(dify-api-py3.10) PS D:\projects\dify\api> pip install Flask-Migrate SQLAlchemy

然後在您的 Flask 應用中初始化 Flask-Migrate:

# app.py 或者 main.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///example.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)

# 其他配置和路由定義...

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=True)

接下來,您可以使用 Flask 命令來生成新的遷移檔案:

(dify-api-py3.10) PS D:\projects\dify\api> flask db init

這將初始化 Flask-Migrate。

然後生成一個新的遷移檔案:

(dify-api-py3.10) PS D:\projects\dify\api> flask db migrate -m "initial migration"

最後,應用遷移:

(dify-api-py3.10) PS D:\projects\dify\api> flask db upgrade

錯誤排查

如果您在使用 flask db upgrade 時遇到錯誤,可能的原因包括:

  1. 未初始化 Flask-Migrate:請確保您已經執行了 flask db init
  2. 未生成遷移檔案:請確保您已經執行了 flask db migrate
  3. 配置錯誤:請檢查您的配置檔案,確保 SQLALCHEMY_DATABASE_URI 設定正確。
  4. 依賴項未安裝:請確保安裝了 Flask-MigrateSQLAlchemy

如果您的專案中確實使用了 Flask-Migrate,並且您仍然遇到問題,請提供更多關於您的配置檔案(如 app.py 或者 config.py)的內容,以便進一步診斷問題。

flask db upgrade 時遇到錯誤

錯誤資訊:

[2024-10-14 16:05:44,770: ERROR/MainProcess] consumer: Cannot connect to redis://:**@localhost:6379/1: Error 10061 connecting to localhost:6379. [WinError 10061] 由於目標計算機積極拒絕,無法連線。..
Trying again in 10.00 seconds... (5/100)

根據您提供的錯誤資訊,看來您的應用程式在嘗試連線到 Redis 伺服器時遇到了問題。具體錯誤資訊為“由於目標計算機積極拒絕,無法連線”,這意味著 Redis 服務端拒絕了來自客戶端的連線請求。

解決方案

  1. 確認 Redis 服務狀態

    • 確保 Redis 服務已經在本地機器上啟動。您可以透過命令列檢查 Redis 服務的狀態:

      bash深色版本

      redis-server --version
      

      如果沒有啟動,啟動 Redis 服務:

      bash深色版本

      redis-server
      
  2. 檢查防火牆設定

    • 確認您的防火牆設定沒有阻止 Redis 的埠(預設為 6379)。您可以暫時禁用防火牆檢查連線是否成功:

      bash深色版本

      netsh advfirewall set allprofiles state off
      

      測試完畢後記得重新啟用防火牆:

      bash深色版本

      netsh advfirewall set allprofiles state on
      
  3. 確認 Redis 配置

    • 開啟 Redis 的配置檔案

      redis.conf
      

      檢查

      bind
      

      protected-mode
      

      設定:

      bind 127.0.0.1 ::1
      protected-mode yes
      

      如果您希望從任意 IP 地址連線到 Redis,可以修改

      bind
      

      0.0.0.0
      
      bind 0.0.0.0
      

      如果您更改了

      bind
      

      請重啟 Redis 服務。

  4. 檢查 Redis 連線字串

    • 確認您的應用程式中連線 Redis 的字串是正確的。例如:

      REDIS_URL = 'redis://:password@localhost:6379/1'
      

      如果您在連線字串中指定了密碼,請確保密碼正確,並且在連線字串中包含了密碼部分。

  5. 埠監聽檢查

    • 您可以使用

      netstat
      

      命令檢查 Redis 是否正在監聽埠:

      netstat -ano | findstr 6379
      

      輸出應顯示 Redis 正在監聽埠 6379。

  6. 測試 Redis 連線

    • 使用 Redis 的命令列工具

      redis-cli
      

      測試連線:

      redis-cli -h localhost -p 6379
      

      輸入密碼(如果有)並檢查是否可以正常使用 Redis。

進一步排查

如果上述步驟都不能解決問題,您還可以:

  • 檢查 Redis 日誌檔案,通常位於 Redis 資料目錄中,檢視是否有任何錯誤資訊。
  • 確認應用程式的配置檔案中 Redis 的地址和埠號是否正確。
  • 檢查是否有任何安全軟體(如防毒軟體)阻止了 Redis 的網路訪問。

列出已知的虛擬環境

列出已知的虛擬環境的方法取決於你使用的工具和環境。這裡有幾種常見的方法來檢視已有的虛擬環境:

  1. 使用 poetry
    你可以使用 poetry env info 來檢視當前啟用的環境資訊,但是 poetry 並沒有直接列出所有環境的命令。你需要手動檢視 .venv(預設情況下)或者你在 pyproject.toml 檔案中指定的目錄。

  2. 使用 virtualenvvirtualenvwrapper

    • 如果你使用的是 virtualenv,通常會在某個目錄(如 ~/Envs.virtualenvs)下找到這些環境。
    • 對於 virtualenvwrapper 使用者,可以使用 lsvirtualenv 命令來列出所有環境。
  3. 使用 conda
    如果你使用的是 conda 來管理環境,可以使用以下命令來列出所有已建立的環境:

    conda info --envs
    
  4. 使用 pipenv
    如果你使用的是 pipenv,則可以在專案根目錄下找到 .venv 目錄,或者透過 pipenv --where 檢視當前專案的環境位置。

  5. 使用 pyenvpyenv-virtualenv 外掛
    如果你使用的是 pyenv 並且安裝了 pyenv-virtualenv 外掛,你可以使用下面的命令來列出所有虛擬環境:

    pyenv virtualenv-list
    
  6. 手動查詢
    在某些情況下,你可能需要手動查詢虛擬環境的位置,通常它們會被建立在一個特定的目錄內,例如使用者的主目錄下的 .virtualenvs 資料夾裡。

controllers/console/workspace/members.py

成員檔案程式碼:

from flask_login import current_user
from flask_restful import Resource, abort, marshal_with, reqparse
# from jupyter_server.tests.auth.test_login import login

import services
from configs import dify_config
from controllers.console import api
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
from extensions.ext_database import db
from fields.member_fields import account_with_role_list_fields
from libs.login import login_required
from models.account import Account, TenantAccountRole
from services.account_service import RegisterService, TenantService
from services.errors.account import AccountAlreadyInTenantError


class MemberListApi(Resource):
    """List all members of current tenant.獲取當前租戶的所有成員列表"""

    @setup_required
    @login_required
    @account_initialization_required
    @marshal_with(account_with_role_list_fields)
    def get(self):
        members = TenantService.get_tenant_members(current_user.current_tenant)
        return {"result": "success", "accounts": members}, 200


class MemberInviteEmailApi(Resource):
    """Invite a new member by email."""
    """
    透過郵箱邀請新成員加入租戶
    驗證角色有效性並返回邀請結果或錯誤資訊
    """
    @setup_required
    @login_required
    @account_initialization_required
    @cloud_edition_billing_resource_check("members")
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument("emails", type=str, required=True, location="json", action="append")
        parser.add_argument("role", type=str, required=True, default="admin", location="json")
        parser.add_argument("language", type=str, required=False, location="json")
        args = parser.parse_args()

        invitee_emails = args["emails"]
        invitee_role = args["role"]
        interface_language = args["language"]
        if not TenantAccountRole.is_non_owner_role(invitee_role):
            return {"code": "invalid-role", "message": "Invalid role"}, 400

        inviter = current_user
        invitation_results = []
        console_web_url = dify_config.CONSOLE_WEB_URL
        for invitee_email in invitee_emails:
            try:
                token = RegisterService.invite_new_member(
                    inviter.current_tenant, invitee_email, interface_language, role=invitee_role, inviter=inviter
                )
                invitation_results.append(
                    {
                        "status": "success",
                        "email": invitee_email,
                        "url": f"{console_web_url}/activate?email={invitee_email}&token={token}",
                    }
                )
            except AccountAlreadyInTenantError:
                invitation_results.append(
                    {"status": "success", "email": invitee_email, "url": f"{console_web_url}/signin"}
                )
                break
            except Exception as e:
                invitation_results.append({"status": "failed", "email": invitee_email, "message": str(e)})

        return {
            "result": "success",
            "invitation_results": invitation_results,
        }, 201


class MemberCancelInviteApi(Resource):
    """Cancel an invitation by member id."""
    # 透過成員ID取消邀請,處理各種異常情況
    @setup_required
    @login_required
    @account_initialization_required
    def delete(self, member_id):
        member = db.session.query(Account).filter(Account.id == str(member_id)).first()
        if not member:
            abort(404)

        try:
            TenantService.remove_member_from_tenant(current_user.current_tenant, member, current_user)
        except services.errors.account.CannotOperateSelfError as e:
            return {"code": "cannot-operate-self", "message": str(e)}, 400
        except services.errors.account.NoPermissionError as e:
            return {"code": "forbidden", "message": str(e)}, 403
        except services.errors.account.MemberNotInTenantError as e:
            return {"code": "member-not-found", "message": str(e)}, 404
        except Exception as e:
            raise ValueError(str(e))

        return {"result": "success"}, 204


class MemberUpdateRoleApi(Resource):
    """Update member role."""
    # 更新指定成員的角色,驗證角色有效性並執行更新
    @setup_required
    @login_required
    @account_initialization_required
    def put(self, member_id):
        parser = reqparse.RequestParser()
        parser.add_argument("role", type=str, required=True, location="json")
        args = parser.parse_args()
        new_role = args["role"]

        if not TenantAccountRole.is_valid_role(new_role):
            return {"code": "invalid-role", "message": "Invalid role"}, 400

        member = db.session.get(Account, str(member_id))
        if not member:
            abort(404)

        try:
            TenantService.update_member_role(current_user.current_tenant, member, new_role, current_user)
        except Exception as e:
            raise ValueError(str(e))

        # todo: 403

        return {"result": "success"}


class DatasetOperatorMemberListApi(Resource):
    """List all members of current tenant."""
    # 獲取當前租戶的資料集操作成員列表
    @setup_required
    @login_required
    @account_initialization_required
    @marshal_with(account_with_role_list_fields)
    def get(self):
        members = TenantService.get_dataset_operator_members(current_user.current_tenant)
        return {"result": "success", "accounts": members}, 200


import requests,re
from flask import jsonify
import json

class CustomMemberSignUpApi(Resource):
    # 實現自定義成員註冊流程,包括登入,邀請和啟用使用者賬戶,並驗證密碼強度
    def post(self):
        try:
            parser = reqparse.RequestParser()
            parser.add_argument('email', type=str, required=True, help='Email address is required.')
            parser.add_argument('user_name', type=str, required=True, help='User name is required.')
            parser.add_argument('password', type=str, required=True, help='Password is required.')
            args = parser.parse_args()

            email = args['email']
            user_name = args['user_name']
            password = args['password']

            if not self.is_valid_password(password):
                return jsonify({"error": "Password must be at least 8 characters long and contain both letters and numbers."}), 400

            login_url = 'http://localhost/console/api/login'
            invite_url = 'http://localhost/console/api/workspaces/current/members/invite-email'
            activate_url = 'http://localhost/console/api/activate'

            login_payload = {
                "email": "cheng@foxmail.com",
                "password": "Aa123456",
                "remember_me": True
            }
            login_response = requests.post(login_url, json=login_payload)
            # print(login_response)
            # print(dir(login_response))
            # print(login_response.json)
            # print(login_response.text)
            # print('minicheckpoint 3')
            if login_response.status_code != 200: return jsonify({"error": "Failed admin login"}), 401
            login_text = login_response.text
            admin_token = json.loads(login_text)['data']

            # print('checkpoint 3')
            # Step 2: Invite user
            invite_payload = {
                "emails": [email],
                "role": "normal",
                "language": "en-US"
            }
            invite_headers = {
                "Authorization": f"Bearer {admin_token}",
                "Content-Type": "application/json"
            }
            # print('minicheck 1')
            invite_response = requests.post(invite_url, headers=invite_headers, json=invite_payload)
            # print('minicheck 2')
            # print(dir(invite_response))
            # print(invite_response.status_code)
            if invite_response.status_code != 201:
                return jsonify({"error": "Failed to invite user"}), 400
            # print('minicheck 2.5')

            # print('minicheck 2.7')
            # print(invite_response.text)
            invite_data = json.loads(invite_response.text)
            # print('minicheck 3')
            if invite_data['result'] != 'success':
                return jsonify({"error": "Failed to invite user"}), 400
            # print('minicheck 4')
            # print(invite_data['invitation_results'][0])
            # print(type(invite_data['invitation_results'][0]['url']))
            # print('!!!!!!!!!')
            try: token = invite_data['invitation_results'][0]['url'].split("token=")[1]
            except: return json.loads('{"error": "User already exists"}'),400
            # print('checkpoint 4')

            # Step 3: Activate user account
            activate_payload = {
                "email": email,
                "interface_language": "en-US",
                "name": user_name,
                "password": password,
                "timezone": "Asia/Shanghai",
                "token": token,
                "workspace_id": None
            }

            activate_response = requests.post(activate_url, headers = invite_headers, json=activate_payload)
            # print('minicheck 1')
            # print(activate_response.status_code)
            if activate_response.status_code != 200:
                return jsonify({"error": "Failed to activate member"}), activate_response.status_code
            # print('minicheck 2')
            # print(dir(activate_response))
            # print(activate_response.text)
            # print(dir(activate_response.text))
            activate_data = json.loads(activate_response.text)
            # print('!!!')
            # print(activate_data)
            # print(type(activate_data))
            if activate_data['result'] == 'success':
                return activate_data, 200
            else:
                return json.loads('{"error": "Failed to activate member"}'), activate_response.status_code

        except Exception as e:
            return jsonify({"error": str(e)}), 500

    def is_valid_password(self, password):
        if len(password) < 8:
            return False
        if not re.search(r"[A-Za-z]", password) or not re.search(r"[0-9]", password):
            return False
        return True


api.add_resource(CustomMemberSignUpApi, '/workspaces/current/members/custom_signup')
api.add_resource(MemberListApi, "/workspaces/current/members")
api.add_resource(MemberInviteEmailApi, "/workspaces/current/members/invite-email")
api.add_resource(MemberCancelInviteApi, "/workspaces/current/members/<uuid:member_id>")
api.add_resource(MemberUpdateRoleApi, "/workspaces/current/members/<uuid:member_id>/update-role")
api.add_resource(DatasetOperatorMemberListApi, "/workspaces/current/dataset-operators")

測試介面

透過GET請求對Dify API架構執行內省查詢,在PowerShell中執行curl請求:Invoke-WebRequest無法繫結引數“Headers”。

用curl或Invoke-RestMethod進行測試:

1,在Linux上命令

curl -X POST http://localhost/console/api/workspaces/current/members/custom_signup -H "Content-Type: application/json" -d '{"email": "test5@mail.com", "user_name": "test5", "password": "Aa123456"}'

2,在Windows上命令

Invoke-RestMethod -Uri http://localhost:5001/console/api/workspaces/current/members/custom_signup -Method POST -Headers @{ "Content-Type"="application/json" } -Body '{"email": "test9@mail.com", "user_name": "test9", "password": "Aa123456"}' -ContentType "application/json"

如果在Windows上使用curl命令可能會出現下面的錯誤:

如果用Windows shell提示錯誤如下:
情況一:
"""
Invoke-WebRequest : 無法繫結引數“Headers”。無法將“S
ystem.String”型別的“Content-Type: application/json”
值轉換為“System.Collections.IDictionary”型別。
所在位置 行:1 字元: 18
+  curl -X POST -H "Content-Type: application/json" -D
ata '{"email": "t ...
+                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [I
   nvoke-WebRequest],ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNo
   Message,Microsoft.PowerShell.Commands.InvokeWebRe
  questCommand
"""

情況二:
"""
引數“Headers”。無法將“S ystem.String”型別的“Content-Type: application/json” 值轉換為“System.Collections.IDictionary”型別。
所在位置 行:1 字元: 18
+  curl -X POST -H "Content-Type: application/json" -D
ata '{"email": "t ...
+                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [I
   nvoke-WebRequest],ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNo
   Message,Microsoft.PowerShell.Commands.InvokeWebRe
  questCommand
"""

提示:在Linux系統下,沒有問題,單引號雙引號都正常使用。

獲取Invoke-WebRequest的相關文件。執行命令:

get-help invoke-webrequest


會出現下面的資訊:

"""
是否要執行 Update-Help?
Update-Help cmdlet 下載 Windows PowerShell
模組的最新幫助檔案,並將其安裝在你的計算機上。有關
Update-Help cmdlet 的詳細資訊,請參閱
https:/go.microsoft.com/fwlink/?LinkId=210614。
[Y] 是(Y)  [N] 否(N)  [S] 暫停(S)  [?] 幫助
(預設值為“Y”):Y

名稱
    Invoke-WebRequest

摘要
    Gets content from a web page on the internet.


語法
    Invoke-WebRequest [-Uri] <System.Uri> [-Body <Syst
    em.Object>] [-Certificate <System.Security.Cryptog
    raphy.X509Certificates.X509Certificate>] [-Certifi
    cateThumbprint <System.String>] [-ContentType <Sys
    tem.String>] [-Credential <System.Management.Autom
    ation.PSCredential>] [-DisableKeepAlive] [-Headers
     <System.Collections.IDictionary>] [-InFile <Syste
    m.String>] [-MaximumRedirection <System.Int32>] [-
    Method {Default | Get | Head | Post | Put | Delete
     | Trace | Options | Merge | Patch}] [-OutFile <Sy
    stem.String>] [-PassThru] [-Proxy <System.Uri>] [-
    ProxyCredential <System.Management.Automation.PSCr
    edential>] [-ProxyUseDefaultCredentials] [-Session
    Variable <System.String>] [-TimeoutSec <System.Int
    32>] [-TransferEncoding {chunked | compress | defl
    ate | gzip | identity}] [-UseBasicParsing] [-UseDe
    faultCredentials] [-UserAgent <System.String>] [-W
    ebSession <Microsoft.PowerShell.Commands.WebReques
    tSession>] [<CommonParameters>]


說明
    The `Invoke-WebRequest` cmdlet sends HTTP, HTTPS,
    FTP, and FILE requests to a web page or web servic
    e. It parses the response and returns collections
    of forms, links, images, and other significant HTM
    L elements.

    This cmdlet was introduced in Windows PowerShell 3
    .0.

    > [!NOTE] > By default, script code in the web pag
    e may be run when the page is being parsed to popu
    late the > `ParsedHtml` property. Use the `-UseBas
    icParsing` switch to suppress this.

    > [!IMPORTANT] > The examples in this article refe
    rence hosts in the `contoso.com` domain. This is a
     fictitious > domain used by Microsoft for example
    s. The examples are designed to show how to use th
    e cmdlets. > However, since the `contoso.com` site
    s don't exist, the examples don't work. Adapt the
    examples > to hosts in your environment.


相關連結
    Online Version: https://learn.microsoft.com/powers
    hell/module/microsoft.powershell.utility/invoke-we
    brequest?view=powershell-5.1&WT.mc_id=ps-gethelp
    Invoke-RestMethod
    ConvertFrom-Json
    ConvertTo-Json

備註
    若要檢視示例,請鍵入: "get-help Invoke-WebRequest
    -examples".
    有關詳細資訊,請鍵入: "get-help Invoke-WebRequest
    -detailed".
    若要獲取技術資訊,請鍵入: "get-help Invoke-WebRequ
    est -full".
    有關線上幫助,請鍵入: "get-help Invoke-WebRequest
-online"
"""

再次執行上面的執行命令:

Invoke-RestMethod -Uri http://localhost:5001/console/api/workspaces/current/members/custom_signup -Method POST -Headers @{ "Content-Type"="application/json" } -Body '{"email": "test9@mail.com", "user_name": "test9", "password": "Aa123456"}' -ContentType "application/json"

這個時候如果Powershell出現下面的錯誤:

Invoke-RestMethod : {
    "message": "Internal Server Error",
    "code": "unknown"
}
所在位置 行:1 字元: 1
+ Invoke-RestMethod -Uri http://localhost:5001/console
/api/workspaces/c ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (Syst
   em.Net.HttpWebRequest:HttpWebRequest) [Invoke-Res
  tMethod],WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseExc
   eption,Microsoft.PowerShell.Commands.InvokeRestMe
  thodCommand

這個時候可以看一下Pycharm執行,有報錯有反應的話就算這個介面的初步成功

會出現下面的錯誤:

Traceback (most recent call last):
  File "D:\projects\dify\api\.venv\lib\site-packages\flask\app.py", line 880, in full_dispatch_request
    rv = self.dispatch_request()
  File "D:\projects\dify\api\.venv\lib\site-packages\flask\app.py", line 865, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
  File "D:\projects\dify\api\.venv\lib\site-packages\flask_restful\__init__.py", line 493, in wrapper
    return self.make_response(data, code, headers=headers)
  File "D:\projects\dify\api\.venv\lib\site-packages\flask_restful\__init__.py", line 522, in make_response
    resp = self.representations[mediatype](data, *args, **kwargs)
  File "D:\projects\dify\api\.venv\lib\site-packages\flask_restful\representations\json.py", line 21, in output_json
    dumped = dumps(data, **settings) + "\n"
  File "D:\projects\pyenv\pyenv-win-master\pyenv-win\versions\3.10.11\lib\json\__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "D:\projects\pyenv\pyenv-win-master\pyenv-win\versions\3.10.11\lib\json\encoder.py", line 201, in encode
    chunks = list(chunks)
  File "D:\projects\pyenv\pyenv-win-master\pyenv-win\versions\3.10.11\lib\json\encoder.py", line 438, in _iterencode
    o = _default(o)
  File "D:\projects\dify\api\.venv\lib\site-packages\frozendict\__init__.py", line 31, in default
    return BaseJsonEncoder.default(self, obj)
  File "D:\projects\pyenv\pyenv-win-master\pyenv-win\versions\3.10.11\lib\json\encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Response is not JSON serializable
2024-10-15 09:39:33,742.742 INFO [Thread-2 (process_request_thread)] [_internal.py:97] - 127.0.0.1 - - [15/Oct/2024 09:39:33] "POST /console/api/workspaces/current/members/custom_signup HTTP/1.1" 500 -

當出現這個錯誤的時候,就可以根據錯誤進行debug聯調了

錯誤情況:

1,OSError: [WinError 10038] 在一個非套接字上嘗試了一個操作。

OSError: [WinError 10038] 在一個非套接字上嘗試了一個操作 這個錯誤通常意味著你在嘗試執行一個僅適用於套接字(socket)上的操作,但實際上你正在使用的不是一個有效的套接字物件。這個錯誤通常出現在網路程式設計中,特別是在使用 requests 庫傳送 HTTP 請求時。

這個錯誤可能有幾個原因:

  1. 無效的套接字操作:你在非套接字物件上呼叫了某個僅適用於套接字的方法。
  2. 網路問題:請求超時或目標伺服器不可達。
  3. 認證問題:請求需要認證但未提供正確的認證資訊。
  4. 代理配置問題:如果你的網路環境使用了代理,但請求中沒有正確配置代理設定。
如何解決這個問題

我們可以從以下幾個方面排查和解決問題:

1. 檢查網路連線

確保你的應用程式能夠訪問網際網路並且目標 URL 是可達的。你可以嘗試使用命令列工具如 pingcurl 來測試目標 URL 的連通性。

2. 檢查請求 URL

確保請求的 URL 是正確的,並且服務端正在執行。你可以手動訪問 URL 來確認這一點。

3. 檢查認證資訊

如果目標服務需要認證(如 Basic Auth、OAuth 等),確保你在請求中包含了正確的認證資訊。

4. 檢查代理設定

如果你的網路環境使用了代理伺服器,確保你在發起請求時設定了正確的代理引數。例如,在使用 requests 庫時,可以透過設定 proxies 引數來指定代理伺服器:

proxies = {
    "http": "http://your-proxy-server:port",
    "https": "http://your-proxy-server:port",
}

response = requests.post(url, proxies=proxies)
5. 檢查程式碼邏輯

確保你的程式碼邏輯正確,特別是在處理網路請求的部分。例如,確保你在處理異常時不會對非套接字物件執行套接字相關的操作。

示例程式碼

假設你的程式碼中有一個請求:

import requests

def send_request(url):
    try:
        response = requests.post(url)
        response.raise_for_status()  # 如果響應狀態碼不是 200,將丟擲 HTTPError 異常
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        return None

在這個例子中,response.raise_for_status() 會檢查響應的狀態碼,並在狀態碼錶示錯誤時丟擲異常。如果網路請求失敗,異常會被捕獲並列印錯誤資訊。

總結

OSError: [WinError 10038] 通常提示你的請求中存在問題,可能是網路不通、認證問題或代理設定不當。透過檢查網路連線、請求 URL、認證資訊、代理設定以及程式碼邏輯,可以定位並解決這個問題。如果問題依然存在,建議檢視更詳細的錯誤資訊或日誌輸出,以便進一步診斷。

2,TypeError: Object of type Response is not JSON serializable

TypeError: Object of type Response is not JSON serializable

這個錯誤通常是由於試圖將一個response.models.Response物件直接序列化為JSON引起的。這類錯誤通常會發生在試圖將非字典型別的物件作為JSON響應返回時。Flask的Response物件不是JSON序列化的物件,因此會導致這個錯誤。

常見原因
  1. 直接返回Response物件:在某些情況下,你可能直接返回了一個Response物件而不是一個字典或列表。
  2. 使用jsonify返回響應:jsonify函式會自動將字典轉換為JSON格式的響應,但如果傳遞的物件不是字典或列表,就會導致錯誤。
解決方法
  1. 確保返回的是字典或列表:確保你的函式返回的是一個可以被jsonify處理的字典或列表。
  2. 使用jsonify而不是直接返回Response物件:如果你需要返回JSON格式的資料,使用jsonify函式。

讓我們再次審視一下 CustomMemberSignUpApi 類中的 post 方法,看看是否存在這樣的問題:

class CustomMemberSignUpApi(Resource):
    def post(self):
        try:
            # ... 省略了部分程式碼 ...

            # Step 3: Activate user account
            activate_response = requests.post(activate_url, headers=invite_headers, json=activate_payload)
            if activate_response.status_code != 200:
                return jsonify({"error": "Failed to activate member"}), activate_response.status_code

            activate_data = json.loads(activate_response.text)

            if activate_data['result'] == 'success':
                return activate_data, 200
            else:
                return jsonify({"error": "Failed to activate member"}), activate_response.status_code

        except Exception as e:
            return jsonify({"error": str(e)}), 500

在這段程式碼中,activate_response 是透過 requests.post 發起請求後得到的一個 Response 物件。當你試圖返回 activate_data 時,實際上 activate_data 是透過 json.loads(activate_response.text) 得到的一個 Python 字典,這通常是正確的做法。

然而,我們還需要注意以下幾點來確保不會發生錯誤:

  1. 確保 activate_response.text 可以被正確解析
    • 如果 activate_response.text 不是一個有效的 JSON 字串,則 json.loads() 會丟擲異常。
    • 確保後端服務返回的是一個有效的 JSON 格式的字串。
  2. 檢查 activate_data 是否為預期的字典型別
    • 在返回 activate_data 之前,可以透過列印或斷言來檢查它是否為字典型別。

為了確保 activate_data 是一個字典,可以在返回前新增一些檢查:

activate_data = json.loads(activate_response.text)
if not isinstance(activate_data, dict):
    return jsonify({"error": "Invalid response from activation endpoint"}), 500

if activate_data['result'] == 'success':
    return activate_data, 200
else:
    return jsonify({"error": "Failed to activate member"}), activate_response.status_code

這樣可以確保即使 activate_response.text 不是一個有效的 JSON 字串,也不會導致 TypeError

總結

確保在整個過程中:

  • 傳送請求並接收響應後,從響應物件中提取 JSON 資料。
  • 檢查提取的資料是否為字典型別。
  • 只有在確認資料為字典型別時才返回。

透過這些步驟,可以有效地避免 TypeError: Object of type Response is not JSON serializable 的錯誤。如果還有問題,請檢查其他地方是否也有類似的情況,確保所有返回的資料都是可以被 JSON 序列化的型別。

3,SyntaxError: expected 'except' or 'finally' block

SyntaxError: expected 'except' or 'finally' block 這個錯誤表明你的 Python 程式碼中存在語法錯誤,特別是與 try-except 結構有關的錯誤。這個錯誤通常出現在 try 塊之後沒有正確跟隨 exceptfinally 塊時。

問題分析

根據你提供的資訊,我們可以推測程式碼中可能存在一個未閉合的 try 塊。讓我們再次檢查一下 CustomMemberSignUpApi 類中的 post 方法,看看是否有類似的語法錯誤。

程式碼檢查

讓我們重新檢查一下 CustomMemberSignUpApi 類中的 post 方法,並確保 try-except 塊是完整的:

class CustomMemberSignUpApi(Resource):
    def post(self):
        try:
            parser = reqparse.RequestParser()
            parser.add_argument('email', type=str, required=True, help='Email address is required.')
            parser.add_argument('user_name', type=str, required=True, help='User name is required.')
            parser.add_argument('password', type=str, required=True, help='Password is required.')
            args = parser.parse_args()

            email = args['email']
            user_name = args['user_name']
            password = args['password']

            if not self.is_valid_password(password):
                return jsonify({"error": "Password must be at least 8 characters long and contain both letters and numbers."}), 400

            login_url = 'http://localhost/console/api/login'
            invite_url = 'http://localhost/console/api/workspaces/current/members/invite-email'
            activate_url = 'http://localhost/console/api/activate'

            login_payload = {
                "email": "cheng@foxmail.com",
                "password": "Aa123456",
                "remember_me": True
            }

            login_response = requests.post(login_url, json=login_payload)

            if login_response.status_code != 200:
                return jsonify({"error": "Failed admin login"}), 401

            login_text = login_response.text
            admin_token = json.loads(login_text)['data']

            invite_payload = {
                "emails": [email],
                "role": "normal",
                "language": "en-US"
            }

            invite_headers = {
                "Authorization": f"Bearer {admin_token}",
                "Content-Type": "application/json"
            }

            invite_response = requests.post(invite_url, headers=invite_headers, json=invite_payload)

            if invite_response.status_code != 201:
                return jsonify({"error": "Failed to invite user"}), 400

            invite_data = json.loads(invite_response.text)
            if invite_data['result'] != 'success':
                return jsonify({"error": "Failed to invite user"}), 400

            token = invite_data['invitation_results'][0]['url'].split("token=")[1]

            activate_payload = {
                "email": email,
                "interface_language": "en-US",
                "name": user_name,
                "password": password,
                "timezone": "Asia/Shanghai",
                "token": token,
                "workspace_id": None
            }

            activate_response = requests.post(activate_url, headers=invite_headers, json=activate_payload)

            if activate_response.status_code != 200:
                return jsonify({"error": "Failed to activate member"}), activate_response.status_code

            activate_data = json.loads(activate_response.text)

            if activate_data['result'] == 'success':
                return activate_data, 200
            else:
                return jsonify({"error": "Failed to activate member"}), activate_response.status_code

        except Exception as e:
            return jsonify({"error": str(e)}), 500

    def is_valid_password(self, password):
        if len(password) < 8:
            return False
        if not re.search(r"[A-Za-z]", password) or not re.search(r"[0-9]", password):
            return False
        return True
確認 try-except

在這段程式碼中,try-except 塊是完整的,因此我們需要檢查是否有其他潛在的語法錯誤。常見的錯誤包括:

  1. 缺少冒號: 在 try, except, finally 後面必須跟一個冒號 :
  2. 縮排錯誤: try-except 塊內的程式碼必須正確縮排。
  3. 多餘或缺失的程式碼行: 確保 try-except 塊內的所有程式碼行都在同一層次上。
檢查其他部分

此外,還需要檢查其他部分的程式碼,確保沒有遺漏的 try-except 塊或其他語法錯誤。

逐步排查
  1. 檢查匯入語句:確保所有的匯入語句都是正確的。
  2. 檢查全域性變數宣告:確保所有的全域性變數都有正確的宣告。
  3. 檢查函式定義:確保所有的函式定義都是完整的。
示例程式碼

確保所有的 try-except 塊都是完整的,並且沒有多餘的縮排或缺失的程式碼行:

class CustomMemberSignUpApi(Resource):
    def post(self):
        try:
            # 程式碼邏輯...
        except Exception as e:
            return jsonify({"error": str(e)}), 500

總結

確保你的 try-except 塊是完整的,並且沒有多餘的縮排或缺失的程式碼行。如果仍然遇到問題,請仔細檢查整個檔案中的所有程式碼,確保沒有遺漏的語法錯誤。如果問題依舊存在,可以提供更多具體的程式碼片段以便進一步診斷。

解決方案:

  • 這個時候可以檢查一下程式碼,例如login_payload中的郵箱是不是執行環境下的伺服器登入的管理員的郵箱和密碼。
  • 逐行排查錯誤,根據列印出來的資訊進行捕捉錯誤
  • 檢查一下jsonifyjson之間的轉換和呼叫
  • 根據判斷條件進行判斷狀態碼以及token的變化

相關文章