# SKILL: Web Space Uploader API (agent.php) 這是一支單檔 PHP API,讓 AI Agent 把做好的網頁 / 靜態檔案上傳到這個 web 空間並設定入口頁。 本文件為完整用法說明,讀完即可直接操作,不需要再猜測。 - API 端點:`https://website.jiapi.net/index.php` - 網站根位置(此 PHP 所在目錄的公開網址):`https://website.jiapi.net/` - 所有檔案都會被放在此 PHP 所在目錄底下的子目錄中。 --- ## 0. 回應格式 - **不帶任何參數** → 回傳這份 Markdown 純文字(`text/plain`)。 - **帶 `act` 參數** → 回傳 JSON(`application/json; charset=utf-8`)。 - JSON 使用 `JSON_UNESCAPED_UNICODE`,中文等 Unicode 直接輸出正常字元,**不會**變成 `\uXXXX`。 - 斜線 `/` 也不跳脫。輸出有做縮排美化。 - **唯一例外:`act=download` 成功時回傳的是二進位檔案串流**(`application/octet-stream` + `Content-Disposition: attachment`),**不是 JSON**。只有失敗時才會回 JSON 錯誤物件。 解析前請先看 `Content-Type`。 成功時的外層固定為: ```json { "ok": true, "act": "upload", "data": { } } ``` 失敗時的外層固定為(HTTP 400 / 401 / 403 / 404 / 500): ```json { "ok": false, "act": "upload", "error": "錯誤訊息" } ``` `ok` 恆為布林值;`act` 是你呼叫時用的動作名轉小寫(例如用 `LIST` 呼叫就回 `"list"`, 無法辨識時為 `null`); 成功時所有內容都在 `data` 物件內,失敗時只有 `error` 字串。 **每個 act 的 `data` 欄位完整定義見第 11 節「回應欄位總表」。** --- ## 1. 參數總覽 | 參數 | 必要 | 說明 | |---|---|---| | `act` | 是 | 動作,見下表。大小寫不拘,內部會轉小寫。 | | `pwd` | 是 | 密碼。**所有 act 操作都必須帶密碼。** | | `folder` | 視 act | 專案目錄名稱(單層),相對於本 PHP 所在目錄。例:`folder=abc` → `./abc/` | | `path` | 視 act | 目錄內的相對路徑(含檔名)。`mock/a.txt` 與 `/mock/a.txt` 等價,皆從 `folder` 往下算。 | | `text` | `append_text` | 要追加的純文字內容(需 URL Encode)。只適合短內容。 | | `strip` | `extract` 選用 | 解壓時要去掉的前導路徑層數,預設 `0`。等同 tar 的 `--strip-components`。 | | `clean` | `extract` 選用 | `1` 代表解壓前先清空目標目錄,預設 `0`。 | | `format` | `extract` 選用 | 壓縮格式:`tar.gz`(預設) / `tgz` / `tar` / `zip`。 | | `keep` | `rmdir` 選用 | `1` 代表只清空 folder 內容、保留目錄本身,預設 `0`(清空後連目錄一起移除)。 | | `top` | `download` 選用 | `1` 代表壓縮檔內包一層原始目錄名,預設 `0`(不包,直接就是檔案本身)。 | ### 先決定用哪一個 act(很重要,先讀這段) 上傳檔案有兩種路徑,照下面判斷: | 你的情況 | 用哪個 | 為什麼 | |---|---|---| | 只要傳一兩個檔案 | **`act=upload`**(第 3 節) | 位元組完全一致、無長度限制、任何大小都行 | | 要傳整個專案(多檔) | **`act=extract`**(第 4 節) | 整包 tar.gz 一次送完,N 個檔只要一趟 | 兩者都需要 POST。檔案內容一律走 POST body,不要塞進 URL。 ### act 一覽 | act | 用途 | 成功時回傳 | |---|---|---| | `list` | 列出 folder / 目錄內容 / 單一檔案資訊 | JSON | | `upload` | 上傳單一檔案(二進位,POST body)**← 不受限環境的首選** | JSON | | `extract` | 一次上傳整包壓縮檔並解壓(多檔時最快) | JSON | | `append_text` | 用 `text` 追加短文字到檔案結尾(不覆蓋) | JSON | | `delete` | 刪除 folder **底下**的檔案或目錄(`folder`+`path` 皆必填) | JSON | | `rmdir` | 清空或移除**第一層目錄本身**(只吃 `folder`) | JSON | | `redirect` | 建立 index.php 入口轉址 | JSON | | `download` | 下載檔案,或把目錄打包成 tar.gz 下載 | **二進位檔案** | ### 關於密碼 `pwd` 密碼由使用者自己保管,**本文件不揭露密碼**。 AI Agent 在第一次呼叫任何 `act` 之前,必須先向使用者詢問:「請提供上傳用的密碼」, 取得後放進每一次請求的 `pwd` 參數。密碼錯誤會回 HTTP 401。 ### 關於 `folder` - 只能是**單層目錄名**,允許字元 `A-Z a-z 0-9 . _ -`,長度 ≤ 64。 **可以用點開頭**(例如 `.well-known`),但不可以整個名稱只有點(`.` 與 `..` 一律拒絕)。 - **一定是目錄。** 若同名的是本 PHP 同層的一個檔案,任何 act 都會回 400—— 本 API 只操作目錄底下的內容,不會動到根目錄那一層的檔案(包含本 API 自己)。 - 不存在時,`upload` / `extract` / `append_text` / `redirect` 會自動建立。 - 建議一個專案用一個 folder,之後所有操作都在其下進行。 ### 讀取與寫入的邊界(很重要) | 類別 | act | 可以碰到哪裡 | |---|---|---| | 讀取 | `list` `download` | 可以看到本 PHP 所在目錄(web root)那一層,例如列出有哪些 folder | | 寫入 | `upload` `extract` `append_text` `redirect` `delete` | **只能作用在第一層目錄底下**,`folder` 必填 | | 寫入(唯一例外) | `rmdir` | 目標就是第一層目錄本身,因此獨立成一個 act 並額外檢查 | 也就是說:**沒有任何寫入動作能碰到與本 PHP 同層的檔案**(包含本 API 自己)。 `act=delete` 一定要帶 `path`;想對 folder 本身動手只有 `act=rmdir` 一條路。 ### 關於 `path` - 一律相對於 `folder`,前面有沒有 `/` 都一樣。 - 可含多層子目錄,寫入時會自動遞迴建立中間目錄。 - **禁止** `..`(會回 400),無法跳出 `folder` 之外。 ### 關於 URL Encode(很重要) 所有參數值在放進 URL 前都必須做 **URL Encode**(percent-encoding, UTF-8): - 空白 → `%20`(或 `+`,在 query string 中兩者皆可被解回空白) - `&` → `%26`、`=` → `%3D`、`#` → `%23`、`+` → `%2B`、`%` → `%25` - 換行 `\n` → `%0A`、`\r` → `%0D` - 中文等 UTF-8 字元 → 例如「你好」→ `%E4%BD%A0%E5%A5%BD` 伺服器端會自動 URL Decode 還原成原始字串再寫入檔案。 > **⚠ `text` 的長度限制(實測後訂定,務必遵守)** > `text` 走 URL 傳輸。超過長度上限時參數會被**靜默丟棄**——不會報錯, > 而是「API 回 `ok: true`、`size` 卻是 0」的假成功。這是最容易踩的坑。 > > 實測單一參數值上限約 **96 字元**(不同工具可能更低,保守抓 92)。因此: > > - **`text` 只用來追加短字串**(`act=append_text`)。編碼後超過約 92 字元請改用 POST: > `Content-Type: application/x-www-form-urlencoded`,`text` 放 POST body, > `act`/`pwd`/`folder`/`path` 仍放 query string。 > - **不要用 `text` 寫入完整的 HTML / CSS / JS**,那必定超過門檻。 > 要寫入檔案請用 `act=upload`(第 3 節)。 > - 每次呼叫後檢查回應的 `size` 是否等於預期位元組數,為 0 或偏小即代表被截斷。 --- ## 2. act=list — 列出內容 | 傳入 | 行為 | `data.scope` | |---|---|---| | 只有 `act`+`pwd` | 列出本 PHP 目錄下的所有 folder(第一層目錄) | `root` | | 再加 `folder` | 列出該 folder 底下第一層的檔案與目錄 | `dir` | | 再加 `path`(指向目錄) | 列出該路徑底下第一層內容 | `dir` | | 再加 `path`(指向檔案) | 回傳該檔案的 UNIX 基本資料(等同 `ls -l`) | `file` | ``` GET https://website.jiapi.net/index.php?act=list&pwd= GET https://website.jiapi.net/index.php?act=list&pwd=&folder=abc GET https://website.jiapi.net/index.php?act=list&pwd=&folder=abc&path=mock GET https://website.jiapi.net/index.php?act=list&pwd=&folder=abc&path=mock/a.txt ``` 目錄內容一律「目錄在前、檔案在後」,各自依名稱排序。 ### 2a. scope = root ```json { "ok": true, "act": "list", "data": { "scope": "root", "base_url": "https://website.jiapi.net/", "count": 1, "items": [ { "name": "abc", "type": "dir", "size": 4096, "perms": "drwxr-xr-x", "mode": "0755", "uid": 141499303, "gid": 1049170745, "owner": "u141499303", "group": "o49170745", "nlink": 3, "mtime": "2026-08-07 14:11:26", "mtime_unix": 1786083086, "readable": true, "writable": true, "url": "https://website.jiapi.net/abc/", "has_index": true } ] } } ``` `has_index` 只在 scope=root 出現,代表該 folder 內已有 `index.php` 或 `index.html`。 scope=root 只列出目錄,本 PHP 同層的檔案不會出現在清單裡。 ### 2b. scope = dir ```json { "ok": true, "act": "list", "data": { "scope": "dir", "folder": "abc", "path": "mock", "rel": "abc/mock", "url": "https://website.jiapi.net/abc/mock/", "count": 1, "items": [ { "name": "a.txt", "type": "file", "size": 5, "perms": "-rw-r--r--", "mode": "0644", "uid": 141499303, "gid": 1049170745, "owner": "u141499303", "group": "o49170745", "nlink": 1, "mtime": "2026-08-07 14:08:04", "mtime_unix": 1786082884, "readable": true, "writable": true, "url": "https://website.jiapi.net/abc/mock/a.txt" } ] } } ``` 目錄項目的 `url` 結尾會多一個 `/`。列 folder 根目錄時 `path` 為空字串、`rel` 等於 folder 名。 ### 2c. scope = file ```json { "ok": true, "act": "list", "data": { "scope": "file", "folder": "abc", "path": "mock/a.txt", "rel": "abc/mock/a.txt", "item": { "name": "a.txt", "type": "file", "size": 25, "perms": "-rw-r--r--", "mode": "0644", "uid": 141499303, "gid": 1049170745, "owner": "u141499303", "group": "o49170745", "nlink": 1, "mtime": "2026-08-07 14:08:04", "mtime_unix": 1786082884, "readable": true, "writable": true, "url": "https://website.jiapi.net/abc/mock/a.txt" } } } ``` 注意 scope=file 用的是單數的 `item` 物件,**沒有** `items` / `count` / `url`。 --- ## 3. act=upload — 上傳單一檔案(二進位) > **★ 單檔上傳一律用這個,包含 HTML / CSS / JS 等純文字檔。** > `upload` 走 POST body 傳原始位元組,沒有 URL 長度限制、沒有編碼問題、位元組完全一致, > 而且文字檔和二進位檔用同一條路徑,Agent 的迴圈只要寫一種。 > `append_text` 只是給「短字串直接塞進 URL」的便利用法,不是主力。 > > **要一次傳整個專案(多個檔案)時,改用第 4 節的 `act=extract` 會快非常多** > ——打包成一個 tar.gz 一次送完,不必每個檔案跑一趟 HTTP。 一次上傳一個檔案。**多個檔案請遞迴/逐一呼叫**(第 10 節有可直接執行的遞迴上傳腳本)。 檔案內容放在 **POST body**,支援三種攜帶方式(擇一): ### 方式 A:raw binary body(建議,最省事) ```bash curl -X POST \ -H "Content-Type: application/octet-stream" \ --data-binary @./dist/index.html \ "https://website.jiapi.net/index.php?act=upload&pwd=&folder=abc&path=index.html" ``` ### 方式 B:multipart/form-data,欄位名 `file` ```bash curl -X POST \ -F "file=@./img/logo.png" \ "https://website.jiapi.net/index.php?act=upload&pwd=&folder=abc&path=img/logo.png" ``` ### 方式 C:base64(無法送二進位時) 欄位 `content_base64`,值為檔案內容的 base64 字串。GET / POST 皆可,但 GET 受 URL 長度限制。 ```bash curl -X POST \ --data-urlencode "content_base64=$(base64 -w0 ./img/logo.png)" \ "https://website.jiapi.net/index.php?act=upload&pwd=&folder=abc&path=img/logo.png" ``` 必要參數:`act=upload`、`pwd`、`folder`、`path`(含檔名)。 中間目錄不存在會自動建立。同名檔案會被覆蓋。 上傳 0 位元組的空檔案:用 POST 送空 body 即可(會正確建立空檔,不會報錯)。 `path` 若含空白或中文,記得 URL Encode 後再放進 query string。 ```json { "ok": true, "act": "upload", "data": { "folder": "abc", "path": "img/logo.png", "rel": "abc/img/logo.png", "url": "https://website.jiapi.net/abc/img/logo.png", "size": 20480, "source": "raw", "overwritten": false } } ``` `source` 值為 `raw` / `multipart` / `base64`,代表伺服器實際採用了哪一種攜帶方式—— 可用來確認你的請求有被正確辨識。 --- ## 4. act=extract — 一次上傳整包並解壓(批次部署) > **★ 要把一整個做好的專案傳上來,用這個,不要用 N 次 `upload`。** > 把輸出目錄打包成一個 tar.gz,一次 POST 上來,伺服器端直接解壓到 folder。 > 50 個檔案從 50 趟 HTTP 變成 1 趟。 參數:`act=extract`、`pwd`、`folder`(必要);`path`、`strip`、`clean`、`format`(選用)。 壓縮檔內容的攜帶方式與 `upload` 完全相同(raw binary body / multipart `file` / `content_base64`), **建議用 raw binary body**。 ```bash # 在輸出目錄內打包(不含最上層資料夾),然後一次送上去 tar -czf /tmp/site.tar.gz -C ./dist . curl -X POST \ -H "Content-Type: application/octet-stream" \ --data-binary @/tmp/site.tar.gz \ "https://website.jiapi.net/index.php?act=extract&pwd=&folder=abc&clean=1" ``` ### 選用參數 | 參數 | 預設 | 說明 | |---|---|---| | `path` | 空 | 解壓到 folder 底下的子目錄,例如 `path=assets`。不填就解到 folder 根部。 | | `strip` | `0` | 去掉壓縮檔內前導路徑層數,等同 tar 的 `--strip-components`。 | | `clean` | `0` | `clean=1` 會在解壓**之前**先清空目標目錄,適合乾淨重新部署。 | | `format` | `tar.gz` | 可用 `tar.gz` / `tgz` / `tar` / `zip`。 | ### 關於 `strip` - 你用 `tar -czf x.tar.gz -C ./dist .` 打包 → 壓縮檔內是 `./index.html`,用預設 `strip=0`。 - 你用 `tar -czf x.tar.gz dist` 打包 → 壓縮檔內是 `dist/index.html`, 要用 `strip=1` 才不會變成 `abc/dist/index.html`。 - 本 API 的 `act=download` **預設不包最上層目錄名**,所以抓下來編輯完直接送回來時, 用預設的 `strip=0` 就會原地還原——這是刻意設計成對稱的。 只有在你當初是用 `download&top=1` 抓的,才需要 `strip=1`。 ### 安全限制 - 壓縮檔內**每一個路徑都會先檢查**,只要有任何一筆含 `..`,**整包直接拒絕**(回 400), 不會做部分解壓,因此不可能寫到 folder 外面。 - 檔名前導的 `/` 會被去掉,一律視為相對路徑。 - 目錄項目本身不會被建立,只有實際檔案會被寫出(空目錄不會保留)。 - 同名檔案會被覆蓋。 ```json { "ok": true, "act": "extract", "data": { "folder": "abc", "path": "", "rel": "abc", "url": "https://website.jiapi.net/abc/", "source": "raw", "format": "tar.gz", "archive_size": 48210, "strip": 0, "cleaned": true, "files": 23, "bytes": 186433, "entries": ["index.html", "style.css", "assets/app.js", "img/logo.png"], "entries_truncated": false } } ``` | 欄位 | 意義 | |---|---| | `archive_size` | 收到的壓縮檔位元組數 | | `files` | 實際解出的檔案數 | | `bytes` | 解出檔案的位元組總和(解壓後大小) | | `entries` | 解出的相對路徑清單,**最多只列 200 筆** | | `entries_truncated` | 為 `true` 代表 `entries` 被截斷,實際檔案數以 `files` 為準 | | `cleaned` | 是否有執行 `clean=1` 的清空動作 | **驗收方式**:比對 `files` 是否等於你打包的檔案數, 再用 `act=list&folder=` 確認結構正確。 若本主機沒有 PharData 可用會回 500,此時請退回逐檔 `act=upload`。 --- ## 5. act=append_text — 追加純文字(不覆蓋) 用 `text` 參數把內容**接在檔案最後面**,不會覆蓋既有內容。 適合累積 log、記錄部署時間戳這類小量文字。 > **⚠ 只適合短內容。** `text` 走 URL 傳輸,超過長度上限會被靜默截斷(見第 1 節)。 > 要寫入完整檔案請用 `act=upload`(第 3 節)。 ```bash curl -X POST \ --data-urlencode "text=第二段內容" \ "https://website.jiapi.net/index.php?act=append_text&pwd=&folder=abc&path=notes.txt" ``` ### 換行規則 - 檔案**不存在** → 直接建立,內容就是 `text`(不會多加換行)。 - 檔案**已存在且結尾不是換行字元** → 先自動補一個 `\n`,再接上 `text`, 確保新內容從新的一行開始。 - 檔案**已存在且結尾已經是換行字元** → 不再補換行,直接接上 `text`(避免產生空白行)。 長度規則見第 1 節:編碼後超過約 92 字元請改用 POST,或直接改用 `act=upload`。 ```json { "ok": true, "act": "append_text", "data": { "folder": "abc", "path": "notes.txt", "rel": "abc/notes.txt", "url": "https://website.jiapi.net/abc/notes.txt", "source": "text", "existed": true, "separator_added": true, "bytes_written": 16, "size": 48 } } ``` | 欄位 | 意義 | |---|---| | `existed` | 追加前檔案是否已存在(`false` 代表這次是新建) | | `separator_added` | 這次是否自動補了一個換行字元 | | `bytes_written` | 本次實際寫入的位元組數(**含**自動補的換行) | | `size` | 追加後檔案的總位元組數 | 注意這裡**沒有** `overwritten` 欄位(`append_text` 永遠不覆蓋), 而 `upload` 則**沒有** `existed` / `separator_added` / `bytes_written`。 --- ## 6. act=delete / act=rmdir — 刪除 刪除拆成兩個 act,**因為它們的作用範圍不同**: | act | 作用範圍 | 必要參數 | |---|---|---| | `delete` | 第一層目錄**底下**的檔案或目錄 | `folder` + `path` | | `rmdir` | 第一層目錄**本身**(清空或移除) | 只有 `folder` | ### 6a. act=delete — 刪 folder 底下的東西 ``` GET https://website.jiapi.net/index.php?act=delete&pwd=&folder=abc&path=mock/a.txt # 刪單檔 GET https://website.jiapi.net/index.php?act=delete&pwd=&folder=abc&path=mock # 遞迴刪整個子目錄 ``` **`path` 必填。** 沒帶會回 400 並提示改用 `act=rmdir`——這是刻意的: 寫入類動作一律不准作用在 folder 本身或 web root 那一層。 ### 6b. act=rmdir — 清空或移除第一層目錄 ``` GET https://website.jiapi.net/index.php?act=rmdir&pwd=&folder=abc # 清空並移除 abc 這個目錄 GET https://website.jiapi.net/index.php?act=rmdir&pwd=&folder=abc&keep=1 # 只清空內容,保留 abc 目錄 ``` **不可以帶 `path`**(帶了回 400)。這是唯一會動到 web root 那一層的寫入動作, 所以它比其他 act 多做兩道檢查: 1. `folder` 必須是**目錄**——同名的若是檔案(例如本 API 自己)直接回 400 2. `realpath` 圍堵——解析後必須真的是本 PHP 目錄的直接子目錄,擋掉 symlink 逃逸 移除後 `/abc/` 完全不存在,也不會再出現在 `act=list` 的 folder 清單裡; 之後任何一次 `upload` / `extract` / `append_text` / `redirect` 都會自動重新建立。 ```json { "ok": true, "act": "delete", "data": { "folder": "abc", "path": "mock", "target": "abc/mock", "mode": "recursive", "deleted": true } } ``` `mode` 的四種值分屬兩個 act: | act | `mode` | 情境 | |---|---|---| | `delete` | `file` | `path` 指向單一檔案 | | `delete` | `recursive` | `path` 指向目錄,連同底下所有內容一起刪除 | | `rmdir` | `empty-folder` | 帶了 `keep=1`,清空內容但保留目錄 | | `rmdir` | `remove-folder` | 預設,清空後連目錄本身一起移除 | `act=rmdir` 的回應會額外附帶 `note` 字串說明實際做了什麼。 --- ## 7. act=redirect — 設定入口頁 在 `folder` 根目錄建立一支 `index.php`,訪問 `https://website.jiapi.net/abc/` 時自動 302 轉址到指定檔案。 ``` GET https://website.jiapi.net/index.php?act=redirect&pwd=&folder=abc&path=wrong.html ``` ```json { "ok": true, "act": "redirect", "data": { "folder": "abc", "path": "wrong.html", "index": "abc/index.php", "entry_url": "https://website.jiapi.net/abc/", "target_url": "https://website.jiapi.net/abc/wrong.html", "target_exists": true } } ``` - `index`:實際建立的檔案相對路徑,固定是 `/index.php`。 - `entry_url`:使用者要訪問的入口網址,回報給使用者時用這個。 - `target_url`:轉址的目的地完整網址。 - `target_exists`:目標檔案目前是否存在。若為 `false`,仍會建立 index.php, 但會**額外附帶 `warning` 字串**提醒你還沒上傳該檔案。 - `path` 不可指向 `index.php` 本身(會造成無限轉址,回 400)。 --- ## 8. act=download — 下載檔案或打包目錄 **這個 act 成功時回傳的是檔案本身,不是 JSON。** 只有出錯時才回 JSON 錯誤物件。 | 傳入 | 行為 | 下載檔名 | |---|---|---| | `folder` + `path` 指向檔案 | 直接下載該檔案(強制附件下載,不會用瀏覽器開啟) | 原檔名 | | `folder` + `path` 指向目錄 | 打包成 tar.gz 後下載 | `<目錄名>.tar.gz` | | 只有 `folder` | 打包整個 folder | `.tar.gz` | ```bash # 下載單一檔案(即使是 .html 也會是下載而非開啟) curl -OJ "https://website.jiapi.net/index.php?act=download&pwd=&folder=abc&path=index.html" # 打包下載整個 folder curl -OJ "https://website.jiapi.net/index.php?act=download&pwd=&folder=abc" # 打包下載某個子目錄 curl -OJ "https://website.jiapi.net/index.php?act=download&pwd=&folder=abc&path=img" ``` `curl -OJ` 會採用伺服器給的 `Content-Disposition` 檔名。 ### 回應標頭 - `Content-Type: application/octet-stream`(一律強制下載,`.html` 也不會被瀏覽器開啟) - `Content-Disposition: attachment; filename="..."` - 檔名為非 ASCII(例如中文目錄名)時,會**同時**帶上 RFC 5987 的 `filename*=UTF-8''<百分比編碼>`,瀏覽器會優先採用它以保留原始名稱; 不支援的用戶端則退回 ASCII 版本(非 ASCII 字元被換成 `_`)。 - `Content-Length`、`X-Content-Type-Options: nosniff`、`Cache-Control: no-store` ### 打包細節 - tar.gz 產生在本 PHP 所在目錄下的 `./tmp/`,檔名為隨機 hash(避免被猜到)。 - **預設不包最上層資料夾**:壓縮檔內直接就是 `index.html`、`css/s.css` … 這樣抓下來編輯後可以直接用 `act=extract`(預設 `strip=0`)原地送回去,兩邊對稱。 - 帶 `top=1` 才會包一層原始目錄名(解開後是 `abc/...` 而不是散落一地), 適合人直接下載解開;此時要送回本 API 就得配 `strip=1`。 - 下載的檔名一律是 `<目錄名>.tar.gz`,不受 `top` 影響。 - 檔案送出後即刪除暫存檔;若傳輸中斷造成殘留,下次呼叫 `download` 時會自動清掉 超過 1 小時的暫存檔。 - `./tmp/` 是內部工作目錄,**不會**出現在 `act=list` 的 folder 清單中。 - 空目錄無法打包,會回 400。 ### 失敗時(回 JSON) ```json { "ok": false, "act": "download", "error": "path 不存在:abc/nope" } ``` **AI Agent 解析建議**:先檢查 HTTP 狀態碼與 `Content-Type`。 `application/json` → 當作錯誤處理;`application/octet-stream` → 當作檔案內容存檔。 --- ## 9. 錯誤碼 | HTTP | 意義 | |---|---| | 400 | 參數錯誤(缺參數、`delete` 沒帶 `path`、`rmdir` 帶了 `path`、folder 名稱不合法或不是目錄、path 含 `..`、未知 act、打包空目錄、redirect 指向 index.php、壓縮檔損毀、壓縮檔內含 `..` 路徑、format 不支援) | | 401 | `pwd` 錯誤或未提供 | | 403 | 檔案系統權限不足,無法建立目錄/寫入/刪除 | | 404 | 指定的 folder / path 不存在 | | 500 | 伺服器端寫入、打包或解壓失敗(例如本主機沒有 PharData) | 失敗回應一律是 `{"ok": false, "act": "...", "error": "..."}`,沒有 `data` 欄位。 --- ## 10. AI Agent 標準作業流程(照這個做) **這一節的用途**:你(AI Agent)在自己的沙箱/容器裡做好了一個網頁專案, 但沙箱內的檔案使用者沒辦法直接點開看,只能下載。 照下面的流程做,就能把整包檔案搬到這個公開空間,並回給使用者一個能直接點開的網址。 ### 流程 1. **問密碼**:向使用者索取上傳密碼,之後每次請求都帶在 `pwd`。 2. **問 folder**:向使用者確認這次專案要用的目錄名稱(例如 `myapp`)。 先呼叫 `act=list` 看現有有哪些 folder,避免撞名。 3. **上傳整包**:把輸出目錄打包成 tar.gz,用 `act=extract` 一次送上去, 並帶 `clean=1` 順便清掉上一版的殘留檔案。一趟就完成。 若主機不支援解壓而回 500,就改對每個檔案呼叫 `act=upload` (文字檔和二進位檔一律都用 `upload`),重新部署前先 `act=rmdir&folder=&keep=1` 清空。 4. **設定入口**:判斷入口檔案(通常是 `index.html`),呼叫 `act=redirect&folder=&path=<入口檔>`。 即使入口就是 `index.html` 也建議照做,這樣 `/folder/` 一定會通。 若入口不明確,問使用者要用哪一個檔案當入口。 5. **驗收**:呼叫 `act=list&folder=` 確認檔案數量與大小都對, 然後把 `entry_url`(也就是 `https://website.jiapi.net//`)回報給使用者。 **這一步是整個流程的目的,不要漏掉——使用者要的就是這個可以點開的網址。** ### 首選:整包上傳(一趟搞定) ```bash E="https://website.jiapi.net/index.php" P="<使用者提供的密碼>" F="myapp" # folder 名稱 SRC="./dist" # 你的輸出目錄 tar -czf /tmp/deploy.tar.gz -C "$SRC" . curl -s -X POST \ -H "Content-Type: application/octet-stream" \ --data-binary @/tmp/deploy.tar.gz \ "$E?act=extract&pwd=$P&folder=$F&clean=1" # 對照本機檔案數,確認 files 一致 find "$SRC" -type f | wc -l curl -s "$E?act=redirect&pwd=$P&folder=$F&path=index.html" echo "請把這個網址給使用者:https://website.jiapi.net/$F/" ``` ### 備援:逐檔遞迴上傳腳本(bash + curl) 主機不支援解壓、或只要更新少數幾個檔案時用這個。 ```bash E="https://website.jiapi.net/index.php" P="<使用者提供的密碼>" F="myapp" # folder 名稱 SRC="./dist" # 你的輸出目錄 enc() { python3 -c "import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=''))" "$1"; } # (可選) 清空舊版本,確保沒有殘留檔案 curl -s "$E?act=rmdir&pwd=$P&folder=$F&keep=1" > /dev/null fail=0 cd "$SRC" || exit 1 while IFS= read -r -d '' f; do rel="${f#./}" local_size=$(wc -c < "$f") resp=$(curl -s -X POST \ -H "Content-Type: application/octet-stream" \ --data-binary @"$f" \ "$E?act=upload&pwd=$P&folder=$F&path=$(enc "$rel")") remote_size=$(printf '%s' "$resp" | tr -d ' \n' | sed -n 's/.*"size":\([0-9]*\).*/\1/p') if [ "$local_size" = "$remote_size" ]; then echo "OK $rel ($local_size bytes)" else echo "FAIL $rel (local=$local_size remote=$remote_size) $resp" fail=1 fi done < <(find . -type f -print0) [ "$fail" = "0" ] || { echo "有檔案上傳失敗,請重試失敗的項目"; exit 1; } # 設定入口並取回網址 curl -s "$E?act=redirect&pwd=$P&folder=$F&path=index.html" echo "請把這個網址給使用者:https://website.jiapi.net/$F/" ``` ### 沒有 bash 時(Python 版本核心迴圈) ```python import os, json, urllib.parse, urllib.request E, P, F, SRC = "https://website.jiapi.net/index.php", "", "myapp", "./dist" for root, _, files in os.walk(SRC): for name in files: full = os.path.join(root, name) rel = os.path.relpath(full, SRC).replace(os.sep, "/") data = open(full, "rb").read() url = (E + "?act=upload&pwd=" + P + "&folder=" + F + "&path=" + urllib.parse.quote(rel, safe="")) req = urllib.request.Request(url, data=data, method="POST", headers={"Content-Type": "application/octet-stream"}) r = json.load(urllib.request.urlopen(req)) assert r["ok"] and r["data"]["size"] == len(data), (rel, r) print("OK", rel, r["data"]["url"]) ``` ### 收尾檢查清單 - [ ] 用 `extract` 時:回應的 `files` 等於本機檔案數;用 `upload` 時:每個檔案的 回應 `size` 都等於本機檔案大小 - [ ] `act=list&folder=` 的檔案數量與本機一致 - [ ] `act=redirect` 回應的 `target_exists` 是 `true`(`false` 代表入口檔沒傳到) - [ ] 已經把 `entry_url` 交給使用者 - [ ] 需要備份時:`curl -OJ "$E?act=download&pwd=$P&folder=$F"` 可整包取回 --- ## 11. 回應欄位總表 所有成功回應都是 `{"ok":true,"act":"","data":{...}}`,下表列出各 act 的 `data` 內容。 型別:`str` 字串、`int` 整數、`bool` 布林、`obj` 物件、`arr` 陣列、`null` 可能為 null。 ### 共用:檔案/目錄項目物件(list 的 `items[]` 與 `item`) | 欄位 | 型別 | 說明 | |---|---|---| | `name` | str | 檔名或目錄名(不含路徑) | | `type` | str | `file` 或 `dir` | | `size` | int | 位元組數;目錄為該 inode 大小(通常 4096) | | `perms` | str | `ls -l` 格式權限字串,例如 `-rw-r--r--`、`drwxr-xr-x` | | `mode` | str | 八進位權限後四碼,例如 `0644` | | `uid` | int\|null | 擁有者數字 ID | | `gid` | int\|null | 群組數字 ID | | `owner` | str | 擁有者名稱;系統不支援 posix 函式時為空字串 | | `group` | str | 群組名稱;同上 | | `nlink` | int\|null | 硬連結數 | | `mtime` | str | 修改時間 `Y-m-d H:i:s`(時區 Asia/Taipei) | | `mtime_unix` | int\|null | 修改時間 Unix timestamp | | `readable` | bool | PHP 是否可讀 | | `writable` | bool | PHP 是否可寫 | | `url` | str | 公開網址;目錄結尾帶 `/` | | `has_index` | bool | **僅 scope=root 出現**,該 folder 內是否已有 index.php 或 index.html | ### act=list | scope | `data` 欄位 | |---|---| | `root` | `scope`(str) `base_url`(str) `count`(int) `items`(arr of 項目物件) | | `dir` | `scope`(str) `folder`(str) `path`(str) `rel`(str) `url`(str) `count`(int) `items`(arr) | | `file` | `scope`(str) `folder`(str) `path`(str) `rel`(str) `item`(obj 單一項目物件) | `path` 為正規化後的相對路徑(去掉前導 `/`);`rel` 為 `folder/path` 合併後的相對路徑。 ### act=upload | 欄位 | 型別 | 說明 | |---|---|---| | `folder` | str | folder 名稱 | | `path` | str | 正規化後的相對路徑(含檔名) | | `rel` | str | `folder/path` | | `url` | str | 檔案的公開網址 | | `size` | int | 寫入後的檔案總位元組數 | | `source` | str | `raw` / `multipart` / `base64` | | `overwritten` | bool | 是否覆蓋了既有檔案 | ### act=extract | 欄位 | 型別 | 說明 | |---|---|---| | `folder` | str | folder 名稱 | | `path` | str | 解壓目標子目錄;解到 folder 根部時為空字串 | | `rel` | str | `folder/path` | | `url` | str | 解壓目標的公開網址(結尾帶 `/`) | | `source` | str | `raw` / `multipart` / `base64`,壓縮檔的攜帶方式 | | `format` | str | 實際採用的格式:`tar.gz` / `tar` / `zip` | | `archive_size` | int | 收到的壓縮檔位元組數 | | `strip` | int | 實際套用的 strip 層數 | | `cleaned` | bool | 解壓前是否清空了目標目錄 | | `files` | int | 解出的檔案數 | | `bytes` | int | 解出檔案的位元組總和 | | `entries` | arr | 解出的相對路徑清單,最多 200 筆 | | `entries_truncated` | bool | `entries` 是否被截斷(實際數量以 `files` 為準) | ### act=append_text | 欄位 | 型別 | 說明 | |---|---|---| | `folder` | str | folder 名稱 | | `path` | str | 正規化後的相對路徑(含檔名) | | `rel` | str | `folder/path` | | `url` | str | 檔案的公開網址 | | `source` | str | 固定為 `text` | | `existed` | bool | 追加前檔案是否已存在 | | `separator_added` | bool | 是否自動補了換行 | | `bytes_written` | int | 本次寫入位元組數(含補的換行) | | `size` | int | 追加後檔案總位元組數 | ### act=delete | 欄位 | 型別 | 說明 | |---|---|---| | `folder` | str | folder 名稱 | | `path` | str | 正規化後的相對路徑(必填,不會是空字串) | | `target` | str | 實際被刪除的相對路徑 `folder/path` | | `mode` | str | `file` / `recursive` | | `deleted` | bool | 恆為 `true`(失敗會走錯誤回應) | ### act=rmdir | 欄位 | 型別 | 說明 | |---|---|---| | `folder` | str | folder 名稱 | | `target` | str | 同 `folder` | | `mode` | str | `remove-folder`(預設)或 `empty-folder`(帶了 `keep=1`) | | `deleted` | bool | 恆為 `true`(失敗會走錯誤回應) | | `note` | str | 說明 folder 是保留還是已移除 | ### act=redirect | 欄位 | 型別 | 說明 | |---|---|---| | `folder` | str | folder 名稱 | | `path` | str | 轉址目標的相對路徑 | | `index` | str | 產生的 index.php 相對路徑 | | `entry_url` | str | 入口網址(回報給使用者用這個) | | `target_url` | str | 轉址目的地完整網址 | | `target_exists` | bool | 目標檔案是否已存在 | | `warning` | str | **僅 `target_exists=false` 出現**,提醒尚未上傳目標檔案 | ### act=download 成功時**沒有 JSON**,直接是檔案串流;失敗時為標準錯誤物件 `{ok:false, act:"download", error:"..."}`。 --- ## 12. 注意事項 - 單檔上傳大小受主機 `upload_max_filesize` / `post_max_size` 限制,過大請分檔或壓縮。 - `folder` 是單層;要多層結構請用 `path`(例如 `path=sub/dir/file.html`)。 - 重新部署同一個 folder 時,`upload` 只會覆蓋同名檔案,**不會**刪掉已經不需要的舊檔。 要乾淨部署請先 `act=rmdir&folder=&keep=1` 清空(或用 `act=extract` 的 `clean=1`)。 - 上傳後使用者若看到舊內容,多半是瀏覽器快取,請他強制重新整理(Ctrl/Cmd + Shift + R)。 - 網頁內的相對路徑(`./style.css`、`img/logo.png`)在 `//` 底下可正常運作; 但**絕對路徑**(`/style.css`)會指到網站根目錄而不是你的 folder,做網頁時請一律用相對路徑。 - 本 PHP 檔本身不會出現在 `act=list` 的 folder 清單中,也無法被刪除。 - `./tmp/` 為 download 打包用的內部暫存目錄,同樣不會出現在 folder 清單中。 - 這支 API 僅供擁有者本人使用,請勿外流密碼。