Open
feat(app): 登录态持久化(Preferences)+ 冷启动恢复首屏 (#3) #5
雅皮士Add on 9 days ago
feat(app): 登录态持久化(Preferences)+ 冷启动恢复首屏 (#3) #5
Open
Total: 32 files changed+715-108
| @@ -0,0 +1,21 @@ | |||
| 1 | +MIT License | ||
| 2 | + | ||
| 3 | +Copyright (c) 2026 Wenhao Jiang | ||
| 4 | + | ||
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 6 | +of this software and associated documentation files (the "Software"), to deal | ||
| 7 | +in the Software without restriction, including without limitation the rights | ||
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 9 | +copies of the Software, and to permit persons to whom the Software is | ||
| 10 | +furnished to do so, subject to the following conditions: | ||
| 11 | + | ||
| 12 | +The above copyright notice and this permission notice shall be included in all | ||
| 13 | +copies or substantial portions of the Software. | ||
| 14 | + | ||
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 21 | +SOFTWARE. | ||
| @@ -70,7 +70,8 @@ Function 执行端点,Function 用 Server SDK(API Key)读写 Database, | |||
| 70 | | 项目 | 要求 | 检查或获取方式 | | 70 | | 项目 | 要求 | 检查或获取方式 | |
| 71 | | --- | --- | --- | | 71 | | --- | --- | --- | |
| 72 | | 基础知识 | ArkTS、HTTP、数据库基础 | 可选:HarmonyOS 官方 ArkTS 教程 | | 72 | | 基础知识 | ArkTS、HTTP、数据库基础 | 可选:HarmonyOS 官方 ArkTS 教程 | |
| 73 | -| 开发环境 | Windows/macOS、DevEco Studio、Node.js | `node -v`、DevEco 界面路径 | | 73 | +| 开发环境 | Windows/macOS、DevEco Studio、Node.js、Python 3.8+ | `node -v`、`python --version`、DevEco 界面路径 | |
| 74 | +| Python 依赖 | 无(`deploy.py` 仅用标准库) | 无需 `pip install` | | ||
| 74 | | 测试设备 | 模拟器或真机 | `hdc list targets` | | 75 | | 测试设备 | 模拟器或真机 | `hdc list targets` | |
| 75 | | 账号与服务 | 自托管 Appwrite 服务器可达、项目与 API Key 已建 | `GET /v1/health` 返回 `pass` | | 76 | | 账号与服务 | 自托管 Appwrite 服务器可达、项目与 API Key 已建 | `GET /v1/health` 返回 `pass` | |
| 76 | | 权限与网络 | 服务器 80 端口可达;API Key 含 functions/databases 权限 | `GET /v1/functions` 正常返回 | | 77 | | 权限与网络 | 服务器 80 端口可达;API Key 含 functions/databases 权限 | `GET /v1/functions` 正常返回 | |
| @@ -87,7 +88,7 @@ Function 执行端点,Function 用 Server SDK(API Key)读写 Database, | |||
| 87 | 88 | ||
| 88 | | 占位符 | 含义 | 获取位置 | 安全存放位置 | | 89 | | 占位符 | 含义 | 获取位置 | 安全存放位置 | |
| 89 | | --- | --- | --- | --- | | 90 | | --- | --- | --- | --- | |
| 90 | -| `<appwrite_url>` | 服务器地址 | 服务器 IP + 端口 | 端侧公开配置 | | 91 | +| `APPWRITE_HOST` | 服务器地址 | 服务器 IP + 端口 | 端侧公开配置 | |
| 91 | | `<project_id>` | Appwrite 项目 ID | 控制台 → 项目设置 | 端侧公开配置 | | 92 | | `<project_id>` | Appwrite 项目 ID | 控制台 → 项目设置 | 端侧公开配置 | |
| 92 | | `<api_key>` | 服务器级 API Key | 控制台 → API Keys | 仅函数环境变量 / `.env` | | 93 | | `<api_key>` | 服务器级 API Key | 控制台 → API Keys | 仅函数环境变量 / `.env` | |
| 93 | 94 | ||
| @@ -96,12 +97,28 @@ Function 执行端点,Function 用 Server SDK(API Key)读写 Database, | |||
| 96 | 97 | ||
| 97 | ## 3. 快速体验 | 98 | ## 3. 快速体验 |
| 98 | 99 | ||
| 99 | -> 这条路线面向「先把现成案例跑起来」的读者,目标是在 10–15 分钟内看到第一个成功结果。原理和完整搭建过程见[完整实践](#4-完整实践)。 | 100 | +> 这条路线面向「先把现成案例跑起来」的读者,目标是约 1–2 小时(含一次性云环境准备)内看到第一个成功结果。原理和完整搭建过程见[完整实践](#4-完整实践)。 |
| 101 | + | ||
| 102 | +### 0)用 AI 辅助配置云环境(可选) | ||
| 103 | + | ||
| 104 | +想跳过下面的手动步骤?把本仓库 `README.md` 与 `SKILL.md` 交给 AI 编程助手(码道 / CodeArts、Cursor 等),让它读完自动配置并部署云环境: | ||
| 105 | + | ||
| 106 | +```text | ||
| 107 | +请阅读本仓库 README.md 与 SKILL.md,帮我自动配置并部署「单词打卡」云环境: | ||
| 108 | +1. 按第 2 章「配置与敏感信息」列出需要我填写的占位符(Appwrite 地址、Project ID、API Key),等我填好 harmony_cloud/.env; | ||
| 109 | +2. 运行 deploy/deploy.py 部署 6 个云函数(自动 npm install 并打包 tar.gz); | ||
| 110 | +3. 调用 db-init 建集合与种子数据; | ||
| 111 | +4. 把服务器地址与 Project ID 填入 HttpUtil.ets,编译并安装端侧; | ||
| 112 | +5. 按第 5 章验收表逐项联调。 | ||
| 113 | +每完成一步报告结果;报错时贴完整日志并定位根因;API Key 只写本地 .env,不写入代码、不提交仓库。 | ||
| 114 | +``` | ||
| 115 | + | ||
| 116 | +> **注意**:AI 只能操作你授权的环境。API Key 等敏感信息请只放在本地 `.env`(已被 `.gitignore` 忽略),不要让 AI 把凭据写进代码或提交。部署完成后,仍建议按下面各步骤的「完成标志」人工复核一遍。 | ||
| 100 | 117 | ||
| 101 | ### 1)获取已验证版本 | 118 | ### 1)获取已验证版本 |
| 102 | 119 | ||
| 103 | ```bash | 120 | ```bash |
| 104 | -git clone <repo-url> | 121 | +git clone https://gitcode.com/harmony-practice-center/word-check-in.git |
| 105 | cd word-check-in | 122 | cd word-check-in |
| 106 | ``` | 123 | ``` |
| 107 | 124 | ||
| @@ -113,7 +130,7 @@ cd word-check-in | |||
| 113 | cp harmony_cloud/.env.example harmony_cloud/.env | 130 | cp harmony_cloud/.env.example harmony_cloud/.env |
| 114 | ``` | 131 | ``` |
| 115 | 132 | ||
| 116 | -1. 将 `.env` 中的 API Key、URL、Project ID 替换为第 2 章收集的值。 | 133 | +1. 将 `.env` 中的 API Key、APPWRITE_HOST、Project ID 替换为第 2 章收集的值。 |
| 117 | 2. 在 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets` 填入 `APPWRITE_HOST` 和 `PROJECT_ID`。 | 134 | 2. 在 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets` 填入 `APPWRITE_HOST` 和 `PROJECT_ID`。 |
| 118 | 135 | ||
| 119 | **完成标志**:没有未替换占位符;敏感配置不会出现在 `git status` 中。 | 136 | **完成标志**:没有未替换占位符;敏感配置不会出现在 `git status` 中。 |
| @@ -129,10 +146,14 @@ curl -X POST http://<host>/v1/functions/db-init/executions \ | |||
| 129 | -d '{"body":"{}","async":false}' | 146 | -d '{"body":"{}","async":false}' |
| 130 | ``` | 147 | ``` |
| 131 | 148 | ||
| 149 | +> `deploy.py` 会为每个函数自动执行 `npm install --omit=dev` 并打包依赖,无需手动安装 `node-appwrite`。 | ||
| 150 | + | ||
| 132 | **完成标志**:`db-init` 执行后,`word_db` 下 3 个集合、58 个单词和管理员 `admin@company.com` / `123456` 已创建。 | 151 | **完成标志**:`db-init` 执行后,`word_db` 下 3 个集合、58 个单词和管理员 `admin@company.com` / `123456` 已创建。 |
| 133 | 152 | ||
| 134 | ### 4)构建并运行端侧 | 153 | ### 4)构建并运行端侧 |
| 135 | 154 | ||
| 155 | +> 首次编译前,先用 DevEco Studio 打开 `harmony_app` 目录,让它自动生成 `local.properties`(填入 `sdk.dir`);命令行编译还需设置环境变量 `DEVECO_SDK_HOME`。缺失会报 `00303217`,`sdk.dir` 被改写会报 `00308018`,路径含空格会报 `Space is not supported in HVIGOR_USER_HOME`。详见 [SKILL.md](./SKILL.md) 的「全量故障速查表」。 | ||
| 156 | + | ||
| 136 | ```powershell | 157 | ```powershell |
| 137 | Set-Location 'harmony_app' | 158 | Set-Location 'harmony_app' |
| 138 | ohpm install | 159 | ohpm install |
| @@ -146,12 +167,41 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 146 | 167 | ||
| 147 | > 这条路线面向「理解并复现案例」的读者。步骤按真实工作流排列,每一步都有可观察的完成标志。云函数编写规范、代码模板、打包与排障细节见 [SKILL.md](./SKILL.md)。 | 168 | > 这条路线面向「理解并复现案例」的读者。步骤按真实工作流排列,每一步都有可观察的完成标志。云函数编写规范、代码模板、打包与排障细节见 [SKILL.md](./SKILL.md)。 |
| 148 | 169 | ||
| 170 | +### 一句话让 AI 工作 | ||
| 171 | + | ||
| 172 | +不想手动逐项操作?把下面这句话发给码道(华为云 CodeArts 鸿蒙开发智能体,或任意支持 HarmonyOS / Node.js 的 AI 编程助手),即可让它从需求澄清一路推进到端云联调。你也可以把词库规模、打卡规则换成自己的版本。 | ||
| 173 | + | ||
| 174 | +```text | ||
| 175 | +我想开发一款「单词打卡学习」HarmonyOS 端云一体化应用:端侧 ArkTS/ArkUI(登录、首页、闪卡、测试 4 个页面),云侧 Appwrite 自托管 Functions(Node.js,6 个云函数)+ Appwrite Database(word_db,3 个集合、58 词词库),支持注册登录、每日单词列表、闪卡翻转学习、看词选义测试、进度条与连续打卡。 | ||
| 176 | + | ||
| 177 | +请按照标准研发流程推进:先提问澄清需求,再依次产出 requirements.md、design.md、tasks.md;然后分批实现 6 个云函数(注意 ESM 默认导出、node-appwrite v11、execute=["any"])、3 个集合的数据模型和端侧页面;最后给出部署(deploy.py 打包上传)与端云联调验收步骤。 | ||
| 178 | + | ||
| 179 | +每一步都要有可观察的完成标志,关键节点停下来等我确认。 | ||
| 180 | +``` | ||
| 181 | + | ||
| 182 | +### 如何指挥 AI 工作 | ||
| 183 | + | ||
| 184 | +「一句话」只是开工信号,真正决定质量的是后续每一轮的指挥方式: | ||
| 185 | + | ||
| 186 | +- **一次只给一个明确目标**:每轮指令聚焦单一产出(需求 / 设计 / 任务 / 某一批代码 / 部署),让 AI 给出可观察结果,而不是一次铺开。 | ||
| 187 | +- **要求可观察的完成标志**:每步都让 AI 明确「做完是什么样子」,你据此判断能否进入下一步。 | ||
| 188 | +- **关键节点人工确认**:需求澄清、技术选型、Appwrite 配置、部署上线等不可逆或高成本节点,先看 AI 的方案再放行。 | ||
| 189 | +- **把真实报错带回去**:编译或部署失败时,把完整日志原文贴给 AI 定位,而不是让它凭空猜测。 | ||
| 190 | + | ||
| 191 | +下面的步骤 1–5 每一步都附了「推荐指令」,可照此逐条指挥 AI;想对照手动操作时,看同一步骤的「操作」。 | ||
| 192 | + | ||
| 149 | ### 步骤 1:确认 Appwrite 服务器与收集信息 | 193 | ### 步骤 1:确认 Appwrite 服务器与收集信息 |
| 150 | 194 | ||
| 151 | **目标**:确认自托管 Appwrite 可用,收集 Project ID 与 API Key。 | 195 | **目标**:确认自托管 Appwrite 可用,收集 Project ID 与 API Key。 |
| 152 | 196 | ||
| 153 | **为什么这样做**:自托管环境的可用运行时与端口取决于运维配置,先确认能避免后续部署返工。 | 197 | **为什么这样做**:自托管环境的可用运行时与端口取决于运维配置,先确认能避免后续部署返工。 |
| 154 | 198 | ||
| 199 | +**推荐指令**: | ||
| 200 | + | ||
| 201 | +```text | ||
| 202 | +请帮我梳理这个案例需要确认的 Appwrite 环境信息:服务器地址、Project ID、API Key(含 functions 读写、databases 读写权限)、可用运行时(node-16.0)。把每项的获取位置和安全存放位置整理成表,并给出 /v1/health、/v1/functions/runtimes 等健康检查与权限验证方式。 | ||
| 203 | +``` | ||
| 204 | + | ||
| 155 | **操作**: | 205 | **操作**: |
| 156 | 206 | ||
| 157 | 1. 访问 `/v1/health` 确认服务器 `pass`。 | 207 | 1. 访问 `/v1/health` 确认服务器 `pass`。 |
| @@ -159,7 +209,7 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 159 | 3. 创建 API Key(含 functions.write/read、databases.write/read 权限)。 | 209 | 3. 创建 API Key(含 functions.write/read、databases.write/read 权限)。 |
| 160 | 4. 调用 `GET /v1/functions/runtimes` 确认可用运行时(本例 `node-16.0`)。 | 210 | 4. 调用 `GET /v1/functions/runtimes` 确认可用运行时(本例 `node-16.0`)。 |
| 161 | 211 | ||
| 162 | -**完成标志**:信息清单填齐,`<appwrite_url>` / `<project_id>` / `<api_key>` 均可用。 | 212 | +**完成标志**:信息清单填齐,`APPWRITE_HOST` / `<project_id>` / `<api_key>` 均可用。 |
| 163 | 213 | ||
| 164 | **如果失败**:`missing scopes (["projects.read"])` 说明 API Key 权限不足,用 `GET /v1/functions` 验证,见[验收与排障](#5-验收与排障)。 | 214 | **如果失败**:`missing scopes (["projects.read"])` 说明 API Key 权限不足,用 `GET /v1/functions` 验证,见[验收与排障](#5-验收与排障)。 |
| 165 | 215 | ||
| @@ -169,6 +219,12 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 169 | 219 | ||
| 170 | **为什么这样做**:Appwrite v1.9.0 不会自动安装依赖,且函数入口用 ESM 默认导出,写错会导致运行时报错。 | 220 | **为什么这样做**:Appwrite v1.9.0 不会自动安装依赖,且函数入口用 ESM 默认导出,写错会导致运行时报错。 |
| 171 | 221 | ||
| 222 | +**推荐指令**: | ||
| 223 | + | ||
| 224 | +```text | ||
| 225 | +请按 Appwrite Functions 编写规范实现 6 个云函数(register、login、getDailyWords、learnWord、getProgress、db-init)。注意:ESM 默认导出、node-appwrite@^11.0.0(Node 16 兼容)、Server SDK 用 API Key 读写数据库、统一 {code,data,msg} 返回。完成后给出每个函数的依赖安装与 tar.gz 打包要求(根目录含 main.js + node_modules)。 | ||
| 226 | +``` | ||
| 227 | + | ||
| 172 | **操作**: | 228 | **操作**: |
| 173 | 229 | ||
| 174 | 1. 按 [SKILL.md](./SKILL.md) 的「云函数编写规范」与「代码模板」编写 6 个函数。 | 230 | 1. 按 [SKILL.md](./SKILL.md) 的「云函数编写规范」与「代码模板」编写 6 个函数。 |
| @@ -184,6 +240,12 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 184 | 240 | ||
| 185 | **为什么这样做**:手动路线便于逐项确认权限与运行时,自动路线适合批量复用;两条路线产出相同,交叉创建同名资源会导致冲突。 | 241 | **为什么这样做**:手动路线便于逐项确认权限与运行时,自动路线适合批量复用;两条路线产出相同,交叉创建同名资源会导致冲突。 |
| 186 | 242 | ||
| 243 | +**推荐指令**: | ||
| 244 | + | ||
| 245 | +```text | ||
| 246 | +请编写部署脚本 deploy.py(为每个函数执行 npm install --omit=dev 并打包成 tar.gz 上传),并说明 db-init 建集合与种子数据(3 个集合、58 个单词、管理员 admin@company.com / 123456)的执行方式。同时给出为每个函数配置 API_KEY、DATABASE_ID 环境变量与 execute=["any"] 的要点。 | ||
| 247 | +``` | ||
| 248 | + | ||
| 187 | **操作**: | 249 | **操作**: |
| 188 | 250 | ||
| 189 | 1. 选择一条路线:**手动**在控制台逐项创建,或**自动**用 Python 脚本(见 SKILL 的「部署与打包」)。 | 251 | 1. 选择一条路线:**手动**在控制台逐项创建,或**自动**用 Python 脚本(见 SKILL 的「部署与打包」)。 |
| @@ -201,6 +263,12 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 201 | 263 | ||
| 202 | **为什么这样做**:端侧通过 `HttpUtil.ets` 指向 Appwrite 执行端点;SDK 版本、工具链路径等环境问题会阻塞编译。 | 264 | **为什么这样做**:端侧通过 `HttpUtil.ets` 指向 Appwrite 执行端点;SDK 版本、工具链路径等环境问题会阻塞编译。 |
| 203 | 265 | ||
| 266 | +**推荐指令**: | ||
| 267 | + | ||
| 268 | +```text | ||
| 269 | +请实现端侧 ArkTS 工程:在 HttpUtil.ets 统一配置 APPWRITE_HOST(http:// 协议)与 PROJECT_ID,并确认 functionId 映射与部署一致;4 个页面(登录、首页、闪卡、测试)通过函数执行端点调用云侧。注意配置 ohos.permission.INTERNET 权限;编译前核对 SDK 版本与 DEVECO_SDK_HOME,遇报错先定位根因再做最小修改。 | ||
| 270 | +``` | ||
| 271 | + | ||
| 204 | **操作**: | 272 | **操作**: |
| 205 | 273 | ||
| 206 | 1. 在 `HttpUtil.ets` 填入 `APPWRITE_HOST`(`http://` 协议)和 `PROJECT_ID`,确认 functionId 映射与部署一致。 | 274 | 1. 在 `HttpUtil.ets` 填入 `APPWRITE_HOST`(`http://` 协议)和 `PROJECT_ID`,确认 functionId 映射与部署一致。 |
| @@ -217,6 +285,12 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 217 | 285 | ||
| 218 | **为什么这样做**:这是验证「命令提交成功 ≠ 部署完成」的关键一步,只有实际调用通过才算完成。 | 286 | **为什么这样做**:这是验证「命令提交成功 ≠ 部署完成」的关键一步,只有实际调用通过才算完成。 |
| 219 | 287 | ||
| 288 | +**推荐指令**: | ||
| 289 | + | ||
| 290 | +```text | ||
| 291 | +请给我一份端云联调验收清单:逐项列出注册/登录、查看今日单词、闪卡翻转、看词选义测试、打卡状态与连续天数等操作与预期结果。并在模拟器/真机上执行一轮自动测试,逐项记录结果;发现问题先定位相关代码,修复后做针对性回归。 | ||
| 292 | +``` | ||
| 293 | + | ||
| 220 | **操作**:按第 5 章的业务验收表逐项执行。 | 294 | **操作**:按第 5 章的业务验收表逐项执行。 |
| 221 | 295 | ||
| 222 | **完成标志**:第 5 章验收项全部通过,打卡与进度数据正确。 | 296 | **完成标志**:第 5 章验收项全部通过,打卡与进度数据正确。 |
| @@ -107,7 +107,7 @@ X-Appwrite-Project: {project_id} | |||
| 107 | | 3 | **打包格式错误** | `storage_file_type_unsupported` (400) | 传了 `.zip` | 用 Python `tarfile` 打 `.tar.gz` | | 107 | | 3 | **打包格式错误** | `storage_file_type_unsupported` (400) | 传了 `.zip` | 用 Python `tarfile` 打 `.tar.gz` | |
| 108 | | 4 | **依赖未打包** | `Cannot find package 'node-appwrite'` | v1.9.0 不自动 npm install | 部署前 `npm install --omit=dev` | | 108 | | 4 | **依赖未打包** | `Cannot find package 'node-appwrite'` | v1.9.0 不自动 npm install | 部署前 `npm install --omit=dev` | |
| 109 | | 5 | **API Key 权限不足** | `missing scopes (["projects.read"])` | Application 级 Key 无 projects.read | 用 `GET /v1/functions` 验证连通性 | | 109 | | 5 | **API Key 权限不足** | `missing scopes (["projects.read"])` | Application 级 Key 无 projects.read | 用 `GET /v1/functions` 验证连通性 | |
| 110 | -| 6 | **HTTPS 不可用** | 443 超时 | 自托管只开 80 端口 | URL 改用 `http://` | | 110 | +| 6 | **HTTPS 不可用** | 443 超时 | 自托管只开 80 端口 | APPWRITE_HOST 改用 `http://` | |
| 111 | | 7 | **未授权 401** | 前端调用 401 | `execute` 默认不允许匿名 | 显式 `execute=["any"]` | | 111 | | 7 | **未授权 401** | 前端调用 401 | `execute` 默认不允许匿名 | 显式 `execute=["any"]` | |
| 112 | | 8 | **functionId 位置错** | `Param "functionId" is not optional` | 放到了 URL 路径 | 放在创建请求的 body 里 | | 112 | | 8 | **functionId 位置错** | `Param "functionId" is not optional` | 放到了 URL 路径 | 放在创建请求的 body 里 | |
| 113 | | 9 | **responseBody 为空** | 前端拿到空串 | 函数未调用 `res.json()` | 业务结束时 `return res.json(...)` | | 113 | | 9 | **responseBody 为空** | 前端拿到空串 | 函数未调用 `res.json()` | 业务结束时 `return res.json(...)` | |
| @@ -316,7 +316,7 @@ const url = APPWRITE_HOST + '/v1/functions/' + functionId + '/executions'; | |||
| 316 | ## 8. 安全加固 | 316 | ## 8. 安全加固 |
| 317 | 317 | ||
| 318 | 1. **密码 PBKDF2-SHA256 加盐哈希**(已实现):`register` 存 `password: hashPassword(password, salt)` + `salt`;`login` 用 `user.salt` 重算比对;`db-init` 管理员密码同样哈希。 | 318 | 1. **密码 PBKDF2-SHA256 加盐哈希**(已实现):`register` 存 `password: hashPassword(password, salt)` + `salt`;`login` 用 `user.salt` 重算比对;`db-init` 管理员密码同样哈希。 |
| 319 | -2. **凭据与代码分离**:`.env.example` 只放占位符,真实 API Key / URL / PROJECT_ID 放 git 忽略的 `.env`。 | 319 | +2. **凭据与代码分离**:`.env.example` 只放占位符,真实 API Key / APPWRITE_HOST / PROJECT_ID 放 git 忽略的 `.env`。 |
| 320 | 3. **`execute=["any"]` 是无鉴权公开访问**:`db-init` 也公开可调,任何人可清库重建。生产应改为 `execute=["users"]` 或加应用层鉴权。 | 320 | 3. **`execute=["any"]` 是无鉴权公开访问**:`db-init` 也公开可调,任何人可清库重建。生产应改为 `execute=["users"]` 或加应用层鉴权。 |
| 321 | 4. **HTTP 明文传输**:自托管只开 80 端口时,API Key、密码、用户数据都明文传输;生产必须配 HTTPS(TLS 反代)。 | 321 | 4. **HTTP 明文传输**:自托管只开 80 端口时,API Key、密码、用户数据都明文传输;生产必须配 HTTPS(TLS 反代)。 |
| 322 | 5. **API Key 泄漏要轮换**:真实 Key 一旦进入过仓库/文档/日志,必须在控制台吊销重发。 | 322 | 5. **API Key 泄漏要轮换**:真实 Key 一旦进入过仓库/文档/日志,必须在控制台吊销重发。 |
| @@ -329,7 +329,7 @@ const url = APPWRITE_HOST + '/v1/functions/' + functionId + '/executions'; | |||
| 329 | | `Cannot find package 'node-appwrite'` | 云函数 | 依赖未打包 | 部署前 `npm install --omit=dev` | | 329 | | `Cannot find package 'node-appwrite'` | 云函数 | 依赖未打包 | 部署前 `npm install --omit=dev` | |
| 330 | | `storage_file_type_unsupported` (400) | 部署 | 传了 `.zip` | 打 `.tar.gz` | | 330 | | `storage_file_type_unsupported` (400) | 部署 | 传了 `.zip` | 打 `.tar.gz` | |
| 331 | | `missing scopes (["projects.read"])` (401) | 部署 | API Key 权限不足 | 用 `GET /v1/functions` 验证 | | 331 | | `missing scopes (["projects.read"])` (401) | 部署 | API Key 权限不足 | 用 `GET /v1/functions` 验证 | |
| 332 | -| 443 超时 | 网络 | 只开 80 端口 | URL 用 `http://` | | 332 | +| 443 超时 | 网络 | 只开 80 端口 | APPWRITE_HOST 用 `http://` | |
| 333 | | `Param "functionId" is not optional` (400) | 部署 | functionId 放 URL 了 | 放到 body | | 333 | | `Param "functionId" is not optional` (400) | 部署 | functionId 放 URL 了 | 放到 body | |
| 334 | | EBADENGINE / npm 安装失败 | 打包 | Node 16 不兼容 node-appwrite v14 | 降级 `node-appwrite@^11.0.0` | | 334 | | EBADENGINE / npm 安装失败 | 打包 | Node 16 不兼容 node-appwrite v14 | 降级 `node-appwrite@^11.0.0` | |
| 335 | | `export 'Client' not found` | 云函数 | 命名导入不兼容 v11 | 默认导入 `import sdk from 'node-appwrite'` | | 335 | | `export 'Client' not found` | 云函数 | 命名导入不兼容 v11 | 默认导入 `import sdk from 'node-appwrite'` | |
| @@ -0,0 +1,27 @@ | |||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="2080" height="820" viewBox="0 0 2080 820" role="img" aria-labelledby="title desc"> | ||
| 2 | +<title id="title">单词打卡学习:端云一体化架构</title><desc id="desc">端侧 ArkUI 页面经 Appwrite Functions 执行端点调用云函数,云函数用 Server SDK 读写 Appwrite Database,端侧不直连数据库。</desc> | ||
| 3 | +<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#688f7e"/></marker></defs> | ||
| 4 | +<rect width="2080" height="820" rx="28" fill="#f1f6ef"/><g font-family="Microsoft YaHei, Noto Sans SC, sans-serif"> | ||
| 5 | +<text x="80" y="86" font-size="44" font-weight="700" fill="#243c36">单词打卡学习 · 端云一体化架构</text> | ||
| 6 | +<text x="80" y="137" font-size="26" fill="#5b7266">Appwrite 内建 HTTP 路由 + 文档型 NoSQL,省去独立网关层,端侧不直连数据库。</text> | ||
| 7 | +<rect x="80" y="230" width="400" height="270" rx="24" fill="#ffffff" stroke="#a6beaa" stroke-width="2"/><rect x="80" y="230" width="400" height="70" rx="24" fill="#355a4b"/><text x="104" y="277" font-size="29" font-weight="700" fill="#fff0ce">端侧 ArkUI</text> | ||
| 8 | +<text x="104" y="350" font-size="22" fill="#35564b">登录 / 首页(打卡状态+单词列表)</text> | ||
| 9 | +<text x="104" y="402" font-size="22" fill="#35564b">闪卡(翻转查看释义)</text> | ||
| 10 | +<text x="104" y="454" font-size="22" fill="#35564b">测试(看词选义+进度条)</text> | ||
| 11 | +<path d="M485 367 H535" stroke="#688f7e" stroke-width="4" marker-end="url(#arrow)"/> | ||
| 12 | +<rect x="545" y="230" width="400" height="270" rx="24" fill="#ffffff" stroke="#a6beaa" stroke-width="2"/><rect x="545" y="230" width="400" height="70" rx="24" fill="#355a4b"/><text x="569" y="277" font-size="29" font-weight="700" fill="#fff0ce">执行端点</text> | ||
| 13 | +<text x="569" y="350" font-size="22" fill="#35564b">POST /v1/functions/{id}/executions</text> | ||
| 14 | +<text x="569" y="402" font-size="22" fill="#35564b">execute=["any"] 公开执行</text> | ||
| 15 | +<text x="569" y="454" font-size="22" fill="#35564b">responseBody 二次解析</text> | ||
| 16 | +<path d="M950 367 H1000" stroke="#688f7e" stroke-width="4" marker-end="url(#arrow)"/> | ||
| 17 | +<rect x="1010" y="230" width="400" height="270" rx="24" fill="#ffffff" stroke="#a6beaa" stroke-width="2"/><rect x="1010" y="230" width="400" height="70" rx="24" fill="#355a4b"/><text x="1034" y="277" font-size="29" font-weight="700" fill="#fff0ce">Appwrite Functions</text> | ||
| 18 | +<text x="1034" y="350" font-size="22" fill="#35564b">6 个云函数(Node.js 16)</text> | ||
| 19 | +<text x="1034" y="402" font-size="22" fill="#35564b">ESM 默认导出 + Server SDK</text> | ||
| 20 | +<text x="1034" y="454" font-size="22" fill="#35564b">API Key 读写数据库</text> | ||
| 21 | +<path d="M1415 367 H1465" stroke="#688f7e" stroke-width="4" marker-end="url(#arrow)"/> | ||
| 22 | +<rect x="1475" y="230" width="400" height="270" rx="24" fill="#ffffff" stroke="#a6beaa" stroke-width="2"/><rect x="1475" y="230" width="400" height="70" rx="24" fill="#355a4b"/><text x="1499" y="277" font-size="29" font-weight="700" fill="#fff0ce">Appwrite Database</text> | ||
| 23 | +<text x="1499" y="350" font-size="22" fill="#35564b">3 个集合 / 文档型 NoSQL</text> | ||
| 24 | +<text x="1499" y="402" font-size="22" fill="#35564b">users_col / words_col</text> | ||
| 25 | +<text x="1499" y="454" font-size="22" fill="#35564b">learn_records_col / 58 词词库</text> | ||
| 26 | +<rect x="80" y="583" width="920" height="166" rx="24" fill="#e2ecd9"/><text x="106" y="630" font-size="29" font-weight="700" fill="#35564b">云函数规范输入</text><text x="106" y="682" font-size="26" fill="#35564b">ESM 默认导出 / node-appwrite@^11.0.0 / 统一 {code,data,msg} 返回</text><text x="106" y="723" font-size="26" fill="#35564b">tar.gz 打包上传(根目录含 main.js + node_modules)</text> | ||
| 27 | +<rect x="1020" y="583" width="855" height="166" rx="24" fill="#f3ead5"/><text x="1046" y="630" font-size="29" font-weight="700" fill="#665b3c">关键约束</text><text x="1046" y="682" font-size="26" fill="#665b3c">端侧不持有数据库凭据 / 执行端点是唯一入口</text><text x="1046" y="723" font-size="26" fill="#665b3c">自托管无独立网关层 / 命令成功 ≠ 部署完成</text></g></svg> | ||
| @@ -0,0 +1,300 @@ | |||
| 1 | +{ | ||
| 2 | + "baseInfo": { | ||
| 3 | + "title": "单词打卡学习:HarmonyOS + Appwrite 端云一体化", | ||
| 4 | + "description": "用 ArkTS 和 Appwrite 自托管 Functions 构建卡片式单词打卡应用,支持闪卡学习、看词选义测试、进度条与连续打卡,掌握 NoSQL 文档数据库与自托管部署。", | ||
| 5 | + "coverImage": "cover.jpg", | ||
| 6 | + "difficulty": "入门", | ||
| 7 | + "duration": 120, | ||
| 8 | + "author": "鸿蒙开发实践中心", | ||
| 9 | + "tags": [ | ||
| 10 | + "arkts", | ||
| 11 | + "arkui", | ||
| 12 | + "cloud_function", | ||
| 13 | + "cloud_sync" | ||
| 14 | + ], | ||
| 15 | + "category": "industry_education", | ||
| 16 | + "repoUrl": "https://gitcode.com/harmony-practice-center/word-check-in.git", | ||
| 17 | + "shareImage": "share.png", | ||
| 18 | + "demoVideo": "", | ||
| 19 | + "cardImage": "card.jpg", | ||
| 20 | + "caseForm": "case_form_practice", | ||
| 21 | + "appForm": "app_form_app" | ||
| 22 | + }, | ||
| 23 | + "introduction": { | ||
| 24 | + "overview": "<p>单词打卡学习是一个卡片式背单词应用,演示“HarmonyOS 端侧 + Appwrite 自托管 Serverless 云侧”的端云一体化开发流程。用户注册登录后进入首页,看到今日打卡状态与连续打卡天数;首页展示今日 10 个未学单词,进入闪卡模式点击卡片翻转查看释义;完成看词选义测试后答对计入学习进度,进度条更新;学完一个单词打卡状态变为已完成,连续天数正确累加。与华为云 FunctionGraph/APIG 方案不同,Appwrite 用内建 HTTP 路由 + 文档型 NoSQL,省去了独立 API 网关层。沉淀的 Appwrite Functions 规范与自托管排障经验可复用到其他 Appwrite 后端场景。</p>", | ||
| 25 | + "highlights": [ | ||
| 26 | + { | ||
| 27 | + "icon": "Present", | ||
| 28 | + "title": "闪卡翻转学单词", | ||
| 29 | + "desc": "点击卡片翻转查看释义,看词选义测试答对计入进度,连续打卡天数累加。" | ||
| 30 | + }, | ||
| 31 | + { | ||
| 32 | + "icon": "Cpu", | ||
| 33 | + "title": "Appwrite Functions 规范", | ||
| 34 | + "desc": "ESM 默认导出、node-appwrite v11、Server SDK 用 API Key 读写数据库、execute=[\"any\"]。" | ||
| 35 | + }, | ||
| 36 | + { | ||
| 37 | + "icon": "CollectionTag", | ||
| 38 | + "title": "NoSQL 文档数据库", | ||
| 39 | + "desc": "3 个集合(users_col / words_col / learn_records_col)与 58 词词库,省去独立网关层。" | ||
| 40 | + }, | ||
| 41 | + { | ||
| 42 | + "icon": "Box", | ||
| 43 | + "title": "自托管部署与排障", | ||
| 44 | + "desc": "tar.gz 打包上传、运行时选择 node-16.0、API Key 权限配置与 401 排障。" | ||
| 45 | + } | ||
| 46 | + ], | ||
| 47 | + "deliverables": { | ||
| 48 | + "title": "完成后,你能做出什么", | ||
| 49 | + "intro": "在已发布源码基础上复现并理解以下成果。", | ||
| 50 | + "items": [ | ||
| 51 | + "可运行的卡片式单词打卡应用,含登录、首页、闪卡、测试 4 个页面。", | ||
| 52 | + "6 个 Appwrite 云函数 + 3 个数据集合,端云联调验收通过。", | ||
| 53 | + "58 词词库与连续打卡逻辑,学完单词打卡状态正确更新。", | ||
| 54 | + "Appwrite Functions 编写规范与自托管排障经验,可复用到其他 Appwrite 后端场景。" | ||
| 55 | + ] | ||
| 56 | + }, | ||
| 57 | + "targetAudience": "<p>有 HarmonyOS 或后端基础,希望学习 Appwrite 端云一体化开发的工程师;也适合在教师指导下开展自托管 Serverless 实践。</p>" | ||
| 58 | + }, | ||
| 59 | + "prerequisites": { | ||
| 60 | + "cloud": [ | ||
| 61 | + { | ||
| 62 | + "icon": "Key", | ||
| 63 | + "title": "账号准备", | ||
| 64 | + "desc": "自托管 Appwrite 服务器可达,项目与 API Key 已创建。API Key 含 functions.write/read、databases.write/read 权限。", | ||
| 65 | + "status": "必须" | ||
| 66 | + }, | ||
| 67 | + { | ||
| 68 | + "icon": "Box", | ||
| 69 | + "title": "网络环境", | ||
| 70 | + "desc": "服务器 80 端口可达,GET /v1/health 返回 pass。浏览器能够访问实践平台和 GitCode 公仓。", | ||
| 71 | + "status": "必须" | ||
| 72 | + }, | ||
| 73 | + { | ||
| 74 | + "icon": "Money", | ||
| 75 | + "title": "费用说明", | ||
| 76 | + "desc": "自托管方案无按量云资源,主要成本为承载服务器自身费用。实践结束后建议删除测试函数与集合。", | ||
| 77 | + "status": "建议" | ||
| 78 | + }, | ||
| 79 | + { | ||
| 80 | + "icon": "Mouse", | ||
| 81 | + "title": "浏览器与开发环境", | ||
| 82 | + "desc": "使用电脑和现代浏览器访问平台与 Appwrite 控制台;需安装 DevEco Studio、Node.js、Python 3.8+。", | ||
| 83 | + "status": "必须" | ||
| 84 | + } | ||
| 85 | + ], | ||
| 86 | + "local": [ | ||
| 87 | + { | ||
| 88 | + "icon": "ReadingLamp", | ||
| 89 | + "title": "基础知识", | ||
| 90 | + "desc": "了解 ArkTS 语法、ArkUI 声明式 UI、HTTP 基础。使用已验证 DevEco Studio 环境或核对等效 SDK 配置。", | ||
| 91 | + "status": "必须" | ||
| 92 | + }, | ||
| 93 | + { | ||
| 94 | + "icon": "Phone", | ||
| 95 | + "title": "测试设备", | ||
| 96 | + "desc": "准备 HarmonyOS 手机模拟器或真机,完成调试连接。hdc list targets 应返回设备。", | ||
| 97 | + "status": "必须" | ||
| 98 | + }, | ||
| 99 | + { | ||
| 100 | + "icon": "Timer", | ||
| 101 | + "title": "预计时长", | ||
| 102 | + "desc": "建议预留约 120 分钟用于复现、云服务配置与端云联调;环境安装、首次依赖下载与签名时间另计。", | ||
| 103 | + "status": "建议" | ||
| 104 | + }, | ||
| 105 | + { | ||
| 106 | + "icon": "MagicStick", | ||
| 107 | + "title": "AI 编程工具", | ||
| 108 | + "desc": "可使用码道等能够读写项目、执行构建和分析错误的 AI 工具;也可按教程直接阅读与修改源码。", | ||
| 109 | + "status": "可选" | ||
| 110 | + } | ||
| 111 | + ] | ||
| 112 | + }, | ||
| 113 | + "knowledgePoints": { | ||
| 114 | + "intro": "沿着“端侧交互 → 执行端点 → 云函数处理 → 文档数据库”理解核心实现。", | ||
| 115 | + "list": [ | ||
| 116 | + { | ||
| 117 | + "title": "ArkUI 声明式开发", | ||
| 118 | + "subtitle": "闪卡翻转与进度条", | ||
| 119 | + "importance": "核心", | ||
| 120 | + "content": "<p>使用 ArkTS + ArkUI 构建 4 个页面,覆盖打卡学习的核心交互。本案例中你将学到:</p><ul><li><strong>页面清单</strong>:登录、首页、闪卡、测试</li><li><strong>闪卡模式</strong>:点击卡片翻转查看释义</li><li><strong>测试模式</strong>:看词选义,答对计入进度</li><li><strong>首页与联网</strong>:显示今日打卡状态与连续天数;端侧通过 <code>POST /v1/functions/{id}/executions</code> 调用云函数</li></ul>", | ||
| 121 | + "transferScene": "所有 ArkUI 应用开发场景。" | ||
| 122 | + }, | ||
| 123 | + { | ||
| 124 | + "title": "Appwrite Functions 编写规范", | ||
| 125 | + "subtitle": "Node.js 后端", | ||
| 126 | + "importance": "核心", | ||
| 127 | + "content": "<p>6 个云函数遵循 Appwrite Functions 编写规范。本案例中你将学到:</p><ul><li><strong>入口规范</strong>:ESM 默认导出入口</li><li><strong>SDK 版本</strong>:node-appwrite@^11.0.0(Node 16 兼容),Server SDK 用 API Key 读写数据库</li><li><strong>访问控制</strong>:<code>execute=[\"any\"]</code> 与统一 {code, data, msg} 返回</li><li><strong>函数分工</strong>:注册、登录、取词、学习记录、进度查询与初始化</li></ul>", | ||
| 128 | + "transferScene": "所有 Appwrite 后端场景。" | ||
| 129 | + }, | ||
| 130 | + { | ||
| 131 | + "title": "NoSQL 文档数据库设计", | ||
| 132 | + "subtitle": "文档型数据模型", | ||
| 133 | + "importance": "重要", | ||
| 134 | + "content": "<p>设计 3 个集合的文档型数据模型,Appwrite Database 内建 HTTP 路由。本案例中你将学到:</p><ul><li><strong>集合设计</strong>:users_col / words_col / learn_records_col</li><li><strong>文档型 NoSQL</strong>:属性定义、索引与查询方式</li><li><strong>与关系型差异</strong>:Appwrite Database 内建 HTTP 路由,省去独立 API 网关层</li></ul>", | ||
| 135 | + "transferScene": "所有 Appwrite Database 场景。" | ||
| 136 | + }, | ||
| 137 | + { | ||
| 138 | + "title": "自托管部署与排障", | ||
| 139 | + "subtitle": "运维能力", | ||
| 140 | + "importance": "重要", | ||
| 141 | + "content": "<p>自托管部署有一组实测排障要点。本案例中你将学到:</p><ul><li><strong>打包格式</strong>:tar.gz 打包上传(不是 .zip)</li><li><strong>运行时选择</strong>:node-16.0(先查 /v1/functions/runtimes)</li><li><strong>权限排障</strong>:API Key 权限验证,以及 execute 配置导致的 401</li><li><strong>自动打包</strong>:deploy.py 为每个函数自动 npm install --omit=dev 并打包</li></ul>", | ||
| 142 | + "transferScene": "所有自托管 Serverless 部署。" | ||
| 143 | + }, | ||
| 144 | + { | ||
| 145 | + "title": "端云联调验收", | ||
| 146 | + "subtitle": "工程闭环", | ||
| 147 | + "importance": "了解", | ||
| 148 | + "content": "<p>从确认服务器到端侧联调的完整流程。本案例中你将学到:</p><ul><li><strong>完整流程</strong>:确认服务器、编写云函数、部署、端侧联调</li><li><strong>二次解析</strong>:从 responseBody 二次解析返回数据</li><li><strong>验收原则</strong>:命令提交成功 ≠ 部署完成,需实际调用验证</li></ul>", | ||
| 149 | + "transferScene": "所有端云一体化项目交付。" | ||
| 150 | + } | ||
| 151 | + ] | ||
| 152 | + }, | ||
| 153 | + "practice": { | ||
| 154 | + "content": "practice.html" | ||
| 155 | + }, | ||
| 156 | + "architecture": { | ||
| 157 | + "archImage": "arch-diagram.svg", | ||
| 158 | + "description": "<p>应用采用端云一体化架构。</p><ul><li>端侧 ArkTS/ArkUI(4 个页面)通过 POST /v1/functions/{id}/executions 调用 Appwrite Function 执行端点。</li><li>Appwrite Function 用 Server SDK(API Key)读写 Appwrite Database。</li><li>数据流:ArkTS 页面 → Appwrite Functions 执行端点 → Appwrite Database → 结果包在 responseBody 返回。端侧不直接持有数据库凭据,Function 执行端点是唯一入口。</li></ul>" | ||
| 159 | + }, | ||
| 160 | + "preview": { | ||
| 161 | + "intro": "展示真实模拟器操作和应用实际运行的页面。三张横版合成图均为手机场景,不表示已经完成平板、折叠屏或穿戴设备适配。", | ||
| 162 | + "devices": [ | ||
| 163 | + { | ||
| 164 | + "deviceType": "phone", | ||
| 165 | + "deviceName": "手机", | ||
| 166 | + "previewImage": "preview-home.png", | ||
| 167 | + "description": "首页,显示今日单词列表与打卡状态。" | ||
| 168 | + }, | ||
| 169 | + { | ||
| 170 | + "deviceType": "phone", | ||
| 171 | + "deviceName": "手机", | ||
| 172 | + "previewImage": "preview-flashcard.png", | ||
| 173 | + "description": "闪卡模式,点击卡片翻转查看释义。" | ||
| 174 | + }, | ||
| 175 | + { | ||
| 176 | + "deviceType": "phone", | ||
| 177 | + "deviceName": "手机", | ||
| 178 | + "previewImage": "preview-test.png", | ||
| 179 | + "description": "看词选义测试,答对计入学习进度。" | ||
| 180 | + } | ||
| 181 | + ] | ||
| 182 | + }, | ||
| 183 | + "faqs": { | ||
| 184 | + "intro": "结合实际迭代和源码排查,优先检查可观察的行为。", | ||
| 185 | + "list": [ | ||
| 186 | + { | ||
| 187 | + "title": "运行时不支持", | ||
| 188 | + "question": "部署报 Runtime \"node-18.0\" is not supported?", | ||
| 189 | + "reason": "自托管 Appwrite 未启用 node-18.0 运行时,可用运行时取决于服务器配置。", | ||
| 190 | + "solution": "改为 node-16.0;先调用 GET /v1/functions/runtimes 查询可用运行时列表,选择服务器支持的版本。" | ||
| 191 | + }, | ||
| 192 | + { | ||
| 193 | + "title": "依赖找不到", | ||
| 194 | + "question": "运行报 Cannot find package 'node-appwrite'?", | ||
| 195 | + "reason": "依赖未打包进 tar.gz,Appwrite v1.9.0 不会自动安装依赖。", | ||
| 196 | + "solution": "每个函数目录执行 npm install --omit=dev 后重新打包 tar.gz;确认 node_modules/node-appwrite 在包内。" | ||
| 197 | + }, | ||
| 198 | + { | ||
| 199 | + "title": "文件类型不支持", | ||
| 200 | + "question": "上传报 storage_file_type_unsupported?", | ||
| 201 | + "reason": "Appwrite 只接受 .tar.gz 格式,不支持 .zip。", | ||
| 202 | + "solution": "打成 .tar.gz 格式上传;Windows 下用 7-Zip 或 PowerShell 的 tar 命令生成 tar.gz。" | ||
| 203 | + }, | ||
| 204 | + { | ||
| 205 | + "title": "前端调用 401", | ||
| 206 | + "question": "端侧调用函数返回 401?", | ||
| 207 | + "reason": "函数的 execute 不是公开执行,端侧无 API Key 无法调用。", | ||
| 208 | + "solution": "将函数 execute 设为 [\"any\"],允许公开执行;这是学习演示配置,生产环境应改为 [\"users\"] 并接入用户会话。" | ||
| 209 | + }, | ||
| 210 | + { | ||
| 211 | + "title": "API Key 权限不足", | ||
| 212 | + "question": "调用 GET /v1/functions 报 missing scopes?", | ||
| 213 | + "reason": "API Key 未包含所需权限 scopes。", | ||
| 214 | + "solution": "在 Appwrite 控制台重新创建 API Key,确保包含 functions.write/read、databases.write/read 权限;用 GET /v1/functions 验证权限。" | ||
| 215 | + }, | ||
| 216 | + { | ||
| 217 | + "title": "node-appwrite 版本不兼容", | ||
| 218 | + "question": "安装 node-appwrite 报 EBADENGINE?", | ||
| 219 | + "reason": "node-appwrite v14 要求 Node 18+,与自托管 node-16.0 运行时不兼容。", | ||
| 220 | + "solution": "降级到 node-appwrite@^11.0.0;在 package.json 中确认版本约束,重新 npm install。" | ||
| 221 | + } | ||
| 222 | + ] | ||
| 223 | + }, | ||
| 224 | + "scenarios": { | ||
| 225 | + "intro": "复用已实现的闪卡学习、连续打卡与 NoSQL 数据模型,替换业务场景后拓展。", | ||
| 226 | + "migrationPoints": [ | ||
| 227 | + "闪卡学习模式", | ||
| 228 | + "连续打卡逻辑", | ||
| 229 | + "NoSQL 数据模型" | ||
| 230 | + ], | ||
| 231 | + "list": [ | ||
| 232 | + { | ||
| 233 | + "title": "题库 / 备考应用", | ||
| 234 | + "desc": "复用闪卡 + 测试模式,扩展选择题、填空题、错题本等题型。", | ||
| 235 | + "skills": [ | ||
| 236 | + "arkui", | ||
| 237 | + "Appwrite" | ||
| 238 | + ], | ||
| 239 | + "migrationPoints": [ | ||
| 240 | + "复用闪卡翻转组件", | ||
| 241 | + "扩展题型数据模型", | ||
| 242 | + "沿用进度统计逻辑" | ||
| 243 | + ] | ||
| 244 | + }, | ||
| 245 | + { | ||
| 246 | + "title": "习惯打卡 / 任务管理", | ||
| 247 | + "desc": "基于连续打卡与进度统计逻辑,构建每日习惯养成应用。", | ||
| 248 | + "skills": [ | ||
| 249 | + "arkui", | ||
| 250 | + "Appwrite" | ||
| 251 | + ], | ||
| 252 | + "migrationPoints": [ | ||
| 253 | + "复用 streak 累计逻辑", | ||
| 254 | + "扩展任务集合", | ||
| 255 | + "沿用 NoSQL 数据模型" | ||
| 256 | + ] | ||
| 257 | + }, | ||
| 258 | + { | ||
| 259 | + "title": "课堂自托管 Serverless 实践", | ||
| 260 | + "desc": "以 Appwrite Functions 规范与自托管部署两项重点作为任务,要求学生提交可运行应用与联调验收记录。", | ||
| 261 | + "skills": [ | ||
| 262 | + "Appwrite Functions", | ||
| 263 | + "自托管部署", | ||
| 264 | + "端云联调" | ||
| 265 | + ], | ||
| 266 | + "migrationPoints": [ | ||
| 267 | + "分离端侧与云侧", | ||
| 268 | + "用联调验收替代截图", | ||
| 269 | + "把 401 排障作为部署练习" | ||
| 270 | + ] | ||
| 271 | + } | ||
| 272 | + ] | ||
| 273 | + }, | ||
| 274 | + "challenges": { | ||
| 275 | + "intro": "以下为未计入基础成果的拓展任务,完成后需要补充验证。", | ||
| 276 | + "list": [ | ||
| 277 | + { | ||
| 278 | + "title": "为 execute 接入正式鉴权", | ||
| 279 | + "difficulty": "中等", | ||
| 280 | + "kpTag": "Appwrite", | ||
| 281 | + "desc": "把 execute=[\"any\"] 改为 execute=[\"users\"],端侧接入用户会话。", | ||
| 282 | + "hint": "参考 Appwrite 的 Account / Session 鉴权机制;验证登录后端侧能正常调用函数,未登录时返回 401。" | ||
| 283 | + }, | ||
| 284 | + { | ||
| 285 | + "title": "为服务器配置 HTTPS 反代", | ||
| 286 | + "difficulty": "困难", | ||
| 287 | + "kpTag": "运维", | ||
| 288 | + "desc": "用 Nginx / Caddy 为自托管 Appwrite 配置 TLS 与证书。", | ||
| 289 | + "hint": "关注反向代理对 /v1 路径的转发与 WebSocket 支持;验证 HTTPS 下端侧调用正常。" | ||
| 290 | + }, | ||
| 291 | + { | ||
| 292 | + "title": "db-init 改为幂等可重复执行", | ||
| 293 | + "difficulty": "简单", | ||
| 294 | + "kpTag": "云函数", | ||
| 295 | + "desc": "让 db-init 重复执行不重复插入单词、不报错。", | ||
| 296 | + "hint": "参考现有 try/catch 跳过重复与 existence check 思路;验证重复调用后集合与种子数据不变。" | ||
| 297 | + } | ||
| 298 | + ] | ||
| 299 | + } | ||
| 300 | +} | ||
| @@ -0,0 +1,105 @@ | |||
| 1 | + | ||
| 2 | +<h2>快速体验(约 10 分钟)</h2> | ||
| 3 | +<blockquote>这条路线面向「先把现成案例跑起来」的读者,目标是约 1–2 小时(含一次性云环境准备)内看到第一个成功结果。原理和完整搭建过程见完整实践。</blockquote> | ||
| 4 | +<h3>0)用 AI 辅助配置云环境(可选)</h3> | ||
| 5 | +<p>想跳过下面的手动步骤?把本仓库 <code>README.md</code> 与 <code>SKILL.md</code> 交给 AI 编程助手(码道 / CodeArts、Cursor 等),让它读完自动配置并部署云环境:</p> | ||
| 6 | +<pre><code>请阅读本仓库 README.md 与 SKILL.md,帮我自动配置并部署「单词打卡」云环境: | ||
| 7 | +1. 按第 2 章「配置与敏感信息」列出需要我填写的占位符(Appwrite 地址、Project ID、API Key),等我填好 harmony_cloud/.env; | ||
| 8 | +2. 运行 deploy/deploy.py 部署 6 个云函数(自动 npm install 并打包 tar.gz); | ||
| 9 | +3. 调用 db-init 建集合与种子数据; | ||
| 10 | +4. 把服务器地址与 Project ID 填入 HttpUtil.ets,编译并安装端侧; | ||
| 11 | +5. 按第 5 章验收表逐项联调。 | ||
| 12 | +每完成一步报告结果;报错时贴完整日志并定位根因;API Key 只写本地 .env,不写入代码、不提交仓库。</code></pre> | ||
| 13 | +<blockquote><strong>注意</strong>:AI 只能操作你授权的环境。API Key 等敏感信息请只放在本地 <code>.env</code>(已被 <code>.gitignore</code> 忽略),不要让 AI 把凭据写进代码或提交。部署完成后,仍建议按下面各步骤的「完成标志」人工复核一遍。</blockquote> | ||
| 14 | +<h3>1)获取已验证版本</h3> | ||
| 15 | +<pre><code>git clone https://gitcode.com/harmony-practice-center/word-check-in.git | ||
| 16 | +cd word-check-in</code></pre> | ||
| 17 | +<p><strong>完成标志</strong>:根目录存在 <code>README.md</code>、<code>harmony_app/</code> 和 <code>harmony_cloud/</code>。</p> | ||
| 18 | +<h3>2)准备最小配置</h3> | ||
| 19 | +<pre><code>cp harmony_cloud/.env.example harmony_cloud/.env</code></pre> | ||
| 20 | +<ol><li>将 <code>.env</code> 中的 API Key、APPWRITE_HOST、Project ID 替换为第 2 章收集的值。</li><li>在 <code>harmony_app/entry/src/main/ets/utils/HttpUtil.ets</code> 填入 <code>APPWRITE_HOST</code> 和 <code>PROJECT_ID</code>。</li></ol> | ||
| 21 | +<p><strong>完成标志</strong>:没有未替换占位符;敏感配置不会出现在 <code>git status</code> 中。</p> | ||
| 22 | +<h3>3)启动云侧</h3> | ||
| 23 | +<pre><code>cd harmony_cloud | ||
| 24 | +python deploy/deploy.py | ||
| 25 | +curl -X POST http://<host>/v1/functions/db-init/executions \ | ||
| 26 | + -H "Content-Type: application/json" \ | ||
| 27 | + -H "X-Appwrite-Project: <project_id>" \ | ||
| 28 | + -d '{"body":"{}","async":false}'</code></pre> | ||
| 29 | +<blockquote><code>deploy.py</code> 会为每个函数自动执行 <code>npm install --omit=dev</code> 并打包依赖,无需手动安装 <code>node-appwrite</code>。</blockquote> | ||
| 30 | +<p><strong>完成标志</strong>:<code>db-init</code> 执行后,<code>word_db</code> 下 3 个集合、58 个单词和管理员 <code>admin@company.com</code> / <code>123456</code> 已创建。</p> | ||
| 31 | +<h3>4)构建并运行端侧</h3> | ||
| 32 | +<blockquote>首次编译前,先用 DevEco Studio 打开 <code>harmony_app</code> 目录,让它自动生成 <code>local.properties</code>(填入 <code>sdk.dir</code>);命令行编译还需设置环境变量 <code>DEVECO_SDK_HOME</code>。缺失会报 <code>00303217</code>,<code>sdk.dir</code> 被改写会报 <code>00308018</code>,路径含空格会报 <code>Space is not supported in HVIGOR_USER_HOME</code>。详见 SKILL.md 的「全量故障速查表」。</blockquote> | ||
| 33 | +<pre><code>Set-Location 'harmony_app' | ||
| 34 | +ohpm install | ||
| 35 | +hvigorw assembleHap --mode module -p module=entry -p product=default -p buildMode=debug --no-daemon | ||
| 36 | +hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap</code></pre> | ||
| 37 | +<p><strong>完成标志</strong>:生成 HAP 并安装到设备,应用能启动到登录页并完成一次登录。</p> | ||
| 38 | +<h2>完整实践</h2> | ||
| 39 | + | ||
| 40 | +<p>目标:复现一款 HarmonyOS 端侧 + Appwrite 自托管 Serverless 云侧的端云一体化单词打卡应用,理解闪卡学习、连续打卡与 NoSQL 文档数据库的核心实现。预计约 120 分钟,不含环境安装、首次下载与签名配置。</p> | ||
| 41 | +<h3>1. 确认 Appwrite 服务器与收集信息(约 15 分钟)</h3> | ||
| 42 | +<p>访问 /v1/health 确认服务器返回 pass。在控制台创建项目并记录 Project ID,创建含 functions.write/read、databases.write/read 权限的 API Key。调用 GET /v1/functions/runtimes 确认 node-16.0 可用。</p> | ||
| 43 | +<p>从 <a href="https://gitcode.com/harmony-practice-center/word-check-in">单词打卡公开仓库</a>获取源码:</p> | ||
| 44 | +<pre><code>git clone https://gitcode.com/harmony-practice-center/word-check-in.git | ||
| 45 | +cd word-check-in</code></pre> | ||
| 46 | +<p>敏感信息只放在本地 .env(已被 .gitignore 忽略),不写入代码、不提交仓库。客户端不得包含 API Key;函数用 Server SDK 在服务端持有。</p> | ||
| 47 | +<p><strong>验收:</strong>信息清单填齐,APPWRITE_HOST / Project ID / API Key 均可用;GET /v1/functions 正常返回。</p> | ||
| 48 | +<h3>2. 编写并打包 6 个云函数(约 25 分钟)</h3> | ||
| 49 | +<p>按 Appwrite Functions 编写规范实现 6 个函数:register、login、getDailyWords、learnWord、getProgress、db-init。</p> | ||
| 50 | +<details><summary>查看 Appwrite Functions 编写规范要点</summary><pre><code>// 1. ESM 默认导出:export default async ({ req, res, log, error }) => { ... } | ||
| 51 | +// 2. node-appwrite@^11.0.0(Node 16 兼容,v14 要求 Node 18+) | ||
| 52 | +// 3. Server SDK 用 API Key 读写数据库: | ||
| 53 | +// const client = new Client().setEndpoint(HOST).setProject(PROJECT).setKey(API_KEY) | ||
| 54 | +// const db = new Databases(client) | ||
| 55 | +// 4. execute=["any"] 允许公开执行(学习演示配置) | ||
| 56 | +// 5. 统一返回结构:{ code: 0, data: ..., msg: '...' } | ||
| 57 | +// 6. 打包格式:tar.gz(不是 zip),根目录含 main.js + node_modules</code></pre></details> | ||
| 58 | +<p>每个函数目录执行 npm install --omit=dev 安装依赖。Appwrite v1.9.0 不会自动安装依赖,必须手动打包进 tar.gz。</p> | ||
| 59 | +<p><strong>验收:</strong>6 个函数目录都含 main.js、package.json 和 node_modules/node-appwrite。</p> | ||
| 60 | +<h3>3. 部署云侧(约 25 分钟)</h3> | ||
| 61 | +<p>运行 deploy.py 为每个函数自动执行 npm install --omit=dev 并打包 tar.gz 上传。为每个函数配置 API_KEY、DATABASE_ID 环境变量并设 execute=["any"]。调用 db-init 建集合与种子数据。</p> | ||
| 62 | +<pre><code>cd harmony_cloud | ||
| 63 | +python deploy/deploy.py | ||
| 64 | +curl -X POST http://<host>/v1/functions/db-init/executions \ | ||
| 65 | + -H "Content-Type: application/json" \ | ||
| 66 | + -H "X-Appwrite-Project: <project_id>" \ | ||
| 67 | + -d '{"body":"{}","async":false}'</code></pre> | ||
| 68 | +<p>db-init 执行后创建 3 个集合(users_col / words_col / learn_records_col)、58 个单词和管理员 admin@company.com / 123456。</p> | ||
| 69 | +<p>关键理念:"命令提交成功 ≠ 部署完成"。deploy.py 执行成功后需实际调用函数验证返回 code:0。</p> | ||
| 70 | +<p><strong>验收:</strong>6 个函数调用全部返回 code:0;word_db 下 3 个集合与 58 个单词就绪;管理员可登录。</p> | ||
| 71 | +<h3>4. 配置、编译并安装端侧(约 20 分钟)</h3> | ||
| 72 | +<p>在 HttpUtil.ets 填入 APPWRITE_HOST(http:// 协议)和 PROJECT_ID,确认 functionId 映射与部署一致。配置 ohos.permission.INTERNET 权限。</p> | ||
| 73 | +<pre><code>cd harmony_app | ||
| 74 | +ohpm install | ||
| 75 | +hvigorw assembleHap --mode module -p module=entry -p product=default -p buildMode=debug --no-daemon</code></pre> | ||
| 76 | +<p>推荐在 DevEco Studio 直接点 Run(自动签名、安装、启动)。命令行需先签名,再用 hdc 安装:</p> | ||
| 77 | +<pre><code>hdc install <签名后 HAP> | ||
| 78 | +hdc shell aa start -a EntryAbility -b com.example.word_check_in</code></pre> | ||
| 79 | +<p><strong>验收:</strong>应用启动后显示登录页,无白屏;用 admin@company.com / 123456 可成功登录。</p> | ||
| 80 | +<h3>5. 端云联调验收(约 25 分钟)</h3> | ||
| 81 | +<p>按业务验收表逐项执行:</p> | ||
| 82 | +<ul> | ||
| 83 | +<li>登录 → 进入首页,显示今日打卡状态与连续天数</li> | ||
| 84 | +<li>查看今日单词列表 → 首页展示 10 个未学单词</li> | ||
| 85 | +<li>闪卡翻转 → 点击卡片翻转查看释义</li> | ||
| 86 | +<li>看词选义测试 → 答对计入学习进度,进度条更新</li> | ||
| 87 | +<li>学完一个单词 → 打卡状态变为已完成,连续天数累加</li> | ||
| 88 | +<li>重启应用 → 进度与打卡数据保留(云端持久化)</li> | ||
| 89 | +</ul> | ||
| 90 | +<p>重点验证从 responseBody 二次解析的进度与打卡数据正确。端侧调用返回的 JSON 包在 responseBody 字段内,需二次解析才能获取实际数据。</p> | ||
| 91 | +<p><strong>验收:</strong>所有业务验收项通过,进度与打卡数据正确,端云链路完整。</p> | ||
| 92 | +<h3>6. 关键算法固定样例复核(约 10 分钟)</h3> | ||
| 93 | +<p>连续打卡(streak)累加逻辑、学习进度计算等核心业务规则均可复现。用固定输入验证连续天数递增与重置逻辑、学习进度计算是否符合预期。</p> | ||
| 94 | +<p>固定样例见 docs/quickstart.md"关键算法固定样例"章节。截图只能证明界面状态,不能替代云函数逻辑与数据库验证;后者由固定样例与端云联调步骤验证。</p> | ||
| 95 | +<p><strong>验收:</strong>固定样例的输入输出与预期一致;连续打卡与进度计算逻辑可复现。</p> | ||
| 96 | +<h2>高频问题排查</h2><table><thead><tr><th>问题</th><th>优先检查</th><th>解决方向</th></tr></thead><tbody> | ||
| 97 | +<tr><td>Runtime "node-18.0" is not supported</td><td>服务器可用运行时列表</td><td>改为 node-16.0,先查 GET /v1/functions/runtimes。</td></tr> | ||
| 98 | +<tr><td>Cannot find package 'node-appwrite'</td><td>依赖是否打进 tar.gz</td><td>npm install --omit=dev 后重新打包 tar.gz。</td></tr> | ||
| 99 | +<tr><td>storage_file_type_unsupported</td><td>上传格式是否为 tar.gz</td><td>打成 .tar.gz 格式上传,不支持 .zip。</td></tr> | ||
| 100 | +<tr><td>前端调用 401</td><td>execute 是否为 ["any"]</td><td>将函数 execute 设为 ["any"] 允许公开执行。</td></tr> | ||
| 101 | +<tr><td>missing scopes</td><td>API Key 权限是否完整</td><td>重新创建含 functions/databases 权限的 API Key。</td></tr> | ||
| 102 | +<tr><td>EBADENGINE</td><td>node-appwrite 版本是否兼容 Node 16</td><td>降级到 node-appwrite@^11.0.0。</td></tr> | ||
| 103 | +</tbody></table> | ||
| 104 | +<h2>可选拓展</h2><p>为 execute 接入正式鉴权(改为 ["users"] + 用户会话);为服务器配置 HTTPS 反代(Nginx/Caddy + TLS);db-init 改为幂等可重复执行。以上拓展不包含在基础成果声明中,完成后重新验证功能与安全性。</p> | ||
| 105 | +<h2>交付与授权</h2><p>交付可运行工程、6 个云函数 tar.gz、端云联调验收记录与关键算法固定样例复核结果。应用代码按 MIT 许可;第三方依赖按各自许可处理。本案例聚焦自托管 Serverless 学习,基础实现不包含正式鉴权、限流或 HTTPS 配置。</p> | ||
| @@ -5,8 +5,9 @@ | |||
| 5 | { | 5 | { |
| 6 | "name": "default", | 6 | "name": "default", |
| 7 | "signingConfig": "default", | 7 | "signingConfig": "default", |
| 8 | - "targetSdkVersion": "6.1.1(24)", | 8 | + "compileSdkVersion": "6.1.1(24)", |
| 9 | - "compatibleSdkVersion": "6.1.0(23)", | 9 | + "targetSdkVersion": "6.1.0(23)", |
| 10 | + "compatibleSdkVersion": "6.0.0(20)", | ||
| 10 | "runtimeOS": "HarmonyOS", | 11 | "runtimeOS": "HarmonyOS", |
| 11 | "buildOption": { | 12 | "buildOption": { |
| 12 | "strictMode": { | 13 | "strictMode": { |
Mharmony_app/entry/src/main/ets/entryability/EntryAbility.ets+70-18File content is under review, please try again later
| @@ -1,3 +1,3 @@ | |||
| 1 | API_KEY=<your_appwrite_api_key> | 1 | API_KEY=<your_appwrite_api_key> |
| 2 | -URL=http://<your_appwrite_host> | 2 | +APPWRITE_HOST=http://<your_appwrite_host> |
| 3 | PROJECT_ID=<your_project_id> | 3 | PROJECT_ID=<your_project_id> |
| @@ -1,4 +1,6 @@ | |||
| 1 | -# demo007 部署指南(Appwrite Functions 方案) | 1 | +# word-check-in 部署指南(Appwrite Functions 方案) |
| 2 | + | ||
| 3 | +> **本文为辅助文档,一切以主 [README.md](../README.md) 为准**。若与主 README 冲突,以主 README 为准;本文仅补充手动/自动部署的操作细节。 | ||
| 2 | 4 | ||
| 3 | ## 架构 | 5 | ## 架构 |
| 4 | 6 | ||
| @@ -31,7 +33,7 @@ | |||
| 31 | 33 | ||
| 32 | ### 步骤 1:进入 Appwrite 控制台 | 34 | ### 步骤 1:进入 Appwrite 控制台 |
| 33 | 35 | ||
| 34 | -浏览器打开 `https://XXX.XXX.XXX.XXX`,登录进入你的 Project。 | 36 | +浏览器打开 `http://XXX.XXX.XXX.XXX`,登录进入你的 Project。 |
| 35 | 37 | ||
| 36 | ### 步骤 2:创建数据库和集合 | 38 | ### 步骤 2:创建数据库和集合 |
| 37 | 39 | ||
| @@ -68,37 +70,29 @@ | |||
| 68 | 1. **Functions** → **Create Function** | 70 | 1. **Functions** → **Create Function** |
| 69 | 2. 填写: | 71 | 2. 填写: |
| 70 | - **Name**: 函数名(如 `register`) | 72 | - **Name**: 函数名(如 `register`) |
| 71 | - - **Runtime**: Node.js 18 | 73 | + - **Runtime**: Node.js 16.0 |
| 72 | - **Execute access**: `All users` 或 `Any`(允许公开调用) | 74 | - **Execute access**: `All users` 或 `Any`(允许公开调用) |
| 73 | 3. 创建后,进入 **Settings** → **Environment Variables**,添加: | 75 | 3. 创建后,进入 **Settings** → **Environment Variables**,添加: |
| 74 | - `API_KEY` = 你的 Server API Key | 76 | - `API_KEY` = 你的 Server API Key |
| 75 | - `DATABASE_ID` = `word_db` | 77 | - `DATABASE_ID` = `word_db` |
| 76 | 4. **Deployments** → **Create Deployment** | 78 | 4. **Deployments** → **Create Deployment** |
| 77 | - - 上传 `functions/{函数名}/` 目录下的 `main.js` + `package.json` 打包的 zip | 79 | + - 上传 `functions/{函数名}/` 目录下的 `main.js` + `package.json` 打包的 tar.gz |
| 78 | - **Entrypoint**: `main.js` | 80 | - **Entrypoint**: `main.js` |
| 79 | - **Activate**: 勾选 | 81 | - **Activate**: 勾选 |
| 80 | 82 | ||
| 81 | ### 步骤 4:配置前端 | 83 | ### 步骤 4:配置前端 |
| 82 | 84 | ||
| 83 | -部署完成后,将每个函数的 ID 填入前端: | 85 | +端侧 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets` 已内置短名映射(`getFunctionId()` 直接返回 `register` / `login` / `db-init` / `getDailyWords` / `getProgress` / `learnWord`)。因此**函数的 functionId 必须与这些短名一致**: |
| 84 | 86 | ||
| 85 | -编辑 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets`: | 87 | +- 自动部署脚本会以短名作为 functionId,无需额外处理。 |
| 88 | +- 手动在控制台创建函数时,需把函数 ID 设为对应短名;若控制台自动生成了随机 ID,则需同步修改 `HttpUtil.ets` 的 `getFunctionId()`。 | ||
| 86 | 89 | ||
| 87 | -```typescript | 90 | +此外还需在 `HttpUtil.ets` 顶部填入 `APPWRITE_HOST`(`http://` 协议)和 `PROJECT_ID`。 |
| 88 | -const FN: Record<string, string> = { | ||
| 89 | - register: 'abc123...', // ← 替换为实际 ID | ||
| 90 | - login: 'def456...', | ||
| 91 | - dbInit: 'ghi789...', | ||
| 92 | - getDailyWords: 'jkl012...', | ||
| 93 | - getProgress: 'mno345...', | ||
| 94 | - learnWord: 'pqr678...', | ||
| 95 | -}; | ||
| 96 | -``` | ||
| 97 | 91 | ||
| 98 | ### 步骤 5:初始化数据库 | 92 | ### 步骤 5:初始化数据库 |
| 99 | 93 | ||
| 100 | ```bash | 94 | ```bash |
| 101 | -curl -X POST https://XXX.XXX.XXX.XXX/v1/functions/{dbInit的函数ID}/executions \ | 95 | +curl -X POST http://XXX.XXX.XXX.XXX/v1/functions/{dbInit的函数ID}/executions \ |
| 102 | -H "Content-Type: application/json" \ | 96 | -H "Content-Type: application/json" \ |
| 103 | -H "X-Appwrite-Project: <project_id>" \ | 97 | -H "X-Appwrite-Project: <project_id>" \ |
| 104 | -d '{"body":"{}","async":false}' | 98 | -d '{"body":"{}","async":false}' |
| @@ -120,11 +114,11 @@ python deploy/deploy.py # 完整部署 | |||
| 120 | python deploy/deploy.py --check # 仅检查连通性 | 114 | python deploy/deploy.py --check # 仅检查连通性 |
| 121 | ``` | 115 | ``` |
| 122 | 116 | ||
| 123 | -**需要的环境变量**(自动从 `.env.example` 读取): | 117 | +**需要的环境变量**(优先读取 `.env`,不存在则回退 `.env.example`): |
| 124 | | 变量 | 说明 | | 118 | | 变量 | 说明 | |
| 125 | |------|------| | 119 | |------|------| |
| 126 | | `API_KEY` | Appwrite Server API Key | | 120 | | `API_KEY` | Appwrite Server API Key | |
| 127 | -| `URL` | Appwrite 服务器地址 | | 121 | +| `APPWRITE_HOST` | Appwrite 服务器地址 | |
| 128 | | `PROJECT_ID` | Appwrite Project ID | | 122 | | `PROJECT_ID` | Appwrite Project ID | |
| 129 | 123 | ||
| 130 | ### 方式 B: Node.js 脚本 | 124 | ### 方式 B: Node.js 脚本 |
| @@ -136,21 +130,21 @@ node deploy.js # 完整部署 | |||
| 136 | node deploy.js --check # 仅检查连通性 | 130 | node deploy.js --check # 仅检查连通性 |
| 137 | ``` | 131 | ``` |
| 138 | 132 | ||
| 139 | -需要 Node.js 18+ 环境。依赖会自动通过 `npm install` 安装。 | 133 | +`deploy.js` 会为每个函数自动执行 `npm install --omit=dev` 并打包成 `.tar.gz`,函数运行时同样为 `node-16.0`;脚本本身依赖 node-appwrite v14,本地运行需 Node.js 18+。 |
| 140 | 134 | ||
| 141 | ### 部署成功后的输出示例 | 135 | ### 部署成功后的输出示例 |
| 142 | 136 | ||
| 143 | ``` | 137 | ``` |
| 144 | ══════════════════════════════════ | 138 | ══════════════════════════════════ |
| 145 | - 函数 ID 映射 (复制到 HttpUtil.ets) | 139 | + 函数 ID(functionId 即短名) |
| 146 | ══════════════════════════════════ | 140 | ══════════════════════════════════ |
| 147 | 141 | ||
| 148 | - db-init : '6701a3b5c8f2e9d1a4b6', | 142 | + db-init : 'db-init', |
| 149 | - register : '6701a3b5c8f2e9d1a4b7', | 143 | + register : 'register', |
| 150 | - login : '6701a3b5c8f2e9d1a4b8', | 144 | + login : 'login', |
| 151 | - getDailyWords : '6701a3b5c8f2e9d1a4b9', | 145 | + getDailyWords : 'getDailyWords', |
| 152 | - getProgress : '6701a3b5c8f2e9d1a4c0', | 146 | + getProgress : 'getProgress', |
| 153 | - learnWord : '6701a3b5c8f2e9d1a4c1', | 147 | + learnWord : 'learnWord', |
| 154 | 148 | ||
| 155 | ✅ 部署完成! 6/6 个函数就绪 | 149 | ✅ 部署完成! 6/6 个函数就绪 |
| 156 | ``` | 150 | ``` |
| @@ -162,7 +156,7 @@ node deploy.js --check # 仅检查连通性 | |||
| 162 | 所有函数通过统一的执行端点调用: | 156 | 所有函数通过统一的执行端点调用: |
| 163 | 157 | ||
| 164 | ``` | 158 | ``` |
| 165 | -POST https://XXX.XXX.XXX.XXX/v1/functions/{functionId}/executions | 159 | +POST http://XXX.XXX.XXX.XXX/v1/functions/{functionId}/executions |
| 166 | Headers: | 160 | Headers: |
| 167 | X-Appwrite-Project: <project_id> | 161 | X-Appwrite-Project: <project_id> |
| 168 | Content-Type: application/json | 162 | Content-Type: application/json |
| @@ -193,7 +187,7 @@ Body: | |||
| 193 | 187 | ||
| 194 | ``` | 188 | ``` |
| 195 | API_KEY=<your_api_key> | 189 | API_KEY=<your_api_key> |
| 196 | -URL=http://<your_appwrite_host> | 190 | +APPWRITE_HOST=http://<your_appwrite_host> |
| 197 | PROJECT_ID=<your_project_id> | 191 | PROJECT_ID=<your_project_id> |
| 198 | ``` | 192 | ``` |
| 199 | 193 | ||
| @@ -4,9 +4,9 @@ | |||
| 4 | * 用途: 使用 Appwrite Server SDK 创建、配置、部署 6 个云函数 | 4 | * 用途: 使用 Appwrite Server SDK 创建、配置、部署 6 个云函数 |
| 5 | * | 5 | * |
| 6 | * 需要的环境变量 (从 ../.env.example 读取): | 6 | * 需要的环境变量 (从 ../.env.example 读取): |
| 7 | - * API_KEY - Appwrite Server API Key | 7 | + * API_KEY - Appwrite Server API Key |
| 8 | - * URL - Appwrite 服务器地址 | 8 | + * APPWRITE_HOST - Appwrite 服务器地址 |
| 9 | - * PROJECT_ID - Appwrite Project ID | 9 | + * PROJECT_ID - Appwrite Project ID |
| 10 | * | 10 | * |
| 11 | * 用法: | 11 | * 用法: |
| 12 | * cd deploy && npm install && node deploy.js | 12 | * cd deploy && npm install && node deploy.js |
| @@ -42,13 +42,13 @@ const envPath = existsSync(resolve(ROOT, '.env')) | |||
| 42 | : resolve(ROOT, '.env.example'); | 42 | : resolve(ROOT, '.env.example'); |
| 43 | const env = loadEnv(envPath); | 43 | const env = loadEnv(envPath); |
| 44 | 44 | ||
| 45 | -if (!env.API_KEY || !env.URL || !env.PROJECT_ID) { | 45 | +if (!env.API_KEY || !env.APPWRITE_HOST || !env.PROJECT_ID) { |
| 46 | - console.error('❌ 缺少环境变量: API_KEY, URL, PROJECT_ID'); | 46 | + console.error('❌ 缺少环境变量: API_KEY, APPWRITE_HOST, PROJECT_ID'); |
| 47 | process.exit(1); | 47 | process.exit(1); |
| 48 | } | 48 | } |
| 49 | 49 | ||
| 50 | const client = new Client() | 50 | const client = new Client() |
| 51 | - .setEndpoint(env.URL) | 51 | + .setEndpoint(env.APPWRITE_HOST) |
| 52 | .setProject(env.PROJECT_ID) | 52 | .setProject(env.PROJECT_ID) |
| 53 | .setKey(env.API_KEY); | 53 | .setKey(env.API_KEY); |
| 54 | 54 | ||
| @@ -70,7 +70,7 @@ const COMMON_VARS = [ | |||
| 70 | ]; | 70 | ]; |
| 71 | 71 | ||
| 72 | async function checkConnection() { | 72 | async function checkConnection() { |
| 73 | - console.log(`🔍 检查 Appwrite 服务: ${env.URL}`); | 73 | + console.log(`🔍 检查 Appwrite 服务: ${env.APPWRITE_HOST}`); |
| 74 | try { | 74 | try { |
| 75 | // 简单的健康检查 — 尝试列出函数 | 75 | // 简单的健康检查 — 尝试列出函数 |
| 76 | await functions.list(); | 76 | await functions.list(); |
| @@ -83,28 +83,32 @@ async function checkConnection() { | |||
| 83 | 83 | ||
| 84 | async function deployFunction(name, description) { | 84 | async function deployFunction(name, description) { |
| 85 | const funcDir = resolve(FUNC_DIR, name); | 85 | const funcDir = resolve(FUNC_DIR, name); |
| 86 | - const zipPath = resolve(DIST_DIR, `${name}.zip`); | 86 | + const tarPath = resolve(DIST_DIR, `${name}.tar.gz`); |
| 87 | 87 | ||
| 88 | if (!existsSync(funcDir)) { | 88 | if (!existsSync(funcDir)) { |
| 89 | console.error(` ❌ 函数目录不存在: ${funcDir}`); | 89 | console.error(` ❌ 函数目录不存在: ${funcDir}`); |
| 90 | return null; | 90 | return null; |
| 91 | } | 91 | } |
| 92 | 92 | ||
| 93 | - // 1. 打包 zip | 93 | + // 0. 安装生产依赖 (node-appwrite) |
| 94 | - if (!existsSync(DIST_DIR)) mkdirSync(DIST_DIR, { recursive: true }); | ||
| 95 | try { | 94 | try { |
| 96 | - execSync(`cd "${funcDir}" && zip -r "${zipPath}" .`, { stdio: 'pipe' }); | 95 | + console.log(` 安装依赖: npm install --omit=dev`); |
| 97 | - } catch { | 96 | + execSync('npm install --omit=dev', { cwd: funcDir, stdio: 'pipe' }); |
| 98 | - // Windows fallback: use PowerShell | 97 | + } catch (e) { |
| 99 | - try { | 98 | + console.error(` ⚠️ 依赖安装失败: ${e.message}`); |
| 100 | - execSync(`powershell -Command "Compress-Archive -Path '${funcDir}\\*' -DestinationPath '${zipPath}' -Force"`, { stdio: 'pipe' }); | 99 | + return null; |
| 101 | - } catch (e2) { | ||
| 102 | - console.error(` ❌ 打包失败: ${e2.message}`); | ||
| 103 | - return null; | ||
| 104 | - } | ||
| 105 | } | 100 | } |
| 106 | 101 | ||
| 107 | - console.log(` 代码已打包: ${zipPath}`); | 102 | + // 1. 打包 tar.gz(Appwrite v1.9.0 只接受 .tar.gz) |
| 103 | + if (!existsSync(DIST_DIR)) mkdirSync(DIST_DIR, { recursive: true }); | ||
| 104 | + try { | ||
| 105 | + execSync(`tar -czf "${tarPath}" -C "${funcDir}" .`, { stdio: 'pipe' }); | ||
| 106 | + } catch (e) { | ||
| 107 | + console.error(` ❌ 打包失败: ${e.message}`); | ||
| 108 | + return null; | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + console.log(` 代码已打包: ${tarPath}`); | ||
| 108 | 112 | ||
| 109 | // 2. 创建函数 (execute=["any"] 允许公开调用) | 113 | // 2. 创建函数 (execute=["any"] 允许公开调用) |
| 110 | let fn; | 114 | let fn; |
| @@ -112,7 +116,7 @@ async function deployFunction(name, description) { | |||
| 112 | fn = await functions.create( | 116 | fn = await functions.create( |
| 113 | name, // functionId (slug) | 117 | name, // functionId (slug) |
| 114 | description, // 函数名称 | 118 | description, // 函数名称 |
| 115 | - 'node-18.0', // runtime | 119 | + 'node-16.0', // runtime |
| 116 | ['any'], // execute 权限 (公开访问) | 120 | ['any'], // execute 权限 (公开访问) |
| 117 | [], // events | 121 | [], // events |
| 118 | '', // schedule | 122 | '', // schedule |
| @@ -143,7 +147,7 @@ async function deployFunction(name, description) { | |||
| 143 | 147 | ||
| 144 | // 4. 部署代码 | 148 | // 4. 部署代码 |
| 145 | try { | 149 | try { |
| 146 | - const code = InputFile.fromPath(zipPath, `${name}.zip`); | 150 | + const code = InputFile.fromPath(tarPath, `${name}.tar.gz`); |
| 147 | await functions.createDeployment(fn.$id, code, true, 'main.js'); | 151 | await functions.createDeployment(fn.$id, code, true, 'main.js'); |
| 148 | console.log(` 代码已部署 ✅`); | 152 | console.log(` 代码已部署 ✅`); |
| 149 | } catch (e) { | 153 | } catch (e) { |
| @@ -185,7 +189,7 @@ async function main() { | |||
| 185 | } | 189 | } |
| 186 | 190 | ||
| 187 | console.log(`\n✅ 部署完成! ${Object.keys(ids).length}/${FUNCTIONS.length} 个函数就绪`); | 191 | console.log(`\n✅ 部署完成! ${Object.keys(ids).length}/${FUNCTIONS.length} 个函数就绪`); |
| 188 | - console.log(` 函数 URL: ${env.URL}/v1/functions/{functionId}/executions\n`); | 192 | + console.log(` 函数 URL: ${env.APPWRITE_HOST}/v1/functions/{functionId}/executions\n`); |
| 189 | } | 193 | } |
| 190 | 194 | ||
| 191 | main().catch(e => { console.error(e); process.exit(1); }); | 195 | main().catch(e => { console.error(e); process.exit(1); }); |
| @@ -1,7 +1,7 @@ | |||
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | # -*- coding: utf-8 -*- | 2 | # -*- coding: utf-8 -*- |
| 3 | """ | 3 | """ |
| 4 | -demo007 Appwrite Functions automated deployment script | 4 | +Appwrite Functions automated deployment script |
| 5 | 5 | ||
| 6 | Usage: | 6 | Usage: |
| 7 | python deploy.py # Full deployment | 7 | python deploy.py # Full deployment |
| @@ -16,6 +16,8 @@ import tarfile | |||
| 16 | import io | 16 | import io |
| 17 | import urllib.request | 17 | import urllib.request |
| 18 | import urllib.error | 18 | import urllib.error |
| 19 | +import shutil | ||
| 20 | +import subprocess | ||
| 19 | from pathlib import Path | 21 | from pathlib import Path |
| 20 | 22 | ||
| 21 | ROOT = Path(__file__).resolve().parent.parent | 23 | ROOT = Path(__file__).resolve().parent.parent |
| @@ -54,7 +56,7 @@ def load_env(path): | |||
| 54 | 56 | ||
| 55 | 57 | ||
| 56 | def api_request(env, method, path, body=None, headers=None): | 58 | def api_request(env, method, path, body=None, headers=None): |
| 57 | - url = env["URL"].rstrip("/") + path | 59 | + url = env["APPWRITE_HOST"].rstrip("/") + path |
| 58 | req_headers = { | 60 | req_headers = { |
| 59 | "X-Appwrite-Project": env["PROJECT_ID"], | 61 | "X-Appwrite-Project": env["PROJECT_ID"], |
| 60 | "X-Appwrite-Key": env["API_KEY"], | 62 | "X-Appwrite-Key": env["API_KEY"], |
| @@ -77,7 +79,7 @@ def api_request(env, method, path, body=None, headers=None): | |||
| 77 | 79 | ||
| 78 | 80 | ||
| 79 | def check_connection(env): | 81 | def check_connection(env): |
| 80 | - url = env["URL"].rstrip("/") | 82 | + url = env["APPWRITE_HOST"].rstrip("/") |
| 81 | print(f"\n[Check] Appwrite: {url}") | 83 | print(f"\n[Check] Appwrite: {url}") |
| 82 | 84 | ||
| 83 | # Health check (needs auth headers) | 85 | # Health check (needs auth headers) |
| @@ -108,7 +110,30 @@ def check_connection(env): | |||
| 108 | return True | 110 | return True |
| 109 | 111 | ||
| 110 | 112 | ||
| 113 | +def install_dependencies(func_name): | ||
| 114 | + """Install production dependencies so node-appwrite is bundled into the package.""" | ||
| 115 | + func_path = FUNC_DIR / func_name | ||
| 116 | + if not (func_path / "package.json").exists(): | ||
| 117 | + return | ||
| 118 | + if shutil.which("npm") is None: | ||
| 119 | + print(f" [WARN] npm not found; dependencies will NOT be bundled") | ||
| 120 | + return | ||
| 121 | + print(f" Installing dependencies (npm install --omit=dev)...") | ||
| 122 | + try: | ||
| 123 | + subprocess.run( | ||
| 124 | + "npm install --omit=dev", | ||
| 125 | + cwd=str(func_path), | ||
| 126 | + shell=True, | ||
| 127 | + check=True, | ||
| 128 | + capture_output=True, | ||
| 129 | + text=True, | ||
| 130 | + ) | ||
| 131 | + except subprocess.CalledProcessError as e: | ||
| 132 | + print(f" [WARN] npm install failed: {(e.stderr or e.stdout or '')[:300]}") | ||
| 133 | + | ||
| 134 | + | ||
| 111 | def create_tar_gz(func_name): | 135 | def create_tar_gz(func_name): |
| 136 | + install_dependencies(func_name) | ||
| 112 | func_path = FUNC_DIR / func_name | 137 | func_path = FUNC_DIR / func_name |
| 113 | tgz_path = DIST_DIR / f"{func_name}.tar.gz" | 138 | tgz_path = DIST_DIR / f"{func_name}.tar.gz" |
| 114 | DIST_DIR.mkdir(parents=True, exist_ok=True) | 139 | DIST_DIR.mkdir(parents=True, exist_ok=True) |
| @@ -167,8 +192,8 @@ def deploy_function(env, func_name, description): | |||
| 167 | print(f" Package: {os.path.basename(tgz_path)}") | 192 | print(f" Package: {os.path.basename(tgz_path)}") |
| 168 | 193 | ||
| 169 | # 4. Deploy code (multipart/form-data) | 194 | # 4. Deploy code (multipart/form-data) |
| 170 | - deploy_url = f"{env['URL'].rstrip('/')}/v1/functions/{function_id}/deployments" | 195 | + deploy_url = f"{env['APPWRITE_HOST'].rstrip('/')}/v1/functions/{function_id}/deployments" |
| 171 | - boundary = "---ZdxDeployBoundary2024" | 196 | + boundary = "---DeployBoundary2024" |
| 172 | entrypoint = "main.js" | 197 | entrypoint = "main.js" |
| 173 | 198 | ||
| 174 | # Build multipart body manually (no external deps needed) | 199 | # Build multipart body manually (no external deps needed) |
| @@ -219,7 +244,7 @@ def main(): | |||
| 219 | args = parser.parse_args() | 244 | args = parser.parse_args() |
| 220 | 245 | ||
| 221 | print("=" * 50) | 246 | print("=" * 50) |
| 222 | - print("demo007 - Appwrite Functions Deploy") | 247 | + print("Appwrite Functions Deploy") |
| 223 | print("=" * 50) | 248 | print("=" * 50) |
| 224 | 249 | ||
| 225 | if not ENV_FILE.exists(): | 250 | if not ENV_FILE.exists(): |
| @@ -227,7 +252,7 @@ def main(): | |||
| 227 | sys.exit(1) | 252 | sys.exit(1) |
| 228 | 253 | ||
| 229 | env = load_env(ENV_FILE) | 254 | env = load_env(ENV_FILE) |
| 230 | - print(f"Config: URL={env.get('URL')}, PROJECT={env.get('PROJECT_ID')}") | 255 | + print(f"Config: APPWRITE_HOST={env.get('APPWRITE_HOST')}, PROJECT={env.get('PROJECT_ID')}") |
| 231 | 256 | ||
| 232 | if not check_connection(env): | 257 | if not check_connection(env): |
| 233 | print("\n[FAIL] Appwrite unreachable, aborting") | 258 | print("\n[FAIL] Appwrite unreachable, aborting") |
| @@ -1,5 +1,5 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-deploy", | 2 | + "name": "word-deploy", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "private": true, | 4 | "private": true, |
| 5 | "dependencies": { | 5 | "dependencies": { |
| @@ -1,11 +1,11 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-db-init", | 2 | + "name": "word-db-init", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "lockfileVersion": 3, | 4 | "lockfileVersion": 3, |
| 5 | "requires": true, | 5 | "requires": true, |
| 6 | "packages": { | 6 | "packages": { |
| 7 | "": { | 7 | "": { |
| 8 | - "name": "zdx-word-db-init", | 8 | + "name": "word-db-init", |
| 9 | "version": "1.0.0", | 9 | "version": "1.0.0", |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | "node-appwrite": "^11.0.0" | 11 | "node-appwrite": "^11.0.0" |
| @@ -1,5 +1,5 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-db-init", | 2 | + "name": "word-db-init", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "dependencies": { | 5 | "dependencies": { |
| @@ -1,11 +1,11 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-getDailyWords", | 2 | + "name": "word-getDailyWords", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "lockfileVersion": 3, | 4 | "lockfileVersion": 3, |
| 5 | "requires": true, | 5 | "requires": true, |
| 6 | "packages": { | 6 | "packages": { |
| 7 | "": { | 7 | "": { |
| 8 | - "name": "zdx-word-getDailyWords", | 8 | + "name": "word-getDailyWords", |
| 9 | "version": "1.0.0", | 9 | "version": "1.0.0", |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | "node-appwrite": "^11.0.0" | 11 | "node-appwrite": "^11.0.0" |
| @@ -1,5 +1,5 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-getDailyWords", | 2 | + "name": "word-getDailyWords", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "dependencies": { | 5 | "dependencies": { |
| @@ -1,11 +1,11 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-getProgress", | 2 | + "name": "word-getProgress", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "lockfileVersion": 3, | 4 | "lockfileVersion": 3, |
| 5 | "requires": true, | 5 | "requires": true, |
| 6 | "packages": { | 6 | "packages": { |
| 7 | "": { | 7 | "": { |
| 8 | - "name": "zdx-word-getProgress", | 8 | + "name": "word-getProgress", |
| 9 | "version": "1.0.0", | 9 | "version": "1.0.0", |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | "node-appwrite": "^11.0.0" | 11 | "node-appwrite": "^11.0.0" |
| @@ -1,5 +1,5 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-getProgress", | 2 | + "name": "word-getProgress", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "dependencies": { | 5 | "dependencies": { |
| @@ -1,11 +1,11 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-learnWord", | 2 | + "name": "word-learnWord", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "lockfileVersion": 3, | 4 | "lockfileVersion": 3, |
| 5 | "requires": true, | 5 | "requires": true, |
| 6 | "packages": { | 6 | "packages": { |
| 7 | "": { | 7 | "": { |
| 8 | - "name": "zdx-word-learnWord", | 8 | + "name": "word-learnWord", |
| 9 | "version": "1.0.0", | 9 | "version": "1.0.0", |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | "node-appwrite": "^11.0.0" | 11 | "node-appwrite": "^11.0.0" |
| @@ -1,5 +1,5 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-learnWord", | 2 | + "name": "word-learnWord", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "dependencies": { | 5 | "dependencies": { |
| @@ -1,11 +1,11 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-login", | 2 | + "name": "word-login", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "lockfileVersion": 3, | 4 | "lockfileVersion": 3, |
| 5 | "requires": true, | 5 | "requires": true, |
| 6 | "packages": { | 6 | "packages": { |
| 7 | "": { | 7 | "": { |
| 8 | - "name": "zdx-word-login", | 8 | + "name": "word-login", |
| 9 | "version": "1.0.0", | 9 | "version": "1.0.0", |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | "node-appwrite": "^11.0.0" | 11 | "node-appwrite": "^11.0.0" |
| @@ -1,5 +1,5 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-login", | 2 | + "name": "word-login", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "dependencies": { | 5 | "dependencies": { |
| @@ -1,11 +1,11 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-register", | 2 | + "name": "word-register", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "lockfileVersion": 3, | 4 | "lockfileVersion": 3, |
| 5 | "requires": true, | 5 | "requires": true, |
| 6 | "packages": { | 6 | "packages": { |
| 7 | "": { | 7 | "": { |
| 8 | - "name": "zdx-word-register", | 8 | + "name": "word-register", |
| 9 | "version": "1.0.0", | 9 | "version": "1.0.0", |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | "node-appwrite": "^11.0.0" | 11 | "node-appwrite": "^11.0.0" |
| @@ -1,5 +1,5 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "zdx-word-register", | 2 | + "name": "word-register", |
| 3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "dependencies": { | 5 | "dependencies": { |