From 01753b8f31b762fdd7c98d0cc3e66e2dff8dcb67 Mon Sep 17 00:00:00 2001 From: Wang Qi Date: Wed, 22 Apr 2026 20:42:41 +0800 Subject: [PATCH] Refactor: API connectors (#14228) ### What problem does this PR solve? Refactor /api/v1/connectors to be more RESTful. ### Type of change - [x] Refactoring --- .../connector_api.py} | 53 ++++++++++++------- .../add_data_source/add_google_drive.md | 2 +- .../test_connector_oauth_contract.py | 2 +- .../test_connector_routes_unit.py | 8 +-- .../data-source-detail-page/index.tsx | 2 +- .../pages/user-setting/data-source/hooks.ts | 8 +-- .../pages/user-setting/data-source/index.tsx | 2 +- web/src/services/data-source-service.ts | 10 ++-- web/src/utils/api.ts | 23 ++++---- 9 files changed, 66 insertions(+), 44 deletions(-) rename api/apps/{connector_app.py => restful_apis/connector_api.py} (91%) diff --git a/api/apps/connector_app.py b/api/apps/restful_apis/connector_api.py similarity index 91% rename from api/apps/connector_app.py rename to api/apps/restful_apis/connector_api.py index 0c123f700..8e9403fcd 100644 --- a/api/apps/connector_app.py +++ b/api/apps/restful_apis/connector_api.py @@ -35,15 +35,30 @@ from rag.utils.redis_conn import REDIS_CONN from api.apps import login_required, current_user from box_sdk_gen import BoxOAuth, OAuthConfig, GetAuthorizeUrlOptions - -@manager.route("/set", methods=["POST"]) # noqa: F821 +@manager.route("/connectors/", methods=["PATCH"]) # noqa: F821 @login_required -async def set_connector(): +async def update_connector(connector_id): req = await get_request_json() - if req.get("id"): + e, conn = ConnectorService.get_by_id(connector_id) + if not e: + return get_data_error_result(message="Can't find this Connector!") + + if req: conn = {fld: req[fld] for fld in ["prune_freq", "refresh_freq", "config", "timeout_secs"] if fld in req} - ConnectorService.update_by_id(req["id"], conn) - else: + conn["id"] = connector_id + ConnectorService.update_by_id(connector_id, conn) + + await asyncio.sleep(1) + e, conn = ConnectorService.get_by_id(connector_id) + + return get_json_result(data=conn.to_dict()) + + +@manager.route("/connectors", methods=["POST"]) # noqa: F821 +@login_required +async def create_connector(): + req = await get_request_json() + if req: req["id"] = get_uuid() conn = { "id": req["id"], @@ -65,13 +80,13 @@ async def set_connector(): return get_json_result(data=conn.to_dict()) -@manager.route("/list", methods=["GET"]) # noqa: F821 +@manager.route("/connectors", methods=["GET"]) # noqa: F821 @login_required def list_connector(): return get_json_result(data=ConnectorService.list(current_user.id)) -@manager.route("/", methods=["GET"]) # noqa: F821 +@manager.route("/connectors/", methods=["GET"]) # noqa: F821 @login_required def get_connector(connector_id): e, conn = ConnectorService.get_by_id(connector_id) @@ -80,7 +95,7 @@ def get_connector(connector_id): return get_json_result(data=conn.to_dict()) -@manager.route("//logs", methods=["GET"]) # noqa: F821 +@manager.route("/connectors//logs", methods=["GET"]) # noqa: F821 @login_required def list_logs(connector_id): req = request.args.to_dict(flat=True) @@ -88,7 +103,7 @@ def list_logs(connector_id): return get_json_result(data={"total": total, "logs": arr}) -@manager.route("//resume", methods=["PUT"]) # noqa: F821 +@manager.route("/connectors//resume", methods=["POST"]) # noqa: F821 @login_required async def resume(connector_id): req = await get_request_json() @@ -99,7 +114,7 @@ async def resume(connector_id): return get_json_result(data=True) -@manager.route("//rebuild", methods=["PUT"]) # noqa: F821 +@manager.route("/connectors//rebuild", methods=["POST"]) # noqa: F821 @login_required @validate_request("kb_id") async def rebuild(connector_id): @@ -110,7 +125,7 @@ async def rebuild(connector_id): return get_json_result(data=True) -@manager.route("//rm", methods=["POST"]) # noqa: F821 +@manager.route("/connectors/", methods=["DELETE"]) # noqa: F821 @login_required def rm_connector(connector_id): ConnectorService.resume(connector_id, TaskStatus.CANCEL) @@ -185,7 +200,7 @@ async def _render_web_oauth_popup(flow_id: str, success: bool, message: str, sou return response -@manager.route("/google/oauth/web/start", methods=["POST"]) # noqa: F821 +@manager.route("/connectors/google/oauth/web/start", methods=["POST"]) # noqa: F821 @login_required @validate_request("credentials") async def start_google_web_oauth(): @@ -265,7 +280,7 @@ async def start_google_web_oauth(): ) -@manager.route("/gmail/oauth/web/callback", methods=["GET"]) # noqa: F821 +@manager.route("/connectors/gmail/oauth/web/callback", methods=["GET"]) # noqa: F821 async def google_gmail_web_oauth_callback(): state_id = request.args.get("state") error = request.args.get("error") @@ -316,7 +331,7 @@ async def google_gmail_web_oauth_callback(): return await _render_web_oauth_popup(state_id, True, "Authorization completed successfully.", source) -@manager.route("/google-drive/oauth/web/callback", methods=["GET"]) # noqa: F821 +@manager.route("/connectors/google-drive/oauth/web/callback", methods=["GET"]) # noqa: F821 async def google_drive_web_oauth_callback(): state_id = request.args.get("state") error = request.args.get("error") @@ -366,7 +381,7 @@ async def google_drive_web_oauth_callback(): return await _render_web_oauth_popup(state_id, True, "Authorization completed successfully.", source) -@manager.route("/google/oauth/web/result", methods=["POST"]) # noqa: F821 +@manager.route("/connectors/google/oauth/web/result", methods=["POST"]) # noqa: F821 @login_required @validate_request("flow_id") async def poll_google_web_result(): @@ -386,7 +401,7 @@ async def poll_google_web_result(): REDIS_CONN.delete(_web_result_cache_key(flow_id, source)) return get_json_result(data={"credentials": result.get("credentials")}) -@manager.route("/box/oauth/web/start", methods=["POST"]) # noqa: F821 +@manager.route("/connectors/box/oauth/web/start", methods=["POST"]) # noqa: F821 @login_required async def start_box_web_oauth(): req = await get_request_json() @@ -429,7 +444,7 @@ async def start_box_web_oauth(): "expires_in": WEB_FLOW_TTL_SECS,} ) -@manager.route("/box/oauth/web/callback", methods=["GET"]) # noqa: F821 +@manager.route("/connectors/box/oauth/web/callback", methods=["GET"]) # noqa: F821 async def box_web_oauth_callback(): flow_id = request.args.get("state") if not flow_id: @@ -471,7 +486,7 @@ async def box_web_oauth_callback(): return await _render_web_oauth_popup(flow_id, True, "Authorization completed successfully.", "box") -@manager.route("/box/oauth/web/result", methods=["POST"]) # noqa: F821 +@manager.route("/connectors/box/oauth/web/result", methods=["POST"]) # noqa: F821 @login_required @validate_request("flow_id") async def poll_box_web_result(): diff --git a/docs/guides/dataset/add_data_source/add_google_drive.md b/docs/guides/dataset/add_data_source/add_google_drive.md index 6e040a3b8..65d509305 100644 --- a/docs/guides/dataset/add_data_source/add_google_drive.md +++ b/docs/guides/dataset/add_data_source/add_google_drive.md @@ -44,7 +44,7 @@ You need to configure the OAuth Consent Screen because it is the step where you 2. Select **Web Application** as **Application type** for the created project: ![](https://github.com/infiniflow/ragflow-docs/blob/040e4acd4c1eac6dc73dc44e934a6518de78d097/images/google_drive/image7.png?raw=true) 3. Enter a client name. -4. Add `http://localhost:9380/v1/connector/google-drive/oauth/web/callback` as **Authorised redirect URIs**: +4. Add `http://localhost:9380/api/v1/connectors/google-drive/oauth/web/callback` as **Authorised redirect URIs**: 5. Add **Authorised JavaScript origins**: - If deploying RAGFlow from Docker, use `http://localhost:80`: ![](https://github.com/infiniflow/ragflow-docs/blob/040e4acd4c1eac6dc73dc44e934a6518de78d097/images/google_drive/image8.png?raw=true) diff --git a/test/testcases/test_web_api/test_connector_app/test_connector_oauth_contract.py b/test/testcases/test_web_api/test_connector_app/test_connector_oauth_contract.py index d64f685bd..dc3279ca8 100644 --- a/test/testcases/test_web_api/test_connector_app/test_connector_oauth_contract.py +++ b/test/testcases/test_web_api/test_connector_app/test_connector_oauth_contract.py @@ -20,7 +20,7 @@ import requests from configs import HOST_ADDRESS, VERSION -CONNECTOR_BASE_URL = f"{HOST_ADDRESS}/{VERSION}/connector" +CONNECTOR_BASE_URL = f"{HOST_ADDRESS}/api/{VERSION}/connectors" LLM_API_KEY_URL = f"{HOST_ADDRESS}/{VERSION}/llm/set_api_key" LANGFUSE_API_KEY_URL = f"{HOST_ADDRESS}/{VERSION}/langfuse/api_key" diff --git a/test/testcases/test_web_api/test_connector_app/test_connector_routes_unit.py b/test/testcases/test_web_api/test_connector_app/test_connector_routes_unit.py index 40500e7b0..ea3bad907 100644 --- a/test/testcases/test_web_api/test_connector_app/test_connector_routes_unit.py +++ b/test/testcases/test_web_api/test_connector_app/test_connector_routes_unit.py @@ -321,7 +321,7 @@ def _load_connector_app(monkeypatch): box_mod.GetAuthorizeUrlOptions = _GetAuthorizeUrlOptions monkeypatch.setitem(sys.modules, "box_sdk_gen", box_mod) - module_path = repo_root / "api" / "apps" / "connector_app.py" + module_path = repo_root / "api" / "apps" / "restful_apis" / "connector_api.py" spec = importlib.util.spec_from_file_location("test_connector_routes_unit", module_path) module = importlib.util.module_from_spec(spec) module.manager = _DummyManager() @@ -363,8 +363,8 @@ def test_connector_basic_routes_and_task_controls(monkeypatch): "get_request_json", lambda: _AwaitableValue({"id": "conn-1", "refresh_freq": 7, "config": {"x": 1}}), ) - res = _run(module.set_connector()) - assert update_calls == [("conn-1", {"refresh_freq": 7, "config": {"x": 1}})] + res = _run(module.update_connector("conn-1")) + assert update_calls == [("conn-1", {'id': 'conn-1', "refresh_freq": 7, "config": {"x": 1}})] assert res["data"]["id"] == "conn-1" monkeypatch.setattr( @@ -372,7 +372,7 @@ def test_connector_basic_routes_and_task_controls(monkeypatch): "get_request_json", lambda: _AwaitableValue({"name": "new", "source": "gmail", "config": {"y": 2}}), ) - res = _run(module.set_connector()) + res = _run(module.create_connector()) assert save_calls[-1]["id"] == "generated-id" assert save_calls[-1]["tenant_id"] == "tenant-1" assert save_calls[-1]["input_type"] == module.InputType.POLL diff --git a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx index 63ea3ff4d..ee547bcde 100644 --- a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx +++ b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx @@ -144,7 +144,7 @@ const SourceDetailPage = () => { ]; }, [detail, runSchedule]); - const { addLoading, handleAddOk } = useAddDataSource(); + const { addLoading, handleAddOk } = useAddDataSource({isEdit:true}); const onSubmit = useCallback(() => { formRef?.current?.submit(); diff --git a/web/src/pages/user-setting/data-source/hooks.ts b/web/src/pages/user-setting/data-source/hooks.ts index 7ade48440..73744cb5b 100644 --- a/web/src/pages/user-setting/data-source/hooks.ts +++ b/web/src/pages/user-setting/data-source/hooks.ts @@ -3,7 +3,7 @@ import { useSetModalState } from '@/hooks/common-hooks'; import { useGetPaginationWithRouter } from '@/hooks/logic-hooks'; import dataSourceService, { dataSourceRebuild, - dataSourceResume, + dataSourceResume, dataSourceUpdate, deleteDataSource, featchDataSourceDetail, getDataSourceLogs, @@ -68,7 +68,7 @@ export const useListDataSource = () => { return { list, categorizedList: updatedDataSourceTemplates, isFetching }; }; -export const useAddDataSource = () => { +export const useAddDataSource = ({isEdit=false}:{isEdit?:boolean} ) => { const [addSource, setAddSource] = useState( undefined, ); @@ -90,7 +90,9 @@ export const useAddDataSource = () => { const handleAddOk = useCallback( async (data: any) => { setAddLoading(true); - const { data: res } = await dataSourceService.dataSourceSet(data); + const { data: res } = isEdit + ? await dataSourceUpdate(data.id, data) + : await dataSourceService.dataSourceSet(data); console.log('🚀 ~ handleAddOk ~ code:', res.code); if (res.code === 0) { queryClient.invalidateQueries({ queryKey: ['data-source'] }); diff --git a/web/src/pages/user-setting/data-source/index.tsx b/web/src/pages/user-setting/data-source/index.tsx index d4da96d7b..fc1cab52f 100644 --- a/web/src/pages/user-setting/data-source/index.tsx +++ b/web/src/pages/user-setting/data-source/index.tsx @@ -79,7 +79,7 @@ const DataSource = () => { handleAddOk, hideAddingModal, showAddingModal, - } = useAddDataSource(); + } = useAddDataSource({}); return ( ( ); export const deleteDataSource = (id: string) => - request.post(api.dataSourceDel(id)); + request.delete(api.dataSourceDel(id)); export const dataSourceResume = (id: string, data: { resume: boolean }) => { - return request.put(api.dataSourceResume(id), { data }); + return request.post(api.dataSourceResume(id), { data }); }; export const dataSourceRebuild = (id: string, data: { kb_id: string }) => { - return request.put(api.dataSourceRebuild(id), { data }); + return request.post(api.dataSourceRebuild(id), { data }); +}; + +export const dataSourceUpdate = (id: string, data: { kb_id: string }) => { + return request.patch(api.dataSourceUpdate(id), { data }); }; export const getDataSourceLogs = (id: string, params?: any) => diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 7eb3f64f1..2eb640c77 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -35,19 +35,20 @@ export default { deleteFactory: `${webAPI}/llm/delete_factory`, // data source - dataSourceSet: `${webAPI}/connector/set`, - dataSourceList: `${webAPI}/connector/list`, - dataSourceDel: (id: string) => `${webAPI}/connector/${id}/rm`, - dataSourceResume: (id: string) => `${webAPI}/connector/${id}/resume`, - dataSourceRebuild: (id: string) => `${webAPI}/connector/${id}/rebuild`, - dataSourceLogs: (id: string) => `${webAPI}/connector/${id}/logs`, - dataSourceDetail: (id: string) => `${webAPI}/connector/${id}`, + dataSourceUpdate: (id:string) => `${restAPIv1}/connectors/${id}`, + dataSourceSet: `${restAPIv1}/connectors`, + dataSourceList: `${restAPIv1}/connectors`, + dataSourceDel: (id: string) => `${restAPIv1}/connectors/${id}`, + dataSourceResume: (id: string) => `${restAPIv1}/connectors/${id}/resume`, + dataSourceRebuild: (id: string) => `${restAPIv1}/connectors/${id}/rebuild`, + dataSourceLogs: (id: string) => `${restAPIv1}/connectors/${id}/logs`, + dataSourceDetail: (id: string) => `${restAPIv1}/connectors/${id}`, googleWebAuthStart: (type: 'google-drive' | 'gmail') => - `${webAPI}/connector/google/oauth/web/start?type=${type}`, + `${restAPIv1}/connectors/google/oauth/web/start?type=${type}`, googleWebAuthResult: (type: 'google-drive' | 'gmail') => - `${webAPI}/connector/google/oauth/web/result?type=${type}`, - boxWebAuthStart: () => `${webAPI}/connector/box/oauth/web/start`, - boxWebAuthResult: () => `${webAPI}/connector/box/oauth/web/result`, + `${restAPIv1}/connectors/google/oauth/web/result?type=${type}`, + boxWebAuthStart: () => `${restAPIv1}/connectors/box/oauth/web/start`, + boxWebAuthResult: () => `${restAPIv1}/connectors/box/oauth/web/result`, // plugin llmTools: `${webAPI}/plugin/llm_tools`,