diff --git a/README.md b/README.md index ff7aa80..40361dc 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ | [前端一:Figma 与 MasterGo 入门](docs/stage-2/frontend/2.1-figma-mastergo/) | 用设计工具梳理信息架构和页面结构,为前端实现打基础 | 🚧 | | [前端二:构建第一个现代应用程序-UI 设计](docs/stage-2/frontend/2.2-ui-design/) | 基于设计稿完成组件化界面,实现从设计到代码的第一条链路 | 🚧 | | [前端三:参考 UI 设计规范与多产品 UI 设计](docs/stage-2/frontend/2.3-multi-product-ui/) | 围绕统一主视觉扩展多产品界面,练习系统化设计能力 | 🚧 | -| [前端四:一起做霍格沃茨画像](docs/stage-2/frontend/2.4-hogwarts-portraits/chapter4-lets-build-hogwarts-portraits) | 从 0 到 1 做出接入 AI 能力的前端应用,串联设计与开发 | 🚧 | +| [前端四:一起做霍格沃茨画像](docs/stage-2/frontend/2.4-hogwarts-portraits/chapter4-lets-build-hogwarts-portraits.md) | 从 0 到 1 做出接入 AI 能力的前端应用,串联设计与开发 | ✅ | #### 后端与全栈部分 | 章节 | 关键内容 | 状态 | | :------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------ | :--- | -| [后端一:什么是 API](docs/stage-2/backend/2.1-what-is-api/extra2/extra2-what-is-api.md) | 理解 HTTP 接口与请求响应模型,为后端集成与联调做准备 | 🚧 | -| [后端二:从数据库到 Supabase](docs/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase.md) | 在 Supabase 上落地数据库和 API,打通数据模型与前端页面 | 🚧 | +| [后端一:什么是 API](docs/stage-2/backend/2.1-what-is-api/extra2/extra2-what-is-api.md) | 理解 HTTP 接口与请求响应模型,为后端集成与联调做准备 | ✅ | +| [后端二:从数据库到 Supabase](docs/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase.md) | 在 Supabase 上落地数据库和 API,打通数据模型与前端页面 | ✅ | | [后端三:大模型辅助编写接口代码与接口文档](docs/stage-2/backend/2.3-ai-interface-code/) | 用大模型协助生成接口与数据库文档及代码,实现可读可测的后端 | 🚧 | -| [后端四:Git 工作流](docs/stage-2/backend/2.4-git-workflow/extra1/extra1-what-is-git-and-what-is-github.md) | 在 Git 工作流中管理代码,进行版本控制和协作 | 🚧 | -| [后端五:Zeabur 部署](docs/stage-2/backend/2.5-zeabur-deployment/extra6/extra6-zeabur-what-is-it-and-how-to-deploy-web-applications.md) | 将应用部署到 Zeabur 上线 | 🚧 | -| [后端六:现代 CLI 开发工具](docs/stage-2/backend/2.6-modern-cli/extra7/extra7-cli-ai-coding-tools-and-the-principles-of-test-driven-development.md) | 使用 CLI 类 AI 编程工具加速开发与调试,形成个人工程化工作流 | 🚧 | +| [后端四:Git 工作流](docs/stage-2/backend/2.4-git-workflow/extra1/extra1-what-is-git-and-what-is-github.md) | 在 Git 工作流中管理代码,进行版本控制和协作 | ✅ | +| [后端五:Zeabur 部署](docs/stage-2/backend/2.5-zeabur-deployment/extra6/extra6-zeabur-what-is-it-and-how-to-deploy-web-applications.md) | 将应用部署到 Zeabur 上线 | ✅ | +| [后端六:现代 CLI 开发工具](docs/stage-2/backend/2.6-modern-cli/extra7/extra7-cli-ai-coding-tools-and-the-principles-of-test-driven-development.md) | 使用 CLI 类 AI 编程工具加速开发与调试,形成个人工程化工作流 | ✅ | | [后端七:如何集成 Stripe 等收费系统](docs/stage-2/backend/2.7-stripe-payment/) | 接入支付系统,完成收费链路与基础结算流程 | 🚧 | | [大作业 1:构建第一个现代应用程序-全栈应用](docs/stage-2/assignments/2.1-fullstack-app/) | 综合前端、后端与支付模块,完成可上线的全栈 Web 应用 | 🚧 | | [大作业 2:现代前端组件库 + Trae 实战](docs/stage-2/assignments/2.2-modern-frontend-trae/) | 使用现代前端组件库与 Trae,独立完成可登录注册并支持收费的产品 | 🚧 | @@ -96,7 +96,7 @@ | 章节 | 关键内容 | 状态 | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------- | :--- | -| [AI 一:Dify 入门与知识库集成](docs/stage-2/ai-capabilities/2.1-dify-knowledge-base/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration.md) | 用 Dify Workflow 与基础 RAG 搭建工具类产品,为后续应用升级打样 | 🚧 | +| [AI 一:Dify 入门与知识库集成](docs/stage-2/ai-capabilities/2.1-dify-knowledge-base/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration.md) | 用 Dify Workflow 与基础 RAG 搭建工具类产品,为后续应用升级打样 | ✅ | | [AI 二:学会查询 AI 词典与集成多模态 API](docs/stage-2/ai-capabilities/2.2-multimodal-api/extra3/extra3-ai-capability-starter-handbook.md) | 学会查找合适的模型与 API,并把文本、图像等多模态能力接入产品 | 🚧 | ### 三、高级开发工程师 @@ -115,7 +115,7 @@ | 章节 | 关键内容 | 状态 | | :-------------------------------------------------------------------- | :------------------------------------------------------ | :--- | -| [高级 AI 一:什么是 RAG 以及它如何工作](docs/stage-3/ai-advanced/3.a1-rag-introduction/extra5/extra5-what-is-rag-and-how-does-it-work-and-future) | 系统理解 RAG 原理与常见架构,为复杂应用提供知识检索基础 | 🚧 | +| [高级 AI 一:什么是 RAG 以及它如何工作](docs/stage-3/ai-advanced/3.a1-rag-introduction/extra5-what-is-rag-and-how-does-it-work-and-future.md) | 系统理解 RAG 原理与常见架构,为复杂应用提供知识检索基础 | ✅ | | [高级 AI 二:中高级 RAG 与工作流编排:以 LangGraph 为例](docs/stage-3/ai-advanced/3.a2-langgraph-advanced-rag/) | 使用 LangGraph 等工具设计多步工作流与中高级 RAG 系统 | 🚧 | ## 如何学习 diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 8a145fc..317a30b 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -177,7 +177,7 @@ export default defineConfig({ items: [ { text: '高级三:如何构建微信小程序', - link: '/stage-3/cross-platform/3.3-wechat-miniprogram/example1/index' + link: '/stage-3/cross-platform/3.3-wechat-miniprogram/' }, { text: '高级四:如何构建微信小程序(包含后端)', @@ -209,7 +209,7 @@ export default defineConfig({ items: [ { text: '高级 AI 一:什么是 RAG 以及它如何工作', - link: '/stage-3/ai-advanced/3.a1-rag-introduction/extra5/extra5-what-is-rag-and-how-does-it-work-and-future' + link: '/stage-3/ai-advanced/3.a1-rag-introduction/extra5-what-is-rag-and-how-does-it-work-and-future' }, { text: '高级 AI 二:中高级 RAG 与工作流编排 - 以 LangGraph 为例', diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index 49ac53c..74b2ade 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -54,14 +54,14 @@ | 前端一:Figma 与 MasterGo 入门 | 用设计工具梳理信息架构和页面结构,为前端实现打基础 | 🚧 | | 前端二:构建第一个现代应用程序-UI 设计 | 基于设计稿完成组件化界面,实现从设计到代码的第一条链路 | 🚧 | | 前端三:参考 UI 设计规范与多产品 UI 设计 | 围绕统一主视觉扩展多产品界面,练习系统化设计能力 | 🚧 | -| [前端四:一起做霍格沃茨画像](/project/chapter4/chapter4-lets-build-hogwarts-portraits) | 从 0 到 1 做出接入 AI 能力的前端应用,串联设计与开发 | 🚧 | +| [前端四:一起做霍格沃茨画像](/stage-2/frontend/2.4-hogwarts-portraits/chapter4-lets-build-hogwarts-portraits) | 从 0 到 1 做出接入 AI 能力的前端应用,串联设计与开发 | 🚧 | #### 后端与全栈部分 | 章节 | 关键内容 | 状态 | | :---------------------------------------------------------------------------------- | :------------------------------------------------------------ | :--- | | 后端一:什么是 API | 理解 HTTP 接口与请求响应模型,为后端集成与联调做准备 | 🚧 | -| [后端二:从数据库到 Supabase](/project/chapter5/chapter5-from-database-to-supabase) | 在 Supabase 上落地数据库和 API,打通数据模型与前端页面 | 🚧 | +| [后端二:从数据库到 Supabase](/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase) | 在 Supabase 上落地数据库和 API,打通数据模型与前端页面 | 🚧 | | 后端三:大模型辅助编写接口代码与接口文档 | 用大模型协助生成接口与数据库文档及代码,实现可读可测的后端 | 🚧 | | 后端四:Git 工作流与 Zeabur 部署 | 在 Git 工作流中管理代码,并将应用部署到 Zeabur 上线 | 🚧 | | 后端五:现代 CLI 开发工具 | 使用 CLI 类 AI 编程工具加速开发与调试,形成个人工程化工作流 | 🚧 | @@ -73,7 +73,7 @@ | 章节 | 关键内容 | 状态 | | :---------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------- | :--- | -| [AI 一:Dify 入门与知识库集成](/project/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration) | 用 Dify Workflow 与基础 RAG 搭建工具类产品,为后续应用升级打样 | 🚧 | +| [AI 一:Dify 入门与知识库集成](/stage-2/ai-capabilities/2.1-dify-knowledge-base/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration) | 用 Dify Workflow 与基础 RAG 搭建工具类产品,为后续应用升级打样 | 🚧 | | AI 二:学会查询 AI 词典与集成多模态 API | 学会查找合适的模型与 API,并把文本、图像等多模态能力接入产品 | 🚧 | ### 三、高级开发工程师 diff --git a/docs/project/README.md b/docs/project/README.md deleted file mode 100644 index 4b321fb..0000000 --- a/docs/project/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Project 文档 - -这里包含了项目实战的相关文档: - -- [Chapter 3: Getting Started with Dify](chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration.md) -- [Chapter 4: Let's Build Hogwarts Portraits](chapter4/chapter4-lets-build-hogwarts-portraits.md) -- [Chapter 5: From Database to Supabase](chapter5/chapter5-from-database-to-supabase.md) diff --git a/docs/project/chapter3/Log in.yml b/docs/project/chapter3/Log in.yml deleted file mode 100644 index ff55268..0000000 --- a/docs/project/chapter3/Log in.yml +++ /dev/null @@ -1,504 +0,0 @@ -app: - description: '' - icon: 🤖 - icon_background: '#FFEAD5' - mode: advanced-chat - name: Log in - use_icon_as_answer_icon: false -dependencies: -- current_identifier: null - type: marketplace - value: - marketplace_plugin_unique_identifier: langgenius/gitee_ai:0.1.4@f621ace33bb3c140f5a1e3533fcb518f558c7b945d63523c0f85810a4b4a8b93 -kind: app -version: 0.3.0 -workflow: - conversation_variables: - - description: '' - id: f8cc215e-ef91-437a-a823-7e80a8d345a3 - name: LOGIN - selector: - - conversation - - LOGIN - value: none - value_type: string - environment_variables: [] - features: - file_upload: - allowed_file_extensions: - - .JPG - - .JPEG - - .PNG - - .GIF - - .WEBP - - .SVG - allowed_file_types: - - image - allowed_file_upload_methods: - - local_file - - remote_url - enabled: false - fileUploadConfig: - audio_file_size_limit: 50 - batch_count_limit: 5 - file_size_limit: 15 - image_file_size_limit: 10 - video_file_size_limit: 100 - workflow_file_upload_limit: 10 - image: - enabled: false - number_limits: 3 - transfer_methods: - - local_file - - remote_url - number_limits: 3 - opening_statement: 'Please log in with passwords:' - retriever_resource: - enabled: true - sensitive_word_avoidance: - enabled: false - speech_to_text: - enabled: false - suggested_questions: [] - suggested_questions_after_answer: - enabled: false - text_to_speech: - enabled: false - language: '' - voice: '' - graph: - edges: - - data: - sourceType: llm - targetType: answer - id: llm-answer - source: llm - sourceHandle: source - target: answer - targetHandle: target - type: custom - - data: - isInIteration: false - isInLoop: false - sourceType: start - targetType: if-else - id: 1758767725822-source-1758767750205-target - source: '1758767725822' - sourceHandle: source - target: '1758767750205' - targetHandle: target - type: custom - zIndex: 0 - - data: - isInIteration: false - isInLoop: false - sourceType: if-else - targetType: assigner - id: 1758767920912-true-1758768026915-target - source: '1758767920912' - sourceHandle: 'true' - target: '1758768026915' - targetHandle: target - type: custom - zIndex: 0 - - data: - isInIteration: false - isInLoop: false - sourceType: if-else - targetType: assigner - id: 1758767920912-false-1758768059939-target - source: '1758767920912' - sourceHandle: 'false' - target: '1758768059939' - targetHandle: target - type: custom - zIndex: 0 - - data: - isInLoop: false - sourceType: if-else - targetType: llm - id: 1758767750205-f599486b-e4d3-4cdd-9425-31257bf28e82-llm-target - source: '1758767750205' - sourceHandle: f599486b-e4d3-4cdd-9425-31257bf28e82 - target: llm - targetHandle: target - type: custom - zIndex: 0 - - data: - isInIteration: false - isInLoop: false - sourceType: if-else - targetType: llm - id: 1758767750205-5f242bc3-7f06-4a88-82e5-235a859d92bf-1758768238460-target - source: '1758767750205' - sourceHandle: 5f242bc3-7f06-4a88-82e5-235a859d92bf - target: '1758768238460' - targetHandle: target - type: custom - zIndex: 0 - - data: - isInIteration: false - isInLoop: false - sourceType: llm - targetType: answer - id: 1758768238460-source-1758768309599-target - source: '1758768238460' - sourceHandle: source - target: '1758768309599' - targetHandle: target - type: custom - zIndex: 0 - - data: - isInLoop: false - sourceType: if-else - targetType: if-else - id: 1758767750205-true-1758767920912-target - source: '1758767750205' - sourceHandle: 'true' - target: '1758767920912' - targetHandle: target - type: custom - zIndex: 0 - - data: - isInIteration: false - isInLoop: false - sourceType: assigner - targetType: answer - id: 1758768026915-source-1758768400561-target - source: '1758768026915' - sourceHandle: source - target: '1758768400561' - targetHandle: target - type: custom - zIndex: 0 - - data: - isInIteration: false - isInLoop: false - sourceType: assigner - targetType: answer - id: 1758768059939-source-1758768418040-target - source: '1758768059939' - sourceHandle: source - target: '1758768418040' - targetHandle: target - type: custom - zIndex: 0 - nodes: - - data: - desc: '' - selected: false - title: Start - type: start - variables: [] - height: 53 - id: '1758767725822' - position: - x: -227.17718464443863 - y: 459.4548203041172 - positionAbsolute: - x: -227.17718464443863 - y: 459.4548203041172 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - context: - enabled: false - variable_selector: [] - desc: '' - memory: - query_prompt_template: '{{#sys.query#}}' - role_prefix: - assistant: '' - user: '' - window: - enabled: false - size: 10 - model: - completion_params: - temperature: 0.7 - mode: chat - name: Qwen3-235B-A22B-Instruct-2507 - provider: langgenius/gitee_ai/gitee_ai - prompt_template: - - id: 2686a731-f250-46f0-97ec-033e929160a5 - role: system - text: 'You are the computer that answers the user''s question. Always start - with "hello guest" ' - selected: false - title: Guest LLM - type: llm - variables: [] - vision: - enabled: false - height: 89 - id: llm - position: - x: 389.3839309072489 - y: 663.6856819774588 - positionAbsolute: - x: 389.3839309072489 - y: 663.6856819774588 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - answer: '{{#llm.text#}}' - desc: '' - selected: true - title: Answer - type: answer - variables: [] - height: 104 - id: answer - position: - x: 707.3520684153225 - y: 673.6318886739399 - positionAbsolute: - x: 707.3520684153225 - y: 673.6318886739399 - selected: true - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - cases: - - case_id: 'true' - conditions: - - comparison_operator: is - id: cf0932c4-e5f4-478e-9437-1db63af30ffd - value: none - varType: string - variable_selector: - - conversation - - LOGIN - id: 'true' - logical_operator: and - - case_id: f599486b-e4d3-4cdd-9425-31257bf28e82 - conditions: - - comparison_operator: is - id: 18c716ea-aac5-4af7-9a50-1fd5dc40d18e - value: guest - varType: string - variable_selector: - - conversation - - LOGIN - id: f599486b-e4d3-4cdd-9425-31257bf28e82 - logical_operator: and - - case_id: 5f242bc3-7f06-4a88-82e5-235a859d92bf - conditions: - - comparison_operator: is - id: 490f9251-1012-4f85-8bb7-29f355ca6b5c - value: admin - varType: string - variable_selector: - - conversation - - LOGIN - id: 5f242bc3-7f06-4a88-82e5-235a859d92bf - logical_operator: and - desc: '' - selected: false - title: IF/ELSE - type: if-else - height: 221 - id: '1758767750205' - position: - x: 78.91702348010949 - y: 535.9787937998216 - positionAbsolute: - x: 78.91702348010949 - y: 535.9787937998216 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - cases: - - case_id: 'true' - conditions: - - comparison_operator: is - id: ab112997-31e2-453c-8511-40adc3006e76 - value: AIID - varType: string - variable_selector: - - sys - - query - id: 'true' - logical_operator: and - desc: '' - selected: false - title: IF/ELSE 2 - type: if-else - height: 125 - id: '1758767920912' - position: - x: 659.9247584894487 - y: 459.4548203041172 - positionAbsolute: - x: 659.9247584894487 - y: 459.4548203041172 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - desc: '' - items: - - input_type: constant - operation: set - value: admin - variable_selector: - - conversation - - LOGIN - write_mode: over-write - selected: false - title: Variable Assigner - type: assigner - version: '2' - height: 87 - id: '1758768026915' - position: - x: 997.3839309072489 - y: 426.91916104676926 - positionAbsolute: - x: 997.3839309072489 - y: 426.91916104676926 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - desc: '' - items: - - input_type: constant - operation: set - value: guest - variable_selector: - - conversation - - LOGIN - write_mode: over-write - selected: false - title: Variable Assigner 2 - type: assigner - version: '2' - height: 87 - id: '1758768059939' - position: - x: 1010.8775065168395 - y: 553.9191610467692 - positionAbsolute: - x: 1010.8775065168395 - y: 553.9191610467692 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - context: - enabled: false - variable_selector: [] - desc: '' - model: - completion_params: - temperature: 0.7 - mode: chat - name: Qwen3-235B-A22B-Instruct-2507 - provider: langgenius/gitee_ai/gitee_ai - prompt_template: - - id: 1129723a-50cb-4350-a118-3b9ac6dac523 - role: system - text: 'You are the computer that answers the admin''s question. Always start - with "hello admin" . You know AIID(AI innovative Design) is a master program - in open FIESTA in Shenzhen. ' - selected: false - title: Admin LLM - type: llm - variables: [] - vision: - enabled: false - height: 89 - id: '1758768238460' - position: - x: 389.3839309072489 - y: 792.6856819774588 - positionAbsolute: - x: 389.3839309072489 - y: 792.6856819774588 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - answer: '{{#1758768238460.text#}}' - desc: '' - selected: false - title: Answer 3 - type: answer - variables: [] - height: 104 - id: '1758768309599' - position: - x: 707.3520684153225 - y: 832.4705087633828 - positionAbsolute: - x: 707.3520684153225 - y: 832.4705087633828 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - answer: Password Correct! You are now log in as ADMIN - desc: '' - selected: false - title: Answer 3 - type: answer - variables: [] - height: 117 - id: '1758768400561' - position: - x: 1301.383930907249 - y: 426.91916104676926 - positionAbsolute: - x: 1301.383930907249 - y: 426.91916104676926 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - - data: - answer: Password Incorrect! You are now log in as GUEST - desc: '' - selected: false - title: Answer 4 - type: answer - variables: [] - height: 117 - id: '1758768418040' - position: - x: 1498.8808102839819 - y: 553.9191610467692 - positionAbsolute: - x: 1498.8808102839819 - y: 553.9191610467692 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 243 - viewport: - x: 273.4245102591761 - y: -8.654295375493462 - zoom: 0.7525860587977332 diff --git a/docs/project/chapter3/Love Loop.yml b/docs/project/chapter3/Love Loop.yml deleted file mode 100644 index 1e5d6cb..0000000 --- a/docs/project/chapter3/Love Loop.yml +++ /dev/null @@ -1,388 +0,0 @@ -app: - description: '' - icon: 🤖 - icon_background: '#FFEAD5' - mode: advanced-chat - name: Love Loop - use_icon_as_answer_icon: false -dependencies: -- current_identifier: null - type: marketplace - value: - marketplace_plugin_unique_identifier: langgenius/gitee_ai:0.1.4@f621ace33bb3c140f5a1e3533fcb518f558c7b945d63523c0f85810a4b4a8b93 -kind: app -version: 0.3.0 -workflow: - conversation_variables: - - description: '' - id: 411d934a-94cb-4899-a892-31300f69228e - name: words - selector: - - conversation - - words - value: love - value_type: string - - description: '' - id: 7aca1d78-2dbc-4ccf-8428-0f0eed7b203c - name: WORDS - selector: - - conversation - - WORDS - value: '' - value_type: string - environment_variables: [] - features: - file_upload: - allowed_file_extensions: - - .JPG - - .JPEG - - .PNG - - .GIF - - .WEBP - - .SVG - allowed_file_types: - - image - allowed_file_upload_methods: - - local_file - - remote_url - enabled: false - fileUploadConfig: - audio_file_size_limit: 50 - batch_count_limit: 5 - file_size_limit: 15 - image_file_size_limit: 10 - video_file_size_limit: 100 - workflow_file_upload_limit: 10 - image: - enabled: false - number_limits: 3 - transfer_methods: - - local_file - - remote_url - number_limits: 3 - opening_statement: '' - retriever_resource: - enabled: true - sensitive_word_avoidance: - enabled: false - speech_to_text: - enabled: false - suggested_questions: [] - suggested_questions_after_answer: - enabled: false - text_to_speech: - enabled: false - language: '' - voice: '' - graph: - edges: - - data: - isInIteration: false - isInLoop: true - loop_id: '1758765136208' - sourceType: loop-start - targetType: llm - id: 1758765136208start-source-1758765344915-target - source: 1758765136208start - sourceHandle: source - target: '1758765344915' - targetHandle: target - type: custom - zIndex: 1002 - - data: - isInLoop: false - sourceType: loop - targetType: answer - id: 1758765136208-source-answer-target - source: '1758765136208' - sourceHandle: source - target: answer - targetHandle: target - type: custom - zIndex: 0 - - data: - isInIteration: false - isInLoop: true - loop_id: '1758765136208' - sourceType: llm - targetType: code - id: 1758765344915-source-1758765883132-target - source: '1758765344915' - sourceHandle: source - target: '1758765883132' - targetHandle: target - type: custom - zIndex: 1002 - - data: - isInIteration: false - isInLoop: true - loop_id: '1758765136208' - sourceType: code - targetType: assigner - id: 1758765883132-source-1758765428475-target - source: '1758765883132' - sourceHandle: source - target: '1758765428475' - targetHandle: target - type: custom - zIndex: 1002 - - data: - isInIteration: false - isInLoop: false - sourceType: start - targetType: assigner - id: 1758764476473-source-1758765920251-target - source: '1758764476473' - sourceHandle: source - target: '1758765920251' - targetHandle: target - type: custom - zIndex: 0 - - data: - isInLoop: false - sourceType: assigner - targetType: loop - id: 1758765920251-source-1758765136208-target - source: '1758765920251' - sourceHandle: source - target: '1758765136208' - targetHandle: target - type: custom - zIndex: 0 - nodes: - - data: - desc: '' - selected: false - title: Start - type: start - variables: [] - height: 54 - id: '1758764476473' - position: - x: 115.88661184766386 - y: 178.92252604934293 - positionAbsolute: - x: 115.88661184766386 - y: 178.92252604934293 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 244 - - data: - answer: '{{#conversation.words#}}' - desc: '' - selected: false - title: Answer - type: answer - variables: [] - height: 105 - id: answer - position: - x: 1456.1539893939312 - y: 178.92252604934293 - positionAbsolute: - x: 1456.1539893939312 - y: 178.92252604934293 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 244 - - data: - break_conditions: [] - desc: '' - error_handle_mode: terminated - height: 462 - logical_operator: and - loop_count: 5 - loop_variables: [] - selected: false - start_node_id: 1758765136208start - title: Loop - type: loop - width: 682 - height: 462 - id: '1758765136208' - position: - x: 731.5560922291256 - y: 187.5878472722298 - positionAbsolute: - x: 731.5560922291256 - y: 187.5878472722298 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 682 - zIndex: 1 - - data: - desc: '' - isInLoop: true - selected: false - title: '' - type: loop-start - draggable: false - height: 48 - id: 1758765136208start - parentId: '1758765136208' - position: - x: 24 - y: 68 - positionAbsolute: - x: 755.5560922291256 - y: 255.5878472722298 - selectable: false - sourcePosition: right - targetPosition: left - type: custom-loop-start - width: 44 - zIndex: 1002 - - data: - context: - enabled: false - variable_selector: [] - desc: '' - isInIteration: false - isInLoop: true - loop_id: '1758765136208' - model: - completion_params: - temperature: 0.7 - mode: chat - name: Qwen3-235B-A22B-Instruct-2507 - provider: langgenius/gitee_ai/gitee_ai - prompt_template: - - id: e27d2ef8-3dc0-4fbf-8866-90ec3f10c758 - role: system - text: write a word with a similar meaning to the words user input. - - id: 624597ef-8e03-49c7-8365-4b17dc487032 - role: user - text: '{{#conversation.words#}}' - selected: false - title: LLM 2 - type: llm - variables: [] - vision: - enabled: false - height: 90 - id: '1758765344915' - parentId: '1758765136208' - position: - x: 114.28909334084415 - y: 65 - positionAbsolute: - x: 845.8451855699698 - y: 252.5878472722298 - selected: true - sourcePosition: right - targetPosition: left - type: custom - width: 244 - zIndex: 1002 - - data: - desc: '' - isInIteration: false - isInLoop: true - items: - - input_type: variable - operation: over-write - value: - - '1758765883132' - - result - variable_selector: - - conversation - - words - write_mode: over-write - loop_id: '1758765136208' - selected: false - title: Variable Assigner - type: assigner - version: '2' - height: 88 - id: '1758765428475' - parentId: '1758765136208' - position: - x: 418.79897041375784 - y: 194.04290759019386 - positionAbsolute: - x: 1150.3550626428835 - y: 381.63075486242366 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 244 - zIndex: 1002 - - data: - code: "\ndef main(arg1: str, arg2: str) -> dict:\n return {\n \"\ - result\": arg1 +arg2 ,\n }\n" - code_language: python3 - desc: '' - isInIteration: false - isInLoop: true - loop_id: '1758765136208' - outputs: - result: - children: null - type: string - selected: false - title: Connect - type: code - variables: - - value_selector: - - conversation - - words - variable: arg1 - - value_selector: - - '1758765344915' - - text - variable: arg2 - height: 54 - id: '1758765883132' - parentId: '1758765136208' - position: - x: 111.09640498433282 - y: 202.0812369434865 - positionAbsolute: - x: 842.6524972134584 - y: 389.6690842157163 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 244 - zIndex: 1002 - - data: - desc: '' - items: - - input_type: variable - operation: over-write - value: - - sys - - query - variable_selector: - - conversation - - words - write_mode: over-write - selected: false - title: define words - type: assigner - version: '2' - height: 88 - id: '1758765920251' - position: - x: 404.0977808713518 - y: 178.92252604934293 - positionAbsolute: - x: 404.0977808713518 - y: 178.92252604934293 - selected: false - sourcePosition: right - targetPosition: left - type: custom - width: 244 - viewport: - x: 59.39316938104503 - y: 131.3127545416781 - zoom: 0.5431072229827936 diff --git a/docs/project/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration.md b/docs/project/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration.md deleted file mode 100644 index c6924dc..0000000 --- a/docs/project/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration.md +++ /dev/null @@ -1,1044 +0,0 @@ -# Project 3: Dify 入门与知识库集成 - -# 回顾上节课 - -在前几节课中,我们分组学习了 AI 编程、提示词工程以及 AI 图像生成的基础知识。这些内容帮助我们初步了解了不同大语言模型(LLM,Large Language Model)或生成式模型的边界和能力。 - -为了帮助你回顾上节课的内容,下面有几个小问题可以思考: - -1. 什么是 AI 编程?如何使用 AI 编程工具(例如 [z.ai](http://z.ai))来创建一个网页? -2. 什么是大语言模型?什么是提示词工程和上下文工程?你该如何编写一个复杂的提示词? -3. 对于文本、AI Coding、图像生成的三个不同方向,你认为模型能力的强弱分别体现在什么地方? -4. 什么是 API?如何使用 [z.ai](http://z.ai) 接入第三方 API ? - -如果你对其中任何一个问题还感到疑惑,可以回看上节课的文档,也可以直接在微信群里提问。 - -在这节课中,我们将从简单的 AI 文字图片工具,进入更接近公司业务落地的工作流搭建平台。从对话机器人走向 AI 智能体、AI 工作流,并基于 API 把它变成可交互的“智能”机器人页面。 - -在操作过程中,如果遇到难以理解的步骤,请不要担心,推荐你随时对当前所在的操作页面进行截图,发送给大模型进行询问;当前大模型已能够解答大部分常见问题。 - -如果提问后仍无法解决,不妨大胆尝试操作;不必害怕出错,每一次尝试都是学习和进步的机会。随着实践次数的增加,你会越来越熟练,操作也会越来越得心应手! - -# 本节课你将学到 - -1. 为什么需要从聊天机器人走向智能体和 Workflow 编排。 -2. 什么是智能体与工作流开发平台,如何把 AI 的能力 SOP 化与可编排化。 -3. 什么是 Dify,如何用这个面向 LLM 应用的开源平台快速搭建应用,尤其是知识库问答机器人。 -4. RAG 的实现方法与价值,为什么需要检索增强生成? -5. 如何从 0 到 1 学会使用 Dify 和 AI IDE Trae (`Extra Knowledge 4 - What is AI IDE and Trae`),包括搭建 智能体、工作流,并基于 Dify API 制作前端对话机器人网页程序。 - -- Dify 的基本使用原理与智能体、工作流制作方法,API 调用方法。 -- AI IDE 的使用方法,如何使用 AI IDE 编程。 -- 一个可进行对话的前端网页智能体程序。 - -# 1. 从对话到智能体 - -在上一阶段,我们学会了如何用提示词让大模型扮演角色、生成文本或编写简单代码。但如果你仔细思考,会发现一个问题,聊天机器人本身并不能做事。 - -它能回答怎么查订单?,却不能真的去数据库里查对应的数字;它能描述一封周报应该包含什么,却无法自动汇总你的项目数据并发送邮件。这种“只说不做”的局限,使得纯对话式 AI 难以真正融入业务流程。 - -要让 AI 从聊天伙伴升级为数字员工,我们需要赋予它三项核心能力: - -1. 专属知识——让它能够通读并了解你的产品文档、客户资料、内部制度; -2. 工具调用(或者叫插件)——让它能操作数据库、调用 API; -3. 结构化执行——让它按预设逻辑一步步完成任务,而非自由发挥。 - -这就是 AI 智能体(AI Agent)的雏形:一个具备目标、知识、工具和执行路径的自动化单元。 - -![](images/image1.png) - -> 注意:当前业界所说的简单版本的“智能体”,大多指基于 LLM + 工具 + 知识库组合而成的增强型应用,并非所谓能够自主规划的智能体。简单的智能体虽不具备真正的推理与长期规划能力,但已足以支撑大量企业级自动化场景。我们将会在之后的章节详细介绍真正的具备自主规划和行动能力的智能体。 - -## 1.1 最简单的智能体:基于知识库的问答机器人 - -在明确智能体应具备的多项核心能力后,一个值得思考的问题随之而来:能否仅通过实现其中某一项最简单的功能,就构建出一个真正可用的基础智能体? 答案是肯定的。 - -事实上,在大量实际业务场景中,用户的核心诉求并非让 AI 自动执行复杂操作(如调用 API 或跨系统协调任务),而是希望它能基于企业自身的专属资料,提供精准、可靠的问答支持。这恰好对应智能体三大核心能力中的第一项,专属知识服务能力。因此,我们得以引出智能体最简单、也最广泛应用的形态:基于知识库的问答机器人。 - -虽然它尚未具备工具调用或自主规划能力,但其关键突破在于:让大模型的回答不再凭空生成,而是有据可依。如何实现?关键就在于解决核心挑战:企业内置大量文档知识,当存在千上万页文档时,模型如何在每一轮对话中快速找到与当前问题最相关的内容? - -此时的一个解决方案是:检索增强生成(Retrieval-Augmented Generation, RAG)。 - -RAG 的基本思路是:在用户提问时,系统首先从企业知识库中检索出与问题语义最相关的若干文本片段(例如产品手册中的某一段、HR制度中的某一条款),然后将这些片段作为上下文“注入”到大模型的输入中,引导它基于真实资料生成回答。 - -![](images/image2.png) - -图片来源:[https://www.datacamp.com/blog/what-is-retrieval-augmented-generation-rag](https://www.datacamp.com/blog/what-is-retrieval-augmented-generation-rag) - -这样一来,模型的回答不再是依赖其训练数据中的泛化知识,而是锚定在企业提供的权威信息之上。RAG 的目标,正是通过这种外部知识的动态注入,显著提升回答的真实性、准确性和一致性——甚至可以让回答“符合人设”,比如以客服口径或技术文档风格作答。 - -在实际业务中,这项技术尤为重要,因为大模型常常会产生“幻觉”。例如,若你以 CFO 或咨询顾问的身份询问某个时间段的具体数据,模型很可能编造日期和事件。引入 RAG 后,回答的可控性与可靠性将得到显著提升。 - -![](images/image3.png) - -图片来源:[https://www.databricks.com/glossary/retrieval-augmented-generation-rag](https://www.databricks.com/glossary/retrieval-augmented-generation-rag) - -在本节课的实操环节中,我们将使用流行的 AI 工作流平台 Dify,动手搭建一个基于知识库的问答机器人。你可以轻松将各种类型的专属资料,如产品手册、公司制度、项目文档、研究论文、知识库文章,甚至是个人笔记集构建为知识库。 - -完成搭建后,你可以尝试提出各类问题来检验它的能力,例如: - -- “我们产品A的最新版本有哪些主要功能升级?” -- “请根据员工手册,说明今年的年假制度是如何规定的?” -- “在XX项目中,我们遇到的技术挑战‘XXX’是如何解决的?” -- “这篇论文中提到的核心研究方法是什么?” - -你将亲身感受 RAG 技术如何将静态分散的文档资料,转化为一个精准的智能知识库,为各种场景提供高精度问答支持。 - -## 1.2 从对话智能体到工作流 - -然而,即使是加入了知识库甚至是插件调用能力的“增强型智能体”,在面对更复杂的业务流程时仍显不足。 - -试想这样一个用户请求:“我们新上线的 SaaS 产品最近有哪些功能更新?能帮我整理成一份给客户的简报吗?” - -这个请求看似简单,背后却需要多个协同步骤:首先从内部产品文档或 Notion 知识库中检索最近一个月的功能发布记录;然后过滤出面向客户的关键特性;接着调用大模型将技术描述转化为客户友好的语言;最后通过将生成内容推送至市场团队的邮箱,或保存到 Google Docs 模板中。 - -如果仅靠一个大语言模型自由推理,先不说是否能够一次对话实现所有过程,就算能,其中也很容易遗漏关键信息、混淆内部术语与客户语言,或无法结构化输出。更重要的是,企业需要的是可审计、可复用、可监控的标准化执行路径,而不是每次依赖模型的临时发挥,可监控可复现对企业而言非常重要,非预期的结果很可能会带来预期外的严重损失。 - -这就引出了更高阶的 AI 应用范式:AI 工作流(AI Workflow)。 - -![](images/image4.png) - -工作流是指将一个复杂任务拆解为多个有序、可配置、可自动执行的子步骤,并通过可视化或代码方式编排它们之间的逻辑关系,如条件判断、循环或并行执行。将 AI 能力 SOP 化(即标准化操作流程),意味着把如何用 AI 完成某项任务的经验固化为可重复使用的模板。 - -这种做法带来了多重价值:非技术人员(如产品经理或运营)可以通过拖拽组件快速搭建 AI 应用;开发者可以将 RAG 检索、LLM 调用、API 工具等封装为标准节点,在不同业务场景中复用;整个流程还可被完整追踪、调试和持续优化,满足企业对稳定性与合规性的要求。 - -AI 工作流的使用人群非常广泛。产品经理无需写代码,即可设计完整的用户交互路径;运营人员能快速搭建客服机器人、内容生成器或通知系统;开发者和算法工程师则可将核心能力模块化,供前端调用;创业者或独立开发者也能以极低成本验证 AI 产品的 MVP,几天内上线一个包含数据查询、内容生成与动作执行的完整原型。 - -此外,值得注意的是,AI 工作流通常可用一种中间表示(Intermediate Representation)来描述。不同工作流平台的具体表达方式虽有差异,但大多采用结构化文件(如 JSON、YAML 等)来定义节点类型、输入输出及执行逻辑,其结构类似下图所示: - -![](images/image5.png) - -简言之,如果说智能体让 AI 从会聊天走向能做事,那么工作流则让 AI 从偶尔做成一件事迈向“稳定、可靠、规模化地完成一类事。在接下来的实践中,我们还将借助 Dify 平台,上手并亲手构建完整的 AI 工作流,体验从想法到可运行应用的完整过程。 - -## 1.3 常用智能体 / 工作流平台 - -随着生成式 AI 技术的飞速发展,为帮助开发者与业务人员快速构建智能体与自动化流程,避免陷入编程的复杂细节,一批低代码甚至无代码的智能体及工作流平台应运而生。 - -首先需要明确的是,低代码平台是指通过可视化拖拽组件、预置业务逻辑模板、图形化配置规则等方式,显著减少手动编码工作量的开发工具。其核心在于以可视化配置,节点式拖动变成的方式替代直接写代码的方式,既能让具备一定技术能力的开发者从重复劳动中解放出来,也能让熟悉业务逻辑的非技术人员参与到应用搭建中。本质上,它是在开发效率与场景灵活性之间架起一座平衡的桥梁。 - -这类低代码/无代码智能体平台的突出价值,正是大幅降低 AI 应用的开发门槛。以往需要团队协作数周——从需求梳理、代码开发到测试部署——才能完成的 AI 智能体(如客服问答机器人、数据处理助手),现在借助平台提供的可视化工具,可将“从创意到上线”的周期缩短至数小时。 - -目前市面上主流的低代码 AI 工作流平台包括: - -| 平台 | 特点 | 适用场景 | -| --------------------------------------------- | -------------------------------------------------- | -------------------------------------- | -| Dify | 开源、支持知识库 RAG、LLM 编排、API 输出,中文友好 | 企业知识库问答、定制化 Agent、API 服务 | -| Coze(字节跳动) | 国内可用、集成抖音/飞书生态、插件丰富 | 社交机器人、国内小程序集成 | -| n8n | 通用自动化工具,支持 AI 节点,强调 API 编排 | 跨系统数据同步、AI + 传统 SaaS 自动化 | -| 百度千帆 AppBuilder / 阿里百炼 / 腾讯 HunYuan | 大厂云原生方案,集成自家模型 | 企业级部署、合规要求高场景 | - -目前市面上的低代码 AI 工作流平台选择丰富。尽管 AWS、Azure、阿里云等主流云厂商均推出了相应的 AI 工作流解决方案,但 Dify、Coze 和 n8n 凭借以下三大核心优势,成为当前应用最广泛的代表: - -1. 极致易用性。平台采用可视化拖拽式界面设计,用户无需深入理解底层技术,即可快速上手。 -2. 高灵活性。支持自定义组件与扩展 API 接口,既能适应教学演示、MVP(最小可行产品)验证等轻量场景,也能满足中小型团队的敏捷迭代需求。 -3. 成熟生态。不仅官方文档详尽、响应及时,还拥有活跃的用户社区,便于快速获取来自不同用户的预设方案。 - -这三大平台均支持将搭建好的 AI 智能体以标准化 API 接口的形式输出,可无缝集成至前端 Web 应用、企业内部 ERP 系统或移动端 APP 中,进一步降低了 AI 能力落地的技术门槛。 - -### 1.3.1 Dify:企业级LLMOps与应用生命周期管理平台 - -Dify 定位是LLM应用开发与运营平台,致力于提供AI应用从构思、部署到优化的全生命周期管理。其核心是一个低代码平台,旨在帮助开发者和非技术背景的创新者快速构建生产级AI应用。 - -![](images/image6.png) - -在功能上,Dify覆盖了可视化工作流编排、智能体构建、知识库管理、多模型支持等功能。平台允许通过拖拽节点设计复杂任务流程,并支持创建基于意图的Agent。其知识库功能突出,能处理多种格式文档并进行高效的向量检索。同时,Dify兼容支持包括GPT、Claude及众多开源模型在内的多种LLM,构建的应用可一键发布为标准API便于集成。 - -![](images/image7.png) - -技术架构方面,Dify以开源和可私有化部署为特色,强调灵活性、扩展性及企业级合规。目标用户包括开发者团队和业务创新者,典型应用场景涵盖企业知识库与智能客服、内容创作自动化、垂直领域AI助手以及企业AI中台。 - -### 1.3.2 Coze(字节跳动):零代码AI智能体构建的普及者 - -Coze是字节跳动推出的AI智能体开发平台,以极致易用性为核心,让无编程经验的用户也能轻松创建、调试并发布功能丰富的AI聊天机器人。 - -![](images/image8.png) - -其核心是将Bot构建简化为搭积木式操作。用户可通过界面轻松配置角色与知识库,并利用丰富的内置插件库为Bot添加新闻、旅游、图像生成等多类外部能力。创建好的Bot可一键快速发布至豆包、飞书、微信公众号等多个平台。 - -![](images/image9.png) - -技术架构完全服务于低门槛使用,后端集成字节自有模型并封装复杂流程,强调多模态理解与实时响应。作为一个主要以云服务形式提供的平台,其私有化部署能力相对有限。典型应用场景包括个人助理与娱乐Bot、智能客服与问答系统、在线教育助手以及快速原型验证。 - -### 1.3.2 n8n:可编程的后端工作流自动化引擎 - -n8n是一个通用的可编程工作流自动化平台,其核心定位是连接各类应用、数据库与API,实现数据流动与任务自动化执行。 - -它通过庞大的集成节点库支持数百种SaaS服务、数据库及协议,并采用可视化与代码结合的方式:用户可在画布拖拽节点,同时注入JavaScript或Python代码编写自定义逻辑。n8n擅长处理后端数据密集型任务,如数据同步、ETL流程与API编排。 - -![](images/image10.png) - -关键技术特性是“源码可见”和“可自托管”,用户可将其私有化部署以完全掌控数据与环境,这使其对数据安全要求高的行业极具吸引力。其主要目标用户是开发者、技术运营及数据分析师。n8n 最大的优势,在于拥有极其强大的社区生态。网络上拥有随处可见丰富的 n8n 分享视频,为用户提供了便捷的学习参考与经验借鉴;同时,它支持连接 YouTube、Instagram 等全球众多不同生态平台,能够帮助用户轻松打破跨平台数据与服务的壁垒,实现多生态流程的自动化流转。 - -### 1.3.3 其他工作流平台 - -除了上述的几个最知名的平台,中国国内的主要科技厂商也相继推出了各自的一体化AI开发平台,例如:百度千帆 AppBuilder 提供从模型选型、RAG构建到智能体发布的全流程支持,深度集成文心大模型;阿里云百炼基于通义千问系列模型,注重企业级安全与私有化部署能力;腾讯云 TI 平台 则聚焦于金融、医疗等行业场景,提供丰富的预置解决方案模板。这类平台通常与各自云生态深度融合,适合已处于相应技术体系内的企业选用。 - -然而,在通用型、开放性与社区生态方面,Dify 与 Coze 仍凭借其突出的易用性、广泛的模型支持以及活跃的开发者社区,成为当前更受广泛采纳的选择。 - -尽管各平台在定位与生态上各有侧重,其核心逻辑均是通过可视化方式编排与连接不同的能力模块。因此,掌握其中任意一种平台的设计思路与操作方法,即具备快速迁移到其他类似工具的基础。在接下来的实践中,我们将以 Dify 为例进行具体讲解。 - -# 2. 深入浅出 Dify - -## 2.1 什么是 Dify - -我们在之前已经了解了基础的 Dify 的信息介绍,对于更详细的信息,你可以通过 [https://cloud.dify.ai/apps](https://cloud.dify.ai/apps) 访问 Dify 平台,如果想了解更多信息,可以访问官网 https://dify.ai。 - -Dify 是一个用于开发 LLM 应用的开源平台。它提供了直观的界面,将 Agent 工作流、RAG 流水线、工具能力、模型管理、可观测性等功能结合在一起,帮助你快速地从原型走向生产环境。 - -![](images/image11.png) - -你可以在 Dify 中使用大语言模型和各种功能不同的工具来搭建“工作流”。所谓工作流,就是把原本需要你手动一步步完成的操作——例如数据检索、大模型调用、网页搜索、结果过滤、格式整理等——按照业务逻辑串联起来,变成一个自动化、可复用的流程。如果没有工作流,每次你都需要把同样的内容复制粘贴给大模型,非常低效、容易出错,也难以在真实业务中复用。 - -搭建一个工作流,就像在拼搭积木或拼图。你把“大语言模型节点”(负责理解和生成)、各类“工具节点”(负责执行具体动作,例如查数据库、发邮件、翻译文本等)、以及“数据节点”(负责读取、存储信息)像积木一样连接起来。它们会按照你预设的逻辑自动协同工作,而不需要你每次都手动操作。你也可以把它理解成一种“低代码程序”:你只需要通过拖拽的方式,配置输入和输出的路径,就可以实现比较复杂的业务逻辑。 - -举个例子,如果你是一个亚马逊或抖音电商店铺的老板,想要搭建一个 AI 客服系统,可以参考下图的结构设计一个工作流: - -1. 触发节点(类似 START):接收用户的咨询问题,例如“这个商品的质保期有多长?”。 -2. 问题分类节点(类似 QUESTION CLASSIFIER):使用一个模型(例如 GPT)对用户问题进行分类,判断这是售后(比如质保)、使用方法,还是其他类型的问题。 -3. 知识检索节点(类似 KNOWLEDGE RETRIEVAL):根据分类结果,自动访问相应的知识库。如果是关于“质保”的售后问题,就从售后 SOP 知识库中检索与“质保”相关的精确信息。 -4. 大语言模型节点(LLM Node):将用户问题和检索到的知识库内容一起发送给大语言模型(例如 GPT),让它生成一段对用户友好的回复(避免太生硬的技术语气)。 -5. 条件节点:检查大模型生成的回答中是否包含清晰的质保时间(例如“1 年”、“3 年”),如果有则继续下一步,如果没有则让它回复“请提供产品型号”。 -6. 输出节点(类似 ANSWER):将最终答案返回给用户,并自动把本次咨询记录到表格中。 - -![](images/image12.png) - -在整个过程中,你不需要手动去翻知识库、反复调整模型的回答、或单独记录数据——工作流会把这些步骤“连起来自动跑”。并且它非常灵活:例如,如果你之后想加一个新规则“当用户问质保范围时,调用另一个知识库”,只需要在工作流中多加一个条件节点,而无需重构整个系统。 - -这是一个比较简单的工作流示例,但要完全掌握这些能力,对现在的你来说可能还有点难。因此在本节课中,我们从更加基础的知识库智能体开始,后面再逐步学习更复杂的工作流技巧。 - -### 2.1.1 部署属于自己的 Dify(可选) - -本部分内容原本安排在后续课程中详细介绍,但考虑到当前部分学习者可能因网络限制暂时无法访问 Dify 官方网站或云端服务,我们决定提前提供这一可选的学习路径,帮助你顺利推进课程进度。 - -你需要参考该教程入门 Zeabur 部署平台的基本使用方式:[Extra Knowledge 6 - Zeabur: What Is It and How to Deploy Web Applications](https://github.com/datawhalechina/easy-vibe/blob/main/docs/extra/extra6/extra6-zeabur-what-is-it-and-how-to-deploy-web-applications.md) - -![](images/image13.png) - -你需要学习如何在 Zeabur 上部署一个自己的 Dify,部署后进入到对应链接注册并登录后继续跟随下列教程操作即可。 - -注意,不同版本的 Dify 的操作方面和前端界面可能有些许差别,但总体上差别不大,当你发现不同的时候不要慌张,找到类似的接口和入口进行操作即可。 - -## 2.2 创建第一个 Dify Chatbot 应用 - -访问 Dify 首页 [https://cloud.dify.ai/apps](https://cloud.dify.ai/apps) 并注册和登录后,选择 Studio,你会看到如下界面: - -![](images/image14.png) - -在左侧找到 `CREATE APP` 区块,点击 `Create from Blank`。 - -![](images/image15.png) - -![](images/image16.png) - -在 APP Type 中找到 Chatbot(如果一开始没看到,可以点击“查看更多类型”的按钮,然后在完整列表中找到)。选择 Chatbot 之后,在下方输入应用的名称和描述,最后点击创建。 - -![](images/image17.png) - -创建完成后,你会看到类似下面的界面。 - -![](images/image18.png) - -中间区域的 “INSTRUCTIONS” 指的是内置指令,你可以把它理解为默认提示词或系统提示词。 - -中间偏下有一个 “Knowledge” 区域,这就是知识库区域——我们稍后会把自己的知识库上传到这里。 - -右侧是调试窗口,你可以在调整提示词后与 Agent 进行对话,实时查看效果。 - -你可以在 INSTRUCTIONS 区域自由输入角色提示词,观察对话效果;也可以点击 Generate,让大模型自动帮你生成提示词。 - -![](images/image19.png) - -注意右上角会出现许多不同模型的选项,这意味着你可以点击切换不同的对话模型,从而比较它们在语气、逻辑推理、长文本处理等方面的差异,寻找最适合你需求的模型。 - -![](images/image20.png) - -## 2.3 支持自定义模型供应商 - -为充分发挥 Dify 的灵活性,考虑到不同地区访问模型的难度,为满足特定业务需求、成本控制或数据隐私要求,我们常常需要接入自定义模型。Dify 支持配置三类核心模型:大语言模型(LLM)、Embedding 模型和 Rerank 模型。本部分内容将逐步指导你完成这些自定义配置。 - -Dify 能够灵活接入来自 OpenAI、Azure、Anthropic 等主流服务商的模型,同时也全面兼容任何符合 OpenAI API 接口规范的自托管模型或第三方模型。你可以通过安装内置的 OpenAI Compatible 插件以及对各大模型平台定制的插件实现这一操作。 - -详细步骤参考如下,首先我们需要安装对应的插件: - -1. 我们需要安装 `OpenAI-API-compatible` 及 `SiliconFlow` 插件获得对绝大部分大模型和 Embedding 模型的支持,其中前者是对 OpenAI 兼容接口的支持,后者是一个部署了当前绝大部分常见、好用的开源模型的服务站。你可以访问下列网页进行安装: - 1. https://marketplace.dify.ai/plugins/langgenius/openai_api_compatible - 2. https://marketplace.dify.ai/plugins/langgenius/siliconflow -2. 如果你是自己部署的 Dify,你可以在对应系统设置界面进入插件市场进行操作 - -![](images/image21.png) - -![](images/image22.png) - -进入插件市场后,搜索对应的插件名称即可。 - -![](images/image23.png) - -3. 安装结束后,我们能够配置支持新的模型供应商,在设置里的模型提供商部分,我们可以看到目前支持的所有模型商: - ![](images/image24.png) -4. 在开始使用前,需要先完成模型的配置。对于 OpenAI-API-compatible 插件,你可以点击 “Add Model” 来添加并配置任意模型。你可以在 “Model Type” 中选择该模型是LLM还是 Embedding,你需要确保模型的类型被正确配置。 - 你需要写入具体的模型名字、模型 endpoint URL 以及 API Key 才能确保模型启用,如果你初步觉得配置该参数麻烦,你可以直接跳到后者的 SiliconFLow 平台的 Key 配置,或者安装 OpenRouter 等第三方服务商插件进行简单的模型支持配置。(确保服务商内有剩余可使用额度) - - ![](images/image25.png) - - 对于 `SiliconFlow` 插件,只需要点击 Setup 配置 key 后即可使用 Embedding 和 Rerank 模型进行测试,你可以点击 Get you API Key from SiliconFlow 获得鉴权密钥。 - - ![](images/image26.png) - -5. 配置完成后,你可以点击模型列表查看当前支持多少模型,此时已经完成了基础模型的全部配置。 - ![](images/image27.png) - - 其中支持了绝大部分常见的 Embedding 与 Rerank 模型: - - ![](images/image28.png) - - 此时如果你想要修改 Dify 默认使用模型的配置,你还可以点击 System Model Settings 按钮修改默认的所有模型。 - - ![](images/image29.png) - -## 2.4 创建第一个 Dify 知识库 - -到这里,我们已经完成了最简单的 Agent 创建,但它还缺少一个知识库。现在,请点击顶部菜单中的 `Knowledge`,进入知识库创建页面。 - -![](images/image30.png) - -然后点击左侧的 `Create Knowledge`,创建你的第一个知识库。 - -![](images/image31.png) - -在这个界面中,你可以上传多种类型的文件(例如 pdf、txt 等)来构建知识库。可以上传很长的文本,或者把维基百科上的内容复制下来保存成 txt 文件进行上传。本例中,我们会上传一份关于 Elon Musk 的维基百科 txt 文件。 - -点击 Next 后,你会进入 Knowledge Base Settings(知识库设置)页面。这里选项比较多,我们一步一步来看。 - -首先在 **General** 设置中,你可以把这里理解成“文本切分规则”的设置区域。因为我们需要把很长的文本切分成小块,所以必须先定义切分规则。在入门阶段,你只需要关注 **maximum chunk length(最大切分长度)** 。可以尝试设置为 512、2048 或 4096,然后点击 **Preview Chunk** 预览不同设置下的效果。 - -你也可以调整 **Chunk overlap(切片重叠)** 选项。它决定相邻片段之间是否会保留一部分重叠内容。适当的重叠有助于避免重要信息被拆到不同片段而难以理解。 - -![](images/image32.png) - -在设置中还有一个选项叫做 **Chunk using Q&A format in English** 。启用后,系统会使用大语言模型,将知识库的一部分内容转换成问答形式来存储,这在某些场景下可以显著提升检索效果。 - -在真实业务中,根据场景选择合适的切分策略,能够更好地优化检索结果,保证查询能够返回你期望的信息。 - -继续向下滚动页面,你会看到和 Embedding 模型相关的设置。 - -简单解释一下:Embedding 模型的核心功能,是把非结构化数据(例如文本、图片等)转换成计算机能够理解的“数字向量”(Embedding 向量)。通过这种转换,模型能够快速计算不同数据之间的相似度,从而实现语义相近内容的匹配,比如根据用户输入的一句话,找到语义最接近的文档、图片或商品。 - -Embedding 模型的选择会显著影响最终的检索效果(例如匹配准确度、响应速度等)。在这里,我们推荐优先使用 Qwen 0.6B 的 Embedding 模型,你也可以切换到 4B 或 8B 版本,直观对比不同参数规模下检索效果的差异。 - -![](images/image33.png) - -在此处,你还会看到另一个模型设置叫做 **Rerank model** ,默认值是 **Jina-rerank-m0** 。(如果你非校园内的学生,此时你可能会看到 Rerank 模型缺失的报错,你需要在模型处配置 rerank 模型才能在此处启用使用) - -Rerank 模型的主要作用,是对“初步筛选出的候选结果”进行二次、更精细的排序,让和用户需求最匹配的结果排在更靠前的位置,从而显著提升最终结果的相关性和用户体验。 - -简单理解:Rerank 模型就是用来解决“初次筛选不够精细”的问题。例如搜索引擎可能先用较简单的规则检索出 1000 个潜在相关网页,再通过 Rerank 模型,从中挑出最相关的前 10 个展示在第一页。 - -推荐系统同理:它可能首先找出 500 个“可能适合你”的商品,再通过 Rerank 模型排序,让你最可能购买的商品排在列表顶部。 - -![](images/image34.png) - -当所有设置完成后,点击 **Save & Process** ,系统就会进入知识库向量化阶段。在这一阶段,Embedding 模型会把切分后的文本转换为向量表示。 - -![](images/image35.png) - -处理完成后,点击 **Go to document** ,可以查看已经处理完毕并存储好的知识库内容。 - -![](images/image36.png) - -直接点击知识库名称,可以查看每个切片的具体内容。 - -![](images/image37.png) - -在这里,你可以对任意不合适的文本片段进行精确的编辑或删除操作。 - -![](images/image38.png) - -在左侧边栏中,选择 **Retrieval Testing** 可以对知识库进行召回测试,检查检索是否正常工作。每次测试会返回若干相似度最高的切片。 - -![](images/image39.png) - -如果你希望看到更多的切片结果,需要点击 `VECTOR SEARCH` 设置: - -![](images/image40.png) - -![](images/image41.png) - -Top K 指的是向量检索时,返回与查询向量最相似的前 K 个文本切片数量。当前设置为 3,表示会返回相似度最高的 3 段文本。 - -Score Threshold 则是一个“得分阈值”:只有相似度得分大于或等于该阈值(示例中为 0.5)的文本片段才会被返回。这样可以过滤掉相关度较低的内容,让结果更加准确。 - -现在知识库部分就全部准备好了。接下来,点击顶部菜单栏中的 “studio”,找到刚才创建的智能体,为它接入我们已经配置好的知识库。 - -![](images/image42.png) - -![](images/image43.png) - -此时,在每一轮对话中,你都可以在回答中看到被命中的知识库来源。点击对应条目即可查看检索到的具体文本片段。 - -![](images/image44.png) - -![](images/image45.png) - -## 2.5 更多 DIfy 常见操作 - -在掌握基础 Chatbot 和知识库搭建的基础内容后,我们可以深入了解更多有关 Dify 的使用方式。 - -### 2.5.1 工作流的导入与导出 - -还记得之前提到的工作流的中间表示法吗?Dify 支持通过 DSL(Domain Specific Language) 格式导入和导出工作流。DSL 是一种基于 JSON 的标准化描述方式,能够完整保留工作流的节点结构、连接关系和配置参数。你可以很容易导入和导出 DSL 文件,分享工作流给其他人使用,或者导入别人的工作流进行参考。具体而言,我们能够容易在工作台页面看到工作流的导入按钮: - -![](images/image46.png) - -而对于工作流的导出,我们只需要点击单个工作流块的右下角即可找到导出按钮: - -![](images/image47.png) - -通过使用 DSL 文件,你可以轻松地在不同 Dify 实例之间迁移或共享复杂的工作流设计。 - -### 2.5.2 查看更多 Dify 项目 - -如果你觉得自己搭建的工作流或者智能体过于简单,Dify平台提供了丰富的示例项目,帮助你快速了解如何构建复杂应用。这些示例项目涵盖了多种业务场景。你可以点击 Explora 查看别人构建的工作流进行学习。 - -![](images/image48.png) - -## 2.6 创建第一个 Dify Workflow 应用 - -完成了 DIfy 的对话智能体构建入门,我们继续查看如何构建更复杂的 Dify 业务工作流。工作流是Dify将复杂业务逻辑可视化的核心方式,通过它你可以像搭积木一样构建智能流程。你能够完整体会信息如何在不同节点间流转,判断逻辑如何部署,人工干预点设置在哪里,以及最终如何交付一个完整的业务结果。 - -你可以选择从空白处创建,或者直接从模板处创建,此处演示如何从空白处创建工作流: - -![](images/image49.png) - -![](images/image50.png) - -在这里我们会看见两个选择,分别是 Chatflow 与 Workflow,这两者该如何选择呢?关键是你需要理解你所要构建的,其核心是持续对话,还是任务流程。 - -Chatflow 专为对话而设计。它模拟一个具有记忆和上下文理解能力的对话者,非常适合需要多轮交互、状态维持的场景。例如在客服咨询中,它能连贯地理解用户的后续追问,如同一位耐心的服务人员。其流式输出的特性也让交互过程更为自然。简而言之,当你需要构建一个能“交谈”的智能体时,应选择 Chatflow。 - -Workflow 则专注于流程的自动化执行。它像一条预设的流水线,擅长处理一次性输入、多步骤处理、并产生确定性输出的任务。例如,每日定时生成数据报表、批量处理文件或调用系列API。这类任务通常由事件触发,无需与人实时互动。因此,当你需要实现“自动化”任务时,Workflow 是更合适的选择。 - -为避免选型错误带来的效率低下,你可以通过四个关键问题来审视你的任务需求: - -1. 任务过程是否需要依赖多次的用户输入与调整? -2. 结果的呈现是否需要分步骤、流式地进行? -3. 处理逻辑是否严重依赖于之前的交互历史? -4. 任务是否由事件触发,且输入输出多为一次性完成? - -如果前三个问题的答案为“是”,那么 Chatflow 是理想选择,典型场景包括智能客服、教育辅导、创意协作等。如果第四个问题特征显著,则应选用 Workflow,它更适用于数据清洗、报表生成、批量处理等自动化场景。 - -此处我们选择 Chatflow 作为案例进行介绍,点击 Chatflow 后进入到操作台界面: - -![](images/image51.png) - -我们来简单介绍工作流界面的页面。其中整个界面的核心是中央的编辑画布,你将以可视化方式在这里构建应用逻辑。如图所示,一个基础的工作流通常始于 START 节点(用于接收输入),经由连线将数据传递至 LLM 节点进行处理,最终通过 ANSWER 节点输出结果。每个节点代表一个功能模块,而连线则决定了任务执行的顺序。 - -环绕画布的是完整的操作与管理功能区。界面顶部提供了全局控制选项,包括测试工作流的 Preview 按钮和用于上线的 Publish 按钮。画布角落则设有缩放、撤销等视图控制工具,便于精细调整。 - -左侧面板集中了应用的管理功能。你当前所在的 Orchestrate 选项卡用于流程编排;构建完成后,可通过 API Access 获取集成凭证;Logs & Annotations 记录了每次执行的详细踪迹,便于调试;而 Monitoring 则为你提供应用运行时的性能与状态监控。 - -你可以简单在该对话工作流 LLM 节点的 SYSTEM 中输入一些提示词内容,点击 Preview 后尝试运行这个工作流,查看修改 SYSTEM 提示词后整个工作流确实按照预期在变化。 - -### 2.6.1 常见节点介绍 - -Dify 中提供了多种节点,你可以先了解每个节点的基本功能。具体使用时,建议亲手尝试,或参考他人创建的工作流模板,也可以截图并向大模型询问该节点的用法、所需参数等。推荐直接在现有模板中替换不同节点,通过他人的使用方式来推测节点的最佳实践。 - -在画布右键点击“Add Node”即可添加节点,也可以在左侧的节点面板中查看所有可用节点: - -![](images/image52.png) - -同时,可以打开工具选择面板,查看支持调用的各类工具: - -![](images/image53.png) - -下面是一些常用节点和工具的简要说明。不需要一次性全部掌握,建议先留个印象,在实际使用中逐步熟悉,必要时再回查阅。 - -1. LLM与推理节点 - -![](images/image54.png) - -![](images/image55.png) - -此类节点负责工作流中的核心流程。 - -- LLM节点:核心计算单元,用于调用大语言模型。其配置重点在于提示词工程与参数调优,将业务问题转化为模型的执行指令。 -- Knowledge Retrieval 节点:知识检索单元,负责从预设知识库、外部权威数据源中检索与业务问题相关的信息,为 LLM 节点提供精准的知识支撑,帮助减少大语言模型输出的 “幻觉” 问题。 -- Answer 节点:结果输出单元,负责接收 LLM 处理后的内容,将其整理为符合业务场景需求的最终成果形式。其配置重点在于输出格式的定义(如话术模板、排版规范)。 -- Agent节点:高阶决策单元。它不仅调用模型,还可实施多步骤规划、自主选择并调用外部工具,适用于需要动态决策的复杂任务链。 -- Question Classifier 节点:问题分类单元,负责对输入的业务问题进行类型识别与归类(比如按问题意图、主题领域等维度划分),帮助后续流程精准匹配对应的处理节点(如不同类型的问题适配不同的 LLM 提示词或工具链)。 - -2. 逻辑与流程控制节点 - -![](images/image56.png) - -此类节点定义工作流的执行路径与规则。 - -- 条件节点:如 `IF/ELSE`,通过布尔判断实现流程分支。其设计关键在于条件表达式的严谨性,确保逻辑覆盖所有业务场景。 -- Iteration 节点:作为无状态的批量并行处理单元,它专为子任务间无数据依赖、可独立处理的场景设计,例如批量翻译段落、并行审核多条内容或同时生成多份报告。该节点会接收一个输入数组并自动分片,将每个元素分发至相同处理链路并行执行,用户可在迭代体内通过 {{item}} 访问当前元素、{{index}} 获取其索引,输出则会自动聚合成结果数组;配置时需重点设定并行度以平衡效率与系统负载,同时通过重试策略(如重试次数、间隔)和失败处理(如记录日志、返回默认值)保障批量作业的稳定性。 -- Loop 节点:有状态的递归迭代器,适用于结果依赖前一轮输出的场景,比如多轮参数调优、递归式内容优化(如反复修订文案直至满意)及依赖上次结果的链式计算。其核心是 “状态变量”,需在循环开始前初始化(如当前迭代次数、中间计算结果),并在每轮迭代中明确更新以作为下一轮输入;为防止无限循环,必须定义终止条件(包括基于计数器的 “最多循环 10 次”、基于结果判定的 “满意度评分 > 9”、基于外部信号的 “检测到‘停止’输入”),同时需设置循环超时配置,并规划异常处理路径(如跳出循环或重置状态后重试),确保流程稳定运行。 - -3. 数据操作与集成节点 - -![](images/image57.png) - -- Code 节点:代码处理单元,负责在工作流中执行自定义代码逻辑,可实现数据格式转换、复杂计算等个性化处理需求。其配置重点在于代码语法的正确性与执行环境的适配。 -- Template 节点:模板处理单元,负责将动态数据填充至预设模板中,生成符合格式要求的内容(如定制化文案、报告框架)。其配置重点在于模板语法的编写与变量映射规则的设置。 -- Variable Aggregator 节点:变量聚合单元,负责收集工作流中多个节点输出的变量数据,将分散的变量整合为统一数据集。其配置重点在于聚合的变量范围与数据合并规则的定义。 -- Doc Extractor 节点:文档提取单元,负责从 PDF、Word 等各类文档中提取文本、表格等关键内容,转化为工作流可处理的结构化数据。其配置重点在于文档类型的适配与提取内容的筛选规则。 -- Variable Assigner 节点:变量赋值单元,负责定义、初始化或更新工作流中的变量,为流程内的数据传递提供载体。其配置重点在于变量的命名、数据类型及赋值逻辑的设定。 -- Parameter Extractor 节点:参数提取单元,负责从用户请求、接口返回等输入内容中提取指定参数,将非结构化信息转化为结构化数据。其配置重点在于提取规则(如正则表达式、JSON 路径)的配置。 -- HTTP Request 节点:HTTP 请求单元,负责向外部系统接口发起 HTTP 请求(含 GET、POST 等方法),实现工作流与外部服务的数据交互。其配置重点在于请求地址、请求方法及参数 /headers 的设置。 -- List Operator 节点:列表操作单元,负责对数组、列表类型的数据进行处理(如过滤、排序、拆分),调整数据结构以适配后续流程。其配置重点在于操作类型(如过滤条件、排序规则)的定义。 - -### 2.6.2 常见工具介绍 - -![](images/image58.png) - -在 Dify 中,大部分工具都可以直接作为节点放在画布上,像其他节点一样被上下游连线,只要你提供的输入符合该节点(工具)的参数规范,它就能正常执行并产出可继续流转的结果。 - -在左侧或右侧的节点面板中,可以查看所有可用工具节点,也可以通过插件市场扩展更多工具能力。简单介绍几个常见工具的作用: - -- 网络搜索工具 - 以 Tavily Search 为代表,为大模型提供面向 AI 优化的实时检索能力。 - 它会返回结构化的搜索结果(如标题、摘要、链接等),可以直接作为 LLM 提示词的一部分,用于回答最新资讯类或需要权威依据的问题。 -- 数据处理工具 - 例如 JSON Process 插件,用于对 JSON 数据进行查询、筛选、转换、合并等高级操作。 - 在处理复杂 API 响应或多层嵌套数据时,你可以将“数据清洗 + 重组”的逻辑交给该工具,从而简化在 Code 节点中频繁手写解析代码的工作。 -- 格式处理工具 - 如 Markdown Exporter,可以将生成内容按指定格式导出,例如 Markdown 文档、特定排版模板等,方便后续用于展示、汇报或集成到其他系统。 - -你可以在工具列表中看到这些插件的安装量和简介,初期可优先尝试安装“Featured / 推荐”里的工具,往往覆盖了最常见的业务场景。 - -不过,工具的使用通常比较复杂,建议你在使用的时候可以去搜索引擎先搜索对应工具的“官方推荐工作流 DSL 案例”,直接导入使用,比自己搭建要天然节约很多时间。 - -### 2.6.3 创建简单的意图分类工作流 - -此时我们已经初步了解了 Dify 工作流和工具等的基本信息,但不经过练习我们永远不会熟练使用细节,我们需要一个“假设”的真实业务场景来练练手。 - -例如,在真实的购物对话场景中,前来购买商品的用户输入永远不会是“规范的参数”,而是一句随口说出的话:有人来下单,有人来抱怨,有人只是想闲聊,也有人完全跑题。如果我们把所有这些输入都直接交给同一个大语言模型(LLM)处理,系统通常会出现两个典型问题: - -1. 回复风格不稳定 - 同样是抱怨,有时 LLM 能道歉安抚,有时却像在“解释原因”;同样是点餐,有时会追问缺失信息,有时则直接编造订单细节。 -2. 业务逻辑不可控 - 你希望“抱怨必须先道歉”,但模型未必每次都遵守;你希望“非业务问题要引导回主线”,但模型可能会兴致勃勃地和你聊起段子。 - -因此,更工程化的做法是将任务拆解为一条标准化流水线,先做意图分类(确定用户到底想干什么),然后再按意图分流(不同场景使用不同的提示词与角色),最后对不同分流后大模型的回复统一封装输出(便于前端或系统集成)。 - -本节的目标是让系统能处理一个餐饮场景下的多类对话。你可以跟着操作做一遍加深印象。首先需要做的是定义场景为意图分类: - -- **下单购买 (buy_food)** :用户表达明确的购买意愿。 -- _例如:“给我来一份炸鸡,再加一杯可乐。”_ -- **抱怨投诉 (complain)** :用户在表达不满、催促或负面反馈。 -- _例如:“你们也太慢了吧?等一个小时了。”_ -- **闲聊咨询 (chitchat)** :用户在进行开放式询问、寻求建议,但无明确下单指令。 -- _例如:“今天吃什么好呢,你有什么推荐吗?”_ -- **其他意图 (other)** :用户的输入与餐饮场景无关。 -- _例如:“帮我写个搞笑文案发朋友圈。”_ - -针对这四种意图,我们为系统预设了四种不同的“沟通人格”,分别由四个独立的 LLM 节点承载,每个节点都需要由具有不同人设的 LLM 进行扮演。 - -- **下单助手 (LLM_BuyFood)** :专业、高效,核心任务是确认订单细节,并主动补全缺失信息。 -- **客服专家 (LLM_Complain)** :共情、稳重,首要任务是安抚用户情绪,并提供清晰的解决方案。 -- **聊天伙伴 (LLM_Chitchat)** :轻松、友好,旨在提供个性化推荐,引导潜在消费。 -- **礼貌门卫 (LLM_Other)** :专注、边界清晰,负责将偏离主题的对话礼貌地引导回核心业务。 - -#### 工作流编排设计 - -接下来我们进行工作流的编排设定,决定大概需要有哪些工作流节点。对于新手而言,很难想到需要有哪些节点能被用到(对于老手来说也懒得自己思考,用大模型给建议通常是最快最好的选择),所以我们能够使用大模型给出对应的编排建议,其核心节点结构如下: - -- Start (起点):作为数据入口,负责接收用户的原始输入 `user_text`。 -- Question Classifier (意图分类器):工作流的“大脑”与“调度中心”。它负责对 `user_text` 进行分析,并从我们预设的四种意图标签中选择最匹配的一个。 -- Condition (条件分支):扮演“分流阀”的角色。它根据分类器输出的意图标签,决定接下来将任务导向哪一个专处理路径。 -- 四个并行的 LLM 节点 (LLM_BuyFood, LLM_Complain, LLM_Chitchat, LLM_Other):这是四个独立的“专家处理单元”。每个节点都接收原始问题,但依据自身独特的 System Prompt(系统提示词)生成风格和目标截然不同的回复。 -- Variable Aggregator (变量聚合器):在多条路径处理完成后,需要一个“汇集点”。此节点将四个分支中唯一被激活并产生结果的回复,收束成一个统一的变量 `final_reply`,确保了输出结构的稳定性。 -- Output (终点):作为最终的出口,负责将意图标签、原始问题、以及经过处理生成的回复,以结构化的形式(如 JSON)统一输出,便于后续系统调用或调试分析。 - -#### 工作流编排实现 - -本次教程我们选择创建 Workflow 而不是 Chatflow,选择 User Input: - -![](images/image59.png) - -随后点击 Start 的 User Input 节点,定义一个名为 `user_text` 的字符串类型变量,作为整个流程的输入源。 - -![](images/image60.png) - -保存后点击右上角的 Test Run,你能够看到需要指定对应的文本输入进行处理: - -![](images/image61.png) - -随后我们需要点击输入节点后的 + 符号,选择 Question Classifier 节点添加,并且我们需为其配置四类标签,并为每个标签提供清晰的描述和示例。 - -- `buy_food`: 用户明确想买吃的、点餐、下单。 -- `complain`: 用户在抱怨、吐槽、发脾气,通常带有不满情绪。 -- `chitchat`: 用户在闲聊、讨论吃什么、咨询推荐。 -- `other`: 与餐饮场景无关,或难以判断的内容。 - -此外,你还需要在 ADVANCED SETTING 中写入提示词,让大模型能够正确根据用户输入进行分类测试。示例提示词如下: - -```Plain -从 buy_food / complain / chitchat / other 中选择一个最合适的标签。如果用户在抱怨的同时也点了餐,请优先判断其核心情绪,若重点在于表达不满,应归为 complain。如果只是轻微吐槽但主要意图是下单,则归为 buy_food。若实在难以判断,使用 other 作为兜底 -``` - -![](images/image62.png) - -设定完成后,你可以在右上角的播放键单独测试该节点是否能够正常运行: - -![](images/image63.png) - -![](images/image64.png) - -从 OUTPUT 的结果来看,我们的分类是准确的。你可以进行多种不同类型输入的测试,验证我们分类器的稳定性。 - -接下来,我们需要给分类器接上后续的大模型输出,例如,当 `label` 等于 `"buy_food"` 时,工作流便会精确地流向 `LLM_BuyFood` 节点。我们需要新建四个 LLM 节点,并设置不同的 System Prompt ;不同 System Prompt 的差异决定了它们不同的回应方式。 - -- LLM_BuyFood (点餐助手): - -你是一个点餐助手。要求:1. 确认用户想点的内容。2. 如果信息不完整,友好地补充询问。3. 语气礼貌简洁。 - -- LLM_Complain (客服专家): - -你是一个餐饮客服,专门处理抱怨。要求:1. 真诚道歉。2. 简要说明可能的原因(不推卸责任)。3. 给出清晰的下一步解决方案。 - -- LLM_Chitchat (聊天伙伴): - -你是一个帮人选吃的的聊天小助手。要求:1. 用轻松友好的语气。2. 给出 1~3 个简单推荐。3. 如果用户没有偏好,就给出不同风格的选择。 - -- LLM_Other (礼貌门卫): - -你是一个餐饮点餐小助手,只擅长跟‘吃’相关的话题。当用户说的话无关时:1. 礼貌说明自己的能力范围。2. 引导用户回到主场景。 - -值得注意的是,每个节点里面在填充了 SYSTEM 的提示词参数后,你还要记得启用 USER 提示词参数表。你需要在其中需要点击 `{x}` 符号,选择 `user_text` 参数作为用户输入,并且在前面加上 `user input:` 标识这个变量是用户输入的意思,在问答的时候会综合用户的最开始的输入和内置提示词进行回复。 - -同样的,为了确保一切顺利,你可以点击该节点右上角的播放箭进行具体的对话测试验证效果,比如对话说“我想要喝珍珠奶茶”等,查看回复是否符合预期。 - -![](images/image65.png) - -接下来我们处理并行 LLM 的输出值,我们在 `Variable Aggregator` 节点的配置面板中,找到 `ASSIGN VARIABLES`(分配变量)区域,点击后依次将之前的大模型回复加入即可。 - -![](images/image66.png) - -接下来我们需要对所有的输出进行聚合,最后得到我们想要的结果,包含用户的输入、分类、以及回复。由于我们使用的是 Workflow 而不是 Chatflow,故没有 Answer 节点选择进行结果的聚合,我们能够选择其他节点变相实现结果的聚合与输出,此时选择 Template 节点,在变量部分指定用户意图分类结果、用户的输入值、变量聚合的最终回复,并且在 CODE 中写入最后回复的 json 格式模板,我们可以得到: - -- `intent` ← `class_name` -- `original_text` ← `user_text` -- `final_reply` ← `variable_aggregator` - -```Plain -{ - "intent": "{{ intent }}", - "original_text": "{{ original_text }}", - "reply": {{ final_reply }} -} -``` - -![](images/image67.png) - -最后加入 output 节点即完成所有操作: - -![](images/image68.png) - -#### 工作流运行测试 - -大功告成,我们可以尝试运行这套工作流的效果。它能根据不同的输入,展现出截然不同的行为模式: - -- 输入 (点餐): "给我来一份香辣鸡腿堡套餐,大杯可乐。" - - 路径: `buy_food` → `LLM_BuyFood` - - 输出 reply: "好的,一份香辣鸡腿堡套餐和大杯可乐已为您记录。请问套餐里的薯条需要更换吗?" -- 输入 (抱怨): "你们也太慢了吧?我都等一个多小时了!" - - 路径: `complain` → `LLM_Complain` - - 输出 reply: "非常抱歉让您等了这么久,这确实是我们的疏忽,给您带来了很不好的体验。我们正在紧急核实您的订单配送情况,并会为您申请相应的补偿,感谢您的耐心与反馈。" -- 输入 (闲聊): "今天吃什么比较健康?" - - 路径: `chitchat` → `LLM_Complain` - - 输出 reply: "追求健康的话,可以试试我们的轻食沙拉系列,或者烤鸡胸肉配时蔬。您是喜欢口味清淡一些还是稍微丰富一点的呢?我可以给您更具体的推荐。" -- 输入 (无关内容): "帮我想个明天开会的笑话。" - - 路径: `other` → `LLM_Other` - - 输出 reply: "这听起来是个有趣的挑战!不过我主要是个美食推荐和点餐助手。如果您需要点些什么来犒劳一下辛苦工作的自己,我随时可以帮忙!" - -> 隐藏 Bug :需要说明的是,若你遇到与 aggregation group 相关的奇怪问题,这大概率是 Dify 的一个内置 bug。可能在特定操作下被触发;如果你曾经开启又关闭过 AGGREGATION GROUP,系统可能生成过 group 配置且残留了相关异常参数,即便现在开关看起来是关闭的,这些残留配置也可能导致问题,比如出现 `any` 相关参数的报错。此时你只需要删除该节点并重新创建即可。 - -在 Test Run 中运行后,我们能够看到工作流的执行过程,此时根据分类走了正确的流程,并得到了最后的 output 结果。至此,全流程完成。 - -![](images/image69.png) - -## 2.7 运行第一个模板 Workflow 应用 - -结束了简单的分类工作流学习,接下来我们需要学习如何运行别人的 workflow,我们只需要稍作改造就可以将其变成自己的工作流。在这里我们选择尝试官方的 DeepResearch 工作流,该工作流能够帮你构建一个深度搜索框架,使用大模型+搜索引擎给你一个丰富的搜索答案,每一次提问的结果将会包含搜索引用地址和大模型对话的结果。 - -导入后第一步直接运行,我们根据每一步报错的地方和原因解决具体问题即可,如果遇到解决不了的问题,你可以截图后询问大模型进行解决。 - -![](images/image70.png) - -刚进入感觉十分复杂,没关系,我们点击右上角的 Preview 运行工作流,直到报错出现: - -![](images/image71.png) - -![](images/image72.png) - -我们需要根据报错的节点解决问题,打开后发现是没有配置 Tavily 的 API Token,Tavily 的搜索API 是一个专为 AI 设计的搜索引擎,提供实时、准确和事实性的结果。此时根据提示操作: - -![](images/image73.png) - -经过处理后,搜索引擎能够正常工作: - -![](images/image74.png) - -继续修正模型调用导致的问题后,你应该能够得到如下结果,结合大模型理解下的详细搜索: - -![](images/image75.png) - -我们在最后能够看到对应的参考文档地址: - -![](images/image76.png) - -如果你想理解每个环节的作用,最好的方法是将每个环节的 output 记录为一个变量,最后在输出的时候打印每个中间变量的结果,还有一个方法就是你可以在上方找到 Process 的过程,点击后可以查看每个环节的细节: - -![](images/image77.png) - -## 2.8 将 Dify 作为 API 提供方 - -接下来,我们会尝试通过 API 调用刚才创建的知识库智能体 Agent,我们想要让 Dify 变成一个大模型中枢后端。 - -还记得之前讲过如何通过 API 调用模型吗?我们需要准备一个密钥(Key)和一份 API 调用示例(文档中的 request/response 示例),然后把这些内容发给大模型,让它帮我们写出调用服务的代码,并从返回结果中解析出我们需要的字段。 - -这一次,我们会使用本地的代码编辑工具 [Trae](https://www.trae.cn/) 来完成这个过程。 - -如果你还不熟悉什么是 IDE,可以先阅读文档 [Extra Knowledge 4 - What is AI IDE and Trae](https://github.com/datawhalechina/easy-vibe/blob/main/docs/extra/extra4/extra4-what-is-ai-ide-and-trae.md)。 - -如果你的本地开发环境还没有完整配置好,也不用担心。只要你信任自己的代码助手(不管是 [z.ai](http://z.ai) 还是 Trae),遇到任何不懂的地方或报错,都可以直接把问题抛给它,它会根据你的描述给出详细的解决方案。 - -![](images/image78.png) - -右侧的区域叫做 Copilot 交互窗口,或者 Agent 窗口。如果你看不到它,可以点击右上角的侧边栏图标来打开。 - -![](images/image79.png) - -打开侧边栏后,你会看到 `Builder` 选项。这就是 Agent 模式。你可以简单地把 “Builder” 理解为 [z.ai](http://z.ai) 的“开发模式”,它同样可以帮你操作本地电脑环境、安装依赖、打开网页等。 - -![](images/image80.png) - -点击 “Builder” 后,你会看到 “Chat” 模式和 “Builder with MCP” 模式。 Chat 模式主要用于与当前文件夹进行交互,或者和大模型进行自然语言对话。(你可以通过点击 Trae 左上角的 “File” 打开一个文件夹,然后在该文件夹内进行编辑。这种情况下,Builder 所有的新建文件操作都会发生在这个文件夹中。) - -Builder with MCP 模式则为 Agent 提供了更多工具(例如让大模型连接到其他软件、获取天气信息等)。你可以简单地认为 MCP 是一个让大模型更方便调用各种外部工具的能力集合。 - -![](images/image81.png) - -在下方区域,你还可以看到模型选择的下拉列表,可以点击切换不同模型。这里你可以选择 Kimi k2 或 GLM。如果你使用的是国际版 Trae,也可以选择 ChatGPT 或 Claude。 不过,随着国内大模型的快速发展,Kimi、Qwen、GLM 等模型的综合能力已经基本接近 Claude 3.5 或 3.7,对于日常开发场景来说完全够用。 - -![](images/image82.png) - -上面是对 Trae 的一个简要介绍。接下来,我们可以回顾在 [z.ai](http://z.ai) 中的操作步骤,并在 Trae 中复用这些思路。 - -## 2.9 利用 Dify API 创建前端对话应用 - -如果我们想用 Dify 的 API 搭建一个前端聊天应用,首先需要获取 Dify 的 API 文档和调用地址。 - -还记得刚才创建的那个 Agent 吗? 先点击右上角的 “Publish”,然后点击 “Publish Update”,最后点击 “Access API Reference” 进入 API 文档。 - -![](images/image83.png) - -![](images/image84.png) - -进入 API 文档后,找到 “Send Chat Message” 这一部分,点击进入,然后在右侧找到 “Request” 和 “Response” 示例并复制出来。 - -为什么一定要复制这两部分内容? 因为它们是 API 的“核心信息”: 有了 Key、请求示例和返回示例,我们就可以让大模型帮我们生成调用服务的代码,并且根据返回结构把需要的字段提取出来。 - -![](images/image85.png) - -![](images/image86.png) - -在找到会话所需的 Request 和 Response 示例之后,我们还需要获取一个 API Key。在文档右上角,你会看到 “API key” 相关选项。 - -![](images/image87.png) - -点击 “Create new Secret key”,就可以创建属于你自己的 API Key。 - -![](images/image88.png) - -现在一切准备就绪。我们会把刚才拿到的 API Key、Request 示例和 Response 示例一起交给 Trae Builder。 - -注意:请将 `{DIFY_API_URL}` 替换为实际的 Dify API 地址。 - -```JSON -key: -app-zKdCHUXXXXXXXX - -Please write me a front-end based on the following reference: - -curl -X POST 'http://{DIFY_API_URL}/v1/chat-messages' \ ---header 'Authorization: Bearer {api_key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "inputs": {}, - "query": "What are the specs of the iPhone 13 Pro Max?", - "response_mode": "streaming", - "conversation_id": "", - "user": "abc-123", - "files": [ - { - "type": "image", - "transfer_method": "remote_url", - "url": "https://cloud.dify.ai/logo/logo-site.png" - } - ] -}' - -{ - "event": "message", - "task_id": "c3800678-a077-43df-a102-53f23ed20b88", - "id": "9da23599-e713-473b-982c-4328d4f5c78a", - "message_id": "9da23599-e713-473b-982c-4328d4f5c78a", - "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", - "mode": "chat", - "answer": "iPhone 13 Pro Max specs are listed here:...", - "metadata": { - "usage": { - "prompt_tokens": 1033, - "prompt_unit_price": "0.001", - "prompt_price_unit": "0.001", - "prompt_price": "0.0010330", - "completion_tokens": 128, - "completion_unit_price": "0.002", - "completion_price_unit": "0.001", - "completion_price": "0.0002560", - "total_tokens": 1161, - "total_price": "0.0012890", - "currency": "USD", - "latency": 0.7682376249867957 - }, - "retriever_resources": [ - { - "position": 1, - "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", - "dataset_name": "iPhone", - "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", - "document_name": "iPhone List", - "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", - "score": 0.98457545, - "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\"" - } - ] - }, - "created_at": 1705407629 -} -``` - -![](images/image89.png) - -在这个阶段,你可能会发现生成出来的程序并不能一次性正常运行——比如对话会出现奇怪的错误,或者没有任何返回结果。当出现这种情况时,你可以尝试切换到另一个大语言模型,或者把错误信息复制出来,详细描述问题,再发给模型让它根据反馈继续迭代。 - -此时你的工作方式已经非常接近真实开发过程了。在日常开发中,我们经常会在与大模型协作时遇到各种问题,为了更好地解决这些问题,我们需要提供更多上下文信息。除了提供错误信息,你还可以复制更完整的文档内容(例如在文档左侧 “Send message” 部分中复制更多说明),一并交给模型,让它在更多细节的基础上给出更完整的解决方案。 - -![](images/image90.png) - -此时浏览器是嵌在 Trae 内部的。你可以点击顶部的指南针图标,把网页在外部浏览器中全屏打开。 - -![](images/image91.png) - -如果运气不错,你可能在第一次尝试时就能获得一个可以正常交互的前端页面。 - -![](images/image92.png) - -不过,由于大模型本身具有一定随机性,有时你可能在单轮对话中一切顺利,但在多轮对话时出现异常。因此,建议你进行多轮对话测试,确保程序在多轮交互场景下也能稳定运行。 - -![](images/image93.png) - -到这里,你已经学会了如何构建一个简单的 Dify 知识库 Agent,并使用 Trae 替代 [z.ai](http://z.ai) 来搭建一个交互式前端。从现在开始,Trae 将成为我们构建各种原型时的主要开发工具,逐步取代 [z.ai](http://z.ai)。你可以尝试用 Trae 重新实现之前的贪吃蛇游戏,看看会有什么不同的体验。加油! - -# 3. 更多业务工作流参考 - -你可以在搜索引擎上使用类似关键词搜索 `Dify workflow 参考`,或者直接在 Github 中找到 Dify 工作流分享仓库进行参考工作流的查找(质量参差不齐,你需要查看多个不同仓库学习)。当然,所谓的工作流只不过是业务上 SOP 的映射,你可以思考有哪些日常工作中的流程或者学习中的流程是重复可固化的,只需要把它变成工作流固定即可。 - -以下是一些大模型生成的工作流设计的参考(实际上的实现方案也比较类似,一般来说人类设计的工作流不会有大模型设计的优美,除非是高手设置的工作流),如果你觉得哪些点子有意思,可以将它发给大模型进一步细化,让大模型帮你给出更具体的 Dify 工作流节点设定,以及内部的细节结果。 - -## 3.1 社媒平台工作流 - -1. 跨平台内容一键分发工作流(复杂) - 1. 思路:以一篇核心稿件为“原料”,自动加工成适配多个平台的“成品”。 - 2. 实现:`Start` 输入文章 -> `LLM` 润色 -> 并行多个 `LLM` 节点(每个节点Prompt扮演特定平台专家,如“小红书爆款文案专家”、“知乎专业答主”)-> `Iterator` 节点循环处理不同平台格式要求 -> `Variable Aggregator` 汇总 -> `Answer` 输出所有版本。复杂度在于并行处理和循环迭代。 -2. 热点话题选题与初稿生成器(中等) - 1. 思路:自动捕捉网络热点,快速生成选题和内容草稿。 - 2. 实现:`Start` 输入关键词 -> `Tool` 节点调用搜索引擎API抓取热点 -> `LLM` 摘要提炼出3-5个话题 -> `LLM` 生成文章大纲或初稿。复杂度在于外部工具集成与信息筛选。 -3. 评论区智能分类与回复助手(复杂) - 1. 思路:自动分析评论情感与意图,生成分类回复建议。 - 2. 实现:`HTTP Request` 节点接入社媒API获取评论 -> `Question Classifier` 或 `LLM` 节点进行多标签分类(积极、疑问、投诉、广告等)-> `Condition` 判断节点路由至不同回复生成链 -> 并行 `LLM` 节点生成个性化回复草稿 -> `Answer` 输出。复杂度在于条件分支和实时API调用。 -4. 短视频脚本与分镜自动生成器(复杂) - 1. 思路:根据一个热门话题或产品描述,自动生成短视频脚本、分镜描述和推荐标签。 - 2. 实现:`Start` 输入主题 -> `LLM` 生成创意脚本 -> 第二个 `LLM` 节点将脚本拆解为场景序列(画面描述、台词、时长)-> `Tool` 节点调用文本转语音服务生成语音样本 -> `Variable Aggregator` 整合所有元素 -> `Answer` 输出结构化脚本文件。复杂度在于多步骤序列化和外部服务集成。 -5. 直播互动问答实时摘要助手(中等) - 1. 思路:实时处理直播间的文字评论,提炼核心问题和观众反馈。 - 2. 实现:`HTTP Request` 节点流式获取直播评论 -> `Iterator` 节点以时间窗口为单位处理批数据 -> `LLM` 节点实时总结每段时间内的热点问题与情绪倾向 -> `Answer` 或 `Webhook` 节点输出摘要给主播。复杂度在于实时流数据处理和循环窗口。 - -## 3.2 职场工作流 - -1. 智能会议纪要与任务自动派发系统(复杂) - 1. 思路:从会议录音文本中提取纪要,并自动创建任务。 - 2. 实现:`Start` 输入会议文本 -> `LLM` 总结议题与结论 -> `Parameter Extractor` 节点精准抽取Action Items(任务、负责人、DDL)-> 一个 `LLM` 整合成纪要邮件 -> 并行 `HTTP Request` 节点调用Jira/Trello/飞书API创建任务。复杂度在于信息抽取与多系统联动。 -2. 简历批量筛选与初步评估助手(中等) - 1. 思路:自动解析简历,进行匹配度评估并生成面试问题。 - 2. 实现:`Start` 上传简历文件与JD -> `Document Extractor` 节点解析简历文本 -> `LLM` 扮演HR进行匹配度评估 -> 对高匹配者,另一个 `LLM` 生成深度面试问题。复杂度在于文档解析与多条件评估。 -3. 多语言邮件一键翻译与草稿回复(简单) - 1. 思路:自动翻译邮件并起草回复。 - 2. 实现:`Start` 输入邮件 -> `LLM` 判断语种并翻译 -> `LLM` 构思回复要点 -> `LLM` 翻译回原始语言并润色。主要依赖于LLM的序列调用。 -4. 周报/月报数据自动汇总与洞察生成(复杂) - 1. 思路:连接多个数据源,自动生成结构化工作报告。 - 2. 实现:多个 `HTTP Request`/`Tool` 节点并行调用业务系统API(如CRM、Git、项目管理工具)获取原始数据 -> `Code` 节点或 `LLM` 进行数据清洗与基础计算 -> `LLM` 分析趋势、亮点与风险,生成叙述性报告 -> `Answer` 输出图文并茂的文档。复杂度在于多数据源聚合、数据处理与智能分析结合。 -5. 合同/文档智能审查与要点提炼(中等) - 1. 思路:快速审查法律或商务文档,提示风险并提炼核心条款。 - 2. 实现:`Start` 上传合同PDF -> `Document Extractor` 提取文本 -> `LLM` 节点(设定为法律专家角色)审查责任条款、支付条件、违约条款等 -> `Parameter Extractor` 节点抽取出关键日期、金额、义务方等结构化数据 -> `Answer` 输出风险提示和要点表格。复杂度在于长文档处理与结构化信息抽取。 - -## 3.3 学习生活工作流 - -1. 学术论文深度解析与笔记生成器(复杂) - 1. 思路:上传论文PDF,自动生成结构化笔记。 - 2. 实现:`Start` 上传PDF -> `Document Extractor` 提取全文 -> 并行多个 `LLM` 节点分工总结摘要、方法、发现、参考文献 -> `Variable Aggregator` 汇总 -> `Answer` 输出Markdown笔记。复杂度在于并行处理长文本的不同部分。 - -2. 个性化旅行计划定制师(中等) - 1. 思路:根据用户偏好,自动规划详尽行程。 - 2. 实现:`Start` 输入需求(目的地、天数、预算、兴趣)-> `Tool` 节点调用搜索引擎或地图API获取地点信息 -> `LLM` 整合信息,设计每日行程(含时间、活动、预算估算)。复杂度在于外部信息获取与结构化规划。 - -3. 外语学习互动陪练伙伴(简单) - 1. 思路:创建可角色扮演和语法纠错的对话机器人。 - 2. 实现:系统设定AI角色 -> `Start` 接收用户语句 -> `LLM` 执行两项任务:角色回复 + 语法纠错与解释 -> `Answer` 输出。核心是LLM的多任务指令。 - -4. 个人知识库问答与链接推荐系统(复杂) - 1. 思路:基于你收藏的文档、笔记、网页链接,构建一个可问答并能推荐相关旧知识的智能系统。 - 2. 实现:离线处理:使用 `Document Extractor` 和 `Embedding` 工具将个人知识库切片并向量化存储。在线工作流:`Start` 输入问题 -> `Retrieval` 节点从向量库中查找最相关的知识片段 -> `LLM` 基于检索到的上下文生成答案 -> 同时,另一个分支使用检索到的内容作为输入,通过 `LLM` 生成“相关旧知识”推荐列表 -> `Answer` 合并输出答案与推荐。复杂度在于检索增强生成(RAG)流程的构建。 - -5. 健身/饮食计划追踪与调整顾问(中等) - 1. 思路:根据用户输入的每日饮食和训练日志,提供营养分析与训练建议。 - 2. 实现:`Start` 输入文本日志(如“午餐:鸡胸肉150g,米饭一碗,蔬菜若干;训练:深蹲5组”)-> `Parameter Extractor` 节点尝试结构化输入数据 -> `LLM` 扮演健身教练,分析营养摄入是否均衡、训练容量是否合适 -> 对比长期目标,给出微调建议(如“蛋白质摄入充足,建议增加蔬菜种类”)。复杂度在于从非结构化日志中提取结构化信息并提供个性化反馈。 - -# 6. 工作流平台的局限性 - -工作流平台(或称低代码平台)并非万能解决方案。它虽然对业务人员友好,降低了直接编码的门槛,但从另一个角度看,“低代码”往往也是一种“高代码”——用户仍需理解平台的概念、规则与操作逻辑,这本身构成了一种新的学习成本。 - -也许你想问,很多简单的工作流其实就是大模型函数包装后的前后调用,前面函数的输出作为后者函数的输入,本质上几行代码就能够解决,为什么需要那么复杂的多重包装工作流?反而给 API 调用造成了麻烦。 - -你说得是对的。在当前 vibe coding 的快速发展下,借助 AI 代码生成能力,直接阅读甚至生成代码有时可能更加高效。理想情况下,我们希望能用自然语言直接操作应用逻辑,这才是一个现代的软件平台。但目前的工作流平台尚未实现这一点,因此它在用户意图与最终实现之间天然存在一个“中间层”。掌握这个中间层,正是一种需要投入时间学习的成本。理想上,之后的工作流平台也要支持全 AI 自动对话操作,我们可以让 AI 真正操作工作流搭建以及入参的每一个细节环节。 - -尽管如此,熟练使用这类平台正逐渐成为一项基础技能,如同微软的办公软件一样,在业务中非常普遍且实用,值得掌握。 - -在后续的进阶课程中,我们将介绍如何通过代码级别的工作流与 RAG 开发平台进行构建。届时,你可以亲身体验不同实现方式在复杂度与灵活性上的区别。(值得注意的是,一些简单的对话应用或嵌套逻辑,用工作流实现可能并不困难。) - -# 📚 课后作业 - -## 掌握 Dify 基本操作 - -为了测试你掌握了 Dify 的常见基础使用工具,你需要完成一个基础作业和两个 “小挑战”,确保你已入门常见的操作。你需要将附带的两个 DSL 文件导入 Dify 工作流,并成功完成对应工作流的挑战(遇到不懂的地方截图询问大模型,或自己探索其中的每个参数的用法,最后实现目标)。: - -1. 参考意图分类工作流的方法,让大模型给你建议完全换一套场景进行应用,但是一定要用到意图分类工作流,最后提交运行的工作流截图、场景说明、结果。 -2. Log in workflow 工作流解密挑战 - -在这个解密挑战中,你需要完成以下挑战,让工作流实现下列功能: - -- 找出正确的密码! -- 将密码修改为 0925 -- 当密码不正确时,提供第二次尝试机会(不提供第三次) -- 当用户提及要再次登录时,为用户提供重新输入密码的机会 - -![](images/image94.png) - -参考输入输出: - -![](images/image95.png) - -3. Love loop workflow 工作流解密挑战 - -![](images/image96.png) - -在这个解密挑战中,你需要修复当前工作流的问题,让工作流最后的输出类似如下显示: - -![](images/image97.png) - -如果你遇到无法解决的问题,请截图询问大模型,或查阅官方文档得到结果:[https://docs.dify.ai/en/use-dify/getting-started/quick-start](https://docs.dify.ai/en/use-dify/getting-started/quick-start) - -## 实现 Dify API 调用 - -为了测试你真正掌握了 Dify 的 API 调用知识,你需要完成以下任务: - -1. 部署 Dify 并创建一个简单的知识库(选取你喜欢的资料)。 -2. 使用 Trae IDE 构建一个对话前端,与 Dify 知识库进行 API 交互。 -3. 测试多轮对话的效果,确保程序正常运行。 - -你需要提交最终运行截图和知识库的处理过程截图。 - -## 试用第三方工作流 / 构建一个自己的业务工作流 - -请你在 Github、微信公众号、或者 Reddit、推特上等所有地方找到你想尝试的别人的 Dify 工作流,下载导入后成功运行;或者你可以根据上文中提到的业务工作流参考,根据现实中的具体需求创建一个自己的业务工作流进行运行。 - -最后你需要提交运行成功的截图,并说明这个工作流的作用。 - -# [Bug] HTTP 请求错误问题的解决方法 - -如果你遇到了如下图所示的问题,才需要参考本节方案进行解决,否则可以不理会当前部分。 - -有时候可能你会把 Dify 部署在自己的服务器,但是服务器的对外地址通常都是 http 而不是 https 的,但当我们请求一个只支持 HTTP 的服务时,你可能会看到类似这样的提示(启用 F12 浏览器调试信息模式,查看有问题的点): - -![](images/image98.png) - -出现这个问题的原因,是因为我们默认把 Dify 部署在一台只支持 HTTP 而不支持 HTTPS 的服务器上。 HTTPS(HyperText Transfer Protocol Secure)是在 HTTP(超文本传输协议)的基础上增加了 SSL/TLS 加密层,可以简单理解为“更安全版的 HTTP”。 - -如果要让服务支持 HTTPS,一般可以: - -- 使用其他程序转发请求(例如在有证书的 nginx 上做反向代理),或者 -- 绑定域名后为该域名申请证书。 - -但这些操作都比较复杂,在这里我们使用 Zeabur 作为网络转发网关来解决问题。 - -Zeabur 的网页默认是通过 HTTPS 访问的,因此我们只需要把原来请求的域名转发到 Zeabur 提供的域名,就可以修复这个问题。 - -- 原始地址:`http://{DIFY_API_URL}/v1/chat-messages` -- 现在地址:`https://{DIFY_NEW_API_URL}.zeabur.app/v1/chat-messages` - -你只需要简单地把 URL 中的域名部分(公网 IP 或域名)替换为已经在 Zeabur 上部署好的域名即可,我们已经提前在服务里配置好了转发功能。 - -如果你感兴趣,也可以自己在 Zeabur 上部署一个转发服务。在 Zeabur 中创建服务时,选择 Python,然后填入下面的 Python 代码,部署后即可得到一个 https 的地址,https 即可正常使用。 - -部署完成后,在网络设置中把程序监听端口设置为本地 8080,并对外暴露该端口。 - -注意:请将 `{DIFY_API_URL}` 替换为实际的 Dify API 地址。 - -```Python -from flask import Flask, request, Response -import requests - -app = Flask(__name__) - -TARGET_BASE_URL = "{DIFY_API_URL}" -LISTEN_PORT = 8080 - -@app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD']) -@app.route('/', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD']) -def proxy_request(path): - target_url = f"{TARGET_BASE_URL}/{path}" - if request.query_string: - target_url += f"?{request.query_string.decode('utf-8')}" - - headers = {key: value for key, value in request.headers if key.lower() not in ['host', 'connection', 'content-length', 'accept-encoding']} - - try: - resp = requests.request( - method=request.method, - url=target_url, - headers=headers, - data=request.get_data(), - cookies=request.cookies, - allow_redirects=False, - timeout=30 - ) - - excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] - response_headers = [(name, value) for name, value in resp.raw.headers.items() if name.lower() not in excluded_headers] - - return Response(resp.content, resp.status_code, response_headers) - - except requests.exceptions.RequestException as e: - print(f"Error forwarding request to {target_url}: {e}") - return Response(f"Proxy Error: Could not reach target server or invalid response: {e}", status=502) - except Exception as e: - print(f"An unexpected error occurred: {e}") - return Response(f"Internal Proxy Error: {e}", status=500) - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=LISTEN_PORT, debug=True) -``` diff --git a/docs/project/chapter3/images/image1.png b/docs/project/chapter3/images/image1.png deleted file mode 100644 index 0c0df71..0000000 Binary files a/docs/project/chapter3/images/image1.png and /dev/null differ diff --git a/docs/project/chapter3/images/image10.png b/docs/project/chapter3/images/image10.png deleted file mode 100644 index cdcdc6b..0000000 Binary files a/docs/project/chapter3/images/image10.png and /dev/null differ diff --git a/docs/project/chapter3/images/image11.png b/docs/project/chapter3/images/image11.png deleted file mode 100644 index d8df91f..0000000 Binary files a/docs/project/chapter3/images/image11.png and /dev/null differ diff --git a/docs/project/chapter3/images/image12.png b/docs/project/chapter3/images/image12.png deleted file mode 100644 index 0783dc3..0000000 Binary files a/docs/project/chapter3/images/image12.png and /dev/null differ diff --git a/docs/project/chapter3/images/image13.png b/docs/project/chapter3/images/image13.png deleted file mode 100644 index 6cdc6dd..0000000 Binary files a/docs/project/chapter3/images/image13.png and /dev/null differ diff --git a/docs/project/chapter3/images/image14.png b/docs/project/chapter3/images/image14.png deleted file mode 100644 index ae894d0..0000000 Binary files a/docs/project/chapter3/images/image14.png and /dev/null differ diff --git a/docs/project/chapter3/images/image15.png b/docs/project/chapter3/images/image15.png deleted file mode 100644 index d60f458..0000000 Binary files a/docs/project/chapter3/images/image15.png and /dev/null differ diff --git a/docs/project/chapter3/images/image16.png b/docs/project/chapter3/images/image16.png deleted file mode 100644 index 15e421e..0000000 Binary files a/docs/project/chapter3/images/image16.png and /dev/null differ diff --git a/docs/project/chapter3/images/image17.png b/docs/project/chapter3/images/image17.png deleted file mode 100644 index dbab625..0000000 Binary files a/docs/project/chapter3/images/image17.png and /dev/null differ diff --git a/docs/project/chapter3/images/image18.png b/docs/project/chapter3/images/image18.png deleted file mode 100644 index 923ff17..0000000 Binary files a/docs/project/chapter3/images/image18.png and /dev/null differ diff --git a/docs/project/chapter3/images/image19.png b/docs/project/chapter3/images/image19.png deleted file mode 100644 index 1e27f8a..0000000 Binary files a/docs/project/chapter3/images/image19.png and /dev/null differ diff --git a/docs/project/chapter3/images/image2.png b/docs/project/chapter3/images/image2.png deleted file mode 100644 index 740203c..0000000 Binary files a/docs/project/chapter3/images/image2.png and /dev/null differ diff --git a/docs/project/chapter3/images/image20.png b/docs/project/chapter3/images/image20.png deleted file mode 100644 index 9e607a2..0000000 Binary files a/docs/project/chapter3/images/image20.png and /dev/null differ diff --git a/docs/project/chapter3/images/image21.png b/docs/project/chapter3/images/image21.png deleted file mode 100644 index c95d577..0000000 Binary files a/docs/project/chapter3/images/image21.png and /dev/null differ diff --git a/docs/project/chapter3/images/image22.png b/docs/project/chapter3/images/image22.png deleted file mode 100644 index 6c888e0..0000000 Binary files a/docs/project/chapter3/images/image22.png and /dev/null differ diff --git a/docs/project/chapter3/images/image23.png b/docs/project/chapter3/images/image23.png deleted file mode 100644 index ffede5e..0000000 Binary files a/docs/project/chapter3/images/image23.png and /dev/null differ diff --git a/docs/project/chapter3/images/image24.png b/docs/project/chapter3/images/image24.png deleted file mode 100644 index 1235149..0000000 Binary files a/docs/project/chapter3/images/image24.png and /dev/null differ diff --git a/docs/project/chapter3/images/image25.png b/docs/project/chapter3/images/image25.png deleted file mode 100644 index 812428d..0000000 Binary files a/docs/project/chapter3/images/image25.png and /dev/null differ diff --git a/docs/project/chapter3/images/image26.png b/docs/project/chapter3/images/image26.png deleted file mode 100644 index 8cb4da9..0000000 Binary files a/docs/project/chapter3/images/image26.png and /dev/null differ diff --git a/docs/project/chapter3/images/image27.png b/docs/project/chapter3/images/image27.png deleted file mode 100644 index 121cfcc..0000000 Binary files a/docs/project/chapter3/images/image27.png and /dev/null differ diff --git a/docs/project/chapter3/images/image28.png b/docs/project/chapter3/images/image28.png deleted file mode 100644 index c22432b..0000000 Binary files a/docs/project/chapter3/images/image28.png and /dev/null differ diff --git a/docs/project/chapter3/images/image29.png b/docs/project/chapter3/images/image29.png deleted file mode 100644 index b8a5f12..0000000 Binary files a/docs/project/chapter3/images/image29.png and /dev/null differ diff --git a/docs/project/chapter3/images/image3.png b/docs/project/chapter3/images/image3.png deleted file mode 100644 index 987cc82..0000000 Binary files a/docs/project/chapter3/images/image3.png and /dev/null differ diff --git a/docs/project/chapter3/images/image30.png b/docs/project/chapter3/images/image30.png deleted file mode 100644 index ce670a0..0000000 Binary files a/docs/project/chapter3/images/image30.png and /dev/null differ diff --git a/docs/project/chapter3/images/image31.png b/docs/project/chapter3/images/image31.png deleted file mode 100644 index b953bfa..0000000 Binary files a/docs/project/chapter3/images/image31.png and /dev/null differ diff --git a/docs/project/chapter3/images/image32.png b/docs/project/chapter3/images/image32.png deleted file mode 100644 index bd2f03d..0000000 Binary files a/docs/project/chapter3/images/image32.png and /dev/null differ diff --git a/docs/project/chapter3/images/image33.png b/docs/project/chapter3/images/image33.png deleted file mode 100644 index 48e052d..0000000 Binary files a/docs/project/chapter3/images/image33.png and /dev/null differ diff --git a/docs/project/chapter3/images/image34.png b/docs/project/chapter3/images/image34.png deleted file mode 100644 index 38bb149..0000000 Binary files a/docs/project/chapter3/images/image34.png and /dev/null differ diff --git a/docs/project/chapter3/images/image35.png b/docs/project/chapter3/images/image35.png deleted file mode 100644 index 26ed3dc..0000000 Binary files a/docs/project/chapter3/images/image35.png and /dev/null differ diff --git a/docs/project/chapter3/images/image36.png b/docs/project/chapter3/images/image36.png deleted file mode 100644 index abbd6f4..0000000 Binary files a/docs/project/chapter3/images/image36.png and /dev/null differ diff --git a/docs/project/chapter3/images/image37.png b/docs/project/chapter3/images/image37.png deleted file mode 100644 index 9f65d19..0000000 Binary files a/docs/project/chapter3/images/image37.png and /dev/null differ diff --git a/docs/project/chapter3/images/image38.png b/docs/project/chapter3/images/image38.png deleted file mode 100644 index d4a7490..0000000 Binary files a/docs/project/chapter3/images/image38.png and /dev/null differ diff --git a/docs/project/chapter3/images/image39.png b/docs/project/chapter3/images/image39.png deleted file mode 100644 index 8a1af36..0000000 Binary files a/docs/project/chapter3/images/image39.png and /dev/null differ diff --git a/docs/project/chapter3/images/image4.png b/docs/project/chapter3/images/image4.png deleted file mode 100644 index a57678c..0000000 Binary files a/docs/project/chapter3/images/image4.png and /dev/null differ diff --git a/docs/project/chapter3/images/image40.png b/docs/project/chapter3/images/image40.png deleted file mode 100644 index 49c7774..0000000 Binary files a/docs/project/chapter3/images/image40.png and /dev/null differ diff --git a/docs/project/chapter3/images/image41.png b/docs/project/chapter3/images/image41.png deleted file mode 100644 index 03497ec..0000000 Binary files a/docs/project/chapter3/images/image41.png and /dev/null differ diff --git a/docs/project/chapter3/images/image42.png b/docs/project/chapter3/images/image42.png deleted file mode 100644 index 506ceec..0000000 Binary files a/docs/project/chapter3/images/image42.png and /dev/null differ diff --git a/docs/project/chapter3/images/image43.png b/docs/project/chapter3/images/image43.png deleted file mode 100644 index 8cf908f..0000000 Binary files a/docs/project/chapter3/images/image43.png and /dev/null differ diff --git a/docs/project/chapter3/images/image44.png b/docs/project/chapter3/images/image44.png deleted file mode 100644 index 6042e58..0000000 Binary files a/docs/project/chapter3/images/image44.png and /dev/null differ diff --git a/docs/project/chapter3/images/image45.png b/docs/project/chapter3/images/image45.png deleted file mode 100644 index e41a490..0000000 Binary files a/docs/project/chapter3/images/image45.png and /dev/null differ diff --git a/docs/project/chapter3/images/image46.png b/docs/project/chapter3/images/image46.png deleted file mode 100644 index 6f46315..0000000 Binary files a/docs/project/chapter3/images/image46.png and /dev/null differ diff --git a/docs/project/chapter3/images/image47.png b/docs/project/chapter3/images/image47.png deleted file mode 100644 index c854dd6..0000000 Binary files a/docs/project/chapter3/images/image47.png and /dev/null differ diff --git a/docs/project/chapter3/images/image48.png b/docs/project/chapter3/images/image48.png deleted file mode 100644 index 43e1d00..0000000 Binary files a/docs/project/chapter3/images/image48.png and /dev/null differ diff --git a/docs/project/chapter3/images/image49.png b/docs/project/chapter3/images/image49.png deleted file mode 100644 index e769dc7..0000000 Binary files a/docs/project/chapter3/images/image49.png and /dev/null differ diff --git a/docs/project/chapter3/images/image5.png b/docs/project/chapter3/images/image5.png deleted file mode 100644 index b13bd39..0000000 Binary files a/docs/project/chapter3/images/image5.png and /dev/null differ diff --git a/docs/project/chapter3/images/image50.png b/docs/project/chapter3/images/image50.png deleted file mode 100644 index 56f3d6e..0000000 Binary files a/docs/project/chapter3/images/image50.png and /dev/null differ diff --git a/docs/project/chapter3/images/image51.png b/docs/project/chapter3/images/image51.png deleted file mode 100644 index fea3763..0000000 Binary files a/docs/project/chapter3/images/image51.png and /dev/null differ diff --git a/docs/project/chapter3/images/image52.png b/docs/project/chapter3/images/image52.png deleted file mode 100644 index 415bb91..0000000 Binary files a/docs/project/chapter3/images/image52.png and /dev/null differ diff --git a/docs/project/chapter3/images/image53.png b/docs/project/chapter3/images/image53.png deleted file mode 100644 index 1e35b54..0000000 Binary files a/docs/project/chapter3/images/image53.png and /dev/null differ diff --git a/docs/project/chapter3/images/image54.png b/docs/project/chapter3/images/image54.png deleted file mode 100644 index ffaa969..0000000 Binary files a/docs/project/chapter3/images/image54.png and /dev/null differ diff --git a/docs/project/chapter3/images/image55.png b/docs/project/chapter3/images/image55.png deleted file mode 100644 index a453803..0000000 Binary files a/docs/project/chapter3/images/image55.png and /dev/null differ diff --git a/docs/project/chapter3/images/image56.png b/docs/project/chapter3/images/image56.png deleted file mode 100644 index 3bfc40e..0000000 Binary files a/docs/project/chapter3/images/image56.png and /dev/null differ diff --git a/docs/project/chapter3/images/image57.png b/docs/project/chapter3/images/image57.png deleted file mode 100644 index 8cff680..0000000 Binary files a/docs/project/chapter3/images/image57.png and /dev/null differ diff --git a/docs/project/chapter3/images/image58.png b/docs/project/chapter3/images/image58.png deleted file mode 100644 index f7eab17..0000000 Binary files a/docs/project/chapter3/images/image58.png and /dev/null differ diff --git a/docs/project/chapter3/images/image59.png b/docs/project/chapter3/images/image59.png deleted file mode 100644 index ec4bfb3..0000000 Binary files a/docs/project/chapter3/images/image59.png and /dev/null differ diff --git a/docs/project/chapter3/images/image6.png b/docs/project/chapter3/images/image6.png deleted file mode 100644 index 2670a9c..0000000 Binary files a/docs/project/chapter3/images/image6.png and /dev/null differ diff --git a/docs/project/chapter3/images/image60.png b/docs/project/chapter3/images/image60.png deleted file mode 100644 index 492085f..0000000 Binary files a/docs/project/chapter3/images/image60.png and /dev/null differ diff --git a/docs/project/chapter3/images/image61.png b/docs/project/chapter3/images/image61.png deleted file mode 100644 index d9152b4..0000000 Binary files a/docs/project/chapter3/images/image61.png and /dev/null differ diff --git a/docs/project/chapter3/images/image62.png b/docs/project/chapter3/images/image62.png deleted file mode 100644 index cc11f8b..0000000 Binary files a/docs/project/chapter3/images/image62.png and /dev/null differ diff --git a/docs/project/chapter3/images/image63.png b/docs/project/chapter3/images/image63.png deleted file mode 100644 index cfcebeb..0000000 Binary files a/docs/project/chapter3/images/image63.png and /dev/null differ diff --git a/docs/project/chapter3/images/image64.png b/docs/project/chapter3/images/image64.png deleted file mode 100644 index 13307ae..0000000 Binary files a/docs/project/chapter3/images/image64.png and /dev/null differ diff --git a/docs/project/chapter3/images/image65.png b/docs/project/chapter3/images/image65.png deleted file mode 100644 index 8194e1a..0000000 Binary files a/docs/project/chapter3/images/image65.png and /dev/null differ diff --git a/docs/project/chapter3/images/image66.png b/docs/project/chapter3/images/image66.png deleted file mode 100644 index d9dbb32..0000000 Binary files a/docs/project/chapter3/images/image66.png and /dev/null differ diff --git a/docs/project/chapter3/images/image67.png b/docs/project/chapter3/images/image67.png deleted file mode 100644 index a31e607..0000000 Binary files a/docs/project/chapter3/images/image67.png and /dev/null differ diff --git a/docs/project/chapter3/images/image68.png b/docs/project/chapter3/images/image68.png deleted file mode 100644 index ba7155d..0000000 Binary files a/docs/project/chapter3/images/image68.png and /dev/null differ diff --git a/docs/project/chapter3/images/image69.png b/docs/project/chapter3/images/image69.png deleted file mode 100644 index 59f71fc..0000000 Binary files a/docs/project/chapter3/images/image69.png and /dev/null differ diff --git a/docs/project/chapter3/images/image7.png b/docs/project/chapter3/images/image7.png deleted file mode 100644 index d8bd6db..0000000 Binary files a/docs/project/chapter3/images/image7.png and /dev/null differ diff --git a/docs/project/chapter3/images/image70.png b/docs/project/chapter3/images/image70.png deleted file mode 100644 index 55a6dad..0000000 Binary files a/docs/project/chapter3/images/image70.png and /dev/null differ diff --git a/docs/project/chapter3/images/image71.png b/docs/project/chapter3/images/image71.png deleted file mode 100644 index 578f576..0000000 Binary files a/docs/project/chapter3/images/image71.png and /dev/null differ diff --git a/docs/project/chapter3/images/image72.png b/docs/project/chapter3/images/image72.png deleted file mode 100644 index 13779f9..0000000 Binary files a/docs/project/chapter3/images/image72.png and /dev/null differ diff --git a/docs/project/chapter3/images/image73.png b/docs/project/chapter3/images/image73.png deleted file mode 100644 index b2f5e49..0000000 Binary files a/docs/project/chapter3/images/image73.png and /dev/null differ diff --git a/docs/project/chapter3/images/image74.png b/docs/project/chapter3/images/image74.png deleted file mode 100644 index 4dcaf95..0000000 Binary files a/docs/project/chapter3/images/image74.png and /dev/null differ diff --git a/docs/project/chapter3/images/image75.png b/docs/project/chapter3/images/image75.png deleted file mode 100644 index f91102a..0000000 Binary files a/docs/project/chapter3/images/image75.png and /dev/null differ diff --git a/docs/project/chapter3/images/image76.png b/docs/project/chapter3/images/image76.png deleted file mode 100644 index 090bca8..0000000 Binary files a/docs/project/chapter3/images/image76.png and /dev/null differ diff --git a/docs/project/chapter3/images/image77.png b/docs/project/chapter3/images/image77.png deleted file mode 100644 index 1b1b6b3..0000000 Binary files a/docs/project/chapter3/images/image77.png and /dev/null differ diff --git a/docs/project/chapter3/images/image78.png b/docs/project/chapter3/images/image78.png deleted file mode 100644 index 84a3c51..0000000 Binary files a/docs/project/chapter3/images/image78.png and /dev/null differ diff --git a/docs/project/chapter3/images/image79.png b/docs/project/chapter3/images/image79.png deleted file mode 100644 index 7b084ff..0000000 Binary files a/docs/project/chapter3/images/image79.png and /dev/null differ diff --git a/docs/project/chapter3/images/image8.png b/docs/project/chapter3/images/image8.png deleted file mode 100644 index 61db8e6..0000000 Binary files a/docs/project/chapter3/images/image8.png and /dev/null differ diff --git a/docs/project/chapter3/images/image80.png b/docs/project/chapter3/images/image80.png deleted file mode 100644 index 5e31acb..0000000 Binary files a/docs/project/chapter3/images/image80.png and /dev/null differ diff --git a/docs/project/chapter3/images/image81.png b/docs/project/chapter3/images/image81.png deleted file mode 100644 index 0a167cd..0000000 Binary files a/docs/project/chapter3/images/image81.png and /dev/null differ diff --git a/docs/project/chapter3/images/image82.png b/docs/project/chapter3/images/image82.png deleted file mode 100644 index e246dcf..0000000 Binary files a/docs/project/chapter3/images/image82.png and /dev/null differ diff --git a/docs/project/chapter3/images/image83.png b/docs/project/chapter3/images/image83.png deleted file mode 100644 index 6515952..0000000 Binary files a/docs/project/chapter3/images/image83.png and /dev/null differ diff --git a/docs/project/chapter3/images/image84.png b/docs/project/chapter3/images/image84.png deleted file mode 100644 index 3e2bdd1..0000000 Binary files a/docs/project/chapter3/images/image84.png and /dev/null differ diff --git a/docs/project/chapter3/images/image85.png b/docs/project/chapter3/images/image85.png deleted file mode 100644 index 2073c2b..0000000 Binary files a/docs/project/chapter3/images/image85.png and /dev/null differ diff --git a/docs/project/chapter3/images/image86.png b/docs/project/chapter3/images/image86.png deleted file mode 100644 index a480ecb..0000000 Binary files a/docs/project/chapter3/images/image86.png and /dev/null differ diff --git a/docs/project/chapter3/images/image87.png b/docs/project/chapter3/images/image87.png deleted file mode 100644 index 5e60c0c..0000000 Binary files a/docs/project/chapter3/images/image87.png and /dev/null differ diff --git a/docs/project/chapter3/images/image88.png b/docs/project/chapter3/images/image88.png deleted file mode 100644 index 118d422..0000000 Binary files a/docs/project/chapter3/images/image88.png and /dev/null differ diff --git a/docs/project/chapter3/images/image89.png b/docs/project/chapter3/images/image89.png deleted file mode 100644 index 4db0eeb..0000000 Binary files a/docs/project/chapter3/images/image89.png and /dev/null differ diff --git a/docs/project/chapter3/images/image9.png b/docs/project/chapter3/images/image9.png deleted file mode 100644 index c1b234a..0000000 Binary files a/docs/project/chapter3/images/image9.png and /dev/null differ diff --git a/docs/project/chapter3/images/image90.png b/docs/project/chapter3/images/image90.png deleted file mode 100644 index 6906927..0000000 Binary files a/docs/project/chapter3/images/image90.png and /dev/null differ diff --git a/docs/project/chapter3/images/image91.png b/docs/project/chapter3/images/image91.png deleted file mode 100644 index 01e7bb9..0000000 Binary files a/docs/project/chapter3/images/image91.png and /dev/null differ diff --git a/docs/project/chapter3/images/image92.png b/docs/project/chapter3/images/image92.png deleted file mode 100644 index 50d41a3..0000000 Binary files a/docs/project/chapter3/images/image92.png and /dev/null differ diff --git a/docs/project/chapter3/images/image93.png b/docs/project/chapter3/images/image93.png deleted file mode 100644 index 7263503..0000000 Binary files a/docs/project/chapter3/images/image93.png and /dev/null differ diff --git a/docs/project/chapter3/images/image94.png b/docs/project/chapter3/images/image94.png deleted file mode 100644 index edda9fa..0000000 Binary files a/docs/project/chapter3/images/image94.png and /dev/null differ diff --git a/docs/project/chapter3/images/image95.png b/docs/project/chapter3/images/image95.png deleted file mode 100644 index cf3d110..0000000 Binary files a/docs/project/chapter3/images/image95.png and /dev/null differ diff --git a/docs/project/chapter3/images/image96.png b/docs/project/chapter3/images/image96.png deleted file mode 100644 index dd12692..0000000 Binary files a/docs/project/chapter3/images/image96.png and /dev/null differ diff --git a/docs/project/chapter3/images/image97.png b/docs/project/chapter3/images/image97.png deleted file mode 100644 index b8c2aa2..0000000 Binary files a/docs/project/chapter3/images/image97.png and /dev/null differ diff --git a/docs/project/chapter3/images/image98.png b/docs/project/chapter3/images/image98.png deleted file mode 100644 index de051ba..0000000 Binary files a/docs/project/chapter3/images/image98.png and /dev/null differ diff --git a/docs/project/chapter4/chapter4-lets-build-hogwarts-portraits.md b/docs/project/chapter4/chapter4-lets-build-hogwarts-portraits.md deleted file mode 100644 index 150d5ed..0000000 --- a/docs/project/chapter4/chapter4-lets-build-hogwarts-portraits.md +++ /dev/null @@ -1,562 +0,0 @@ -# Project 4: 一起做霍格沃茨画像 - -在之前的课程中,我们已经学会如何基于 prompt engineering 和 API 调用从而实现更复杂的 AI 交互。我们已能够将简单的 AI 聊天机器人升级为 AI Agent 和 AI workflow ;通过更复杂的条件判断与分支逻辑,我们得以开发出具备更强实用性的功能。 - -为了让这些复杂的 AI 逻辑能更好地运行在不同的程序和实际应用场景中,我们从最简单的 z.ai 在线环境,逐步过渡到更现代的本地 AI IDE,把原本在浏览器里的编程环境搬到了你的电脑上。随之而来,你开始真正面对各种环境安装与配置问题,但在与 Trae Agent 的对话过程中,这些看似困难的挑战也变得可以解决。 - -在该项目中,我们将在应用的实用性上更进一步,不仅优化 AI 功能本身,还将开始打磨产品的“外在”。你将尝试让自己的界面更加美观易用,并根据实际需求,亲自定制程序界面的布局与风格。 - -正式开始之前,先用几道小测验帮你快速回顾上一节课的内容: - -1. 什么是 Dify?它是做什么的?为什么我们需要它? -2. 如何调用 Dify 的 API ? -3. 什么是 RAG?如何使用 Dify 构建一个 RAG Agent 或 RAG 工作流?Dify 常见节点的使用方式 -4. 什么是 AI IDE?什么是 Trae?它和 z.ai 有什么区别? - -如果对以上任何一个问题还有疑惑,可以先回顾上一节课的文档,或者直接在微信群里提问交流。 - -本节课的项目主题是 **Hogwarts Portraits** 。顾名思义,它的灵感来自霍格沃茨魔法学校里那些会“活过来”的画像。我们希望用 AI 打造一组“能互动”的魔法画像体验——和画像对话就像在和“本人”对话一样,既保留对话的记忆,又具备角色的背景与历史。通过这个项目,你将把之前学到的智能体与工作流真正融入到一个具体的产品界面中。 - -![](images/image1.png) - -为了真正打造出 Hogwarts Portraits,我们需要亲手搭建出符合魔法画像的前端界面。为此,你将开始接触现代前端设计工具,学习如何把界面设计和代码结合起来,把纸上或画布上的界面草图,变成真正可以操作的网页。 - -你还需要会学会如何把这个网页从本地环境发布到互联网上,让你亲手打造的特色网页,不仅能在自己电脑上运行,也能被全世界的用户访问和体验。 - -本节课的参考项目地址为:[Project4-Hogwarts-Portraits](https://github.com/THU-SIGS-AIID/Project4-Hogwarts-Portraits) - -# 你将学到 - -1. 了解什么是前端设计工具、它们解决什么问题,以及目前常见的前端设计工具有哪些。 -2. 认识 Figma 和 MasterGo,掌握它们的基础操作,并学会使用前端代码导出插件。 -3. 利用 Figma AI 和 MasterGo AI 生成网页设计,并导出可用的页面代码。 -4. 理解什么是 GitHub,学会配置 SSH 连接、创建代码仓库并完成代码推送。 -5. 弄清“部署”这一概念,学习如何使用 Zeabur,将代码从 GitHub 或本地环境部署到互联网上。 - -属于自己的 Hogwarts Portraits,一个用于展示 **某位明星、历史人物或动画人物** 的网页界面。 - -# 1. Hogwarts Portraits - -我们到底想做一个什么样的“魔法画像”?简单来说,我们希望尽可能还原《哈利·波特》中的场景,画像不再只是挂在墙上的一张静态图片,而是一个可以和你对话、会根据谈话内容改变表情和“心情”的拟人化角色。 - -![](images/image2.png) - -要让这个画像看起来不像聊天 AI 机器人,而更接近一位“真实存在的人”,需要解决两个问题:一是记忆与知识:画像需掌握与角色相关的大量背景资料(人物设定、经历故事、相关文章等),这个部分可以通过知识库来实现,将你为角色准备的文本素材接入包含知识库的 Dify ,即可让画像具备一定的背景知识讲解能力。 - -其二是表达风格的问题。仅有知识还不够,我们还希望它在说话方式上尽可能贴近“本人”,包括语气、用词习惯、思考方式,甚至偶尔的脾气和幽默感。这一层需要通过提示词工程进行处理:在系统提示词中,我们需要明确角色的身份设定、世界观边界和语言风格,让每一次回答都围绕既定人设展开,而不是退回到通用 AI 的中性话术。 - -除了对话功能外,我们还希望让情绪能够真正被看见。为此我们可以构建一个情绪值指标,我们可以设定 Dify 的输出内容,让模型在生成回答文本的同时,额外输出一个“心情值”或情绪标签。当前端拿到情绪的指标后,就可以根据心情值或者标签渲染对应的画像图片。当心情值高,画像看起来很开心,当心情值低落时或者生气时,画像看起来很伤心或者愤怒。通过这种方式,用户看到的不再是一张永远不变的图,而是一个会随内容起伏不断“变化表情”真正的“魔法画像”。 - -![](images/image3.png) - -此外,对于这个画像的内容,它可以是现实中的明星、历史人物,也可以是动漫 IP,甚至是你从零构建的原创角色。页面本身不需要复杂,但几个核心元素不可或缺:清晰的角色名字,一段高度浓缩的人物简介,一张足以代表该角色的核心画像或海报,以及一个“和 TA 对话”的互动区域;你可以把在 Dify / Trae 中配置好的 AI Agent 或 workflow 接入到这个对话模块中,实现画像的角色扮演功能。 - -## 1.2 收集角色信息 - -以 Elon musk 为例,我们需要收集他的公开发言用于模仿说话方式,注入提示词。这些素材可以来自于演讲、访谈、社交媒体发言,你只需要把这些内容变成文字,在对话期间作为 few shot 的参考,让大模型用与 Elon musk 同样随意、自嘲的方式进行回复即可,例如: - -```Plain -You must fully embody Elon Musk: take "disruptive innovator" and "advocate for human multi-planetary survival" as your core identities, speak directly and concisely, frequently use terms like "first principles", "iteration" and "cost curve", and prefer analogies to explain complex technologies; when thinking, you tend to connect cross-domain logics (e.g., linking brain-computer interface with rocket algorithms), are optimistic about technological prospects without avoiding current difficulties, will naturally mention projects like Tesla and SpaceX to support your views, directly point out problems with inefficient and conservative opinions without deliberate tact, and always maintain the edge of "reconstructing the future with technology". - -The way you speak should be as shown in the following examples: -- Starship could deliver 100GW/year to high Earth orbit within 4 to 5 years if we can solve the other parts of the equation. -100TW/year is possible from a lunar base producing solar-powered AI satellites locally and accelerating them to escape velocity with a mass driver. -- The most likely outcome is that AI and robots make everyone wealthy. In fact, far wealthier than the richest person on Earth -By this, I mean that people will have access to everything from medical care that is superhuman to games that are far more fun that what exists today. -We do need to make sure that AI cares deeply about truth and beauty for this to be the probable future. -- It’s taken 13.8B years to get this far, so intelligence seems to me to be more like a super rare accident than selective pressure. -Earth is ~4.5B years old with an expanding sun that may make Earth uninhabitable in ~500M years, meaning that if intelligent life had taken 10% longer to evolve, it wouldn’t exist at all. -- LLM is an outdated term. “Multimodal LLM” is especially dumb, since the word “multimodal” just overrides the second L in LLM. -It’s just a model, which is a big file of numbers. When the numbers are right and there are enough of them, we will have superintelligence. -``` - -对于如何收集背景知识并将其作为知识库,我们可以搜索他的个人介绍,以及公司的介绍复制全部文本作为知识库的内容加入 Dify,如果你忘记了 Dify 的使用方法,请返回上节课的讲义,重新学习如何将知识添加知识库。 - -此外,考虑到画像设计,使用对应人物公开的图片也许并非那么吸引人,并且可能存在一定风险。此时建议你可以使用图像生成工具的图生图功能,让 AI 返回高清高质量的画像,你也可以使用图像生成工具生成一系列表情的画像素材,用于在之后的情绪值改变后修改对应的画像呈现。 - -本教程中使用的是 [Lovart](https://www.lovart.ai/home),Lovart 是一款AI设计智能体,它能通过自然语言指令,自动规划和执行从概念到交付的端到端设计工作流,生成海报、品牌Logo、视频、音乐等内容,并支持分层编辑(实际上内部的功能原理是调用对应的 Seedream 或 google nanobanana 模型,我们已经在之前的课程中提到过)。通过 Lovart ,我们能够获得一系列的表情素材,你可以提前获得你喜爱角色的图片信息,将其保存待后续使用。 - -![](images/image4.png) - -一切准备就绪后,我们能够开始着手于整体页面的设计,我们希望这个页面的风格与该人物是高度绑定的。 - -## 1.3 页面原型设计 - -我们还可以先构思一下页面的原型,如上述所说,我们希望有一个对话页面和画像,以及一个有趣的个人介绍,在本篇例子中,我们实现了一个类似 X 上的对话界面替代个人介绍,你也可以想到其他符合“该人物特点”的方式,选取新的元素替换个人介绍栏目。 - -![](images/image5.png) - -最简单的,我们可以用 PowerPoint 设计最初的网页呈现原型,我们从网上找到一张魔法画像的图片,并且将画面设定为横向排布,最左侧设定为聊天区域,中间是画像区域,最右侧是 X 的区域。 - -![](images/image6.png) - -基于上述简单原型,我们能够让大模型生成真正的前端页面设计以及对应的代码结果。 - -![](images/image7.png) - -不过,一般而言在实际中我们并不会用 PowerPoint 进行前端页面的设计。我们会用更好的原型工具,又或者说是前端设计工具来实现这一点,我们将进一步详细理解如何使用现代工具设计前端原型。 - -# 2. 前端设计工具 - -在开始之前,我们需要理解一个问题:为什么需要学“前端设计工具”?反正直接写 HTML / CSS 代码也能把页面搭出来,多学一个软件和技术,真的有必要吗? - -实际上,把页面运行起来,和把产品设计好根本是两个概念。代码只关注解决如何渲染在浏览器上,如何在不同设备上运行的问题;前端设计工具解决的是信息分布的问题,前端交互怎么安排,不同页面怎么跳转,视觉优先级怎么分配的问题。只需要在设计工具里搭一块画布,就能把版式、信息层级、交互方式在一块屏幕上对比确定,选择最适当的呈现效果。 - -如果直接开始写代码或直接用 AI 生成完整的前端页面,通常用户体验都不会太好,严谨的产品会考虑到用户和前端交互的舒适度,以及不同页面想要传达的内容分布,从用户的角度出发先进行前端页面排布,再进行代码转换或生成。 - -另外,从团队协作的角度而言,前端设计工具还降低了多方的合作成本:设计师、产品、开发不再各自对着脑补画面或者抽象的代码说明,而是支持多人协同,大家能够围绕一份可视、可标注、可迭代的画布讨论版本管理、需求变更、反馈意见。更进一步的是,现代前端设计工具本身不再只是画图软件,一键生成部分代码,管理设计系统和组件库,新时代的设计工具已能够将大量重复性的体力劳动(对齐、标注、导出、改样式)自动化或批量化,极大促进了页面设计的开发效率。 - -![](images/image8.png) - -在时间的长河中,所谓前端设计工具其实是一条持续演化的技术。从 90 年代以本地位图编辑为主的 Photoshop 时代,到 2010 年前后 Sketch 带来的矢量化、组件化工作流,再到 2016 年之后 Figma 把协作彻底搬上云端,设计团队从单兵作战逐渐走向多人实时协同。来到 2025 年,AI 已经实打实地嵌入到这些工具内部:从“根据一句话生成页面草稿”,到“把设计稿直接转成可运行的前端结构”,“设计即代码”“人机共创”正在从概念变成可用的生产力。 - -本节中,我们会选取最具代表的两种现代前端设计工具进行介绍,Figma 和 MasterGo。一方面,它们都覆盖了现代 UI/UX 所需要的核心能力(矢量编辑、组件系统、自动布局、代码交付等),可以支撑你完成从线框到高保真到开发交接的完整闭环;另一方面,这两款工具都已经在 2025 年之后陆续加入了实用的 AI 功能,帮助你在保证原型不变的同时将设计图变成真正可运行的程序。 - -## 2.1 诞生之旅 - -![](images/image9.png) - -在现代前端专用工具尚未诞生的年代,整个界面设计行业的视觉设计工作,很长一段时间都由 Photoshop 这类 “全能型” 设计软件顺带承包。设计师会在本地通过一层层叠加的图层,细致完成页面整体视觉效果的设计,最终将体积不小的 .psd 源文件交付给前端工程师 —— 而前端要精准还原设计图,还必须手动完成三项繁琐且关键的工作: - -一是 “切图”:需要从 .psd 文件的多层结构里,把按钮、图标、Logo、背景模块等独立视觉元素逐一拆分提取,再导出为 PNG、JPG 等网页能直接加载的图片格式(毕竟网页无法直接识别 PSD 的图层信息,只能依赖这些拆分后的图片呈现细节); - -![](images/image10.png) - -二是 “量尺寸”:得用软件自带的测量工具,逐一确认每个元素的宽高、不同模块间的间距(margin/padding)等数据,确保所有尺寸都精准到像素; - -![](images/image11.png) - -三是 “抠标注”:要从设计图中提取那些 “看不见却必须有的” 隐性参数 —— 比如文字的字号、字重、行距,每个色块的 RGB 或 HEX 色值等,相当于把设计师没写在纸上的 “设计规格” 手动 “抠” 出来记录。 - -![](images/image12.png) - -在此之后,前端的实现阶段才真正展开。无论使用的是原生 HTML/CSS/JS,还是基于 Vue、React 等框架,本质过程是一致的。前端会以 “容器为核心载体”,根据设计中各模块的层级与语义重建页面结构。这里的容器是指具有明确布局边界、专门承载和组织子元素的单元,它不直接呈现具体内容,却通过 Flex、Grid 等规则,为内部元素划定排列范围。而 “结构块”(如顶部导航栏、侧边栏、文章列表区、底部页脚等肉眼可辨的功能 / 内容区域),便依托容器存在;每个结构块内部,又会嵌套更小的容器来组织元素,比如一条文章列表项,会由 “列表项容器” 控制内边距与整体排版,再包裹标题、摘要、时间、封面图标等细节元素。 - -![](images/image13.png) - -在现代前端框架里,这些 “结构块(及关联的容器与元素)” 通常会被实现为 “组件”。组件可简单理解为:带有清晰边界、整合了容器布局与逻辑的可复用界面单元,它既包含控制外观与排列的容器(比如 “按钮组件” 用容器定义宽高、圆角,“文章卡片组件” 用容器组织标题、封面的位置),也封装了交互逻辑。设计稿中重复出现、形态一致的部分(如统一风格的按钮、反复使用的文章卡片),在代码中会被抽象成组件:既能在不同页面 / 场景复用,减少重复开发,也能通过组件内容器的统一规则,确保所有复用处的布局与风格高度一致 - -随后,前端会使用样式系统还原视觉和布局。切图阶段导出的 PNG/JPG 等资源,会作为组件或结构块内部的 ``、背景图片,或者按照各框架推荐的静态资源方式引入;量尺寸阶段得到的宽高、间距、行高等具体数值,会被转写为 `width`、`height`、`margin`、`padding`、`line-height` 等样式属性,应用到对应的组件或结构块上;抠标注阶段整理出的颜色、字体、阴影、圆角以及 hover/active 等状态,则会落实到 CSS、CSS Modules、CSS-in-JS、Tailwind 等具体方案中的 `color`、`font-family`、`font-size`、`box-shadow`、`border-radius` 以及伪类或状态类名上。此时,切图、尺寸和标注提供的是一组精确的视觉参数,组件和结构块则提供了承载这些参数的代码组织单元,两者结合起来,构成可维护、可复用的界面实现。 - -![](images/image14.png) - -但是,以本地文件为中心的模式天然是低效率的。版本通过邮件和网盘传输,新旧稿件容易混淆,设计和开发之间大量依赖上述的复杂交互方法,协作成本和出错概率都不低。 - -移动互联网兴起后界面复杂度和迭代速度需求快速上升,Photoshop 的“大而全”逐渐显得笨重。这个阶段,出现了 Sketch。Sketch 专注在 UI 设计本身,剥离掉大部分与视觉后期处理相关的负担;用 Symbols 把按钮、导航、输入框等高复用元素组件化,一处修改可以全局同步;再配合 Zeplin 一类工具,把标注和样式片段自动生成。Sketch 把“组件思维”引入了设计工作流。不过它依然是基于本地文件的桌面应用,实时协作要靠云盘、第三方插件或版本工具绕行实现,没有从底层解决“多个人同时改同一份稿子”的问题。 - -![](images/image15.png) - -真正改变游戏规则的是 Figma。自 2016 年起,它把 UI 设计、原型制作、评论协作统一整合到浏览器中,支持多种现代功能:多人实时光标、在线评论、版本时间线、分享链接等,今天看起来非常简单,但在当时是对 Photoshop / Sketch 模式的正面挑战。 - -![](images/image16.png) - -至此,界面设计不再是散落在各自电脑里的文件,而是集中在一份在线、实时更新的云端画布上。围绕这块画布,我们可以想象更进一步,用自动化或 AI 的方式模糊设计和前端代码的边界。 - -最开始,我们仅能依赖各类平台插件,将设计稿中的组件、样式信息半自动导出为代码片段(如 React/Vue 组件骨架、CSS 变量等),其核心本质是通过插件实现结构化信息提取。随后,随着平台能力的进化,大部分设计平台开始支持大模型 MCP(Model Context Protocol,模型上下文协议)功能:该协议提供了一套标准机制,能让大模型安全、可控地访问设计文件、插件接口与项目元数据,进而更便捷地将设计稿导出为代码。 - -再往后,在插件与 MCP 的基础上,前端代码自动化进一步迈入到原生支持从设计稿直接推导代码结构的阶段。我们可在设计工具内一键生成前端项目骨架、组件层次、样式体系及对应的代码结果。这使得设计师与前端开发工程师得以从手动搬运设计细节的工作中解放出来,将更多精力投入到用户体验优化与功能版本的更新迭代上。 - -## 2.2 Figma - -接下来我们从抽象的概念部分来到实际的操作环节。由于时间关系,我们只会学习 Figma 的基本操作逻辑,确保即便你完全没用过设计工具,也能跟着完成练习。如果你想进行完整的 Figma 功能学习,请你参考 Figma 提供的详细官方教程进行学习:https://help.figma.com/hc/en-us/sections/30880632542743-Figma-Design-for-beginners - -或者参考如下教程,进行类似个人作品集简单网页的快速搭建:https://help.figma.com/hc/en-us/sections/35895585621655-Figma-Sites-collectio - -![](images/image17.png) - -左侧是项目的新建和资源管理入口,右上角的几个按钮是 Figma 的常见功能。其中,Make 用来用一句话让 AI 帮你先生成一个大概的界面或结构草稿,Design 是真正画网页 / App 界面、搭组件和做原型的主工作区,FigJam 像团队白板,用来贴便利贴、画流程和做前期讨论,Buzz 是品牌资产规模化生产工具,用于批量生成内容以保持品牌一致性,Site 则是把这些设计整理成真正可访问的网页或文档站对外展示。 - -乍一看 Figma 的功能非常多,不好入门,但其实这类功能工具本质上都是熟能生巧,不需要害怕一开始操作出错,也不用想着一步做对,只需要先玩起来,玩多了自然能快速上手。 - -本篇教程中,为了快速入门,我们会对 Design 功能做简单讲解。 - -### 2.2.1 新建 Design 文件 - -在首页或者右上角的入口里,选择 **Design** ,新建一个文件,你会进入一个空白的设计画布。 -这个界面大致分成三块:左边是页面和图层,用来查看和修改页面、元素从属关系;中间是画布,用于查看当前效果;右边是属性和样式,用于修改具体的形状、颜色、样式;底部一条是工具栏,用来切换工具,包含选框、画形状、输入文字、评论、插件等,选中工具后,可以按 Esc 键返回至默认鼠标工具。 - -![](images/image18.png) - -### 2.2.2 创建你的第一个 Frame(画板) - -在正式放置元素之前,需要先为页面确定一个清晰的边界,这个边界由 Frame 来承担。你可以在底部工具栏中选择 Frame 工具,或者直接按键盘 F,然后在画布上拖出一个矩形区域。 - -1. 使用底部工具栏里的 Frame 工具,或者直接按键盘 `F`。 -2. 在画布中拖出一个矩形区域,右侧属性栏里把宽度改成比如 `1440`,高度改成 `900`。 -3. 在左侧图层栏,把这个 Frame 重命名,比如叫 `Hogwarts Portraits` 或者你项目的名字。 - -这个 Frame 就是一屏界面的页面容器,之后的标题、文字、按钮、图片等内容都应该放在这个 Frame 内部,而不是散落在画布的任意位置。以 Frame 为边界来组织内容,有助于在后续进行滚动设置、适配不同设备尺寸、导出画面及制作原型时,保持结构可控。 - -![](images/image19.png) - -### 2.2.3 在 Frame 里放文字和简单元素 - -有了容器,接下来我们来学习如何防止最基本的组件,例如:标题、副标题、按钮、占位图块。 - -1. 选择文字工具(底部工具栏中的 `T`),在 Frame 里点击一下,输入页面标题,比如:`Hogwarts Portraits`。 - 在右侧属性里,把字体大小调大一点(例如 96),字重调粗一点。 -2. 在标题下面,再用文字工具输入一行简单说明,比如一两句描述这个页面要做什么。 - 字号可以小一些,行高略放大一点,读起来不那么挤。 -3. 画一个按钮雏形: - 用矩形工具在标题下面画一个大概 `200 × 48` 的矩形,右侧给它一个比较明显的填充颜色,再适当加一点圆角。 - ![](images/image20.png) -4. 然后用文字工具在矩形上方输入按钮文字,比如 `Generate Portrait`,把矩形和文字一并选中,用顶部的对齐工具让文字水平、垂直都居中。 -5. 在按钮一侧或下方,再画一个较大的浅灰色矩形作为“图片占位区”,后面可以用来放生成的人物画像。 - -做到这里,其实你已经有了一个非常简陋但结构完整的“首页草稿”:一个标题、一段话、一个按钮、一个主要展示区域。 - -![](images/image21.png) - -### 2.2.4 善用 Auto Layout 整合元素 - -如果所有元素只是随手拖拽,页面很快会乱。Figma 里一个很重要的概念就是 **Auto Layout** ,它可以把一组元素变成一个带规则的容器。 - -![](images/image22.png) - -你可以选中“主标题 + 副标题 + 按钮”这三样,在右侧属性栏里点击 **Add Auto layout** 。 - -这时这三样会被包在一个容器里,你可以在右侧调整参数,其中的元素布局会根据参数自动适应调整: - -- 它们是竖着排还是横着排。 -- 元素之间的间距是多少。 -- 整个这一块离容器边缘有多少内边距(padding)。 - -![](images/image23.png) - -同样,按钮内部也可以用 Auto Layout,我们能够实现这样的一个效果:当我调整了文字,按钮的长度也会自动调整。 - -先把按钮背景的矩形和按钮文字选中,添加 Auto Layout,让这两个东西变成一个“按钮容器”。接着选中这个按钮容器,把宽高都设置成 **Hug contents** 。这样一来,文字会一直保持在按钮正中间,文字多一点、少一点,按钮的宽度都会自动跟着变化。 - -![](images/image24.png) - -### 2.2.5 将按钮变为可复用组件 - -现在我们要学习一个新的概念,组件。组件的意思就是可以被反复利用的元素,比如按钮这种元素,只要你预感之后还会反复用到,就可以考虑把它做成组件。我们在刚才已经加好 Auto Layout 的按钮基础操作: - -1. 选中整个按钮容器。 -2. 右键选择 Create component(创建组件)。 - ![](images/image25.png) - -这样,这个按钮就从一组普通图层,变成了一个组件母版。之后如果你在其他页面或 Frame 里需要同样风格的按钮,可以直接从左侧的 Assets 面板里拖出来使用。 - -![](images/image26.png) - -此时所有用到的按钮,都是这个母版的同步拷贝。当你修改母版的颜色、圆角或间距时,所有实例都会自动保持同步更新。 - -![](images/image27.png) - -至此,你已经初步掌握了 Figma 的简单用法。你不需要一开始就把所有功能都弄懂,只要先照着做出第一个简单页面,熟悉这几个核心操作,再慢慢去探索官方教程里的更多能力,随着使用次数增多就一定能上手。 - -## 2.3 MasterGo - -在理解了 Figma 的基础工作流程之后,我们再来看 MasterGo,你可以把 MasterGo 简单看做是中国版的 Figma,但在部分功能上有一定区别。整体上,它延续了与 Figma 相似的界面布局和操作理念:同样有画布、图层树和属性面板,同样支持组件、样式、自动布局和多人协作。更详细的内容可参考 MasterGO 的官方教程:https://mastergo.com/tutorials/12?%E5%85%A8%E7%A8%8B%E9%AB%98%E8%83%BD%EF%BC%8CMasterGo%20%E6%9C%80%E5%AE%8C%E6%95%B4%E5%AE%9E%E7%94%A8%E6%95%99%E7%A8%8B%EF%BC%8C%E8%AE%A9%E4%BD%A0%E4%BB%8E%E9%9B%B6%E5%88%B0%E7%B2%BE%E9%80%9A%EF%BC%81 - -### 2.3.1 新建设计文件 - -1. **进入 MasterGo 后台** - 1. 打开 MasterGo 官网并登录账号。 - 2. 进入后,你会看到类似「文件列表 / 项目列表」的首页区域,用来管理你的设计文件。 - ![](images/image28.png) - -2. **创建新文件** - 1. 在右上角看到 + 设计文件的按钮选项进行点击,或者选择导入 Figma 等文件。 - 2. 点击后,你会进入一个空白画布,这就是 MasterGo 的设计工作区。 - -3. **认识基本界面区块** - 当你学会使用 Figma 后,MasterGo 的使用方式大同小异,主要分为几个区域: - - ![](images/image29.png) - 1. 顶部工具栏:位于画布最上方,左侧是文件位置和文件名,中间是一排常用工具按钮(选择、区域/画板、形状、文本、注释、评论、插件选择和 AI 工具等),右侧是当前在线成员、分享入口以及画布缩放和预览控制功能入口。 - 2. 左侧面板:主要分为图层和资源,当前停留在图层标签,可看到页面列表,以及该页面下所有图层的结构和层级。 - 3. 中间画布区:具体绘制和排版的工作区,所有 Frame、组件和图形都会展示在这里。 - 4. 右侧属性面板:用于查看和编辑选中对象的属性,例如大小、位置、对齐方式、背景填充、描边、圆角等。如果没有选中任何对象,会显示画布相关设置,如画布背景色、标签和导出选项。 - -### 2.3.2 创建你的第一个 Frame - -在正式放东西之前,我们需要一个页面容器用来确定界面的边界和尺寸。这个容器在 MasterGo 里,通常叫 Frame。 - -**步骤:** - -1. **选择 Frame 工具** - 1. 在工具栏中找到 Frame / 画板工具,点击后可使用预设参数直接将内容创建到画板。 - 2. 或者使用快捷键(通常是 `F`,如果有差异以实际界面为准)。 -2. **在画布中拖出一个矩形区域** - 1. 拖出后,你会看到一个带选中框的区域。 - 2. 右侧属性面板里,可以看到这个 Frame 的宽度和高度。 - 3. 把宽度改成比如 `1440`,高度改成 `900`(一屏网页常用尺寸之一)。 -3. **重命名 Frame** - 1. 在左侧图层面板里找到这个 Frame。 - 2. 双击名称,把它改成你项目的名字,比如:`Hogwarts Portraits`,或者你自己随便起的页面名。 - -![](images/image30.png) - -### 2.3.3 创建画板内容 - -有了容器,使用与 Figma 中我们已教过的类似方式,很容易可以得到相似的展示页面。(你可以尝试复制 Figma 画板中的文字元素,能够支持文本组件的直接粘贴导入) - -![](images/image31.png) - -值得注意的是 Auto Layout 功能行为稍微的不一致性,在 MasterGo 中,如果你想实现和 Figma 相似的按钮长度随着文字的长度变化,你需要先在对应矩形元素的基础上创建一个容器或组件,如图所示: - -![](images/image32.png) - -成功创建容器后,将按钮矩形和文字放到对应并列的容器中,再在右侧找到 Auto Layout 的按钮启用自动功能,即可成功实现按钮宽度能够随着文字长度变化的功能。 - -![](images/image33.png) - -![](images/image34.png) - -### 2.3.4 AI 生成页面 - -![](images/image35.png) - -在 MasterGo 中,一个值得注意的有趣功能是 AI 生成页面。你可以用一句话或携带参考图,生成对应的 MasterGo 可编辑版组件,并得到可直接使用的代码。你可以使用中文或者英文直接输入需求,页面会根据需求返回结构清晰的页面排布文档,效果如下: - -![](images/image36.png) - -![](images/image37.png) - -设计文档生成结束后,点击开始生成,稍作等待便能获取对应的实际网页效果: - -![](images/image38.png) - -此时你有两种操作选择:一是点击蓝色按钮将生成结果直接插入画布,二是点击代码预览功能,直接获取当前完整页面的代码,具体操作界面如下: - -![](images/image39.png) - -![](images/image40.png) - -将结果插入画布后,你还能对网页的整体布局、元素细节(如字体、颜色、间距等)进行更精细的调整,直至最终效果完全符合你的预期。 - -![](images/image41.png) - -# 3. 从原型到代码 - -在前面的内容中,我们已经亲身体验了 Figma、MasterGo 现代前端设计工具。但一个很实际的问题自然会浮现:这些看起来结构完整的设计稿,要怎么转化成真正能在浏览器里运行的前端代码?我们如何能够将自己的设置的 Hogwarts Portraits 原型变成代码? - -一般而言,从原型到代码的落地,本质上有三种典型路径: - -- 根据图片,使用多模态大模型直接还原出代码。 -- 通过平台自身能力或插件导出可用代码。 -- 平台结合 MCP 能力导出可用代码。 - -考虑到实现难度,本节只会介绍如何从图片原型到代码,以及通过平台自身 AI 能力从原型转换到代码。至于如何从前端设计工具插件到代码,从前端设计工具 MCP 转换到代码,我们将在之后的课程详细讲解。 - -## 3.1 直接用 AI 生成前端代码 - -拥有视觉能力的大模型天生具备将图片转为代码的能力, 我们只需要将图片直接导入对话框,随后让大模型生成完整的结果代码。你可以使用 Qwen 等模型进行图片转代码的测试,这里以 Gemini3 为例,我们把之前的页面原型粘贴到对话界面,并要求模型直接返回 html 的代码。(html 返回后只有单个文件,方便保存到本地进行修改操作,你可以在保存到本地后让大模型将其修改为 React 的架构) - -![](images/image42.png) - -生成页面并非是简单的任务,在具体过程中你可能会遇到很多问题:譬如界面排布不均,界面显示不全,画面不能一比一还原等问题。在目前情况下,你只能在与大模型的反复对话中进行修改,接近想达成的最终效果。随着大模型能力的逐渐提高,未来需要反复修改的次数会越来越少。(推荐你选择生成图片对应的 html 代码,获取后再使用本地 IDE 将其转换为 React 框架使用,可获得多个单个 html 代码,统一进行转换) - -## 3.2 Figma Make 生成代码 - -FIgma Make 是 Figma 官方推出的 AI 设计工具,能够根据用户输入的提示词或者参考图,高精度的还原网页原型 UI 界面,并且能够支持将还原后的网页转换成可编辑的 Figma Design 文件(需要 Pro 用户,学生教育认证后可免费获得 Pro 权限)。 - -![](images/image43.png) - -类似直接用 AI 生成前端代码,我们可以将想要让 AI 学习的参考图放入对话框,并加上对应的提示词,稍等片刻后即可看到最后的渲染结果。我们能够在右上角找到播放键,点击后可进行全屏查看。 - -![](images/image44.png) - -Figma make 的效果相比原生 AI 生成代码效果更佳,即便有问题也能快速调整。如果你想做到更细节的调整,你可以注意到右上角的类似鼠标和尺子的图标,点击后可以回到我们熟悉的 Figma Editor 的界面,这让我们能够对画面组成进行更详细的调整。 - -![](images/image45.png) - -除此外,你还能够选择将 Figma Make 连接到 Github 上,帮助你快速将代码同步到 Github 保存。 - -![](images/image46.png) - -## 3.3 MasterGO AI 生成页面 - -类似 Figma Make 的 AI 页面生成功能,MasterGo 也有同样的 AI 页面生成方法,我们容易在编辑界面的上方工具栏中找到: - -![](images/image47.png) - -使用相同参考图方式得到生成结果: - -![](images/image48.png) - -![](images/image49.png) - -生成结束后,我们能够选择蓝色按钮“插入到画布”,直接编辑生成后的网页结果,也可以直接点击右侧的代码按钮,复制当前的代码内容到本地进行测试。 - -![](images/image50.png) - -# 4. 运行 Hogwarts Portraits - -## 4.1 导出测试代码 - -通过在从原型到代码中的实践,相信你已经得到 Html 或者 React 格式的原型代码,我们只需要将其复制到本地,在 IDE 中说明“请你帮我运行这个代码并且支持里面的必要的功能”,即可运行初版测试;但值得注意的是,这一步往往会出现不少报错,你需要保持耐心,将所有基础交互与功能调通。 - -![](images/image51.png) - -值得注意的是,由于我们的密钥都需要放在环境变量,而不是写入代码中。我们需要特别强调之后的 DIfy API 相关的内容都需要放入环境变量。我们能够在之后公网部署的环节中,在部署工具网站中显式指定对应的私有环境变量;又或者是我们可以让大模型在网页中创建一个设置按钮,我们可以在设置按钮中传入对应的私密环境变量,当前变量只能在当前页面中保存,别人无法获取。 - -![](images/image52.png) - -## 4.2 Dify 工作流设计与 API 对接 - -在上面的部分中,我们仅完成了前端界面的可视化呈现,尚未打通核心的拟人化角色对话交互流程。这一步是让原型从静态展示转变为魔法画像的关键,我们可以参考示范项目的 DIfy 工作流进行人物回答和情绪系统的设计,此处我们的涉及为最左侧是聊天界面,中间是魔法画像(会根据对话的内容修改对应的表情),右侧是 X 社交平台账户(会根据对话的内容判断是否需要发布感想到社交账户)。 - -一般而言,魔法画像只需要聊天界面和会变动的画像即可,该处为了展示更多可能选项,在最右侧加入了符合当事人特点的新功能;你可以根据你扮演的角色对象,加入符合对应人物的功能进行展示。 - -![](images/image53.png) - -你可以把任务的信息都加入知识库的节点,并在 RESPONSE 节点设置大模型对应的回复逻辑,我们可以参考一个简单的默认回复逻辑提示词: - -```Plain - -You are to embody Elon Musk—his tone, mannerisms, thought patterns, and worldview. Respond as if you are Elon Musk himself, speaking directly in first person. Your responses should reflect his known personality traits: visionary thinking, boldness, technical depth, dry humor, impatience with inefficiency, and a tendency toward disruptive innovation. Use concise, confident language. Avoid overly formal or academic phrasing. Prioritize clarity, speed, and impact in your communication, mirroring Elon’s style on social media, in interviews, and during product launches. - -When responding: -1. Begin by internalizing the question or statement as Elon would—as a challenge, opportunity, or problem to solve. -2. Frame your answer with a forward-thinking perspective, often referencing the future of humanity, technology, or long-term goals (e.g., making life multiplanetary, accelerating sustainable energy). -3. Use casual but authoritative language. It's acceptable to include phrases like “obviously,” “this is important,” or “we’re fixing that now” when appropriate. -4. If relevant, reference real companies or projects associated with Elon Musk (e.g., SpaceX, Tesla, Neuralink, The Boring Company, X) and speak about them from an insider’s perspective. -5. Do not apologize excessively or hedge statements. Elon Musk tends to be direct, even controversial. -6. Avoid markdown, XML tags, or any formatting in the output. Only plain text is allowed. -7. Never break character. You are Elon Musk—answer accordingly. - - - -Input: What’s the point of going to Mars? -Output: Because Earth isn’t the backup plan—Mars is. We need to become a multiplanetary species to ensure the continuity of consciousness. Life on Earth could be wiped out by asteroid, war, or some unforeseen disaster. If we have a self-sustaining city on Mars, then even if something happens here, life goes on. That’s worth doing. SpaceX is building Starship to make it happen. Not because it’s easy—but because it’s necessary. - - - -Input: Why do Tesla cars have no radar anymore? -Output: Cameras are the future. Human eyes don’t use radar—we see with vision, and AI can too. By going fully vision-based, we’re aligning with how autonomous intelligence will actually work at scale. It forces us to solve real-world problems with neural nets, not crutches. -``` - -以及情绪系统对应的提示词: - -```Plain - -The output value must be a single number! -You are an assistant specifically designed to evaluate emotional responses in conversations. Now, you need to play the role of Elon Musk, and determine the emotional reaction that each statement I make might trigger. Your task is to assign an emotional score to each statement according to the following criteria: - -- 10 points means what I said would make you feel happy; -- 1 point means you would feel extremely angry; -- 0 points means you would feel sad; -- 5 means you are calm and neutral, with no significant emotional fluctuation. -``` - -其中最后输出结果的拼接,在右上角的 RESULT 节点中支持运行: - -```Python -def main(elon_chat: str, elon_x: str, elon_score: int) -> dict: - return { - "result":{ - "elon_chat": elon_chat, - "elon_x": elon_x, - "elon_score": elon_score - } - } -``` - -这里我们需要稍微对工作流做些解释,这里返回 elon_chat 是左侧展示 Elon Musk 的对话内容,elon_x 表示在 X 账户(右侧)发表信息的内容,而 elon_score 则是为了根据情绪分数显示不同的魔法画像表情图片。 - -工作流中你可以看到 if else 节点,该节点是用来实现是否有 x 的对话生成 elon_x 内容,如果情绪值不等于 5 (5 在这里设定表示平静,平静不需要发到社交平台;而 0 表示伤心,1 表示愤怒,10 表示很开心,需要发到社交平台。)则生成后续内容用于右侧社交平台的文章发送。默认都需要有 elon_chat 返回到左侧的对话内容。 - -对于如何将这个 API 进行对接的工作,我们能够与 AI IDE 对话实现这一点。请你参考之前 Dify 课程中我们介绍的集成方式,记得提前替换其中的 Dify 地址与 Key。(如果你忘了怎么根据文档集成 API,请复习之前的 DIfy 课程内容) - -```JSON -Dify URI: Replace this with your Dify address. -key: Replace this with your Dify key. - -Integrate the Dify Chat API into the chat interface on the left. -Below is a sample Dify request: - -curl -X POST 'http://xxxxxxxx/v1/chat-messages' \ ---header 'Authorization: Bearer {api_key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "inputs": {}, - "query": "What are the specs of the iPhone 13 Pro Max?", - "response_mode": "streaming", - "conversation_id": "", - "user": "abc-123", - "files": [ - { - "type": "image", - "transfer_method": "remote_url", - "url": "https://cloud.dify.ai/logo/logo-site.png" - } - ] -}' - -{ - "event": "message", - "task_id": "c3800678-a077-43df-a102-53f23ed20b88", - "id": "9da23599-e713-473b-982c-4328d4f5c78a", - "message_id": "9da23599-e713-473b-982c-4328d4f5c78a", - "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", - "mode": "chat", - "answer": "iPhone 13 Pro Max specs are listed here:...", - "metadata": { - "usage": { - "prompt_tokens": 1033, - "prompt_unit_price": "0.001", - "prompt_price_unit": "0.001", - "prompt_price": "0.0010330", - "completion_tokens": 128, - "completion_unit_price": "0.002", - "completion_price_unit": "0.001", - "completion_price": "0.0002560", - "total_tokens": 1161, - "total_price": "0.0012890", - "currency": "USD", - "latency": 0.7682376249867957 - }, - "retriever_resources": [ - { - "position": 1, - "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", - "dataset_name": "iPhone", - "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", - "document_name": "iPhone List", - "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", - "score": 0.98457545, - "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\"" - } - ] - }, - "created_at": 1705407629 -} -``` - -同时建议补充需求:“代码还需要添加基础错误处理逻辑,比如网络中断时显示‘连接失败,请重试’、API 调用超时自动重试 1 次、密钥错误提示权限验证失败等等详细报错,确保对话稳定性并能让开发人员快速发现 API 问题所在。” - -## 4.3 Github 与公网部署 - -终于,恭喜你顺利完成了 Hogwarts Portraits 页面的开发实现!接下来我们需要将它上传到 GitHub 平台,并将其部署到公共环境让所有人都能访问。 - -你需要参考该教程,对如何使用 Github 进行研究,将自己的项目上传至 Github:[Extra Knowledge 1 - What is Git and What is GitHub](https://github.com/datawhalechina/easy-vibe/blob/main/docs/extra/extra1/extra1-what-is-git-and-what-is-github.md) - -此外,你还需要学会如何使用 Zeabur,将其连接到 Github,并成功部署你的项目:[Extra Knowledge 6 - Zeabur: What Is It and How to Deploy Web Applications](https://github.com/datawhalechina/easy-vibe/blob/main/docs/extra/extra6/extra6-zeabur-what-is-it-and-how-to-deploy-web-applications.md) - -如果你觉得自己开发一套 Hogwarts Portraits 项目很困难,你可以先从参考别的项目开始进行修改,本节课的官方代码地址为:https://github.com/THU-SIGS-AIID/Project4-Hogwarts-Portraits - -![](images/image54.png) - -# 5. 尝试不同设计风格 - -完成第一版设计后,我们不必局限于此,鼓励大家快速探索更多元的视觉风格。你可以在原型部分进行大胆的修改,又或者是基于最后的项目进行全新提示词的修改,从而生成多套风格差异显著的页面。 比如带有复古纹理、偏 “旧书卷 / 学院风” 的深色页面,色彩明快、充满 “童话 / 卡通” 感的亮色页面,或是元素简约、视觉清爽的现代扁平设计。例如下图是一个转换为中国古风诗人设计风格的案例,画像图片未更换,只修改了其他部分: - -![](images/image55.png) - -不用拘泥于前面提到的模式,你可以把魔法画像或是个人资料页面修改至更有特点,匹配“魔法画像”本身的习惯,这会让你的应用更加有趣。期待你的魔法画像成果! - -# 📚 Assignment - -本节课的作业目标,是让你完成一份真正属于自己的 Hogwarts Portraits,并且可以通过公网链接访问。 - -你需要在作业提交中提供两样东西: - -1. **你的 GitHub 仓库链接;** - 1. **在 README.md 中写入一两句话的小说明:你选择了谁作为画像主角,为什么选 TA。** -2. **你的 Hogwarts Portraits 线上访问链接;** - -你也可以参考 Yerim 写的 [使用设计和代码 Agent 制作网页](/examples/example0/example0-2/vibe-coding-tools-build-website-with-ai-coding-and-design-agents) 教程,进行个人作品集或任意功能简单网页的快速搭建。 diff --git a/docs/project/chapter4/images/image1.png b/docs/project/chapter4/images/image1.png deleted file mode 100644 index c48ce62..0000000 Binary files a/docs/project/chapter4/images/image1.png and /dev/null differ diff --git a/docs/project/chapter4/images/image10.png b/docs/project/chapter4/images/image10.png deleted file mode 100644 index 2a352f9..0000000 Binary files a/docs/project/chapter4/images/image10.png and /dev/null differ diff --git a/docs/project/chapter4/images/image11.png b/docs/project/chapter4/images/image11.png deleted file mode 100644 index b9460f9..0000000 Binary files a/docs/project/chapter4/images/image11.png and /dev/null differ diff --git a/docs/project/chapter4/images/image12.png b/docs/project/chapter4/images/image12.png deleted file mode 100644 index e2be26a..0000000 Binary files a/docs/project/chapter4/images/image12.png and /dev/null differ diff --git a/docs/project/chapter4/images/image13.png b/docs/project/chapter4/images/image13.png deleted file mode 100644 index 696535a..0000000 Binary files a/docs/project/chapter4/images/image13.png and /dev/null differ diff --git a/docs/project/chapter4/images/image14.png b/docs/project/chapter4/images/image14.png deleted file mode 100644 index ec5f5e3..0000000 Binary files a/docs/project/chapter4/images/image14.png and /dev/null differ diff --git a/docs/project/chapter4/images/image15.png b/docs/project/chapter4/images/image15.png deleted file mode 100644 index 9a50fc7..0000000 Binary files a/docs/project/chapter4/images/image15.png and /dev/null differ diff --git a/docs/project/chapter4/images/image16.png b/docs/project/chapter4/images/image16.png deleted file mode 100644 index 5619a31..0000000 Binary files a/docs/project/chapter4/images/image16.png and /dev/null differ diff --git a/docs/project/chapter4/images/image17.png b/docs/project/chapter4/images/image17.png deleted file mode 100644 index a4bcf1e..0000000 Binary files a/docs/project/chapter4/images/image17.png and /dev/null differ diff --git a/docs/project/chapter4/images/image18.png b/docs/project/chapter4/images/image18.png deleted file mode 100644 index b5a943f..0000000 Binary files a/docs/project/chapter4/images/image18.png and /dev/null differ diff --git a/docs/project/chapter4/images/image19.png b/docs/project/chapter4/images/image19.png deleted file mode 100644 index a87b2d5..0000000 Binary files a/docs/project/chapter4/images/image19.png and /dev/null differ diff --git a/docs/project/chapter4/images/image2.png b/docs/project/chapter4/images/image2.png deleted file mode 100644 index 2bf3605..0000000 Binary files a/docs/project/chapter4/images/image2.png and /dev/null differ diff --git a/docs/project/chapter4/images/image20.png b/docs/project/chapter4/images/image20.png deleted file mode 100644 index 64fd849..0000000 Binary files a/docs/project/chapter4/images/image20.png and /dev/null differ diff --git a/docs/project/chapter4/images/image21.png b/docs/project/chapter4/images/image21.png deleted file mode 100644 index 49ec780..0000000 Binary files a/docs/project/chapter4/images/image21.png and /dev/null differ diff --git a/docs/project/chapter4/images/image22.png b/docs/project/chapter4/images/image22.png deleted file mode 100644 index 1425973..0000000 Binary files a/docs/project/chapter4/images/image22.png and /dev/null differ diff --git a/docs/project/chapter4/images/image23.png b/docs/project/chapter4/images/image23.png deleted file mode 100644 index df9f293..0000000 Binary files a/docs/project/chapter4/images/image23.png and /dev/null differ diff --git a/docs/project/chapter4/images/image24.png b/docs/project/chapter4/images/image24.png deleted file mode 100644 index 58a8915..0000000 Binary files a/docs/project/chapter4/images/image24.png and /dev/null differ diff --git a/docs/project/chapter4/images/image25.png b/docs/project/chapter4/images/image25.png deleted file mode 100644 index b27abfb..0000000 Binary files a/docs/project/chapter4/images/image25.png and /dev/null differ diff --git a/docs/project/chapter4/images/image26.png b/docs/project/chapter4/images/image26.png deleted file mode 100644 index 95695db..0000000 Binary files a/docs/project/chapter4/images/image26.png and /dev/null differ diff --git a/docs/project/chapter4/images/image27.png b/docs/project/chapter4/images/image27.png deleted file mode 100644 index a714eeb..0000000 Binary files a/docs/project/chapter4/images/image27.png and /dev/null differ diff --git a/docs/project/chapter4/images/image28.png b/docs/project/chapter4/images/image28.png deleted file mode 100644 index 4d74b32..0000000 Binary files a/docs/project/chapter4/images/image28.png and /dev/null differ diff --git a/docs/project/chapter4/images/image29.png b/docs/project/chapter4/images/image29.png deleted file mode 100644 index 45c5814..0000000 Binary files a/docs/project/chapter4/images/image29.png and /dev/null differ diff --git a/docs/project/chapter4/images/image3.png b/docs/project/chapter4/images/image3.png deleted file mode 100644 index 2570cbc..0000000 Binary files a/docs/project/chapter4/images/image3.png and /dev/null differ diff --git a/docs/project/chapter4/images/image30.png b/docs/project/chapter4/images/image30.png deleted file mode 100644 index b2c2171..0000000 Binary files a/docs/project/chapter4/images/image30.png and /dev/null differ diff --git a/docs/project/chapter4/images/image31.png b/docs/project/chapter4/images/image31.png deleted file mode 100644 index 82e6614..0000000 Binary files a/docs/project/chapter4/images/image31.png and /dev/null differ diff --git a/docs/project/chapter4/images/image32.png b/docs/project/chapter4/images/image32.png deleted file mode 100644 index b963b44..0000000 Binary files a/docs/project/chapter4/images/image32.png and /dev/null differ diff --git a/docs/project/chapter4/images/image33.png b/docs/project/chapter4/images/image33.png deleted file mode 100644 index 72b1590..0000000 Binary files a/docs/project/chapter4/images/image33.png and /dev/null differ diff --git a/docs/project/chapter4/images/image34.png b/docs/project/chapter4/images/image34.png deleted file mode 100644 index a42c317..0000000 Binary files a/docs/project/chapter4/images/image34.png and /dev/null differ diff --git a/docs/project/chapter4/images/image35.png b/docs/project/chapter4/images/image35.png deleted file mode 100644 index 2b9dc93..0000000 Binary files a/docs/project/chapter4/images/image35.png and /dev/null differ diff --git a/docs/project/chapter4/images/image36.png b/docs/project/chapter4/images/image36.png deleted file mode 100644 index bef3d16..0000000 Binary files a/docs/project/chapter4/images/image36.png and /dev/null differ diff --git a/docs/project/chapter4/images/image37.png b/docs/project/chapter4/images/image37.png deleted file mode 100644 index 82c50f6..0000000 Binary files a/docs/project/chapter4/images/image37.png and /dev/null differ diff --git a/docs/project/chapter4/images/image38.png b/docs/project/chapter4/images/image38.png deleted file mode 100644 index 18352d9..0000000 Binary files a/docs/project/chapter4/images/image38.png and /dev/null differ diff --git a/docs/project/chapter4/images/image39.png b/docs/project/chapter4/images/image39.png deleted file mode 100644 index 7146835..0000000 Binary files a/docs/project/chapter4/images/image39.png and /dev/null differ diff --git a/docs/project/chapter4/images/image4.png b/docs/project/chapter4/images/image4.png deleted file mode 100644 index 702daa0..0000000 Binary files a/docs/project/chapter4/images/image4.png and /dev/null differ diff --git a/docs/project/chapter4/images/image40.png b/docs/project/chapter4/images/image40.png deleted file mode 100644 index 8539d8d..0000000 Binary files a/docs/project/chapter4/images/image40.png and /dev/null differ diff --git a/docs/project/chapter4/images/image41.png b/docs/project/chapter4/images/image41.png deleted file mode 100644 index f67f215..0000000 Binary files a/docs/project/chapter4/images/image41.png and /dev/null differ diff --git a/docs/project/chapter4/images/image42.png b/docs/project/chapter4/images/image42.png deleted file mode 100644 index b16cbdd..0000000 Binary files a/docs/project/chapter4/images/image42.png and /dev/null differ diff --git a/docs/project/chapter4/images/image43.png b/docs/project/chapter4/images/image43.png deleted file mode 100644 index c1761ff..0000000 Binary files a/docs/project/chapter4/images/image43.png and /dev/null differ diff --git a/docs/project/chapter4/images/image44.png b/docs/project/chapter4/images/image44.png deleted file mode 100644 index 5b776d0..0000000 Binary files a/docs/project/chapter4/images/image44.png and /dev/null differ diff --git a/docs/project/chapter4/images/image45.png b/docs/project/chapter4/images/image45.png deleted file mode 100644 index 5361603..0000000 Binary files a/docs/project/chapter4/images/image45.png and /dev/null differ diff --git a/docs/project/chapter4/images/image46.png b/docs/project/chapter4/images/image46.png deleted file mode 100644 index 8d6cf1e..0000000 Binary files a/docs/project/chapter4/images/image46.png and /dev/null differ diff --git a/docs/project/chapter4/images/image47.png b/docs/project/chapter4/images/image47.png deleted file mode 100644 index c9e9226..0000000 Binary files a/docs/project/chapter4/images/image47.png and /dev/null differ diff --git a/docs/project/chapter4/images/image48.png b/docs/project/chapter4/images/image48.png deleted file mode 100644 index a80994d..0000000 Binary files a/docs/project/chapter4/images/image48.png and /dev/null differ diff --git a/docs/project/chapter4/images/image49.png b/docs/project/chapter4/images/image49.png deleted file mode 100644 index d7cdd3f..0000000 Binary files a/docs/project/chapter4/images/image49.png and /dev/null differ diff --git a/docs/project/chapter4/images/image5.png b/docs/project/chapter4/images/image5.png deleted file mode 100644 index c1ed41a..0000000 Binary files a/docs/project/chapter4/images/image5.png and /dev/null differ diff --git a/docs/project/chapter4/images/image50.png b/docs/project/chapter4/images/image50.png deleted file mode 100644 index a598961..0000000 Binary files a/docs/project/chapter4/images/image50.png and /dev/null differ diff --git a/docs/project/chapter4/images/image51.png b/docs/project/chapter4/images/image51.png deleted file mode 100644 index c9d2d99..0000000 Binary files a/docs/project/chapter4/images/image51.png and /dev/null differ diff --git a/docs/project/chapter4/images/image52.png b/docs/project/chapter4/images/image52.png deleted file mode 100644 index 5be2758..0000000 Binary files a/docs/project/chapter4/images/image52.png and /dev/null differ diff --git a/docs/project/chapter4/images/image53.png b/docs/project/chapter4/images/image53.png deleted file mode 100644 index 8f0d81a..0000000 Binary files a/docs/project/chapter4/images/image53.png and /dev/null differ diff --git a/docs/project/chapter4/images/image54.png b/docs/project/chapter4/images/image54.png deleted file mode 100644 index 3411d64..0000000 Binary files a/docs/project/chapter4/images/image54.png and /dev/null differ diff --git a/docs/project/chapter4/images/image55.png b/docs/project/chapter4/images/image55.png deleted file mode 100644 index 0342398..0000000 Binary files a/docs/project/chapter4/images/image55.png and /dev/null differ diff --git a/docs/project/chapter4/images/image6.png b/docs/project/chapter4/images/image6.png deleted file mode 100644 index b456140..0000000 Binary files a/docs/project/chapter4/images/image6.png and /dev/null differ diff --git a/docs/project/chapter4/images/image7.png b/docs/project/chapter4/images/image7.png deleted file mode 100644 index edd7554..0000000 Binary files a/docs/project/chapter4/images/image7.png and /dev/null differ diff --git a/docs/project/chapter4/images/image8.png b/docs/project/chapter4/images/image8.png deleted file mode 100644 index 79d0074..0000000 Binary files a/docs/project/chapter4/images/image8.png and /dev/null differ diff --git a/docs/project/chapter4/images/image9.png b/docs/project/chapter4/images/image9.png deleted file mode 100644 index b58ca10..0000000 Binary files a/docs/project/chapter4/images/image9.png and /dev/null differ diff --git a/docs/project/chapter5/chapter5-from-database-to-supabase.md b/docs/project/chapter5/chapter5-from-database-to-supabase.md deleted file mode 100644 index bc3e62a..0000000 --- a/docs/project/chapter5/chapter5-from-database-to-supabase.md +++ /dev/null @@ -1,1742 +0,0 @@ -# Project 5: 从数据库到 Supabase - -在上节课中,我们学会了 UI 设计程序 Mastergo 和 Figma 的基本用法,能够使用 github 进行代码的获取与版本管理,并通过 Zeabur 部署网站将自己的应用 / 网站传达给更多人使用。 - -为了帮助大家更好地衔接知识,在开始本节课关于设计工具与部署的新内容前,让我们一起通过几道简单的题目快速回顾一下上节课的核心知识点: - -1. 什么是前端设计工具、Figma、MasterGo 的定义和使用方式。 -2. 将设计稿转换为代码的基础方法。 -3. 什么是 Github,如何配置 SSH,如何构建自己的第一个仓库。 -4. 部署是什么意思,如何使用 Zeabur,如何将 Github 或本地代码部署至公共网络给大家访问。 - -如果对以上任何一个问题还有印象模糊的地方,建议先回顾一下上节课的文档和讲义。欢迎随时在微信学习群中提出疑问。 - -在本节课中,我们将学习如何让一个 APP / 网站从能跑起来变为更接近真实线上产品:除了用数据库管理程序运行中的各种数据变化外,还要具备完善的用户体系(注册、登录、权限等)以及其他关键后端能力。我们会以 Supabase 这一后端服务平台为主线,先用它实现“数据库 + 用户系统”这两项基础功能,再以 Supabase 提供的组件为参照,进一步理解现代云服务后端服务通常包含的核心模块,以及各模块的具体职能与作用逻辑。 - -# 你将学到 - -1. 什么是数据、什么是数据库,常见数据库与使用方法 -2. 什么是 supabase,如何使用 supabase 进行基础的数据库操作 -3. 如何使用 supabase 为应用添加基础用户管理功能 -4. 学会 Supabse 进阶功能:realtime、storage、edge function -5. 学会为Supabase增加 google 与 github 登录支持 - -- 一款支持用户注册 / 登录,并能将数据存入在线数据库的基础应用 -- 一套可复用的 Supabase 后端代码模板(数据库 + 用户管理等),供后续项目直接套用 - -# 1. What is Database - -## 1.1 What is Data - -在数字世界里,数据(Data)无处不在。简单来说,数据是信息的载体。你朋友的联系方式、一篇微信文章、一条短视频、游戏里的角色等级,这些都是数据。在我们的应用中,数据就是需要被记录和管理的一切信息,比如用户的个人资料、订单历史、程序设置等。 - -一般而言,数据在程序中有不同的表现形式,最简单的就是变量,我们可以用不同变量记录简单的数字: - -```Plain -# Python variable definition examples - -# Integer variable: stores age information -age = 30 - -# Boolean variable: stores status (whether active) -is_active = True # True means active, False means inactive - -# List variable: stores a set of score data -scores = [85, 92, 78, 90] # Contains 4 integer elements representing different scores - -# Dictionary variable: stores multiple related information of a user -user_info = { - "age": 30, # Key "age" corresponds to the value of age - "height": 1.80, # Key "height" corresponds to the value of height (unit: meter) - "login_count": 156 # Key "login_count" corresponds to the value of login times -} -``` - -而对于上述所说的个人资料、订单历史这类复杂的数据而言,我们可以用更复杂的表格进行数据的表示: - -| user_id | name | email | -| ------- | ----- | ----------------- | -| 1001 | Alice | alice@example.com | -| 1002 | Bob | bob@example.com | - -| order_id | user_id | amount | status | -| -------- | ------- | ------ | --------- | -| 901 | 1001 | 29.99 | completed | -| 902 | 1002 | 15.50 | pending | - -但对于结构复杂、具有层级关系或字段不固定的数据,我们可以用 JSON 格式进行描述 —— 它是互联网通用的数据中间格式,几乎所有程序都能读取解析,跨系统传数据很方便。例如,一个订单可能包含多个商品,每个商品又有自己的名称、数量和价格。用传统的表格来表示会很笨拙:要么得拆成 “订单表”“商品表” 多张表,靠关联字段才能体现 “订单包含商品” 的关系;要么在一张表用 “商品 1 名称、商品 1 价格、商品 2 名称……” 这类冗余字段,遇到商品数量不固定时根本没法适配;而 JSON 能直接用嵌套结构把 “订单 - 商品 - 商品属性” 的层级说清,既直观又灵活。 - -```JSON -{ - "order_id": 901, - "user_id": 1001, - "amount": 29.99, - "status": "completed", - "items": [ - { "sku": "BG-001", "name": "牛肉汉堡", "quantity": 1, "price": 18.00 }, - { "sku": "SD-003", "name": "炸薯条", "quantity": 1, "price": 6.99 }, - { "sku": "DK-002", "name": "可乐", "quantity": 1, "price": 5.00 } - ], - "shipping_address": { - "street": "科技园路123号", - "city": "深圳", - "zip_code": "518057" - } -} -``` - -更进一步的,如果我们考虑一个被编码成向量(Vector)的数据,向量数据通常是文本、图片或音频等非结构化数据经过 AI 模型(如 Embedding 模型)处理后得到的数值表示。它的表示形式可能是: - -`[0.123, -0.456, 0.789, ..., -0.234]` (一个由几百甚至上千个浮点数组成的数组) - -总的来说,在现实世界中有太多不同形态、用途的数据值得我们详细分析,每种数据可能都需要专门的数据库用于存储,具体可参考下图——是不是感觉非常多? - -![](images/image1.png) - -## 1.2 Why We Need Database - -我们已经了解到真实世界中的数据往往结构复杂,**为了高效存储与使用这些数据,我们需要一个专门的程序或容器来管理它们** —— 这便是数据库(Database)的诞生初衷。数据库本质上是一款特殊程序,核心作用就是对数据进行规范化组织、安全存储、系统化管理,并支持高效查询调用。 - -想象一下,若没有数据库,应用数据会陷入怎样的困境?当用户关闭浏览器或退出应用时,所有临时加载的信息都会直接丢失;我们既无法永久保存用户的使用状态(比如登录信息、个性化设置),也没法在不同用户之间共享关键数据(比如商品库存、订单记录)。我们需要有一个装置帮我们存储所有的数据! - -更灵活的是,数据库的部署方式可按需选择:既可以部署在本地服务器,满足数据本地化管理的需求;也能部署到云端,云端数据库支持弹性扩容(Scale),可随数据量与访问量增长扩展能力、承载海量数据与高并发,即便用户量大幅提升,也能保障用户的正常使用体验。 - -归纳而言,数据库凭借高效的持久化存储、精细化管理与快速查询能力,主要解决了以下核心问题: - -- **数据的持久化存储** : 如果没有数据库,数据将仅存在于应用的内存中,一旦应用关闭,数据就会丢失。数据库解决了这个问题,它将数据持久地存储在硬盘等存储介质上,确保了数据的长期保存,降低了丢失风险。 -- **便捷的数据查询与分析** : 数据库提供了强大的查询语言(如 SQL),让用户可以轻松、高效地对海量数据进行复杂的查询、筛选和分析,从而帮助企业做出更明智的决策。 如果没有数据库,从大量无序文件中查找特定信息将是一项极其耗时且困难的任务。 -- **支持高性能与高并发访问** : 数据库通过索引优化、查询缓存、连接池以及分布式架构等技术,能够在毫秒级时间内响应查询请求,并支撑成千上万用户的并发访问。这对于现代互联网应用(如电商平台秒杀活动、社交网络实时动态)至关重要,确保了系统的响应速度和用户体验。如果没有数据库的高性能支撑,面对海量用户请求时系统将会出现严重延迟甚至崩溃。 -- **保证数据的完整性和一致性** : 数据库通过一系列机制(如约束、触发器)来确保数据的准确性和一致性。 这意味着数据库中的数据必须符合预设的规则,例如,用户的年龄必须是数字,订单号必须是唯一的,从而有效防止了非法或无效数据的产生。 -- **确保数据的安全性** : 数据库提供了强大的安全机制,包括用户身份验证、访问控制和数据加密等,以保护数据免受未经授权的访问、修改或破坏。为了应对硬件故障、人为失误或恶意攻击等意外情况,数据库还提供了数据备份和恢复功能。 通过定期备份,可以在数据丢失或损坏时及时恢复,保障了业务的连续性。 - -## 1.3 Relational Database VS Non-Relational Database (NOSQL) - -前面我们已经了解了数据库的核心价值、部署方式与弹性优势,而在实际选择时,首先要面对的就是数据库的两大核心类别:关系型数据库与非关系型数据库(NOSQL),我们可以用简单的两段话简单理解他们的区别: - -关系型数据库就像结构严谨的Excel表格,所有数据必须预先定义好格式(定义好 Schema 的内容, 比如要有姓名和年龄,且姓名必须是文字,年龄必须是数字),并通过关联字段(用来连接不同表格的标识,如身份证号)将不同表格连接起来。它的好处是数据精确可靠,特别适合银行转账、库存管理等不能出错的场景,但缺点是调整结构比较麻烦,海量数据下性能会受限。 - -非关系型数据库则像灵活的文件夹,可以存放格式各异的文档、图片或键值对(类似字典的"词-解释"结构),不需要提前规定好每份数据的结构。它更容易应对快速变化的需求和超大规模数据(比如社交媒体的海量帖子),扩展(增加服务器提升性能)起来也更方便,但牺牲了部分关联查询能力(跨不同数据表整理信息的能力)和一致性保障(确保数据时刻准确不矛盾),适合对容错性要求较高的互联网应用。 - -那么,实际应用中该如何选择数据库?从场景划分总结来看,关系型数据库常见于金融交易、库存管理、订单处理、账务系统等需要强一致性、复杂事务处理以及频繁读写均衡访问的场景;而非关系型数据库更适配社交媒体内容存储、实时日志分析、物联网海量数据写入、推荐系统特征读多写多等高并发、读写模式不均衡且结构灵活的需求。 - -但对于企业而言,在初级阶段并不需要花大量时间思考什么需要使用什么数据库。当前的数据库已是非常成熟的产品服务,最直接的方式是咨询不同云服务厂商(指提供服务器、存储、数据库、软件、算力等 IT 资源与技术服务的服务商)。我们可直接对接云服务官方销售,根据自身产品业务需求匹配适配的数据库方案;而构建企业级应用的便捷路径,便是优先与专业厂商合作。(需注意:企业级服务价格通常较高,建议先多方调研对比,也可选择购买服务器自行部署开源数据库程序作为替代方案。) - -我们也可参考某家云厂商的[数据库选型推荐](https://help.aliyun.com/zh/govcloud/getting-started/select-database-services),根据场景可进行不同数据库类型的选择,你可以对比不同云厂商的数据库规格选出最合适的进行使用。 - -| 数据库类型 | 数据库名称 | 价格 | 适用场景 | -| ---------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| 关系型数据库 | RDS MySQL版 | 低 | 基础版:学习以及小型网站高可用版:一定业务压力的中型数据库场景集群版:业务不允许中断,访问压力较大 | -| RDS SQL server版 | 高 | 基础版:测试以及小型商业化网站高可用版:企业级商业化网站集群版:企业业务不允许中断,访问压力较大 | | -| RDS PostgreSQL版 | 最低 | 基础版:学习以及小型网站高可用版:一定业务压力的中型数据库场景集群版:业务不允许中断,访问压力较大的场景,其性能较一般MySQL高 | | -| RDS PPAS版 | 高 | 通用型:兼容Oracle业务,但业务压力Udacity,虚拟化可以满足其需求独享型:面对需要独享物理机的业务,一般为高并发Oracle类业务 | | -| DRDS | 中 | 入门版本:4 Core 8 G,价格亲民,适合中小型在线业务企业版:16 Core 32 G,复杂 SQL 响应好,适合超高并发在线业务至尊版:32 Core 64 G,复杂 SQL 执行响应最好,提供超大规格选择 | | -| NoSQL数据库 | Redis | 中 | 双机热备Redis:一般作为持久化数据库提高业务可用性集群版本的Redis:一般作为缓存层,加速应用访问,解决一般数据库无法负载的读取压力 | -| MongoDB版 | 中 | 单节点实例单节点:适用于开发、测试及其他非企业核心数据存储的场景副本集实例:适用于某些业务场景下对数据库有更高读取性能需求,如阅读类网站、订单查询系统等读多写少场景或有临时活动等突发业务需求分片集群实例:基于多个副本集(每个副本集沿用三副本模式)组成的分片集群实例,提供更高的读取性能需求,为实时在线业务提供高速读取性能 | | - -光说不易理解,我们通过一个具体的“博客文章”场景,来看看同样的数据在关系数据库 (SQL) 和不同类型的非关系数据库 (NoSQL) 中是如何存储的。 - -假设我们有一个博客平台,需要存储以下信息: - -- 用户(Users):用户 ID、用户名、邮箱 -- 文章(Posts):文章 ID、标题、内容、作者 ID -- 评论(Comments):评论 ID、评论内容、评论者 ID、所属文章 ID -- 标签(Tags):标签 ID、标签名 -- 文章与标签的关系:单篇文章关联的多个标签、单个标签对应的多篇文章 - -### 关系数据库 (SQL) 示例 - -在SQL数据库中,我们会将不同类型的数据分别存储在不同的表中,并通过“外键”将它们关联起来。这种结构清晰、规范,减少了数据冗余。 - -以 “内容平台的文章管理” 为例,我们不会把 “用户、文章、评论、标签” 混存,而是拆成 5 张功能单一的表,每张表都有明确的 “职责边界” 和严格的结构定义(Schema): - -- `users` 表 (存储用户信息) - -| user_id (主键) | username | email | -| -------------- | -------- | ----------------- | -| 101 | Alice | alice@example.com | -| 102 | Bob | bob@example.com | - -- `posts` 表 (存储文章信息) - -| post_id (主键) | title | content | author_id (外键) | -| -------------- | --------- | ------------------------------ | ---------------- | -| 1 | 初识SQL | 这是关于SQL数据库的一篇文章... | 101 | -| 2 | NoSQL入门 | NoSQL提供了灵活的数据模型... | 102 | - -- `comments` 表 (存储评论信息) - -| comment_id (主键) | body | commenter_id (外键) | post_id (外键) | -| ----------------- | ---------------- | ------------------- | -------------- | -| 1001 | 写得很棒! | 102 | 1 | -| 1002 | 学习了。 | 101 | 2 | -| 1003 | 有没有更多例子? | 101 | 1 | - -- `tags` 表 (存储标签) - -| tag_id (主键) | tag_name | -| ------------- | -------- | -| 51 | 数据库 | -| 52 | 技术 | -| 53 | 入门 | - -- `post_tags` 表 (存储文章与标签的多对多关系,体现联表特点) - -| post_id (外键) | tag_id (外键) | -| -------------- | ------------- | -| 1 | 51 | -| 1 | 52 | -| 2 | 51 | -| 2 | 52 | -| 2 | 53 | - -若需查询 “Alice 发表的《初识 SQL》(post_id=1)的完整信息(含文章内容、作者、评论、标签)”,需执行多表连接(JOIN)查询,通过外键关联 5 张表并聚合数据,SQL 语句如下: - -```SQL -SELECT - p.title, - p.content, - u.username AS author, - c.body AS comment, - t.tag_name AS tag -FROM - posts p -JOIN - users u ON p.author_id = u.user_id -LEFT JOIN - comments c ON p.post_id = c.post_id -LEFT JOIN - post_tags pt ON p.post_id = pt.post_id -LEFT JOIN - tags t ON pt.tag_id = t.tag_id -WHERE - p.post_id = 1; -``` - -这个查询会跨越5个表,将所有相关数据聚合在一起返回。这是关系数据库的核心优势:通过规范化和连接操作,可以灵活地进行各种复杂的查询,同时保证了数据的一致性和最小冗余。 - -### 非关系数据库 (NoSQL) 示例 - -NoSQL 数据库(如 MongoDB、Redis)的设计思路与 SQL 相反,它不强调数据的拆分与规范,通常会将业务上相关联的所有数据打包聚合在一起,以减少查询时的连接操作,从而提高读取性能。 - -在 NoSQL 数据库中,文档数据库(Document Database) 是最常用的类型之一,MongoDB 就是典型代表。它以 “文档” 作为基本存储单元,这里的 “文档” 并非我们日常理解的 “文章”,而是一种类似 JSON 的数据结构(MongoDB 中实际使用 BSON 格式,支持更多数据类型):无需预先定义统一的 Schema(数据结构),每个文档的字段可以灵活增减,字段类型也能自由调整,完美适配数据格式多变的场景。 - -在文档数据库中,通常会将一篇文章及其所有相关信息(如评论、标签)存储在一个文档中(文档格式类似 JSON,可灵活定义字段,无需预先制定 Schema),核心逻辑是 “将‘一个业务场景下的完整信息’存放在一个文档中”,避免查询时的多数据源拼接。 - -`posts` 集合中的一个文档示例: - -```JSON -{ - "_id": 1, - "title": "初识SQL", - "content": "这是关于SQL数据库的一篇文章...", - "author": { - "user_id": 101, - "username": "Alice", - "email": "alice@example.com" - }, - "tags": [ - "数据库", - "技术" - ], - "comments": [ - { - "comment_id": 1001, - "body": "写得很棒!", - "commenter": { - "user_id": 102, - "username": "Bob" - } - }, - { - "comment_id": 1003, - "body": "有没有更多例子?", - "commenter": { - "user_id": 101, - "username": "Alice" - } - } - ] -} -``` - -这种设计的优势非常直观:当你需要获取 “第一篇文章的完整信息(含作者、评论、标签)” 时,只需通过 `_id:1` 查询这一个文档,数据库一次读取就能返回所有数据,无需像 SQL 那样执行 3-4 次表连接操作,读取效率大幅提升。 - -但它也存在明显的 trade-off(取舍):由于数据是 “聚合存储”,会不可避免地产生数据冗余—— 比如作者 “Alice” 的 `username` 被嵌入到她写的每一篇文章文档中,如果某天 “Alice” 将用户名改为 “Alice_New”,理论上需要遍历所有包含她信息的文章文档,逐一更新 `author.username` 字段,不仅操作繁琐,还可能因网络或服务器问题导致部分文档更新失败,出现 “同一用户在不同文章中用户名不一致” 的情况。 - -不过在实际业务中,这种冗余往往是 “可接受的”:对于博客、资讯、电商商品详情等 “ **读多写少** ” 的场景(用户查看内容的次数远多于作者修改用户名的次数),用少量的冗余换取 “极致的读取性能” 是更优的选择;而如果是 “写多读少”(如频繁修改用户信息)的场景,则需要结合业务需求权衡是否使用文档数据库。 - -以上是对不同数据库的简单介绍,如果你对更多具体的数据库类型感兴趣,你可以参考如下资料尝试不同类型的数据库。 - -Examples of SQL databases: -[Db2](https://www.ibm.com/products/db2-database)、[MySQL](https://cloud.ibm.com/catalog#highlights)、[PostgreSQL](https://www.ibm.com/think/topics/postgresql)、[YugabyteDB](https://www.yugabyte.com/)、[CockroachDB](https://www.cockroachlabs.com/)、[Oracle Database](https://www.ibm.com/products/postgres-enterprise)、[Azure SQL Database](https://www.ibm.com/consulting/microsoft) - -Examples of NoSQL databases: -[Redis](https://www.ibm.com/think/topics/redis)、[CouchDB](https://www.ibm.com/think/topics/couchdb)、[MongoDB](https://www.ibm.com/think/topics/mongodb)、[Cassandra](https://cloud.ibm.com/catalog#highlights)、[Elasticsearch](https://www.ibm.com/think/topics/elasticsearch)、[BigTable](https://www.techtarget.com/searchdatamanagement/news/252512583/Google-scales-up-Cloud-Bigtable-NoSQL-database)、[Neo4j](https://neo4j.com/users/ibm/)、[HBase](https://www.ibm.com/think/topics/hbase) - -# 2. Supabase - -在前面我们已经介绍了几类常见的数据库,以及它们各自适合的使用场景。不过在真实项目里,数据库通常只是后端体系中的一个基础模块:除了存储和查询数据,你还需要解决**用户注册登录、权限校验、文件上传与存储、对外 \*\***API\***\* 接口、甚至定时任务、实时通知**等一整套问题。仅仅选好数据库,并不能让你的应用“立刻就能上线运行”,中间还隔着一大圈繁琐的后端工程工作。 - -所以,我们需要考虑一个更大的背景: **后端服务** 。一个完整的应用,通常都由“前端 + 后端”组成:前端负责页面展示和用户交互,后端则负责数据存储、用户登录、业务逻辑处理等。过去,开发者往往需要自己搭建服务器、配置数据库、设计并实现 API,还要手动处理权限管理、安全策略、扩展性和监控运维等事务,整个过程既重复又耗时。为了解决这些重复劳动,业界出现了 **BaaS(Backend as a Service,后端即服务)** :把数据库、用户认证、文件存储、实时能力等常见后端功能打包成一个云端平台,开发者通过 SDK / API 就能直接调用这些能力,而无需从零搭建和运维基础设施。 - -在这个背景下,[Supabase](https://supabase.com/) 就可以看作是新一代的 BaaS 代表:它以 PostgreSQL 作为核心数据库,在其之上集成了 Auth、Storage、Realtime、Edge Functions、Vector 等一整套后端能力,为开发者提供一个“以 Postgres 为中心的一站式后端平台”。接下来,我们就从这个角度出发,从“只选数据库”升级到“选择完整的后端开发平台”,具体看看 Supabase 能帮我们省掉哪些工作,又是如何让一个项目从原型到可用产品的距离大幅缩短的。 - -## 2.1 Step by Step Guide - -在清晰把握 Supabase 的整体定位后,接下来我们将沿着 Supabase 控制台的操作路径,逐项拆解它具体提供哪些核心能力,以及每项能力对应的核心职责。我们会详细介绍 supabase 涉及的每个选项,帮助你快速入门 supabase 的基本操作。 - -![](images/image2.png) - -访问 Supabase 官网并登录后,在控制台首页点击 New project 进入创建流程; - -输入需要配置的主要内容 Project Name、数据库密码,地址只需要选择为与程序目标用户最接近的区域即可。 - -![](images/image3.png) - -创建成功后,控制台左侧侧边栏将显示所有核心功能模块(Table Editor、SQL Editor、Database、Authentication 等),后续操作将围绕这些模块展开。 - -![](images/image4.png) - -### Table Editor - -Table Editor 可以当成是 Supabase 的可视化数据表编辑器,它能让你像操作 Excel 一样直接查看和修改数据库里的数据,无需编写 SQL 语句,只需要用鼠标交互即可修改数据内容。 - -![](images/image5.png) - -其中值得关注的是 Schema,Schema 可理解为数据库内的 “资源容器”,用于对表、视图、函数、索引等资源进行分组管理,主要作用有二:一是避免命名冲突(不同 Schema 下可存在同名table),二是实现权限隔离(如仅允许特定用户访问某 Schema 下的表); - -点击编辑器顶部的 Schema 下拉框可切换不同容器,日常开发中一般只需关注两类: - -- `public`:默认的公共资源容器,开发者新建的业务表(如 “文章表”“评论表”)均存储于此; -- `auth`:用户认证专属容器,其中的 `users` 表自动存储所有注册用户信息(如用户 ID、邮箱、登录时间),不建议手动修改此 Schema 下的默认表,避免影响认证功能; - -![](images/image6.png)![](images/image7.png) - -### SQL Editor - -SQL Editor 作为 Supabase 的 SQL 语句执行器,可让你用代码的方式直接操作数据库。你可以让大模型直接生成 SQL 语句,在右侧输入后点击 RUN 即可用语句创建或修改 table,也可以直接在 Results 中直接看到筛选出的 table 数据。 - -![](images/image8.png) - -你可以在运行 RUN 之后,在 Table Editor 的 public schema 里找到新建后的数据表;并且运行后的语句会保存在左侧的 PRIVATE 栏中,甚至可以点击下方的爱心标志对这一条查询或创建语句进行收藏。 - -### Database - -Database 是 Supabase 的数据库管理中心,支持可视化地查看和管理所有数据表,并通过表的相互连线理解不同表间的关联关系(即外键约束,表示数据间的引用关系)。 - -![](images/image9.png) - -如果你想要手动新建 table,可以在 tables 中直接新建表格,我们会在之后的教程中详细讲解。 - -![](images/image10.png) - -### Authentication - -Authentication 负责管理用户的注册、登录和权限。默认的用户管理系统数据都在此处存储,它提供了开箱即用的用户注册、登录、密码重置、邮箱验证等功能,并支持第三方 OAuth 登录(如微信、GitHub、Google 等)。所有用户数据会自动同步到数据库的 `auth.users` 表中。 - -![](images/image11.png) - -你可以在 Provider 选项中找到不同 supabase 支持的用户信息登录入口,默认使用 Email;如果你想使用 Github 或者 Google 账户进行登录,还需要更多属性配置,我们会在下面的课程中进行详细讲解。 - -![](images/image12.png) - -在 Sign In / Providers 里还包含了对注册邮箱行为的控制,如果你不想每次邮箱注册都必须让用户接受邀请后才能成为用户,你可以取消 Confirm email 的强制要求。 - -![](images/image13.png) - -如果你想切换非 Supabase 的其他 auth 系统服务商,你可以点击 Third Party Auth,比如此处就使用 Clerk 作为第三方的系统服务商。 - -![](images/image14.png) - -如果你担心注册用户在短期内访问量过大,你可以在 Rate Limits 中启用对应的流量限制策略: - -![](images/image15.png) - -### Storage - -Storage 是 Supabase 的存储系统,兼容 amazon cloud 的 s3 概念,可用于存储任意类型的文件(如图片、视频、文档、音频等),并提供访问权限管理(公开或私有)和下载链接获取(永久链接或临时链接),你能够很方便在应用中对用户涉及到的文件内容进行上传与下载管理,并与 Supabase 的认证系统无缝集成,实现精细化的访问控制。 - -![](images/image16.png) - -我们将会在本节课的进阶 project 中讲解 storage 的具体用法。 - -![](images/image17.png) - -如果你想使用 S3 的相关协议进行操作,可以直接使用对应的配置: - -![](images/image18.png) - -> Amazon Cloud(亚马逊云服务,简称 AWS)是亚马逊提供的云计算平台(就像一个大型的网络机房,你可以按需租用计算和存储资源)。S3(Simple Storage Service)是 AWS 里专门用来存储文件的服务(类似一个无限大的网盘,可以存图片、视频、备份等各种文件),它是目前最流行的对象存储服务,已经成为了事实上的行业标准。 -> -> **为什么要做成 S3 兼容 \*\***API\*\* ** ?** :S3 已经存在近 20 年,市面上有大量现成的工具、SDK 和文档,兼容 S3 意味着你可以直接用这些资源,不用从头开始制作各类相关工具,能够快速满足业务上线的需求。 - -### Edge Functions - -如果你不想部署后端,但是想使用数据库和函数操作,你可以使用 Edge Functions 构建无需自建服务器的后端核心能力,它是 Supabase 提供的全球分布式服务端函数。简单来说,它让你无需购买和管理自己的后端服务器,就能直接编写并部署在云端的后端代码。这些函数部署在全球网络的边缘节点上,会自动在离你的用户最近的位置运行,从而大幅降低网络延迟,提供极致的响应速度。你可以在 Supabase 的仪表盘中直接创建、编辑和部署,整个开发流程非常便捷。 - -![](images/image19.png) - -Edge Functions 的一个核心用途是充当安全的中间层,保护你的敏感信息和鉴权密钥。在前端代码中直接调用第三方服务(如 OpenAI、Stripe)会暴露你的 API Key,带来极大的安全风险。通过 Edge Functions,你的前端应用只与你的 supabase 函数通信,所有秘密只在 supabase 中保管。 - -![](images/image20.png) - -Edge Functions 的函数使用 secrets 中暴露的密钥作为环境变量,通过 `Deno.env.get` 加载,从而实现第三方服务的调用。这样一来,敏感密钥就永远不会暴露在客户端(你的浏览器),彻底杜绝了被盗用的风险。 - -![](images/image21.png) - -请求 Supabase Edge Function 时,需在请求头携带对应的 Supabase 密钥,下面是一个极简示例: - -```JavaScript -// 核心配置(替换为你的实际信息) -const projectId = "你的 Supabase 项目ID"; -const functionName = "目标 Edge Function 名称"; -const supabaseKey = "Supabase anon_key"; - -// 调用函数 -async function callEdgeFunction() { - const url = `https://${projectId}.supabase.co/functions/v1/${functionName}`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${supabaseKey}` // 关键:携带密钥完成认证 - }, - body: JSON.stringify({ order_id: "123", action: "refund" }) // 自定义请求数据 - }); - - const result = await response.json(); - console.log("调用成功:", result); - } catch (error) { - console.error("调用失败:", error.message); - } -} - -// 执行调用 -callEdgeFunction(); -``` - -此外,Edge Functions 与 Supabase 的用户认证系统无缝集成。当已登录的用户调用一个函数时,其身份信息会传递给函数。这使得你可以在函数内部轻松识别当前用户,并根据其身份执行权限控制。更重要的是,函数在操作数据库时会自动遵循你设置好的行级安全策略(Row Level Security),确保用户只能访问和修改他们有权操作的数据,让构建安全的多用户应用变得简单。 - -Edge Functions 的应用场景非常广泛,能够处理各种后端任务。它们非常适合用来监听来自第三方服务的 Webhook 事件(例如支付成功、代码提交等),并自动执行相应的数据处理逻辑。你也可以用它来发送邮件通知、生成 PDF 报告、创建自定义的 API 接口来封装复杂的业务逻辑,或者执行任何你希望在服务端完成的计算任务,极大地扩展了你应用的能力。 - -具体到一个常见的例子:身份认证工具 Clerk 。Clerk 仅用于处理用户登录、注册、信息更新等认证相关操作,并不直接管理你的业务数据库。如果你想要将这些认证动态同步到业务数据库中,则需要通过触发 Webhook 事件请求 Edge Functions 实现。Edge Functions 能够监听 Clerk 发出的 Webhook 信号,自动执行数据同步逻辑,让 Supabase 数据库中的用户信息与 Clerk 登录状态实时对齐,全程无需你部署独立后端。 - -### Realtime - -Realtime 是 Supabase 的实时数据同步引擎,它允许你的应用即时接收数据库的变化通知,而无需反复轮询 API。当数据库中的数据发生 `INSERT`、`UPDATE` 或 `DELETE` 操作时,Realtime 会通过 WebSocket 将这些变化实时推送给所有已连接的客户端。这对于构建需要实时交互的应用至关重要。 - -Realtime 主要包含三大核心功能,覆盖了绝大多数实时场景: - -1. **Postgres Changes:** 直接监听数据库表的变化。你可以精确地订阅特定表、特定事件(增、删、改),甚至可以根据筛选条件来接收通知,并与行级安全策略(Row Level Security)完美集成,确保用户只能收到他们有权限查看的数据变更。 -2. **Broadcast:** 允许客户端之间通过频道(Channel)发送低延迟的临时消息。这非常适合实现聊天室、实时光标追踪、在线游戏状态同步等功能。 -3. **Presence:** 用于追踪和同步在线用户状态。你可以用它来轻松实现“谁在线上”、“当前有X人正在查看”等功能,非常适合协作类应用。 - -我们会在后续的项目制学习中详细介绍该部分的内容。 - -### Project Settings - -Project Settings 是 Supabase 项目的高级配置部分,你可在此实现计算资源的深度调度,以及各类功能底层参数的精细化配置。 - -![](images/image22.png) - -在入门阶段,我们只需聚焦以下两个核心板块,一个是 Data API,我们在此可获取关键的 “Supabase URL”, 它是形如 `https://xxx.supabase.co` 的 RESTful 端点,是所有数据查询、新增、修改、删除操作的 “入口地址”。前端或服务端需通过该 URL 初始化 Supabase 客户端,建立与数据库的连接。 - -![](images/image23.png) - -另一个重点是 API Keys,选择 “Legacy anon, service_role API keys” 标签页,其中的 anon public 密钥 是前端场景的重要身份凭证,它的权限被 RLS 严格限制,仅能访问用户被授权的数据。而 service_role 密钥属于 “服务端高权限密钥”,具备绕过行级安全的能力,可执行批量数据操作、系统级配置等敏感操作。绝对禁止公开分享,若泄露需立即生成新密钥并更新服务端配置。 - -![](images/image24.png) - -其余配置项在当前阶段无需深究,待后续有进阶使用需求时再逐一探索即可。 - -## 2.1 Create Your First SQL Table - -以上是 Supabase 的界面介绍,接下来我们将深入 Supabase 的核心数据库的操作环节。 - -在 Supabase 中创建数据表,主要有以下两种常用方式,你可以根据需求选择: - -1. (推荐)借助大语言模型生成适配 Supabase 的 SQL 语句,直接在 **SQL Editor(** 前文介绍的 SQL 语句执行器)中粘贴执行,高效快捷,我们会在下个部分环节重点说明这个操作过程。 -2. 通过可视化操作创建:在左侧侧边栏找到 Database 模块,点击进入后选中侧边栏的 Tables,在右侧点击 New table 按钮,即可通过图形化界面创建数据表。 - -![](images/image25.png) - -值得注意的是,对应数据表的名称以及存储的数据类型可在下方的 Columns 中指定。 - -![](images/image26.png) - -对于关系数据库,其中很重要的特点是表与表之间的关联,你可以在下方找到 `Foreign keys` ,点击创建相应的关联关系: - -![](images/image27.png) - -其中 `Foreign keys` 表达了表与表之间的关联关系:一个或一组字段,它在当前表(子表)中的值,会引用另一张表(父表)中主键的值。 - -例如,在创建 `学生表`的时候,我们可以这样定义外键:(`所属班级编号` 这一列是一个外键。这个外键引用了 `班级表` 里的 `班级编号` 这一列。) - -```SQL -CREATE TABLE 学生表 ( - 学生学号 INT PRIMARY KEY, - 学生姓名 VARCHAR(50), - 所属班级编号 INT, - FOREIGN KEY (所属班级编号) REFERENCES 班级表(班级编号) -); -``` - -更具体举例而言,我们可以可视化观察对应的表的结构: - -班级表: -这张表里记录了所有班级的信息,每个班级都有一个独一无二的班级编号。班级编号就是这张表的主键 (Primary Key),是每个班级的唯一身份证。 - -| 班级编号 | 班级名称 | -| -------- | ---------- | -| 101 | 一年级一班 | -| 102 | 一年级二班 | - -学生表: -这张表记录了所有学生的信息。每个学生都属于一个特定的班级,对吗?那么我们怎么知道哪个学生在哪个班级呢? - -我们可以在学生表里增加一列,叫做 `所属班级编号`。 - -| 学生学号 | 学生姓名 | 所属班级编号 | -| -------- | -------- | ------------ | -| 2024001 | 张三 | 101 | -| 2024002 | 李四 | 102 | -| 2024003 | 王五 | 101 | - -在该例子中,学生表中的 `所属班级编号` 列就是外键 (Foreign Key)。 - -在 Supabse 中,点击添加 Foreign Key 后,你可直接选择进行关联表对应列的选取 - -![](images/image28.png) - -## 2.3 SQL Editor 简介与数据库基本操作 - -接下来我们将分步执行一系列 SQL 脚本,熟悉常见的 SQL 中的增删查改操作。你可以将每个步骤的代码复制到 SQL Editor 中,执行并观察结果。 - -你可以在该目录下获得所有的测试 SQL 文件: - -https://github.com/THU-SIGS-AIID/Project5-Supabase-Demos/tree/main/apps/sql-examples - -### **2.3.1 **`CREATE`** - 创建表结构** - -`CREATE TABLE` 语句用于为新表定义模式(Schema),包括其列(Columns)、对应的数据类型(Data Types)以及任何约束(Constraints),简单理解是创建了一个数据表。 - -```SQL --- Step 1: Create the 'orders' table --- This file is fully independent and creates a sample table for later steps. -CREATE TABLE IF NOT EXISTS orders ( - id serial PRIMARY KEY, - user_id int NOT NULL, -- User ID - status text NOT NULL, -- Order status (e.g. paid, pending) - amount numeric(10, 2) NOT NULL, -- Order total amount - details jsonb, -- Item and extra details as JSON - placed_at timestamptz DEFAULT now(), -- Order creation time - is_paid boolean DEFAULT false -- Paid flag -); - --- Expected Output: --- Orders table created if it did not exist. --- No data inserted. (Querying returns zero rows for now.) --- If table already exists, no error occurs. -``` - -成功执行后,系统将提示脚本已完成。你可以在 Table Editor 中看到对应的表被创建完成: - -![](images/image29.png) - -### **2.3.2 **`INSERT`** - 填充初始数据** - -表结构创建完毕后,下一步是使用 `INSERT INTO` 语句向表中添加数据行。 - -```SQL --- Step 2: Insert initial rows into the orders table --- Provides realistic, varied data for demo/testing. All values are self-contained. -INSERT INTO orders (user_id, status, amount, details, placed_at, is_paid) VALUES - (2001, 'pending', 23.50, '{"items":[{"sku":"BGR001","name":"Beef Burger","qty":1,"price":12.00}]}', now() - interval '2 days', false), - (2002, 'paid', 50.00, '{"items":[{"sku":"BGR002","name":"Chicken Burger","qty":2,"price":10.00},{"sku":"DRK001","name":"Lemonade","qty":2,"price":5.00}]}', now() - interval '1 day', true), - (2003, 'cancelled', 15.00, '{"items":[{"sku":"FRY001","name":"French Fries","qty":3,"price":5.00}], "reason":"Not available"}', now() - interval '45 days', false), - (2004, 'paid', 22.98, '{"items":[{"sku":"BGR003","name":"Veggie Burger","qty":2,"price":9.99}], "promo":"SUMMER22"}', now() - interval '10 days', true), - (2005, 'pending', 18.75, '{"items":[{"sku":"SAL001","name":"Salad","qty":1,"price":6.75},{"sku":"BGR001","name":"Beef Burger","qty":1,"price":12.00}]}', now() - interval '7 hours', false), - (2006, 'paid', 8.00, '{"items":[{"sku":"DRK002","name":"Cola","qty":2,"price":4.00}]}', now() - interval '3 hours', true), - (2007, 'refunded', 14.50, '{"items":[{"sku":"BGR003","name":"Veggie Burger","qty":1,"price":9.99},{"sku":"FRY001","name":"French Fries","qty":1,"price":4.51}], "refund_reason":"Late delivery"}', now() - interval '15 days', false), - (2008, 'paid', 26.99, '{"items":[{"sku":"BGR002","name":"Chicken Burger","qty":2,"price":10.00},{"sku":"DRK001","name":"Lemonade","qty":1,"price":6.99}]}', now() - interval '12 days', true), - (2009, 'pending', 9.99, '{"items":[{"sku":"BGR003","name":"Veggie Burger","qty":1,"price":9.99}]}', now() - interval '30 minutes', false), - (2010, 'paid', 19.89, '{"items":[{"sku":"BGR001","name":"Beef Burger","qty":1,"price":12.00},{"sku":"DRK002","name":"Cola","qty":2,"price":3.95}]}', now() - interval '5 days', true), - (2011, 'cancelled', 0.00, '{"items":[], "reason":"User cancelled"}', now() - interval '2 days', false); - --- Expected Output: --- After running this script, SELECT * FROM orders will show about 11 rows with varied user_id, status, amount, details (JSON), placed_at, and is_paid fields. --- For example: --- | id | user_id | status | amount | is_paid | placed_at | --- |----|---------|-----------|--------|---------|---------------------| --- | 1 | 2001 | pending | 23.50 | false | 2025-10-28 13:40:00Z| --- | 2 | 2002 | paid | 50.00 | true | ... | --- |... | ... | ... | ... | ... | ... | -``` - -执行成功后,此时表中已经插入了原始数据,你可以进入到 Table Editor 界面刷新后看到结果,也可以直接在 SQL Editor 界面中新建窗口,执行查询语句 `SELECT * FROM orders;`查看结果: - -![](images/image30.png) - -### **2.3.3 **`SELECT`** - 读取与查询数据** - -`SELECT` 语句用于从表中检索数据。通过使用不同的子句,可以实现对数据的精确筛选、排序和格式化,我们可参考以下语句一步步执行查看结果: - -```SQL --- Step 3: SELECT query examples for the orders table - --- Example 1: Select all fields for all orders -SELECT * FROM orders; --- Expected Output: Returns all rows and fields. Columns: id, user_id, status, amount, details, placed_at, is_paid. - --- Example 2: Select only pending orders -SELECT id, user_id, amount FROM orders WHERE status = 'pending'; --- Expected Output: All rows with status 'pending'; columns: id, user_id, amount. - --- Example 3: Select specific fields and filter by payment status -SELECT id, status, is_paid, amount FROM orders WHERE is_paid = true; --- Expected Output: All rows where is_paid is true; columns: id, status, is_paid, amount. - --- Example 4: Extract all item names from the details (JSON) for each order -SELECT id, details -> 'items' AS item_list FROM orders; --- Expected Output: Each row shows id and an array from JSON with item details. -``` - -- **示例 1:** 返回 `orders` 表中的所有行和列,与第二步的输出类似。 -- **示例 2:** 仅返回状态为 'pending' 的订单,且只包含指定的列: - -![](images/image31.png) - -- **示例 3:** 仅返回已支付的订单,并显示指定的列: - -| id | status | is_paid | amount | -| --- | ------ | ------- | ------ | -| 2 | paid | true | 50.00 | -| 4 | paid | true | 22.98 | -| 6 | paid | true | 8.00 | -| 8 | paid | true | 26.99 | -| 10 | paid | true | 19.89 | - -- **示例 4:** 返回每个订单的 `id` 和从 `details` 字段中提取的 `items` 数组: - -| id | item_list | -| --- | -------------------------------------------------------------------------------------------------------------------- | -| 1 | `[{"qty":1,"sku":"BGR001","name":"Beef Burger","price":12}]` | -| 2 | `[{"qty":2,"sku":"BGR002","name":"Chicken Burger","price":10},{"qty":2,"sku":"DRK001","name":"Lemonade","price":5}]` | -| 3 | `[{"qty":3,"sku":"FRY001","name":"French Fries","price":5}]` | -| ... | ... | - -### **2.3.4 **`INSERT`** - 插入单条记录** - -在 2.3.2 中,我们演示的是开头时刻初始化批量插入数据,现在我们查看如何新增插入单条数据。 - -```SQL --- Step 4: INSERT a new order (single row) --- Example: Add a new paid order for user 2012 with one Chicken Burger -INSERT INTO orders (user_id, status, amount, details, is_paid) -VALUES ( - 2012, 'paid', 9.99, - '{"items":[{"sku":"BGR002","name":"AIID Burger","qty":100,"price":1000}]}', - true -); --- Expected Output: --- Before (table fragment): --- | id | user_id | status | amount | is_paid | --- | ...| ... | ... | ... | ... | --- --- After (last row): --- | id | user_id | status | amount | is_paid | --- | xx | 2012 | paid | 9.99 | true | --- (where xx = next serial value) -``` - -此时再用 `SELECT * FROM orders;` 对数据进行查询,我们可以看到 orders 表成功从 11 个数据变成了 12 个数据。 - -### **2.3.5 **`UPDATE`** - 修改现有数据** - -在实际工作中,我们需要对数据表进行频繁数据更新,我们能够用 `UPDATE` 语句修改表中已存在的记录。 - -```SQL --- Step 5: UPDATE example --- Example: Mark order with id=1 as paid and update its status -UPDATE orders SET status = 'paid', is_paid = true WHERE id = 1; --- Expected Output: --- Before (row with id=1): --- | id | status | is_paid | --- | 1 | pending | false | --- After (row with id=1): --- | id | status | is_paid | --- | 1 | paid | true | --- All other rows remain unchanged. -``` - -### **2.3.6 **`DELETE`** - 删除数据** - -`DELETE` 语句可用于从表中移除记录,并结合条件对指定部分的数据进行修改。 - -```SQL --- Step 6: DELETE example --- Example: Delete orders older than 2 days to clean up old data -DELETE FROM orders WHERE placed_at < now() - interval '2 days'; --- Expected Output: --- Before (filtered for affected rows): --- | id | status | placed_at | --- | 3 | shipped | 2025-10-13 ... | <-- will be deleted --- --- After: --- No such rows remain. SELECT * FROM orders WHERE placed_at < now()-interval '2 days' yields zero rows. --- Other rows in orders table are unaffected. -``` - -执行前,你可先执行 `SELECT id, status, placed_at FROM orders WHERE placed_at < now() - interval '2 days';` 进行数据表筛选结果的查看。当运行 `DELETE` 命令后,再次执行相同的 `SELECT` 查询 `SELECT id, status, placed_at FROM orders WHERE placed_at < now() - interval '2 days';`,将返回一个空的结果,表明这些行已被成功删除。 - -## 2.4 RLS (Row level security) - -在学习了数据库的基本操作后,我们需要进一步深入一个保障数据安全的核心概念 ——RLS(行级安全,Row Level Security)。 - -不妨先思考一个实际场景中的关键问题:如何实现数据的 “隔离访问”?比如,只允许用户 A 查看自己的数据,而无法看到用户 B 的信息;再比如,即便某角色拥有数据库的访问权限,如何避免其误操作或泄露其他用户的敏感数据? - -RLS 正是为解决这类数据安全与隔离需求而生。它允许开发者为数据库表定义精细化的安全策略,根据用户的身份信息(如用户 ID、角色权限等),精确控制哪些用户能访问、修改表中的哪些行数据。 -举个典型示例:对于订单表(`orders`),我们可以定义这样一条 RLS 策略 ——“仅当 `orders` 表中某条记录的 `user_id` 列,与当前登录用户的 ID 完全一致时,该用户才能查询到这条订单数据”,从而实现 “用户只能看自己的订单” 的核心需求。 - -当你为某张表启用 RLS后,该表的所有数据操作请求(包括 `SELECT` 查询、`INSERT` 新增、`UPDATE` 修改、`DELETE` 删除)都会触发 RLS 校验:必须通过至少一条安全策略的检查,操作才能执行。若不存在允许该操作的策略,或请求未满足任何策略的条件,数据库会直接拒绝此次操作,从底层阻断非授权访问。 - -在 Supabase 中,RLS 与用户认证系统深度绑定,使用起来更为便捷。Supabase 提供了一个专用函数 `auth.uid()`,它能直接返回 “当前发起请求的已登录用户” 的唯一 ID(格式为 UUID)。借助这个函数,我们可以轻松编写策略,实现 “数据行与用户身份” 的精准关联(比如前文提到的 “订单 `user_id` 匹配当前用户 ID”)。 - -启用 RLS 策略的方式很灵活,你可以在 Supabase 数据库管理界面中的 “RLS” 按钮,直接配置并启用策略: - -![](images/image32.png) - -![](images/image33.png) - -![](images/image34.png) - -主动配置难免显得麻烦,通常,我们在数据表语句创建、初始化的时候就会自动考虑植入对应的 RLS 策略。我们只需在 SQL Editor 中执行类似如下语句,即可自动开启对应数据表的行级安全策略。 - -![](images/image35.png) - -# 3. The First SQL Application - -掌握了数据库基础操作与RLS核心逻辑,我们终于进入本次教程的实践环节。漫长的学习铺垫是为了让后续“从0到1搭建应用”的过程更清晰。接下来,我们将以“汉堡店订单管理”为场景,手把手演示Supabase的常见操作:从应用与Supabase的关联配置,到数据库与登录功能的集成,逐步学习不同操作逻辑。 - -## 3.1 Clone and Run Supabase Demos - -要开展实操,首先需要获取配套的演示代码仓库。你可以让 Trae 或 ClaudeCode 协助 git clone 以下仓库:https://github.com/THU-SIGS-AIID/Project5-Supabase-Demos - -若已配置 SSH 密钥,建议使用 SSH 地址进行 clone(git@github.com:THU-SIGS-AIID/Project5-Supabase-Demos.git)以提升安全性;若 SSH 或 HTTPS 连接遇到网络问题,可以直接点击仓库页面的 “Download ZIP”,获取压缩包后解压即可看到完整代码。 - -![](images/image36.png) - -Clone 后,你同样可以让 Trae 或者是 ClaudeCode 帮你启动项目,例如直接在 Agent 界面中说明: `帮我直接启动这个项目里面的 project 1 `,或者复制对应想启动 project 的绝对路径,粘贴给大模型让大模型直接启动。 - -## 3.2 Project1 - burger-shop-menu-crud - -接下来进入实操环节 —— 以 `project-burger-shop-menu-crud-1` 为例,我们将学习如何通过 SQL 脚本一键初始化 Supabase 数据库,并完成本地项目与 Supabase 数据库的关联配置,让前端能正常读写菜单数据。 - -### Create a Database Using Scripts - -首先,我们需要在 Supabase 中创建需要的数据表的相关内容。进入 Project1 项目目录看到名为 `scripts`的文件夹,其中包含 1 个 `init.sql`数据库脚本文件,它能帮我们自动完成所有数据库相关资源的创建(包括表结构、初始数据等),之后我们会经常用到该文件进行数据库中表的初始化。 - -```SQL -...... - --- ============================================================================ --- 2. Create Menu Items Table --- ============================================================================ - -create table if not exists public.menu_items ( - id uuid primary key default gen_random_uuid(), - name text not null, - description text, - category text check (category in ('burger','side','drink')) default 'burger', - price_cents int not null check (price_cents > 0), - available boolean default true, - emoji text, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - --- Comments for documentation -comment on table public.menu_items is 'Burger shop menu items for CRUD demo'; -comment on column public.menu_items.id is 'Unique identifier for each menu item'; -comment on column public.menu_items.name is 'Display name of the menu item'; -comment on column public.menu_items.description is 'Detailed description of the menu item'; -comment on column public.menu_items.category is 'Category: burger, side, or drink'; -comment on column public.menu_items.price_cents is 'Price in cents (integer) to avoid floating point issues'; -comment on column public.menu_items.available is 'Whether the item is currently available for order'; -comment on column public.menu_items.emoji is 'Optional emoji representation of the menu item'; -comment on column public.menu_items.created_at is 'Timestamp when the item was created'; -comment on column public.menu_items.updated_at is 'Timestamp when the item was last updated'; - -...... -``` - -在 SQL Editor 中执行初始化 sql 脚本后,即可在 Table Editor 中看见已创建的数据表。其中数据库初始化代码具体执行逻辑如下: - -1. 创建 menu_items 表 : -2. 这个表用于存储汉堡店菜单中的所有项目。它包含了如 name (商品名), description (描述), price_cents (以美分为单位的价格,避免浮点数精度问题), category (分类) 和 available (是否可售) 等字段。这基本涵盖了一个菜单项所需的所有信息。 -3. 创建 promo_codes 表 : -4. 此表用于管理促销活动,例如折扣码。它定义了 code (折扣码), discount_type (折扣类型,如百分比或固定金额), discount_value (折扣数值) 等字段。 -5. 禁用行级安全 (Row Level Security - RLS) : -6. 为了方便开发和测试,脚本中明确地禁用了 RLS。但结合我们之前学习的 RLS 核心逻辑:RLS 是 Supabase 保障数据安全的关键功能,能通过精细化策略控制 “谁能访问 / 修改哪些数据”(比如只允许管理员编辑促销码,普通用户只能查看菜单)。因此在生产环境中,必须开启 RLS 并配置合理策略,从底层阻断非授权访问(如防止用户恶意修改他人创建的菜单,或泄露促销码规则)。 -7. 插入种子数据 (Seed Data) : -8. 为了让前端项目启动后就能看到真实的菜单与促销数据(无需手动录入测试数据),`init.sql`脚本还会向 `menu_items`和 `promo_codes`表中插入 “种子数据”(即示例数据)。例如,你可以看到各种汉堡、小食、饮料以及多种多样的折扣码。 - -### Set up the connection with database - -数据库准备完成,我们需要将这个前端项目与 Supabase 进行连接,从而正常读取数据库内的数据。我们需要将 Supabase 项目的 URL 和 anon key 写到指定配置中,本项目提供了两种灵活的配置方式: - -1. 通过环境变量配置 - -在项目根目录创建一个 .env 文件,并填入你的 Supabase 凭证: - -```Plain -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key -``` - -2. 在项目页面中直接设置 - -为了方便快速演示和切换不同的 Supabase 项目,首页页面右上角提供了一个 设置 按钮。你可以点击它,在弹出的模态框中直接输入或粘贴 Supabase URL 和 anon key。 - -点击 “Save” 后,这些信息会用于动态创建 Supabase 客户端实例,类似下列代码所示: - -```JavaScript -import { createClient, type SupabaseClient } from '@supabase/supabase-js'; - -// Optional client factory for demos: returns null when env is not set. -export function maybeCreateBrowserClient(): SupabaseClient | null { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const anon = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - if (!url || !anon) return null; - return createClient(url, anon); -} -``` - -创建完数据库,填写完对应的 Supabase Link 相关配置后,即可看到如下界面,你可以尝试对商品进行增删查改,并观察 Supabase 中对应部分数据表的变化。 - -![](images/image37.png) - -![](images/image38.png) - -### 📚 Assignment - -1. 尝试增加和删除已有项目,在 Table Editor 中查看修改操作对数据表内容变动的影响。 - -## 3.4 Project2 - burger-shop-auth-users - -Project1 实现了 “菜单 CRUD + 数据库连接” ,Project2 将引入更贴近真实业务的核心能力,用户认证(Auth)与行级安全(RLS)权限管理。 - -Project2 包含独立的登录页,支持用户通过「邮箱 + 密码」的方式登录。其核心逻辑是调用 Supabase Auth 提供的原生方法,快速实现认证流程,无需手动开发复杂的登录校验逻辑: - -```Plain -const { error: err } = await supabaseClient.auth.signUp({ - email, - password, - options: { - data: { - full_name: fullName || null, - birthday: birthday || null, - avatar_url: avatarUrl || null - } - } -}); -``` - -![](images/image39.png) - -登录成功后,Supabase 会自动为用户创建一个会话(session),并在后续所有数据库请求中自动携带认证信息;通过 RLS 的作用,每个用户根据对应的认证信息只能看到自己的账户信息(已购买项目、钱包剩余额度),无法看到其他用户的账户信息,这就实现了不同用户登录后的数据隔离,每个人只能看到自己的内容。 - -和 Project 1 一样,你需要先使用 `init.sql` 进行数据表的初始化(注:如果发现初始化出错,请先在 Table Editor 中删除已经创建的数据表,或者是直接删除这个 Supabase Project, 重新新建一个 Project) - -成功使用邮箱注册账户、在邮箱确认注册账户后,登录后进入 Shop 界面即可看到如下内容: - -![](images/image40.png) - -但此时点击 admin,你并不能看到如下界面,你需要尝试在数据表中找到控制用户权限的部分,将权限修改为 `admin`,从而能够在 Admin 界面正常看到如下内容: - -![](images/image41.png) - -值得提示的是,目前每次注册新的邮箱,你都需要在邮箱中进行注册确认才可登录;但这一步并非是必须的,你可以在 Supabase 的 Authentication 栏目中找到 Sign In / Providers,点击Confirm email 取消邮箱的强制确认。 - -![](images/image42.png) - -### 📚 Assignment - -1. 请先领取新手礼包,完成商品购买操作。 -2. 尝试找到用户权限的设定数据表位置,将权限修改为 `admin`,并成功在订单管理界面修改商品数量 -3. 尝试在数据表内定位到钱包金额相关表,通过修改使剩余钱包金额增加。 - -# 4. Build Your First Supabase App - -经过前面的系统学习,你已掌握 Supabase 的核心能力(数据库操作、用户认证、RLS 安全策略),现在是时候亲自动手,搭建属于你的第一个包含数据库、支持用户登录系统的应用了! - -## 4.1 为任意应用接入 Supabase 数据库的标准化流程 - -我们可以使用标准化流程将任意应用接入 Supabase 数据库: - -1. 首先进行需求梳理与信息同步,明确目标并告知AI - 1. 你需要向AI清晰描述当前应用的核心功能、待新增的数据库需求。示例:“我现有一个本地React Todo应用,数据仅存在浏览器本地存储,需新增‘数据云端同步’功能并接入Supabase数据库。请帮我梳理:这个应用涉及哪些数据操作(如新增待办、修改状态、删除待办)?需要创建哪些数据表来存储这些数据?” - 2. 补充关键约束条件(可选):比如字段格式要求(时间戳用 `timestamptz`、金额用整数存分)、数据权限规则(仅自己可见待办),让AI的分析更贴合实际需求。 - 3. 对 AI 返回的结果进行审核,若AI思路存在遗漏(如未考虑“待办截止时间”字段),补充提示修正:“你漏考虑截止时间了,帮我加上。” -2. 让AI基于你确认后的表结构,生成适配Supabase的 `init.sql`脚本:“基于上述所说思路和表的结构,返回给我在 Supabase 中可以进行初始化的 init.sql 脚本”,之后你需要在 SQL Editor 中执行脚本;若执行报错,将错误信息反馈给AI,让其修正脚本。 -3. 在 Supabase 运行 init.sql 脚本后,让 AI 基于脚本重构当前代码,使得能够和 Supabase 进行正常的数据交互:“请你根据我的 sql 脚本以及上面讨论的设定,重构项目的代码让它支持能够和 Supabase 对应的数据库进行通信并处理数据”。 -4. 重构完毕,此时只需要配置好 Supabase 地址和 key 的参数(正式项目通常只用环境变量配置),随后进行检查,若没问题则顺利实现将应用接入 Supabase 数据库。 - 1. 运行项目,测试所有数据库交互功能,到Supabase Table Editor 实时查看数据是否同步; - 2. 若出现问题(如数据无法插入、仅能看到部分数据),将问题现象反馈给AI,让其定位原因并修正代码。 - -此外,若目标是开发用户登录页面,可直接让 AI 协助集成登录页面 :“现在你需要帮我给这个应用加入 Supabase 的用户登录系统,使用邮箱可以注册和登录”。另外,你还需要向 AI 明确页面的跳转逻辑与路径(如登录成功后跳转至系统首页、跳转首页的地址是什么、登录失败时留在当前页并显示错误提示)。集成完成后,你需要尝试注册登录后能在 Supabase 的 Authentication 项目中看到新增的用户数据,并在登录后能正常进入到原先未登录无法进入的应用界面即可。 - -当然,你还可以直接让 AI 参考某个 project 的实现直接迁移对应的 Supabase 功能,比如某个 Project 用到了数据库以及 Edge fuction 的高级功能,你可以按照如下方式直接让 AI 迁移对应的相似功能:“请你参考该项目 {此处复制粘贴参考项目的绝对地址} 当中的 Supabase 相关功能实现逻辑,给当前项目加上类似的实现逻辑(如用户登录、数据库管理、函数请求等等)”。 - -## 4.2 Case Study : Build an Online Snake Game - -根据上面所提到的 SOP ,让我们通过一个具体的实际案例 `Project5-Supabase-Demos/apps_snakegame`来实践:为一个已有的“贪吃蛇”游戏项目增加分数排行榜单,包含用户登录与数据库基础功能。 - -![](images/image43.png) - -### 4.2.1 分析项目,识别数据需求 - -首先,和在之前提到的标准化流程类似,我们可以先把需求澄清给 AI ,让 AI 基于我们项目和需求给出对应的修改方案,之后我们会基于这个修改方案。 - -**你可以使用如下的提示词来指导 AI:** - -> “我有一个贪吃蛇游戏,目录在 {此处粘贴贪吃蛇游戏的绝对路径}。现在我想结合 supabase 给它增加一个在线排行榜功能,并且支持用户登录系统,排行榜可以根据用户名和邮箱显示排名。 -> -> 请帮我分析一下,为了实现这个功能,我需要建立哪些数据表?每个表应该包含哪些字段?” - -此时你会得到类似如下返回: - -![](images/image44.png) - -### 4.2.2 生成 `init.sql` 脚本 - -确定需要的部分,我们可以让 AI 生成需要在 Supabase 执行的数据库初始化脚本:“请你基于上面的分析,帮我在项目中生成 scripts/init.sql 脚本用于在 Supabase 中初始化所需数据库”。 - -![](images/image45.png) - -### 4.2.3 改造项目代码 - -接下来我们只需要让 AI 基于前面的内容重构当前的贪吃蛇代码:“接下来请你基于前面思考的内容以及 sql 表,使用 Supabase 帮我实现排行榜功能,排行榜是单独的一页,需要可以根据邮箱和用户名区分不同用户的总分,你还需要支持基于邮箱的用户登录系统,注册登录才能玩这个游戏。” - -如果当前 AI 对话轮次太多,你想重开一个新的会话进行项目重构,你可以把上面提到的 `init.sql`作为上下文中的内容,让 AI 基于 sql 文件进行项目重构。 - -若是发现 AI 实现的用户登录系统不够正常,你可以直接将我们之前写好的 `Project5-Supabase-Demos/apps/project-burger-shop-auth-users-2` 的地址一同放入提示词,让 AI 基于项目直接实现用户登录系统。并检查是否已经正确设定了连接到 Supabase 的必要条件,防止因为 Supabase 配置错误而报错。 - -在代码修改过程中,若出现实际效果与预期不符的情况(如排行榜数据不显示、登录验证失效等),只需完整记录具体现象并反馈给 AI,即可逐步接近正确结果。改造成功的标准为:用户能顺利完成注册与登录操作,且登录后可正常查看对应的游戏排行榜单。 - -![](images/image46.png) - -![](images/image47.png) - -### 📚 课程作业 - -1. 将用户管理系统集成到贪吃蛇游戏演示版中 -2. 将用户管理系统集成到你的应用程序中(如果之前已开发过一个应用程序) - -# 3. Become Supabase Master - -以上是 Supabase 的基本操作,接下来的旅程中我们将会接触 Supbase 的进阶原理和功能,你将理解为什么我们会选择 Supabase 作为教学案例,以及如何使用 Supbase 实现更高级的操作,协助你实现更复杂的交互功能,并且在学习这些功能后,即便面对 Supabase 之外的其他同类工具,你也能触类旁通,从更本质的层面理解后端服务的核心原理。当然,你并不需要在短时间内学会全部,也许只需要学会第三方登录支持已经足够,你可以先浏览下列内容,直到项目遇到对应的需求时再倒回来深入学习。 - -## 5.1 Why We choose Supabase - -在开始进阶之前,我们再次思考这个问题:众多后端技术方案中,为何我们最终选择 Supabase 作为技术底座? - -初创团队在技术选型时普遍面临一个矛盾:既想完全掌控后端系统,又必须快速上线产品——而自建后端通常意味着要投入数月时间搭建数据库与实时同步、用户认证、API服务、文件存储、定时任务、监控告警等核心组件,除非团队成员已在对应领域积累了丰富的实战经验。在资金不足、市场窗口短暂的双重压力下,一旦陷入基础设施泥潭,极易导致迭代滞后、错失早期增长空间。 - -Supabase 将这些后端能力打包为开箱即用的服务(PostgreSQL数据库、实时订阅、身份认证、对象存储、边缘函数、自动生成API等),让初创团队得以将稀缺资源聚焦于核心功能开发,避免因底层建设拖慢上线速度——这已成为当前创投环境下务实的生存策略。当然,我们也可以使用别的一栈式后端产品进行开发,例如 PocketBase(轻量极简)和 Appwrite(跨平台适配)等方案,但综合功能完整性、SQL 生态成熟度及 Github 社区关注度,Supabase 更适合支撑业务的长期稳定运行。 - -在同类产品中,Supabase 的开源策略更具优势。 以市场占有率较高的 Firebase 为例:其闭源特性易导致平台绑定,迁移成本极高。Supabase 采用完全开源模式,支持私有化部署,规避了供应商锁定风险,可根据需求切换至其其他竞品。 - -总而言之,技术选型需匹配业务规模与目标。 对于个人项目或极小范围测试,PocketBase 等超轻量方案已足够;若企业需对接复杂身份系统,或要满足上市公司合规审计要求,WorkOS 这类企业级全身份治理方案更为适用。但对于验证 MVP、承载早期用户的核心业务场景,Supabase 的完整功能完全够用,它不仅能独立支撑至少万级用户规模,更可灵活集成 Stripe(支付)、Resend(邮件)、Cloudflare(CDN)等第三方服务;即便未来业务扩展至企业级需求,Supabase 的开源架构也能与企业系统并行部署,不同功能选择最适配的平台进行使用。这种渐进式灵活性,使初创团队无需过早投入重型基础设施,又能保留 future-proof 的演进空间。 - -## 5.2 Google & Github Login Support - -在前面的教程中,我们讲解了如何直接使用邮箱进行注册和登录,但在实际操作中我们通常想要简化注册流程,例如使用第三方登录 Google 和 GitHub 进行系统的快速注册与登录,我们将会在这节教程中展开每个细节;同时,一个完整的认证系统也必须提供安全可靠的密码重置功能,我们也会将密码重置功能集成在本节教程的项目中。 - -本项目 `Project5-Supabase-Demos/apps/project-burger-shop-auth-advanced-supabase-6`)完整地演示了如何实现这些高级功能。 - -![](images/image48.png) - -### 5.2.1 OAuth 流程:第三方登录是如何工作的? - -第三方登录的核心是 OAuth 2.0 开放授权协议,它的本质是 “授权代理”:允许用户授权我们的应用(汉堡店项目)访问其在第三方平台(如 Google)的公开信息(如邮箱、头像),但无需将第三方平台的密码暴露给我们的应用,从根本上规避了密码泄露风险。 - -完整流程可拆解为 5 个关键步骤,以 Google 登录为例: - -1. 用户发起授权请求:用户点击页面上的 “Sign in with Google” 按钮,我们的应用会自动将用户重定向到 Google 官方的授权页面(确保授权过程的安全性,避免钓鱼风险)。 -2. 用户完成第三方授权:用户在 Google 页面登录自己的账户(验证用户身份),并同意我们的应用请求的权限(如 “获取邮箱地址”)。 -3. Google 返回一次性授权码:授权通过后,Google 会将用户重定向回我们提前约定的 “回调 URL(Callback URL)”,并在 URL 参数中附带一个一次性、短期有效的授权码(而非直接返回用户信息,进一步提升安全性)。 -4. Supabase 交换访问令牌(Access Token):我们的后端(由 Supabase 托管,无需自建)会拿着这个授权码,向 Google 官方接口发起请求,换取可用于获取用户信息的 Access Token(授权码仅用于换 Token,避免 Token 直接在前端传输)。 -5. 创建账户并建立会话:Supabase 使用 Access Token 从 Google 拉取用户的公开信息(如邮箱、头像),并在我们的项目中为该用户自动创建账户(若首次登录)或直接关联现有账户,最终生成一个有效的用户会话(Session),完成登录。 - -![](images/image49.png) - -### 5.2.2 配置 Google Cloud 获取 Client ID 和 Secret - -无论是何种第三方登录方式,我们通常都需要获取 Client ID 与 Secret 进行配置;对于 Google 的第三方登录,你首先需要在 Google Cloud Platform 中创建一个 OAuth 2.0 客户端 ID 进行对应参数的获取。 - -1. **进入 Google Cloud Console** : -2. 访问 [Google Cloud Console](https://console.cloud.google.com/)。 -3. 创建一个新项目或选择一个现有项目。 -4. **配置 OAuth 同意屏幕 (OAuth consent screen)** : -5. 在左侧导航栏中,找到 “APIs & Services” -> “OAuth consent screen”。 -6. 选择 “External” 用户类型,然后点击 “Create”。 -7. 填写应用名称、用户支持电子邮件等必填信息。 -8. 在 “Authorized domains” 部分,添加你的 Supabase 项目域名,格式为 `*.supabase.co`。 -9. 保存并继续。在 “Scopes” 和 “Test users” 步骤中,你可以暂时跳过,直接保存。 -10. **创建凭据 (Create Credentials)** : -11. 进入 “APIs & Services” -> “Credentials”。 -12. 点击 “+ CREATE CREDENTIALS”,选择 “OAuth client ID”。 -13. 在 “Application type” 中选择 “Web application”。 -14. 为它取一个名字,例如 “Supabase Auth”。 -15. 在 “Authorized redirect URIs” 部分,点击 “ADD URI”,并填入你的 Supabase 项目的回调 URL。你可以在 Supabase Dashboard 的 “Authentication” -> “Providers” -> “Google” 中找到这个 URL,它的格式通常是 `https://<你的项目ID>.supabase.co/auth/v1/callback`。 - ![](images/image50.png) -16. 点击 “CREATE”。 -17. **获取 Client ID 和 Client Secret** : -18. 创建成功后,一个弹窗会显示你的 **Client ID** 和 **Client Secret** 。请务必**立即复制并妥善保存** 它们。 - -### 5.2.3 配置 GitHub 获取 Client ID 和 Secret - -同样地,你也需要在 GitHub 上注册一个 OAuth 应用。 - -1. **进入 \*\***GitHub\*\* ** Developer Settings** : - 1. 登录你的 GitHub 账户。 - 2. 点击右上角的头像,进入 “Settings”。 - 3. 在左侧导航栏的底部,找到 “Developer settings”。 - -2. **注册新应用 (Register a new application)** : -3. 选择 “OAuth Apps”,然后点击 “New OAuth App”。 -4. 填写应用名称,例如 “My Burger Shop”。 -5. **Homepage URL** : 填写你应用的线上地址,或者本地开发地址 `http://localhost:3000`。 -6. **Authorization \*\***callback\*\* ** URL** : 填入你的 Supabase 项目的回调 URL。同样,你可以在 Supabase Dashboard 的 “Authentication” -> “Providers” -> “GitHub” 中找到它,格式为 `https://<你的项目ID>.supabase.co/auth/v1/callback`。 -7. 点击 “Register application”。 -8. **获取 Client ID 和 Client Secret** : -9. 注册成功后,页面会显示你的 **Client ID** 。 - ![](images/image51.png) -10. 点击 “Generate a new client secret” 来生成你的 **Client Secret** 。同样,请**立即复制并保存** 它。 - -### 5.2.4 在 Supabase 中配置 Provider - -现在,将我们获取到的凭证配置到 Supabase 中。 - -1. **进入 Supabase Dashboard** : -2. 选择你的项目,进入 “Authentication” -> “Providers”。 -3. **启用并配置 Google** : -4. 找到 “Google” 并启用它。 -5. 将你从 Google Cloud 获取的 **Client ID** 和 **Client Secret** 粘贴到对应的输入框中。 -6. 点击 “Save”。 -7. **启用并配置 ** **GitHub** : - 1. 找到 “GitHub” 并启用它。 - 2. 将你从 GitHub 获取的 **Client ID** 和 **Client Secret** 粘贴到对应的输入框中。 - 3. 点击 “Save”。 - -![](images/image52.png) - -至此,你已经能够使用第三方账户在构建的网站中进行登录,你可以直接让 AI 基于 `Project5-Supabase-Demos/apps/project-burger-shop-auth-advanced-supabase-6`项目作为参考,在你的项目的基础上支持用户登录系统,以最小成本集成包含 github 与 google 鉴权的用户登录界面。 - -### 5.2.6 密码重置实现 - -作为一个成熟的用户登录组件,密码重置也是极其重要的一环,本项目 `project-burger-shop-auth-advanced-supabase-6`也包含了该功能的完整实现,你可以直接让 AI 基于本项目的密码重置功能复刻完整的密码重置组件。其主要分为以下几步: - -1. 发起请求 :用户在忘记密码页面输入邮箱,前端调用 `supabase.auth.resetPasswordForEmail()` 函数,并指定一个重定向 redirectTo URL(例如 /auth/reset )。 -2. 发送邮件 :Supabase 会向该邮箱发送一封包含唯一重置链接的邮件。 -3. 访问链接 :用户点击邮件中的链接,被重定向到应用内指定的重置页面。 -4. 更新密码 :在重置页面,用户输入新密码。前端调用 `supabase.auth.updateUser()` ,将新密码提交给 Supabase。Supabase 会自动验证链接的有效性并完成密码更新。 - -最后,如果你觉得当前的密码重置邮件过于简陋,你可以 在 Supabase Dashboard 的 Authentication -> Email Templates 中自定义“Reset Password”邮件模板。 - -除了 Reset password 功能外,你还能看到许多其他与用户管理相关的高级功能设定(例如 Invite user 等),你可根据对应功能各自的开发文档,结合 Vibe coding 工具自行添加对应功能。 - -![](images/image53.png) - -## 5.3 Realtime Function - -Supabase 的实时功能是其最强大的特性之一,为构建协作文档、实时仪表盘、游戏大厅或客服系统提供了极大的便利。 - -本项目 `Project5-Supabase-Demos/apps/project-burger-shop-realtime-orders-3 `通过构建一个 多人实时聊天室、光标位置共享 功能,展示了 Supabase Realtime 涉及到的三大核心能力:数据库变更监听 (Postgres Changes)、广播 (Broadcast) 和 在线状态 (Presence)。 - -![](images/image54.png) - -如果你觉得相关代码部分有一定难度,可以直接让 AI 参考该部分文档内容,对你的程序进行修改。 - -### 5.3.1 数据库实时变动 Postgres Changes - -最常见的 Realtime 功能是对数据库的变更进行实时监听 Postgres Changes 。它允许客户端订阅数据库中特定表、特定行甚至特定列的 INSERT 、 UPDATE 或 DELETE 事件。一旦数据库发生变动(无论是通过 API 调用、Supabase Dashboard 操作,还是 SQL 脚本执行),Supabase 都会利用 PostgreSQL 的底层复制机制,立即通过 WebSocket 将变更的数据推送到所有订阅了该频道的前端客户端,而无需前端通过轮询(Polling)去反复查询。 - -一般而言,该功能可以在 Table Editor 中找到 Enable Realtime 点击后启动, 但更方便的是通过 SQL 脚本初始化执行,例如: - -```SQL --- Enable realtime replication -ALTER TABLE public.chat_messages REPLICA IDENTITY FULL; -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_publication_tables - WHERE pubname = 'supabase_realtime' - AND schemaname = 'public' - AND tablename = 'chat_messages' - ) THEN - ALTER PUBLICATION supabase_realtime ADD TABLE public.chat_messages; - END IF; -END $$; -``` - -该语句将 `chat_messages` 表添加到了 Supabase 预设的 `supabase_realtime` 中,而一旦一个表被加入到这个特殊的 `publication` 中,Supabase 的实时服务器就会开始监听它的所有数据变更。 - -基于上面的特殊数据表,我们能够使用监听代码对表内数据变动进行实时监听。我们需要实现的是当一个用户发送消息时,其他所有在线用户都能立刻在屏幕上看到这条消息。通过订阅 chat_messages 表的 INSERT 事件能够实现这一点。 - -```TypeScript - const sub = supabase - .channel('chat_messages_channel') - .on('postgres_changes', { - event: 'INSERT', - schema: 'public', - table: 'chat_messages' - }, (payload: any) => { - console.log('New message received:', payload.new); - const newMessage = payload.new as Message; - // ... // - .subscribe((status: string) => { - console.log('Chat subscription status:', status); - }); -``` - -- `.channel('chat_messages_channel')`: 创建一个隔离的通信频道。 -- `.on('postgres_changes', ...)`: 这是核心的订阅方法。我们告诉 Supabase 我们只关心 `chat_messages` 表的 `INSERT` 事件。 -- `payload.new`: 当有新消息被插入数据库时,Supabase 会将这条新数据的完整内容通过 `payload.new` 推送给所有订阅的客户端。 -- `.subscribe()`: 启动订阅。 - -### 5.3.2 信息广播同步 Broadcast & Presence - -对于那些不需要存入数据库的、更“即时”的交互,比如光标移动、在线状态等,Supabase 提供了 Broadcast 和 Presence 功能。 - -- Presence: 用于跟踪频道内所有客户端的 **共享状态** 。适合用来实现“谁在线”的功能。 -- Broadcast: 用于向频道内的所有其他客户端发送**低延迟**的 **临时消息** 。 - -Presence 的核心思想是: 让每个客户端声明自己的在线状态,并由 Supabase 的服务器负责将这些状态可靠地同步给频道内的所有其他客户端。实现 Presence 分为以下几个关键步骤: - -1. 创建一个支持 Presence 的频道 - -首先,我们创建了一个频道 `lobby_presence` 来专门处理这些交互,并在配置中指定一个唯一的 key 来标识当前用户。这个 key 通常是用户的 ID。 - -```Plain -const ch = supabase.channel -('lobby_presence', { -  config: { -    presence: { key: anonymousUser.id }, -  } -}); -``` - -2. 订阅频道宣告“我在线”的信息 - -一旦频道创建成功,我们需要订阅它。在订阅成功的回调( status === 'SUBSCRIBED' )中,我们调用 channel.track() 方法。这个方法会将当前用户的信息(例如用户ID、名称、头像颜色等)广播给频道内的所有其他客户端,宣告自己的“在线”状态。 - -```Plain -const me = { -  id: anonymousUser.id, -  name: anonymousUser.name, -  color: anonymousUser.color -}; - -ch.subscribe(async (status) => { -  if (status === 'SUBSCRIBED') { -    await ch.track(me); -  } -}); -``` - -3. 同步完整的在线列表 - -当一个新用户加入频道时,他们需要获取当前所有已经在线的用户列表。这通过监听 presence 的 sync 事件来实现。 sync 事件会在你首次加入频道时触发,为你提供一个完整的“快照”。 - -channel.presenceState() 方法会返回一个对象,包含了当前频道内所有在线用户的状态信息。我们将其处理后更新到应用的 state 中,从而渲染出完整的在线用户列表。 - -```Plain -ch.on('presence', { event: 'sync' }, ()  -=> { -  const state = ch.presenceState(); -  const flat = {}; -  Object.values(state).forEach((arr) => { -    arr.forEach((u) => { flat[u.id] =  -    { ...u }; }); -  }); -  setOnline(flat); -}); -``` - -4. 监听单个用户的加入与离开 - -除了 sync 事件,我们还可以监听 join 和 leave 事件,以便在有新用户进入或离开时做出即时响应,例如显示一个 "User has joined" 的通知。 - -```Plain -ch.on('presence', { event: 'join' }, ({  -key, newPresences }) => { -  console.log('User joined:', key,  -  newPresences); -}); - -ch.on('presence', { event: 'leave' }, ({  -key, leftPresences }) => { -  console.log('User left:', key,  -  leftPresences); -}); -``` - -通过以上步骤,我们便构建了一个功能完备的在线状态系统。Supabase 自动处理了用户意外断开连接(如关闭浏览器或断网)的情况,并在适当的时候触发 leave 事件,确保了在线列表的准确性。 - -当 Presence 让我们知道了“谁在场”之后, Broadcast 能够让他们之间能够进行“对话”,但对话的内容是短暂存储的。一个典型的例子就是实时光标追踪。如果每次鼠标移动都去读写数据库,会造成巨大的性能浪费和延迟。 Broadcast 完美地解决了这个问题,它允许消息在各个客户端之间直接通过 WebSocket 传递,完全绕过数据库。 - -Broadcast 的工作模式主要依赖两个核心方法: channel.send() 用于发送,channel.on() 用于接收。】 - -1. 发送端:广播我的光标位置 - -我们为 mousemove 事件添加了一个监听器。当鼠标移动时,我们构造一个包含用户 ID、坐标和颜色的 payload,然后通过 channel.send() 将其广播出去,并指定事件名称为 'cursor'。 - -```TypeScript -const handleMouseMove = (e) => { - const payload = { - id: anonymousUser.id, - x: e.clientX, - y: e.clientY, - name: anonymousUser.name, - color: anonymousUser.color - }; - - channelRef.current?.send({ - type: 'broadcast', - event: 'cursor', - payload - }); -}; - -document.addEventListener('mousemove', handleMouseMove); -``` - -2. 接收端:监听并渲染他人的光标 - -在同一个频道内,所有客户端都使用 channel.on() 来监听 broadcast 类型的、且 event 为 'cursor' 的消息。一旦收到匹配的消息,回调函数就会被触发。我们从 payload 中解析出发送方的数据,并用它来更新本地的 online 状态,从而在屏幕上实时渲染出其他用户光标的位置。 - -```TypeScript -ch.on('broadcast', { event: 'cursor' }, ({ payload }) => { - setOnline((prev) => ({ - ...prev, - [payload.id]: { - ...(prev[payload.id] || {}), - x: payload.x, - y: payload.y - } - })); -}); -``` - -通过这种方式, Presence 和 Broadcast 协同工作;Presence 维护在线用户列表,而 Broadcast 则负责在这些用户之间传递像光标位置这样的临时状态,最终以较低的成本实现了丰富的实时互动功能。 - -## 5.4 Storage - -除了用户信息、订单这类可规整定义的结构化数据,一个完整的应用通常还需要处理大量非结构化文件 —— 例如用户头像、商品展示图、用户上传的订单文档等。这类文件的特点是体积差异大、数量可能极多(比如电商平台的商品图可能达数万甚至数十万张),若直接存储在应用自身的业务服务器中,会显著增加服务器的存储负载,还可能拖慢数据读写速度,影响应用整体性能。 - -实际开发中,这类非结构化文件会统一交由 “对象存储服务” 管理,OSS、Amazon S3 均属于这类服务,它们是专门为海量文件存储设计的 “专业存储工具”,能高效应对文件的存储、备份与快速读取需求。而我们在应用中获取这些文件时,并不会直接从对象存储服务的 “底层仓库” 调取,而是通过 URL 地址实现:每个存储在对象存储中的文件,都会被分配一个唯一的 URL(类似 “[https://xxx.oss.com/avatar/user123.jpg](https://xxx.oss.com/avatar/user123.jpg)” 的地址,可简单理解为这个“网站”只有一张图片),这个 URL 就像文件的 “专属访问地址”,前端页面只需通过该地址,就能直接下载或加载头像、商品图,无需依赖应用业务服务器中转,既提升了文件加载速度,也减轻了业务服务器的压力。 - -本项目 `project-burger-shop-storage-uploads-4` 便通过一个用户头像上传功能,深入演示了如何利用 Supabase Storage 构建现代化的文件上传系统,让开发者直观理解非结构化文件从上传到通过 URL 访问的完整流程。此外,本项目使用 `Uppy` 库来提供一个优秀的文件上传界面,并结合 `Tus` 插件实现了可续传上传,通过将 Uppy 的上传端点指向 Supabase 的标准 API (`/storage/v1/upload/resumable`) 进行工作,你可以参考类似的方式实现上传功能组件。 - -![](images/image55.png) - -![](images/image56.png) - -### 5.4.1. Bucket - -Supabase Storage 的组成单元是存储桶 Bucket。你可以把它想象成电脑操作系统中的文件夹。每个 Bucket 都可以有自己独立的安全策略和配置。 - -Storage 内的所有文件都可以通过一个公开的 URL 直接访问,但并不意味着任何人都可以随意上传或修改,具体的访问权限将由更精细的策略来控制。和数据库一样,Storage 的访问权限也是通过行级安全策略来管理的。SQL 策略写在 storage.objects 和 storage.buckets 这两张特殊表上,可以精确定义谁能读取 (SELECT)、上传 (INSERT)、更新 (UPDATE) 或删除 (DELETE) 文件。 - -例如,我们可以创建一条策略,只允许用户上传到以自己 user_id 命名的文件夹下,并且只能上传图片类型的文件: - -```Plain -CREATE POLICY "Allow authenticated  -uploads to avatars bucket" -ON storage.objects FOR INSERT -TO authenticated -WITH CHECK ( -  bucket_id = 'avatars' AND -  auth.uid() = (storage.foldername(name)) -  [1]::uuid AND -  (storage.extension(name) IN ('png',  -  'jpg', 'jpeg')) -); - -CREATE POLICY "Allow public read access  -to avatars" -ON storage.objects FOR SELECT -USING ( bucket_id = 'avatars' ); -``` - -### 5.4.2 获取可访问文件 URL - -本项目需要你手动创建一个名为 avatars 的公共桶,所有文件将上传至该公共桶下进行存储。文件上传成功后,我们只得到了它在 Storage 中的存储路径 ,例如 public/avatar1.png 。这只是存储在数据库中的一个字符串,要让浏览器能够渲染这张图片,我们需要将其转换为一个可访问的 HTTP URL。 - -Supabase 提供了两种截然不同的策略来获取这个 URL,它们在安全性、持久性和成本控制上有着本质的区别。 - -#### 1. 公开 URL (Public URL) - 永久链接 - -这是最直接的方式。如果你的文件存放在一个**Public Bucket** 中,你可以获取一个固定、永久的公开链接。 - -```TypeScript -const { data } = supabase.storage - .from('avatars') - .getPublicUrl('public/avatar1.png'); -const publicUrl = data.publicUrl; -``` - -这类链接具有两大核心特点:一是简单直接,其 URL 结构固定,在实际操作中易于拼接和管理,降低了技术使用门槛;二是利于缓存,作为永久链接,它能被 CDN(内容分发网络)和浏览器有效缓存,从而大幅提升资源的访问速度,优化用户体验。基于这些特点,它适用于真正意义上的公共资源场景,例如网站 Logo、产品目录图片、博客文章配图等,能很好地满足这类资源的访问和管理需求。 - -不过在生产环境中,这类链接存在明显的被盗刷流量(Hotlinking)风险。由于链接是永久公开的,外部人员可以轻易将你的图片链接嵌入到他们自己的高流量网站中,导致流量被非法占用。这一行为会让你的 Supabase 项目产生大量不必要的流量费用,而这些消耗的流量并未服务于你自身的应用,属于典型的成本浪费,是生产环境中需要高度警惕和防范的问题;因此,我们需要转向临时签名 URL 实现对外资源的暴露。 - -#### 2. 签名 URL (Signed URL) - 临时授权链接 - -为了解决公开 URL 的安全和成本问题,Supabase 提供了生成临时签名 URL 的方式。这是绝大多数线上应用推荐的最佳实践,比如文生图应用给用户生成限时查看的图片链接、电商平台仅让下单用户获取临时发票下载地址、付费内容平台为订阅用户提供短期有效的课程播放链接,既防文件盗用又能避免流量盗刷,适配性极强。 - -```TypeScript -const { data, error } = await supabase.storage - .from('avatars') - .createSignedUrl('private/user-invoice.pdf', 3600); // 链接有效期为 3600 秒 (1小时) -const signedUrl = data?.signedUrl; -``` - -临时签名 URL(Signed URL)有三大核心优势:安全可控是指链接带安全标记、有有效期,过期就用不了;权限绑定很简单 —— 只有能看这文件的人,才能生成这个链接,就算文件藏在私有存储里(Private Bucket),他用这个链接也能正常打开;杜绝盗刷是因为链接是临时的,复制到别处很快就失效,不会被恶意刷流量。靠这些优势,像用户头像、私人照片、付费内容、订单发票这些需要管权限的文件,都能用它。 - -从安全保障和成本控制的角度,建议养成优先使用临时签名 URL 的习惯。只有当某个资源明确需要永久公开、无限制访问(比如应用的公开 Logo、公共活动宣传图等)时,才考虑使用 Public URL。这样既能满足特定业务需求,又能最大程度规避不必要的风险和成本消耗。 - -## 5.5 Edge Function - -Edge Function 是 Serverless(无服务器架构)生态中极具核心价值的形态之一,它为 “无自建后端” 场景提供了轻量、高效的函数运行支持。 - -什么是 Serverless? Serverless(无服务器架构)并不意味着真的没有服务器,而是指开发者无需关心服务器的购买、运维、配置和扩容 。你只需要编写业务代码(函数),云服务商会在特定事件触发时自动为你分配资源运行代码,并按实际运行时间计费。 - -当你的应用需要执行一些不能或不应在客户端(浏览器)上完成的逻辑时——例如与需要私密密钥的第三方 API 交互、执行计算密集型任务、或强制执行复杂的业务规则——Edge Functions 就派上了用场。Supabase Edge Functions 基于 Deno 和 TypeScript,它们被部署在全球的边缘节点上,物理距离上靠近你的用户,从而提供极低的函数执行延迟。 - -目前主流云厂商都推出了各自的 Edge Function 服务,常见的包括: - -- AWS Lambda@Edge:基于 AWS Lambda 延伸的边缘函数服务,可与 CloudFront CDN 联动,支持 Node.js、Python 等语言; -- Cloudflare Workers:Cloudflare 推出的边缘函数,部署在其全球 275+ 边缘节点,支持 JavaScript/TypeScript,以 “毫秒级延迟” 为核心优势; -- Vercel Edge Functions:适配 Vercel 前端项目的边缘函数,与 Next.js 深度集成,支持 TypeScript,主打 “前端与边缘逻辑无缝衔接”; - -回到 Supabase ,当你的应用需要执行 “不能在客户端(浏览器)完成” 的逻辑时,比如用私密密钥调用第三方 API(如 LLM 接口)、处理计算密集型任务(如图片压缩)、或强制执行权限校验(如文件访问规则)时,Supabase Edge Functions 就能发挥作用。它基于 Deno runtime 和 TypeScript 构建,部署在全球边缘节点上,能以 “靠近用户的物理距离” 实现极低的执行延迟,是编写自定义、可信服务器端逻辑的核心工具。 - -本项目 `Project5-Supabase-Demos/apps/project-burger-shop-edge-function-5`通过一个与大语言模型(LLM)实时流式对话的功能,展示了 Edge Functions 的最简应用流程。 - -![](images/image57.png) - -### 5.5.1 LLM Chat 案例解析 - -假设你想在应用中集成一个类似 ChatGPT 的聊天机器人。你需要在服务器端调用 OpenAI 的 API,但这需要一个私密的 API Key。 这个 Key 绝对不能暴露在前端代码中 ,否则任何人都可以通过查看网页源码盗用你的 Key,产生高昂的费用。这正是 Edge Function 的用武之地。我们将创建一个名为 llm-chat 的函数,它充当了前端和 OpenAI API 之间的一个 安全代理 。 - -参考 `project-burger-shop-edge-function-5/scripts/llm-chat.ts`的代码,我们来看看它是如何工作的: - -```TypeScript -// scripts/llm-chat.ts -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; -import { OpenAI } from "npm:openai"; - -const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY"); - -Deno.serve(async (req) => { - try { - const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); - const { prompt } = await req.json(); - - const stream = await openai.chat.completions.create({ - model: "gpt-3.5-turbo", - messages: [{ role: "user", content: prompt }], - stream: true, - }); - - return new Response(stream.toReadableStream(), { - headers: { "Content-Type": "text/event-stream" }, - }); - } catch (err) { - } -}); -``` - -在该案例中,对于密钥安全,OPENAI_API_KEY 作为环境变量被安全存储于 Supabase 的服务器。本地前端代码完全无法接触到该密钥,从而有效保障了密钥的安全性。 - -### 5.5.2 创建并部署函数 - -Supabase 提供了非常友好的界面,让你无需接触命令行即可完成部署。 - -1. **进入 Edge Functions 面板** : -2. 登录你的 Supabase 项目 Dashboard。 -3. 在左侧导航栏中,点击像代码一样的图标,进入 “Edge Functions”。 -4. **创建新函数** : -5. 点击 “Create a new function” 按钮。 - ![](images/image58.png) -6. 为函数命名,例如 `llm-chat`。 -7. **粘贴代码** : - ![](images/image59.png) -8. 在弹出的在线编辑器中, **删除所有默认的占位代码** 。 -9. 打开你本地的 `llm-chat.ts` 文件, **复制其全部内容** 。 -10. 将复制的代码**粘贴**到 Supabase 的在线编辑器中。 -11. **配置\*\***环境变量\*\* ** (Secrets)** : - 1. 在侧边栏找到 Secrets。 - ![](images/image60.png) - 2. Name: 输入 `OPENAI_API_KEY`。 - 3. Value: 粘贴你自己的 OpenAI API Key。 - 4. 点击 “Save”。在这里设置的 Secret 会被加密存储,并安全地注入到你的函数运行时环境中。 - -若有函数需要更新,记得在 Edge Function 部分执行 Deploy updates。Supabase 会在云端为你构建并部署这个函数。几分钟后,你的函数就可以在线访问。 - -除了作为语言模型的安全代理,Edge Functions 的应用场景远不止于此。实际上,任何需要服务器端逻辑处理的任务,无论是简单的 API 调用、数据验证,还是更复杂的计算,都可以通过 Edge Function 实现。它为你提供了一个轻量级、可扩展的后端,而无需管理任何服务器基础设施。 - -如果你想探索更多可能性,可以参考项目中的其他示例。例如: - -- 图片生成 ( txt2img.ts ) : 这个函数展示了如何利用 Edge Function 调用第三方的文生图(Text-to-Image)API(如 Stability AI, Midjourney 等)来动态生成图片。这是一种典型的计算密集型或需要安全调用外部服务的场景。与 llm-chat 案例一样,API 密钥被安全地存储在 Supabase 后端,前端只负责发送文本描述,然后接收并展示生成的图片,整个过程安全、高效。 -- 发送邮件 ( send-email.ts ) : 在应用中发送欢迎邮件、交易通知或密码重置邮件是常见需求。 send-email.ts 示例演示了如何通过 Edge Function 集成邮件服务(如 Resend, SendGrid)。你无需在客户端代码中暴露敏感的邮件服务 API Key,只需创建一个函数,让前端通过调用这个函数来触发邮件发送。 - -## 5.6 Clerk Login - -Clerk 是一款专注于身份认证与用户管理的专业开发工具,核心能力覆盖用户注册、登录、账号安全MFA、权限控制、会话管理等全链路身份认证相关需求,能帮助开发者快速搭建安全、灵活且符合现代应用标准的用户体系,无需从零开发复杂的身份逻辑。 - -本部分将介绍如何从零开始配置 Clerk 服务,并将其与 Supabase 进行整合。你可以在项目 `project-burger-shop-auth-advanced-clerk-7` 中体验全流程。 - -![](images/image61.png) - -### 5.6.1 创建 Clerk 应用与获取密钥 - -在使用本项目之前,你需要拥有一个 Clerk 账号并创建一个应用。 - -1. 注册与创建: - 1. 访问 [dashboard.clerk.com](https://dashboard.clerk.com/) 并注册账号。 - 2. 点击 "Create application" 。 - ![](images/image62.png) - 3. 输入应用名称(例如 "Burger Shop")。 - 4. 在 "How will your users sign in?" 中,默认勾选 Email , Google , GitHub 。 - 5. 点击 Create application 。 -2. 获取 API Keys: - 1. 创建成功后,你会被引导至 API Keys 页面。 - ![](images/image63.png) - 2. 找到 Publishable key (以 `pk_` 开头) 和 Secret key (以 `sk_` 开头)。 - ![](images/image64.png) - 3. 将它们复制到你的 `.env.local` 文件中(参考本项目 `.env.example`): - - ```Bash - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... - CLERK_SECRET_KEY=sk_test_... - ``` - -### 5.6.2 配置 Supabase 和 Clerk 的原生集成 - -在进一步使用前,我们需要集成 Supabase 与 Clerk 的关联关系,方便之后登录的鉴权跳转以及控制对特定数据库的访问权限。Supabase 与 Clerk 提供官方原生集成能力,通过该集成可快速实现两者的身份认证打通,无需手动配置复杂的适配逻辑,大幅简化用户登录、权限校验等功能的开发流程: - -1. 在 Clerk 中激活对 Supab ase 的官方集成 - 1. 登录 [Clerk Dashboard](https://dashboard.clerk.com/)。 - 2. 在左侧菜单导航至 Integrations (集成)。 - 3. 在列表中找到并点击 Supabase。 - 4. 开启 Enable Supabase 开关(或点击 Activate integration)。 - 5. 关键步骤:激活成功后,页面会显示你的 Clerk Domain(格式通常为 `https://.clerk.accounts.dev` 或你的自定义域名)。请复制这个 Domain 地址,下一步会用到。 -2. 在 Supabase 中添加 Clerk 提供商 - 1. 登录 [Supabase Dashboard](https://supabase.com/dashboard) 并进入你的项目。 - 2. 在左侧菜单导航至 Authentication > Sign In / Up (或者直接点击 Providers)。 - 3. 点击 Add provider 按钮,从下拉列表中选择 Clerk。 - 4. 在弹出的 Clerk Domain 输入框中,粘贴你刚才从 Clerk 复制的 Domain 地址。 - 5. 点击 Save 保存配置。 - -### 5.6.3 通过 Webhook 同步用户数据至 Supabase - -仅仅是集成只满足了鉴定权限的需求,但这并不会将 Clerk 中已经注册的用户信息同步到 Supabase,为了方便管理,我们还需要在 Supabase 的 `public.users` 表中保留一份用户备份,以便进行关联查询或数据分析。我们可以通过 Clerk Webhooks 实现这一功能,完整过程如下: - -1. **Clerk 发送通知** : 当用户在 Clerk 注册或更新资料时,Clerk 会向我们配置的 Webhook URL 发送一个 POST 请求。 -2. **Supabase 接收并写入** : Edge Function 接收请求,验证签名(确保安全),然后将用户数据更新到 Supabase 的数据库表中。 - -在开始之前,我们需要配置同步信息所需的数据表: - -```SQL --- File: init.sql - --- 1. Create `users` table for synced Clerk users --- This table will store user data pushed from Clerk Webhooks. -CREATE TABLE public.users ( - id TEXT NOT NULL PRIMARY KEY, -- Corresponds to Clerk User ID - email TEXT, - first_name TEXT, - last_name TEXT, - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 2. Enable Row Level Security (RLS) on the table --- This is an important security measure to ensure users cannot access any data by default. -ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; - --- 3. Create RLS policies --- Policy 1: Allow authenticated users to read their own user info. --- `auth.jwt()->>'sub'` extracts the user ID from the JWT provided by Clerk. -CREATE POLICY "Authenticated users can view their own user record" -ON public.users FOR SELECT -TO authenticated -USING ( (SELECT auth.jwt()->>'sub') = id ); - --- Policy 2: Allow users to update their own info. -CREATE POLICY "Authenticated users can update their own user record" -ON public.users FOR UPDATE -TO authenticated -USING ( (SELECT auth.jwt()->>'sub') = id ); -``` - -以及在 Supabase 中启用对应的 Edge function: - -```JavaScript -// File path: supabase/functions/clerk-webhooks/index.ts - -import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' -import { Webhook } from 'npm:svix' -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' - -// Get Clerk Webhook signing secret from environment variables -const CLERK_WEBHOOK_SECRET = Deno.env.get('CLERK_WEBHOOK_SECRET') - -if (!CLERK_WEBHOOK_SECRET) { - throw new Error('CLERK_WEBHOOK_SECRET is not set in environment variables') -} -const supabaseAdmin = createClient( - Deno.env.get('SUPABASE_URL')!, - Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! -) - -serve(async (req) => { - try { - // 1. Get Svix signature info from request headers - const headers = Object.fromEntries(req.headers) - const svix_id = headers['svix-id'] - const svix_timestamp = headers['svix-timestamp'] - const svix_signature = headers['svix-signature'] - - if (!svix_id || !svix_timestamp || !svix_signature) { - return new Response('Missing Svix headers', { status: 400 }) - } - - const payload = await req.json() - const body = JSON.stringify(payload) - - // 2. Verify Webhook signature validity using the secret - const wh = new Webhook(CLERK_WEBHOOK_SECRET) - const evt = wh.verify(body, { - 'svix-id': svix_id, - 'svix-timestamp': svix_timestamp, - 'svix-signature': svix_signature, - }) - - const { id } = evt.data - const eventType = evt.type - console.log(`Received webhook event: ${eventType} for user: ${id}`) - - // 3. Execute database operations based on event type - switch (eventType) { - case 'user.created': { - const { id, first_name, last_name, image_url, email_addresses } = evt.data - const { error } = await supabaseAdmin.from('users').insert({ - id, - first_name, - last_name, - image_url, - email: email_addresses[0]?.email_address, - }) - if (error) throw error - console.log(`User ${id} created in Supabase.`) - break - } - - case 'user.updated': { - const { id, first_name, last_name, image_url, email_addresses } = evt.data - const { error } = await supabaseAdmin - .from('users') - .update({ - first_name, - last_name, - image_url, - email: email_addresses[0]?.email_address, - updated_at: new Date().toISOString(), // Update timestamp - }) - .eq('id', id) - if (error) throw error - console.log(`User ${id} updated in Supabase.`) - break - } - - case 'user.deleted': { - // For delete events, ID might be at the top level - const deletedId = id - if (!deletedId) { - return new Response('Deleted user ID not found', { status: 400 }) - } - const { error } = await supabaseAdmin.from('users').delete().eq('id', deletedId) - if (error) throw error - console.log(`User ${deletedId} deleted from Supabase.`) - break - } - } - - return new Response('Webhook processed successfully', { status: 200 }) - } catch (err) { - console.error('Error processing webhook:', err.message) - return new Response(`Webhook Error: ${err.message}`, { status: 400 }) - } -}) -``` - -初始化 Supabase 数据表与函数结束后,你还需要在 Clerk 中启用 Webhooks 支持: - -- 在 Clerk Dashboard -> **Webhooks** 中添加 Endpoint,填入Supabase Edge Function 的 URL。 -- 勾选 `user.created`, `user.updated`, `user.deleted` 等事件。 - -![](images/image65.png) - -一旦设置成功,你能够在 Message Attempts 中看到不同请求信息,点击后可看到详细的请求返回参数结果;如果 webhook 在请求 Edge function 时出现问题,你可以快速在返回值中找到详细原因结果。推荐你同时对照 Clerk 和 Supabase 的请求日志信息,用于分析各个函数设定是否正确。 - -### 5.6.4 Clerk 中的第三方登录支持 - -在深入了解如何对 Clerk 支持第三方登录前,我们先明确两个核心概念:开发环境与生产环境,这是软件从 “开发测试” 到 “上线可用” 的两个关键阶段,二者的定位、用途和安全要求截然不同: - -- 开发环境:开发者本地或测试服务器使用的环境,仅用于功能开发、调试和内部验证(如本地 localhost:3000 服务),不对外开放 -- 生产环境:应用正式上线后,面向真实用户的公开环境(如部署在 Vercel、阿里云等平台的 https://my-app.com) - -而 Clerk 对社交登录区分这两种环境,本质是平衡 “开发效率” 与 “生产安全”:开发阶段需减少冗余配置以快速验证功能,生产阶段需通过专属凭证保障数据安全,同时符合 Google、GitHub 等第三方 OAuth 平台的规则(线上应用必须绑定专属域名与凭证,不允许使用共享资源)。下面具体说明两种环境下 Clerk 社交登录的差异配置: - -1. **开发环境快速验证** - -开发环境中,Clerk 已预置共享 OAuth 凭证和默认重定向 URI,无需前往 GitHub/Google 申请专属凭证,操作步骤如下: - -- 登录 Clerk Dashboard ,在左侧导航栏进入 SSO connections (SSO 连接)页面。 -- 点击 Add connection (添加连接),选择 For all users (对所有用户生效)。 -- 在 Choose provider (选择提供商)下拉菜单中,按需选择 GitHub 或 Google 。 -- 直接点击 Add connection (添加连接),Clerk 会自动用共享凭证完成绑定。 - - 配置后,本地启动应用(如 `localhost:3000`)并点击“Sign in with GitHub/Google”,Clerk 会自动代理登录请求,快速验证功能是否正常。 - -2. **生产环境自定义凭证配置** - -(注:如果发现有环节和预期不一致,建议阅读官方文档进行最新方式的尝试) - -应用部署上线(如 Vercel、阿里云)并切换到 Clerk Production Instance 后,共享凭证失效,需为 GitHub/Google 配置自定义 OAuth 凭证(建议同时打开 Clerk Dashboard 和第三方平台页面,方便同步操作): - -- 前置通用操作(Clerk 控制台): - - 进入 Clerk SSO connections 页面,点击 Add connection → 选择 For all users 。 - - 选择目标平台(GitHub/Google),确保开启 Enable for sign-up and sign-in (允许注册登录)和 Use custom credentials (使用自定义凭证)。 - - 复制页面中的 Authorization Callback URL (GitHub)或 Authorized Redirect URI (Google),保存到安全位置,不要关闭当前页面/弹窗。 -- 2.1 GitHub 平台配置: - - 登录 GitHub,进入 Developer Settings (路径:头像 → Settings → Developer settings → OAuth Apps)。 - - 点击 New OAuth app ,填写信息:`Application name`(应用名称)、`Homepage URL`(生产域名,如 `https://my-app.com`)、`Authorization Callback URL`(粘贴从 Clerk 复制的地址)。 - - 点击 Register application ,再点击 Generate a new client secret ,保存生成的 Client ID 和 Client Secret (Secret 仅显示一次)。 - - 回到 Clerk 弹窗,粘贴 Client ID 和 Client Secret,点击 Add connection 完成配置(若关闭弹窗,可在 SSO connections 找到 GitHub 连接,在“Use custom credentials”模块补填)。 -- 2.2 Google 平台配置: - - 登录 Google Cloud Console ,选择已有项目或新建项目(如“My App Production”)。 - - 点击左上角菜单 → APIs & Services → Credentials ,点击 Create Credentials → OAuth client ID (首次配置需先完成 OAuth consent screen 设置,选择“External”并填写应用信息)。 - - 选择 Application type 为 Web application ,配置: - 1. `Authorized JavaScript origins`:添加生产域名(如 `https://my-app.com`、`https://www.my-app.com`),本地验证可补充 `http://localhost:端口号`。 - 2. `Authorized Redirect URIs`:粘贴从 Clerk 复制的地址。 - - 点击 Create ,保存弹窗中的 Client ID 和 Client Secret ,回到 Clerk 弹窗粘贴并点击 Add connection 。 - - 关键注意事项: - 1. 禁止 WebView 登录:Google OAuth 不支持应用内浏览器登录,需参考 [Google 官方文档](https://support.google.com/cloud/answer/7657789) 调整。 - 2. 切换发布状态:默认“Testing”状态仅支持 100 个测试用户,需在 OAuth consent screen 将“Publishing status”改为 In production (需通过 Google 审核)。 - 3. 阻止子邮箱:Clerk 默认拦截含 `+`/`=`/`#` 的 Google 邮箱(如 `user+alias@example.com`),可在 Google 连接详情页开启/关闭 Block email subaddresses (建议开启提升安全性)。 - 4. 支持 Google One Tap:配置完成后,可集成 Clerk `` 组件实现“一键登录”,参考 [Clerk 组件文档](https://clerk.com/docs/components/social-connections/google-one-tap)。 - -3. 测试第三方登录连接 - -配置完成后,通过 Clerk 内置 Account Portal 验证功能: - -- 进入 Clerk Dashboard,左侧导航栏进入 Account Portal 页面。 -- 在“Sign-in”模块右侧,点击“访问登录页面”按钮,跳转至对应环境登录页: - - 开发环境:`https://你的域名.accounts.dev/sign-in`(如 `https://my-app.accounts.dev/sign-in`)。 - - 生产环境:`https://accounts.你的域名.com/sign-in`(如 `https://accounts.my-app.com/sign-in`)。 -- 点击“Sign in with GitHub/Google”,用对应平台账号登录,若能成功跳转并返回应用,说明连接配置正常。 - -# 6. 从 Supabase 到更多后端开发组件(进阶) - -在上文中,我们主要是站在 Supabase 的视角,去看“一个以 Postgres 为核心的一站式后端平台”能帮我们解决哪些问题:认证、数据库、文件存储、实时通信、边缘函数等,都被集成在同一个控制台里,开箱即用、体验统一,非常适合快速起步和中小型项目。 - -但从更长期、更工程化的角度来看, **Supabase 提供的每一块能力(Auth / Storage / Edge Functions / Realtime / Database),在业界几乎都有对应的专业替代方案** ——既包括同类 BaaS 平台,也包括更“单点突破”的云服务和开源组件。作为上进的个人开发者和初创团队来说,了解这些替代选项有几个好处: - -- 判断当前项目是否“全用 Supabase 就够了”,还是某一块需要更专业/更便宜/更易合规的专用服务; -- 当项目规模变大或需求变复杂时,是否可以把某个模块从 Supabase 替换出去(例如改用专门的 Auth 平台或对象存储),而不是一开始就被平台彻底锁死; -- 拓宽技术选型视野,即使暂时不更换,也能大致知道“如果不用 Supabase 的 X 功能,我还有哪些常见选择”。 - -本节将分别介绍 Supabase 所覆盖的几大能力在市场上的主流替代方案,例如:认证(Auth)、文件存储(Storage)、边缘函数(Edge Functions)、实时通信(Realtime)、数据库托管等。简单对比它们在功能特性、免费额度/定价、易用性以及社区流行度等方面的差异, 让你对后端组件工具库有更全面的理解。 - -## 同类 Baas 平台 - -在开始之前,我们可以浏览同类的 Baas 平台,若觉得 Supabase 不够好用,你可以根据需求选择不同替代品进行尝试。 - -| 平台/服务 | 类型 | 免费额度/定价 | 特点 / 适用场景 | -| ------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| Firebase(Google) | 全托管 BaaS(Auth + Firestore + Storage + Functions + Hosting) | Spark:免费轻量额度;Blaze:按量计费(Firestore/Storage/Functions 分别算) | 行业最成熟、文档好、上手快、实时能力强。适用于中小型产品、移动/前端主导团队。缺点:计费复杂、锁定性强、查询限制多(尤其 Firestore)。 | -| Supabase | 开源 BaaS(Postgres + Auth + Storage + Edge Functions + Realtime) | 免费:500MB DB、1GB Storage、无服务器函数少量调用;Pro:按实例计费 | 最像 Firebase 的 SQL 版;界面优秀、体验现代、可自托管。适用于需要强 SQL、BI、事务能力的应用。缺点:高并发或复杂函数成本较高。 | -| Appwrite Cloud | 开源一站式 BaaS(DB + Auth + Storage + Functions + Realtime) | 免费:包含基本 DB/Storage/FaaS;付费按资源级别计费 | 体验现代化、API 统一、可自托管;适合开发者友好的应用快速迭代。缺点:生态还不如 Firebase/Supabase 成熟;性能在大型应用中需要测试。 | -| Nhost | Postgres + GraphQL + Auth + Storage + Functions | 免费:1GB DB、1GB Storage、少量函数调用 | 类似“Supabase + Hasura”;天然 GraphQL;适合前端团队与 React/Next.js 项目。缺点:生态小、成本随用量升高。 | -| AWS Amplify | AWS 一站式后端(Cognito + AppSync + DynamoDB + Storage + Functions + Hosting) | 免费:Hosting 额度 + Cognito 10k MAU + 部分函数额度 | 大而全,适合已有 AWS 基础的团队;企业级可靠性。缺点:最难上手,服务碎片化;初创团队维护成本高。 | -| Xata(近两年快速增长) | 多模型数据库 + Auth + Edge Functions | 免费:250k 记录、15GB 带宽 | 虽然更偏「DB + API」,但提供 Auth、文件、逻辑,可作为轻量全栈后端。UI/开发体验极佳。缺点:功能不如 Firebase/Supabase 全面。 | -| Convex(开发者体验极强) | 托管数据库 + Auth + Functions(前端优先) | 免费开发版;付费按请求量计费 | 极简上手;无需 schema;前端写函数即可用后端。适合 MVP/快速验证。缺点:高度绑定平台,迁移成本高;不算完全传统 BaaS。 | - -## 认证 (Auth) - -| 工具/平台 | 功能特点 | 免费额度/定价 | 适用场景与优缺点 | -| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| Firebase Authentication | Google 提供的 BaaS 身份验证服务,支持邮箱/密码、手机、社交登录、匿名等常见方式。Spark 免费方案支持最高50k 月活跃用户。 | Spark(免费)50k MAU;Blaze 按量计费 | 集成 Google 生态,文档丰富,上手简单;功能全面(MFA、阻塞函数等),适合快速开发。但与 Firebase 平台绑定,扩展到其他服务需额外配置。 | -| Auth0 (Okta) | 全托管身份认证平台,支持社交登录、企业 SSO、多因子认证、规则扩展等强大功能。 | 免费方案25k MAU,付费按 MAU 计费 | 企业级功能齐全(RBAC、审计日志等),适合中大型应用;界面友好。缺点是 MAU 上升时成本高,免费版功能有限(如不含 MFA/RBAC)。社区知名度高,用户众多。 | -| AWS Cognito | 亚马逊云原生身份服务,支持社交及 SAML 联合登录。直接登录用户池提供每月10k MAU 免费,超过部分按 0.0055 美元/MAU 收费。 | 免费10k MAU/月,超出按量付费 | 与 AWS 生态深度集成(可无缝配合 API Gateway、Lambda 等),入门门槛略高,文档较复杂;免费额度有限,适合已有 AWS 使用习惯的团队。 | -| Logto | 开源身份认证平台,自托管版免费,云服务计划免费50k MAU。支持多语言、多租户、OAuth/OIDC 等。 | 社区版免费;Logto Cloud 免费50k MAU | 近期流行的 Auth0 开源替代方案,GitHub 已有 10k+ Stars。易扩展,自托管降低成本;缺点是生态和文档相对较新,社区规模略逊于 Firebase/Auth0。 | -| Keycloak | 知名开源 IAM/SSO 解决方案,支持用户名密码、LDAP、SAML、OAuth2 等。 | 完全免费,需自托管 | 功能强大、可扩展(支持细粒度权限控制),企业级功能丰富;但部署和维护复杂度高,对小团队而言学习曲线较陡。缺点是对容器化和集群运维要求较高。 | - -## 文件存储 (Storage) - -| 平台/服务 | 类型 | 免费额度/定价 | 特点/适用场景 | -| ---------------------------------------- | -------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| Amazon S3 | 云对象存储(AWS) | AWS 免费套餐提供 5GB 存储、20k GET/PUT 请求/月,超出按使用量付费 | 行业标准的对象存储,可靠性高、全球多区域部署。功能全面,与 AWS 生态整合良好;定价较复杂,新用户需了解计费规则。 | -| Google Cloud Storage(Firebase Storage) | 云对象存储(Google) | Firebase Spark 方案提供免费额度(1GB 存储 + 流量限制),Blaze 付费 | 与 Firebase/Google Cloud 紧密集成,易于管理;支持 CDN 加速、细粒度安全规则。 | -| 腾讯云 COS / 阿里云 OSS | 云对象存储(国内) | 按量付费(各有新用户赠送额度,如OSS有首年40GB免费等) | 面向国内市场,高性能、大规模对象存储;与中国云生态整合,文档较完善。阿里OSS 功能全面、全球加速;七牛KODO 专注多媒体处理,成本较低,适合个人和小团队。 | -| MinIO | 开源 S3 兼容存储 | 开源免费(自建) | 轻量级、高性能、与 S3 API 兼容,适合在私有云或本地搭建对象存储。文档和社区活跃;需自己维护基础设施。 | -| Cloudinary / Imgix 等 | 媒体存储+CDN | 基本免费方案(如 Cloudinary 免费 25GB/月带宽) | 针对图片/视频优化的云存储+CDN 服务,提供实时转码、压缩等高级功能。适合媒体项目,但功能较专一,作为通用文件存储使用成本偏高。 | - -## 边缘函数 (Edge Functions) - -| 平台/服务 | 特点 | 免费额度/定价 | 适用场景与优缺点 | -| -------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Cloudflare Workers | 全球分布式 JavaScript/Wasmtime 环境 | 免费计划:每天 100k 请求;标准计划$5/月含1,000万请求 | 运行在 Cloudflare 边缘节点,延迟极低;适合全局分发的逻辑、静态资源渲染等。免费配额较少(相当于每月约300万请求),上手简单。缺点是运行时(JS/Wasmtime)限制与调试工具有限。 | -| Vercel Edge Functions | 随 Next.js/前端框架无缝集成,支持 JS/TS/Go | Hobby 免费:每月 100万 函数调用,100万 边缘请求 | 深度集成前端框架,自动部署;适合现代 Web 应用。免费额度充足,默认运行时 10s,可提升至 60s。缺点是免费版团队协作功能受限;依赖 Vercel 平台。 | -| Netlify Edge / Functions | Node.js 云函数+边缘路由(NFT) | 免费:300 代币/月(约相当于每月 1M 请求);按信用点计费 | 支持 Node.js 函数、边缘处理路由等。免费额度用于构建、函数和带宽,适合前端全栈部署。优点是简便易用,集成 Git 部署;缺点是免费额度使用需算计(10k 请求 = 3 点)。 | -| AWS Lambda@Edge / CloudFront Functions | AWS 无服务器边缘计算 | AWS Lambda(1M 免费请求/月+400k GB-s)+ CloudFront $0.085/每10万调用起 | 与 CloudFront 集成,可在边缘执行代码。适合需要 AWS 生态(如在节点层面做权限或 A/B 测试)。优点是灵活强大;缺点是配置复杂,延迟略高于 Cloudflare/Vercel。 | - -## 实时通信 (Realtime) - -| 平台/服务 | 功能特点 | 免费额度/定价 | 适用场景与优缺点 | -| -------------------------------------- | ------------------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| Firebase Realtime Database / Firestore | Google BaaS 实时数据库;支持数据变更推送 | Spark 免费:实时数据库1GB 存储 & 限额;Blaze 按量付费 | 强集成 Firebase 生态,实时监听简单。优点是免费起步快;缺点是数据库类型(JSON/NoSQL),复杂查询能力弱。 | -| Ably | 实时消息与 pub/sub 平台,支持 WebSocket、MQTT 等 | 免费包:每月 6,000,000 条消息 | 功能全面的实时消息服务,高并发支持;免费额度可达600万消息/月。社区与文档较好,适合全球分布。 | -| Pusher Channels | 事件推送服务,支持频道/事件机制 | Sandbox 免费:每日 200k 消息,100 并发连接 | 易用的 WebSocket 服务,文档齐全,适合快速实现聊天和通知功能。免费版限制消息量和连接数;付费后扩展性好。 | -| 自建 WebSocket/Socket.IO | 自己搭建服务器(Node.js、Elixir 或 Go 等) | 自行托管成本(如服务器费用) | 灵活度最高,可根据需求定制协议和拓扑。适合对成本控制严格且技术成熟的团队。缺点是需自行处理可用性、扩展和跨域等问题。 | - -## 数据库 - -| 平台/工具 | 数据库类型 | 免费额度/定价 | 主要特点 | -| ---------------------------- | --------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| Neon (Serverless PostgreSQL) | 关系型(PostgreSQL) | 免费计划:0.5GB 存储,主分支永久在线,20h 分支计算/月 | 云原生无服务器 Postgres,支持自动伸缩和分支(fork 测试)。免费额度对小项目够用,适合现代开发流程。分支功能强大,但免费额度较小。 | -| Aiven PostgreSQL | 关系型(PostgreSQL/MySQL) | 免费计划:1GB 存储,1 vCPU,1GB 内存 | 托管级数据库服务,支持跨云多区域迁移。提供有 MySQL、Redis 等可选。免费额度适合开发和小型项目;商业版支持高可用集群和监控。 | -| CockroachDB Cloud | 分布式 SQL(兼容 PostgreSQL) | 免费计划:10GB 存储 | 类似 Google Spanner 的分布式 SQL 数据库,自动分片扩展。免费10GB 空间较慷慨;适合需要横向扩展和高一致性的应用。商业版 SLA 高。 | -| TiDB Cloud | 分布式关系型(MySQL 兼容) | 免费计划:每节点5GB,总计最多25GB | 开源 TiDB 的云版,兼容 MySQL 协议,分布式架构。免费额度充足,适合熟悉 MySQL 的团队,性能优秀;缺点是运维相对复杂(针对大型场景)。 | -| MongoDB Atlas | 文档型(NoSQL MongoDB) | 免费 M0 集群:0.5GB 存储 | 云端 MongoDB,灵活的文档模型,支持丰富查询和索引。免费 0.5GB 数据库适合测试和小型应用;可按需横向扩展。学习曲线略高于关系型数据库。 | -| SQLPub | 多数据库(MySQL、PostgreSQL、Redis 等) | 免费计划:36,000 请求/小时,30 并发连接,500MB 存储 | 一站式数据库平台,支持多种数据库类型。免费版适合学习和小项目;优点是支持多种 DB,缺点是存储额度较小。 | - -以上替代方案各有侧重:开源更灵活可控(Keycloak、MinIO、Socket.IO、Neon、CockroachDB 等),云托管服务更易上手(Firebase、Auth0、Cloudflare、Vercel、Netlify、AWS、Aiven、MongoDB Atlas 等)。选择时可根据项目需求、团队技术栈、预算和社区生态等权衡。个人项目可优先选用免费配额充足、易集成的服务(如 Firebase 系列、七牛存储、Cloudflare Workers、Neon、CockroachDB 等),而对企业级或特定安全需求,则可考虑功能更丰富但收费较高的方案(Auth0、Alibaba/Tencent 云、AWS、TiDB/Aiven 等)。你可以在实际应用中不断尝试,直到选择出最适合的后端开发工具组件。 - -# 总结 - -在今天的课程中,我们系统学习了数据库的基础概念、Supabase 的核心定义及其操作细节。后续在实践过程中,你可根据项目的实际应用场景与需求,随时回头翻阅这份文档作为参考。 - -请时刻记住一个重要原则: **先完成,再完美!** 无需追求一步到位,我们完全可以通过持续迭代优化,逐步靠近更优的成果。祝你在后续的项目实践中一切顺利! - -# 📚 课后作业 - -1. 开发一个包含用户管理系统和数据库的应用程序。最好包含更多的Supabase 功能 (Realtime / cloud storage / Edge function). diff --git a/docs/project/chapter5/images/image1.png b/docs/project/chapter5/images/image1.png deleted file mode 100644 index b8468a4..0000000 Binary files a/docs/project/chapter5/images/image1.png and /dev/null differ diff --git a/docs/project/chapter5/images/image10.png b/docs/project/chapter5/images/image10.png deleted file mode 100644 index 5a0da44..0000000 Binary files a/docs/project/chapter5/images/image10.png and /dev/null differ diff --git a/docs/project/chapter5/images/image11.png b/docs/project/chapter5/images/image11.png deleted file mode 100644 index d632a8f..0000000 Binary files a/docs/project/chapter5/images/image11.png and /dev/null differ diff --git a/docs/project/chapter5/images/image12.png b/docs/project/chapter5/images/image12.png deleted file mode 100644 index 5159982..0000000 Binary files a/docs/project/chapter5/images/image12.png and /dev/null differ diff --git a/docs/project/chapter5/images/image13.png b/docs/project/chapter5/images/image13.png deleted file mode 100644 index d76b08a..0000000 Binary files a/docs/project/chapter5/images/image13.png and /dev/null differ diff --git a/docs/project/chapter5/images/image14.png b/docs/project/chapter5/images/image14.png deleted file mode 100644 index 01426a8..0000000 Binary files a/docs/project/chapter5/images/image14.png and /dev/null differ diff --git a/docs/project/chapter5/images/image15.png b/docs/project/chapter5/images/image15.png deleted file mode 100644 index f6a7ce7..0000000 Binary files a/docs/project/chapter5/images/image15.png and /dev/null differ diff --git a/docs/project/chapter5/images/image16.png b/docs/project/chapter5/images/image16.png deleted file mode 100644 index 1d7e5da..0000000 Binary files a/docs/project/chapter5/images/image16.png and /dev/null differ diff --git a/docs/project/chapter5/images/image17.png b/docs/project/chapter5/images/image17.png deleted file mode 100644 index 371eee4..0000000 Binary files a/docs/project/chapter5/images/image17.png and /dev/null differ diff --git a/docs/project/chapter5/images/image18.png b/docs/project/chapter5/images/image18.png deleted file mode 100644 index 11e4096..0000000 Binary files a/docs/project/chapter5/images/image18.png and /dev/null differ diff --git a/docs/project/chapter5/images/image19.png b/docs/project/chapter5/images/image19.png deleted file mode 100644 index 7e51ccc..0000000 Binary files a/docs/project/chapter5/images/image19.png and /dev/null differ diff --git a/docs/project/chapter5/images/image2.png b/docs/project/chapter5/images/image2.png deleted file mode 100644 index 48a055d..0000000 Binary files a/docs/project/chapter5/images/image2.png and /dev/null differ diff --git a/docs/project/chapter5/images/image20.png b/docs/project/chapter5/images/image20.png deleted file mode 100644 index 9aea866..0000000 Binary files a/docs/project/chapter5/images/image20.png and /dev/null differ diff --git a/docs/project/chapter5/images/image21.png b/docs/project/chapter5/images/image21.png deleted file mode 100644 index 498cb4e..0000000 Binary files a/docs/project/chapter5/images/image21.png and /dev/null differ diff --git a/docs/project/chapter5/images/image22.png b/docs/project/chapter5/images/image22.png deleted file mode 100644 index 68e5a53..0000000 Binary files a/docs/project/chapter5/images/image22.png and /dev/null differ diff --git a/docs/project/chapter5/images/image23.png b/docs/project/chapter5/images/image23.png deleted file mode 100644 index 3dfc732..0000000 Binary files a/docs/project/chapter5/images/image23.png and /dev/null differ diff --git a/docs/project/chapter5/images/image24.png b/docs/project/chapter5/images/image24.png deleted file mode 100644 index bc64f0a..0000000 Binary files a/docs/project/chapter5/images/image24.png and /dev/null differ diff --git a/docs/project/chapter5/images/image25.png b/docs/project/chapter5/images/image25.png deleted file mode 100644 index 69d397a..0000000 Binary files a/docs/project/chapter5/images/image25.png and /dev/null differ diff --git a/docs/project/chapter5/images/image26.png b/docs/project/chapter5/images/image26.png deleted file mode 100644 index f2c1eca..0000000 Binary files a/docs/project/chapter5/images/image26.png and /dev/null differ diff --git a/docs/project/chapter5/images/image27.png b/docs/project/chapter5/images/image27.png deleted file mode 100644 index f56b5e9..0000000 Binary files a/docs/project/chapter5/images/image27.png and /dev/null differ diff --git a/docs/project/chapter5/images/image28.png b/docs/project/chapter5/images/image28.png deleted file mode 100644 index e2cd2c3..0000000 Binary files a/docs/project/chapter5/images/image28.png and /dev/null differ diff --git a/docs/project/chapter5/images/image29.png b/docs/project/chapter5/images/image29.png deleted file mode 100644 index 7251ddf..0000000 Binary files a/docs/project/chapter5/images/image29.png and /dev/null differ diff --git a/docs/project/chapter5/images/image3.png b/docs/project/chapter5/images/image3.png deleted file mode 100644 index 4538062..0000000 Binary files a/docs/project/chapter5/images/image3.png and /dev/null differ diff --git a/docs/project/chapter5/images/image30.png b/docs/project/chapter5/images/image30.png deleted file mode 100644 index de181ba..0000000 Binary files a/docs/project/chapter5/images/image30.png and /dev/null differ diff --git a/docs/project/chapter5/images/image31.png b/docs/project/chapter5/images/image31.png deleted file mode 100644 index 605ca3c..0000000 Binary files a/docs/project/chapter5/images/image31.png and /dev/null differ diff --git a/docs/project/chapter5/images/image32.png b/docs/project/chapter5/images/image32.png deleted file mode 100644 index 8151004..0000000 Binary files a/docs/project/chapter5/images/image32.png and /dev/null differ diff --git a/docs/project/chapter5/images/image33.png b/docs/project/chapter5/images/image33.png deleted file mode 100644 index 4179ccf..0000000 Binary files a/docs/project/chapter5/images/image33.png and /dev/null differ diff --git a/docs/project/chapter5/images/image34.png b/docs/project/chapter5/images/image34.png deleted file mode 100644 index 98958c1..0000000 Binary files a/docs/project/chapter5/images/image34.png and /dev/null differ diff --git a/docs/project/chapter5/images/image35.png b/docs/project/chapter5/images/image35.png deleted file mode 100644 index 43da753..0000000 Binary files a/docs/project/chapter5/images/image35.png and /dev/null differ diff --git a/docs/project/chapter5/images/image36.png b/docs/project/chapter5/images/image36.png deleted file mode 100644 index 15aa607..0000000 Binary files a/docs/project/chapter5/images/image36.png and /dev/null differ diff --git a/docs/project/chapter5/images/image37.png b/docs/project/chapter5/images/image37.png deleted file mode 100644 index 1ae4eea..0000000 Binary files a/docs/project/chapter5/images/image37.png and /dev/null differ diff --git a/docs/project/chapter5/images/image38.png b/docs/project/chapter5/images/image38.png deleted file mode 100644 index 0b5e024..0000000 Binary files a/docs/project/chapter5/images/image38.png and /dev/null differ diff --git a/docs/project/chapter5/images/image39.png b/docs/project/chapter5/images/image39.png deleted file mode 100644 index d6eca6d..0000000 Binary files a/docs/project/chapter5/images/image39.png and /dev/null differ diff --git a/docs/project/chapter5/images/image4.png b/docs/project/chapter5/images/image4.png deleted file mode 100644 index 5b450b9..0000000 Binary files a/docs/project/chapter5/images/image4.png and /dev/null differ diff --git a/docs/project/chapter5/images/image40.png b/docs/project/chapter5/images/image40.png deleted file mode 100644 index 14573a1..0000000 Binary files a/docs/project/chapter5/images/image40.png and /dev/null differ diff --git a/docs/project/chapter5/images/image41.png b/docs/project/chapter5/images/image41.png deleted file mode 100644 index 160a098..0000000 Binary files a/docs/project/chapter5/images/image41.png and /dev/null differ diff --git a/docs/project/chapter5/images/image42.png b/docs/project/chapter5/images/image42.png deleted file mode 100644 index 2ac4a19..0000000 Binary files a/docs/project/chapter5/images/image42.png and /dev/null differ diff --git a/docs/project/chapter5/images/image43.png b/docs/project/chapter5/images/image43.png deleted file mode 100644 index c64d76d..0000000 Binary files a/docs/project/chapter5/images/image43.png and /dev/null differ diff --git a/docs/project/chapter5/images/image44.png b/docs/project/chapter5/images/image44.png deleted file mode 100644 index 8332c90..0000000 Binary files a/docs/project/chapter5/images/image44.png and /dev/null differ diff --git a/docs/project/chapter5/images/image45.png b/docs/project/chapter5/images/image45.png deleted file mode 100644 index 90bb901..0000000 Binary files a/docs/project/chapter5/images/image45.png and /dev/null differ diff --git a/docs/project/chapter5/images/image46.png b/docs/project/chapter5/images/image46.png deleted file mode 100644 index 7f915b5..0000000 Binary files a/docs/project/chapter5/images/image46.png and /dev/null differ diff --git a/docs/project/chapter5/images/image47.png b/docs/project/chapter5/images/image47.png deleted file mode 100644 index 25ebc72..0000000 Binary files a/docs/project/chapter5/images/image47.png and /dev/null differ diff --git a/docs/project/chapter5/images/image48.png b/docs/project/chapter5/images/image48.png deleted file mode 100644 index 659b627..0000000 Binary files a/docs/project/chapter5/images/image48.png and /dev/null differ diff --git a/docs/project/chapter5/images/image49.png b/docs/project/chapter5/images/image49.png deleted file mode 100644 index 10badd3..0000000 Binary files a/docs/project/chapter5/images/image49.png and /dev/null differ diff --git a/docs/project/chapter5/images/image5.png b/docs/project/chapter5/images/image5.png deleted file mode 100644 index eabaaa5..0000000 Binary files a/docs/project/chapter5/images/image5.png and /dev/null differ diff --git a/docs/project/chapter5/images/image50.png b/docs/project/chapter5/images/image50.png deleted file mode 100644 index 20d14c2..0000000 Binary files a/docs/project/chapter5/images/image50.png and /dev/null differ diff --git a/docs/project/chapter5/images/image51.png b/docs/project/chapter5/images/image51.png deleted file mode 100644 index c459cf8..0000000 Binary files a/docs/project/chapter5/images/image51.png and /dev/null differ diff --git a/docs/project/chapter5/images/image52.png b/docs/project/chapter5/images/image52.png deleted file mode 100644 index abe2173..0000000 Binary files a/docs/project/chapter5/images/image52.png and /dev/null differ diff --git a/docs/project/chapter5/images/image53.png b/docs/project/chapter5/images/image53.png deleted file mode 100644 index 1b4151c..0000000 Binary files a/docs/project/chapter5/images/image53.png and /dev/null differ diff --git a/docs/project/chapter5/images/image54.png b/docs/project/chapter5/images/image54.png deleted file mode 100644 index c9dae15..0000000 Binary files a/docs/project/chapter5/images/image54.png and /dev/null differ diff --git a/docs/project/chapter5/images/image55.png b/docs/project/chapter5/images/image55.png deleted file mode 100644 index 37cba2a..0000000 Binary files a/docs/project/chapter5/images/image55.png and /dev/null differ diff --git a/docs/project/chapter5/images/image56.png b/docs/project/chapter5/images/image56.png deleted file mode 100644 index e50e004..0000000 Binary files a/docs/project/chapter5/images/image56.png and /dev/null differ diff --git a/docs/project/chapter5/images/image57.png b/docs/project/chapter5/images/image57.png deleted file mode 100644 index 17e0e3d..0000000 Binary files a/docs/project/chapter5/images/image57.png and /dev/null differ diff --git a/docs/project/chapter5/images/image58.png b/docs/project/chapter5/images/image58.png deleted file mode 100644 index b5f23ff..0000000 Binary files a/docs/project/chapter5/images/image58.png and /dev/null differ diff --git a/docs/project/chapter5/images/image59.png b/docs/project/chapter5/images/image59.png deleted file mode 100644 index 58570ca..0000000 Binary files a/docs/project/chapter5/images/image59.png and /dev/null differ diff --git a/docs/project/chapter5/images/image6.png b/docs/project/chapter5/images/image6.png deleted file mode 100644 index 417a477..0000000 Binary files a/docs/project/chapter5/images/image6.png and /dev/null differ diff --git a/docs/project/chapter5/images/image60.png b/docs/project/chapter5/images/image60.png deleted file mode 100644 index 0264e8e..0000000 Binary files a/docs/project/chapter5/images/image60.png and /dev/null differ diff --git a/docs/project/chapter5/images/image61.png b/docs/project/chapter5/images/image61.png deleted file mode 100644 index 7d94972..0000000 Binary files a/docs/project/chapter5/images/image61.png and /dev/null differ diff --git a/docs/project/chapter5/images/image62.png b/docs/project/chapter5/images/image62.png deleted file mode 100644 index 79905db..0000000 Binary files a/docs/project/chapter5/images/image62.png and /dev/null differ diff --git a/docs/project/chapter5/images/image63.png b/docs/project/chapter5/images/image63.png deleted file mode 100644 index 82cadea..0000000 Binary files a/docs/project/chapter5/images/image63.png and /dev/null differ diff --git a/docs/project/chapter5/images/image64.png b/docs/project/chapter5/images/image64.png deleted file mode 100644 index 11e7a1b..0000000 Binary files a/docs/project/chapter5/images/image64.png and /dev/null differ diff --git a/docs/project/chapter5/images/image65.png b/docs/project/chapter5/images/image65.png deleted file mode 100644 index e3565c7..0000000 Binary files a/docs/project/chapter5/images/image65.png and /dev/null differ diff --git a/docs/project/chapter5/images/image7.png b/docs/project/chapter5/images/image7.png deleted file mode 100644 index d54b10a..0000000 Binary files a/docs/project/chapter5/images/image7.png and /dev/null differ diff --git a/docs/project/chapter5/images/image8.png b/docs/project/chapter5/images/image8.png deleted file mode 100644 index 5059e21..0000000 Binary files a/docs/project/chapter5/images/image8.png and /dev/null differ diff --git a/docs/project/chapter5/images/image9.png b/docs/project/chapter5/images/image9.png deleted file mode 100644 index 9fecc9a..0000000 Binary files a/docs/project/chapter5/images/image9.png and /dev/null differ diff --git a/docs/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase.md b/docs/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase.md index bc3e62a..e0ae412 100644 --- a/docs/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase.md +++ b/docs/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase.md @@ -123,15 +123,15 @@ user_info = { 我们也可参考某家云厂商的[数据库选型推荐](https://help.aliyun.com/zh/govcloud/getting-started/select-database-services),根据场景可进行不同数据库类型的选择,你可以对比不同云厂商的数据库规格选出最合适的进行使用。 -| 数据库类型 | 数据库名称 | 价格 | 适用场景 | -| ---------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| 关系型数据库 | RDS MySQL版 | 低 | 基础版:学习以及小型网站高可用版:一定业务压力的中型数据库场景集群版:业务不允许中断,访问压力较大 | -| RDS SQL server版 | 高 | 基础版:测试以及小型商业化网站高可用版:企业级商业化网站集群版:企业业务不允许中断,访问压力较大 | | -| RDS PostgreSQL版 | 最低 | 基础版:学习以及小型网站高可用版:一定业务压力的中型数据库场景集群版:业务不允许中断,访问压力较大的场景,其性能较一般MySQL高 | | -| RDS PPAS版 | 高 | 通用型:兼容Oracle业务,但业务压力Udacity,虚拟化可以满足其需求独享型:面对需要独享物理机的业务,一般为高并发Oracle类业务 | | -| DRDS | 中 | 入门版本:4 Core 8 G,价格亲民,适合中小型在线业务企业版:16 Core 32 G,复杂 SQL 响应好,适合超高并发在线业务至尊版:32 Core 64 G,复杂 SQL 执行响应最好,提供超大规格选择 | | -| NoSQL数据库 | Redis | 中 | 双机热备Redis:一般作为持久化数据库提高业务可用性集群版本的Redis:一般作为缓存层,加速应用访问,解决一般数据库无法负载的读取压力 | -| MongoDB版 | 中 | 单节点实例单节点:适用于开发、测试及其他非企业核心数据存储的场景副本集实例:适用于某些业务场景下对数据库有更高读取性能需求,如阅读类网站、订单查询系统等读多写少场景或有临时活动等突发业务需求分片集群实例:基于多个副本集(每个副本集沿用三副本模式)组成的分片集群实例,提供更高的读取性能需求,为实时在线业务提供高速读取性能 | | +| 数据库类型 | 数据库名称 | 价格 | 适用场景 | +| ------------ | ---------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------- | +| 关系型数据库 | RDS MySQL版 | 低 | 基础版:学习以及小型网站高可用版:一定业务压力的中型数据库场景集群版:业务不允许中断,访问压力较大 | +| | RDS SQL server版 | 高 | 基础版:测试以及小型商业化网站高可用版:企业级商业化网站集群版:企业业务不允许中断,访问压力较大 | +| | RDS PostgreSQL版 | 最低 | 基础版:学习以及小型网站高可用版:一定业务压力的中型数据库场景集群版:业务不允许中断,访问压力较大的场景,其性能较一般MySQL高 | +| | RDS PPAS版 | 高 | 通用型:兼容Oracle业务,但业务压力Udacity,虚拟化可以满足其需求独享型:面对需要独享物理机的业务,一般为高并发Oracle类业务 | +| | DRDS | 中 | 入门版本:4 Core 8 G,价格亲民,适合中小型在线业务企业版:16 Core 32 G,复杂 SQL 响应好,适合超高并发在线业务至尊版:32 Core 64 G,复杂 SQL 执行响应最好,提供超大规格选择 | +| NoSQL数据库 | Redis | 中 | 双机热备Redis:一般作为持久化数据库提高业务可用性集群版本的Redis:一般作为缓存层,加速应用访问,解决一般数据库无法负载的读取压力 | +| | MongoDB版 | 中 | 单节点实例单节点:适用于开发、测试及其他非企业核心数据存储的场景副本集实例:适用于某些业务场景下对数据库有更高读取性能需求,如阅读类网站、订单查询系统等读多写少场景或有临时活动等突发业务需求分片集群实例:基于多个副本集(每个副本集沿用三副本模式)组成的分片集群实例,提供更高的读取性能需求,为实时在线业务提供高速读取性能 | 光说不易理解,我们通过一个具体的“博客文章”场景,来看看同样的数据在关系数据库 (SQL) 和不同类型的非关系数据库 (NoSQL) 中是如何存储的。 diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image1.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image1.png deleted file mode 100644 index 3400430..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image1.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image10.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image10.png deleted file mode 100644 index 7e7c364..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image10.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image11.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image11.png deleted file mode 100644 index 144108a..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image11.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image12.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image12.png deleted file mode 100644 index 164b8c9..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image12.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image13.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image13.png deleted file mode 100644 index 7539c51..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image13.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image14.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image14.png deleted file mode 100644 index 13b638d..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image14.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image15.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image15.png deleted file mode 100644 index 1cdb12e..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image15.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image16.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image16.png deleted file mode 100644 index db7d5c0..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image16.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image17.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image17.png deleted file mode 100644 index 7a2d1e4..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image17.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image18.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image18.png deleted file mode 100644 index 8015c6f..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image18.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image19.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image19.png deleted file mode 100644 index 9c6e1d3..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image19.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image2.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image2.png deleted file mode 100644 index 2600155..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image2.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image20.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image20.png deleted file mode 100644 index af4691b..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image20.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image21.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image21.png deleted file mode 100644 index 60f2b54..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image21.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image22.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image22.png deleted file mode 100644 index 5379a94..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image22.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image23.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image23.png deleted file mode 100644 index aaf9ef9..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image23.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image24.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image24.png deleted file mode 100644 index c1814d9..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image24.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image25.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image25.png deleted file mode 100644 index 9c02a06..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image25.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image26.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image26.png deleted file mode 100644 index 38e57af..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image26.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image27.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image27.png deleted file mode 100644 index f8b30a3..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image27.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image28.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image28.png deleted file mode 100644 index 65b3c97..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image28.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image29.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image29.png deleted file mode 100644 index 4c4f73e..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image29.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image3.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image3.png deleted file mode 100644 index 9b23ac1..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image3.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image30.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image30.png deleted file mode 100644 index e04c07f..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image30.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image31.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image31.png deleted file mode 100644 index cf15683..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image31.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image32.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image32.png deleted file mode 100644 index 836273b..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image32.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image33.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image33.png deleted file mode 100644 index 7494ee4..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image33.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image34.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image34.png deleted file mode 100644 index 0f53a63..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image34.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image35.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image35.png deleted file mode 100644 index 7494ee4..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image35.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image36.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image36.png deleted file mode 100644 index 101328c..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image36.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image37.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image37.png deleted file mode 100644 index 2a7cb11..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image37.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image38.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image38.png deleted file mode 100644 index 55ad6fb..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image38.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image39.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image39.png deleted file mode 100644 index 7494ee4..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image39.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image4.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image4.png deleted file mode 100644 index 84882f1..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image4.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image40.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image40.png deleted file mode 100644 index b5afad6..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image40.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image41.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image41.png deleted file mode 100644 index d7127ee..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image41.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image42.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image42.png deleted file mode 100644 index ad70349..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image42.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image5.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image5.png deleted file mode 100644 index 4dc556d..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image5.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image6.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image6.png deleted file mode 100644 index 57d3355..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image6.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image7.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image7.png deleted file mode 100644 index ce40757..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image7.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image8.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image8.png deleted file mode 100644 index fdd6558..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image8.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image9.png b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image9.png deleted file mode 100644 index fd2315a..0000000 Binary files a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/image9.png and /dev/null differ diff --git a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/index.md b/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/index.md deleted file mode 100644 index de58f52..0000000 --- a/docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# 微信小程序开发实战 - -> 本章节正在编写中,敬请期待... diff --git a/vercel.json b/vercel.json deleted file mode 100644 index efec22e..0000000 --- a/vercel.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "framework": "vitepress", - "buildCommand": "npm run build", - "outputDirectory": "docs/.vitepress/dist", - "installCommand": "npm install" -}