Open
refactor(app): HttpUtil 映射配置化 + 配置校验 + 网络重试 (#4) #6
refactor(app): HttpUtil 映射配置化 + 配置校验 + 网络重试 (#4) #6
Open
雅皮士Add on 9 days ago
Total: 32 files changed+720-130
@@ -13,3 +13,7 @@ dist/
13 13 
14# 日志14# 日志
15*.log15*.log
16+ 
17+# Python
18+__pycache__/
19+*.py[cod]
ALICENSE+21-0
@@ -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.
MREADME.md+80-6
@@ -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```bash120```bash
104-git clone <repo-url>121+git clone https://gitcode.com/harmony-practice-center/word-check-in.git
105cd word-check-in122cd word-check-in
106```123```
107 124 
@@ -113,7 +130,7 @@ cd word-check-in
113cp harmony_cloud/.env.example harmony_cloud/.env130cp 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 章收集的值。
1172. 在 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets` 填入 `APPWRITE_HOST` 和 `PROJECT_ID`。1342. 在 `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```powershell157```powershell
137Set-Location 'harmony_app'158Set-Location 'harmony_app'
138ohpm install159ohpm 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 
1571. 访问 `/v1/health` 确认服务器 `pass`。2071. 访问 `/v1/health` 确认服务器 `pass`。
@@ -159,7 +209,7 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap
1593. 创建 API Key(含 functions.write/read、databases.write/read 权限)。2093. 创建 API Key(含 functions.write/read、databases.write/read 权限)。
1604. 调用 `GET /v1/functions/runtimes` 确认可用运行时(本例 `node-16.0`)。2104. 调用 `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 
1741. 按 [SKILL.md](./SKILL.md) 的「云函数编写规范」与「代码模板」编写 6 个函数。2301. 按 [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 
1891. 选择一条路线:**手动**在控制台逐项创建,或**自动**用 Python 脚本(见 SKILL 的「部署与打包」)。2511. 选择一条路线:**手动**在控制台逐项创建,或**自动**用 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 
2061. 在 `HttpUtil.ets` 填入 `APPWRITE_HOST`(`http://` 协议)和 `PROJECT_ID`,确认 functionId 映射与部署一致。2741. 在 `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 
3181. **密码 PBKDF2-SHA256 加盐哈希**(已实现):`register` 存 `password: hashPassword(password, salt)` + `salt`;`login` 用 `user.salt` 重算比对;`db-init` 管理员密码同样哈希。3181. **密码 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`。
3203. **`execute=["any"]` 是无鉴权公开访问**:`db-init` 也公开可调,任何人可清库重建。生产应改为 `execute=["users"]` 或加应用层鉴权。3203. **`execute=["any"]` 是无鉴权公开访问**:`db-init` 也公开可调,任何人可清库重建。生产应改为 `execute=["users"]` 或加应用层鉴权。
3214. **HTTP 明文传输**:自托管只开 80 端口时,API Key、密码、用户数据都明文传输;生产必须配 HTTPS(TLS 反代)。3214. **HTTP 明文传输**:自托管只开 80 端口时,API Key、密码、用户数据都明文传输;生产必须配 HTTPS(TLS 反代)。
3225. **API Key 泄漏要轮换**:真实 Key 一旦进入过仓库/文档/日志,必须在控制台吊销重发。3225. **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://&lt;host&gt;/v1/functions/db-init/executions \
26+ -H "Content-Type: application/json" \
27+ -H "X-Appwrite-Project: &lt;project_id&gt;" \
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 &lt;签名后 HAP&gt;
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/utils/HttpUtil.ets+75-40File content is under review, please try again later
@@ -1,3 +1,3 @@
1API_KEY=<your_appwrite_api_key>1API_KEY=<your_appwrite_api_key>
2-URL=http://<your_appwrite_host>2+APPWRITE_HOST=http://<your_appwrite_host>
3PROJECT_ID=<your_project_id>3PROJECT_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 @@
681. **Functions** → **Create Function**701. **Functions** → **Create Function**
692. 填写:712. 填写:
70 - **Name**: 函数名(如 `register`)72 - **Name**: 函数名(如 `register`)
71- - **Runtime**: Node.js 1873+ - **Runtime**: Node.js 16.0
72 - **Execute access**: `All users` 或 `Any`(允许公开调用)74 - **Execute access**: `All users` 或 `Any`(允许公开调用)
733. 创建后,进入 **Settings** → **Environment Variables**,添加:753. 创建后,进入 **Settings** → **Environment Variables**,添加:
74 - `API_KEY` = 你的 Server API Key76 - `API_KEY` = 你的 Server API Key
75 - `DATABASE_ID` = `word_db`77 - `DATABASE_ID` = `word_db`
764. **Deployments** → **Create Deployment**784. **Deployments** → **Create Deployment**
77- - 上传 `functions/{函数名}/` 目录下的 `main.js` + `package.json` 打包的 zip79+ - 上传 `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-```typescript90+此外还需在 `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```bash94```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 # 完整部署
120python deploy/deploy.py --check # 仅检查连通性114python 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 # 完整部署
136node deploy.js --check # 仅检查连通性130node 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}/executions159+POST http://XXX.XXX.XXX.XXX/v1/functions/{functionId}/executions
166Headers:160Headers:
167 X-Appwrite-Project: <project_id>161 X-Appwrite-Project: <project_id>
168 Content-Type: application/json162 Content-Type: application/json
@@ -193,7 +187,7 @@ Body:
193 187 
194```188```
195API_KEY=<your_api_key>189API_KEY=<your_api_key>
196-URL=http://<your_appwrite_host>190+APPWRITE_HOST=http://<your_appwrite_host>
197PROJECT_ID=<your_project_id>191PROJECT_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 Key7+ * API_KEY - Appwrite Server API Key
8- * URL - Appwrite 服务器地址8+ * APPWRITE_HOST - Appwrite 服务器地址
9- * PROJECT_ID - Appwrite Project ID9+ * PROJECT_ID - Appwrite Project ID
10 *10 *
11 * 用法:11 * 用法:
12 * cd deploy && npm install && node deploy.js12 * 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');
43const env = loadEnv(envPath);43const 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 
50const client = new Client()50const 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 
72async function checkConnection() {72async 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 
84async function deployFunction(name, description) {84async 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. 打包 zip93+ // 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 PowerShell97+ } 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', // runtime119+ 'node-16.0', // runtime
116 ['any'], // execute 权限 (公开访问)120 ['any'], // execute 权限 (公开访问)
117 [], // events121 [], // events
118 '', // schedule122 '', // 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 
191main().catch(e => { console.error(e); process.exit(1); });195main().catch(e => { console.error(e); process.exit(1); });
@@ -1,7 +1,7 @@
1#!/usr/bin/env python31#!/usr/bin/env python3
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
3"""3"""
4-demo007 Appwrite Functions automated deployment script4+Appwrite Functions automated deployment script
5 5 
6Usage:6Usage:
7 python deploy.py # Full deployment7 python deploy.py # Full deployment
@@ -16,6 +16,8 @@ import tarfile
16import io16import io
17import urllib.request17import urllib.request
18import urllib.error18import urllib.error
19+import shutil
20+import subprocess
19from pathlib import Path21from pathlib import Path
20 22 
21ROOT = Path(__file__).resolve().parent.parent23ROOT = Path(__file__).resolve().parent.parent
@@ -54,7 +56,7 @@ def load_env(path):
54 56 
55 57 
56def api_request(env, method, path, body=None, headers=None):58def api_request(env, method, path, body=None, headers=None):
57- url = env["URL"].rstrip("/") + path59+ 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 
79def check_connection(env):81def 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 True110 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+ 
111def create_tar_gz(func_name):135def create_tar_gz(func_name):
136+ install_dependencies(func_name)
112 func_path = FUNC_DIR / func_name137 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": {