feat(codex_account): 改用 shell 取得登入帳號 #5
+16
@@ -0,0 +1,16 @@
|
|||||||
|
FROM ubuntu:latest
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
&& npm install -g @openai/codex \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
CMD ["codex", "--version"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Codex CLI Composite Action
|
# Codex CLI Composite Action
|
||||||
|
|
||||||
提供一個 Gitea/GitHub Actions composite action,用於安裝 Codex CLI、寫入 OAuth token、執行指定提示詞,並將 CLI 輸出寫入 action output;另提供透過 `codex app-server` 取得目前登入帳號 email 的工具函式。
|
提供一個 Gitea/GitHub Actions composite action,用於安裝 Codex CLI、寫入 OAuth token、執行指定提示詞,並將 CLI 輸出寫入 action output;另提供透過 `codex app-server` 取得目前登入帳號 email 的 Node.js helper。
|
||||||
|
|
||||||
> 更新時間:2026/06/29 14:18:26
|
> 更新時間:2026/06/29 14:18:26
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
| 專案名稱 | 專案描述 |
|
| 專案名稱 | 專案描述 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [codex](https://gitea.jsc.idv.tw/composite-actions/codex/src/branch/develop/) | 提供 Codex CLI composite action(安裝 `@openai/codex`、寫入 OAuth token、執行提示詞並輸出文字),並提供透過 `codex app-server` JSON-RPC `account/read` 取得目前登入帳號 email 的工具函式。 |
|
| [codex](https://gitea.jsc.idv.tw/composite-actions/codex/src/branch/develop/) | 提供 Codex CLI composite action(安裝 `@openai/codex`、寫入 OAuth token、執行提示詞並輸出文字),並提供透過 `codex app-server` JSON-RPC `account/read` 取得目前登入帳號 email 的 Node.js helper。 |
|
||||||
|
|
||||||
### 參考專案
|
### 參考專案
|
||||||
|
|
||||||
@@ -30,46 +30,55 @@
|
|||||||
|
|
||||||
| 功能名稱 | 功能描述 |
|
| 功能名稱 | 功能描述 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [codex_account.read_account_email](https://gitea.jsc.idv.tw/composite-actions/codex/src/branch/develop/app/codex_account.py#L12) | [透過 codex app-server 的 JSON-RPC `account/read` 取得目前登入帳號的 email,取不到或逾時回傳空字串。](#codex_accountread_account_email) |
|
| [app/codex_account.js](https://gitea.jsc.idv.tw/composite-actions/codex/src/branch/develop/app/codex_account.js#L1) | [透過 codex app-server 的 JSON-RPC `account/read` 取得目前登入帳號的 email,取不到或逾時輸出空字串。](#appcodex_accountjs) |
|
||||||
|
|
||||||
## 使用範例
|
## 使用範例
|
||||||
|
|
||||||
<a id="codex_accountread_account_email"></a>
|
<a id="appcodex_accountjs"></a>
|
||||||
### codex_account.read_account_email
|
### app/codex_account.js
|
||||||
|
|
||||||
透過 `codex app-server` 的 JSON-RPC `account/read` 取得目前登入帳號的 email。函式會啟動 `codex app-server` 子行程,依序送出 `initialize` / `initialized` / `account/read` 三筆 JSON-RPC 訊息,讀取回應中 `result.account.email` 作為回傳值;讀到 EOF、JSON 解析失敗或超過 `timeout_seconds` 逾時都會停止,取不到時回傳空字串 `""`,且不論成功或失敗都會在 `finally` 終止子行程。
|
透過 `codex app-server` 的 JSON-RPC `account/read` 取得目前登入帳號的 email。腳本會啟動 `codex app-server` 子行程,依序送出 `initialize` / `initialized` / `account/read` 三筆 JSON-RPC 訊息,使用 Node.js 標準函式庫讀取回應中 `result.account.email` 並輸出到 stdout;讀到 EOF、JSON 解析失敗或超過逾時都會停止,取不到時輸出空字串。
|
||||||
|
|
||||||
簽名:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def read_account_email(timeout_seconds: float = 25.0) -> str
|
|
||||||
```
|
|
||||||
|
|
||||||
前置條件:
|
前置條件:
|
||||||
|
|
||||||
- `codex` CLI 已安裝(例如本 action 的安裝步驟已執行)。
|
- `codex` CLI 已安裝(例如本 action 的安裝步驟已執行)。
|
||||||
|
- Node.js 已安裝。
|
||||||
- `$HOME/.codex/auth.json` 已寫入有效的 OAuth token。
|
- `$HOME/.codex/auth.json` 已寫入有效的 OAuth token。
|
||||||
|
|
||||||
典型呼叫方式:
|
典型呼叫方式:
|
||||||
|
|
||||||
```python
|
```bash
|
||||||
from app.codex_account import read_account_email
|
# 取得目前登入帳號 email;預設等待 25 秒
|
||||||
|
node app/codex_account.js
|
||||||
|
|
||||||
# 取得目前登入帳號 email;可自訂等待 app-server 回應的逾時秒數
|
# 自訂等待 app-server 回應的逾時秒數
|
||||||
email = read_account_email(timeout_seconds=25.0)
|
node app/codex_account.js 10
|
||||||
if email:
|
|
||||||
print(f"目前登入帳號:{email}")
|
|
||||||
else:
|
|
||||||
print("取不到登入帳號(未登入或逾時)")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
預期結果:
|
預期結果:
|
||||||
|
|
||||||
- 成功時回傳登入帳號的 email 字串。
|
- 成功時輸出登入帳號的 email 字串。
|
||||||
- 未登入、回應缺漏或逾時時回傳空字串 `""`。
|
- 未登入、回應缺漏或逾時時輸出空字串。
|
||||||
|
|
||||||
此模組亦可直接以指令執行,會將 email 印到 stdout(取不到時印空字串):
|
此模組亦可直接以指令執行,會將 email 印到 stdout(取不到時印空字串):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 app/codex_account.py
|
node app/codex_account.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
此專案提供以 `ubuntu:latest` 為基底的 Dockerfile,僅安裝 Node.js/npm 與 `@openai/codex`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t codex-cli .
|
||||||
|
docker run --rm codex-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
若要在容器內執行 Codex 並使用本機登入憑證,可掛載 `$HOME/.codex`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-v "$HOME/.codex:/root/.codex:ro" \
|
||||||
|
codex-cli codex --version
|
||||||
```
|
```
|
||||||
|
|||||||
+3
-3
@@ -7,7 +7,7 @@
|
|||||||
# 讓 codex CLI 取得登入憑證。
|
# 讓 codex CLI 取得登入憑證。
|
||||||
# 3. 執行 inputs.prompt 指定的提示詞,並把 Codex 的輸出寫入
|
# 3. 執行 inputs.prompt 指定的提示詞,並把 Codex 的輸出寫入
|
||||||
# steps.codex.outputs.text 供後續 step 使用。
|
# steps.codex.outputs.text 供後續 step 使用。
|
||||||
# - 若未提供 prompt(PROMPT 為空),則改呼叫 app/codex_account.py 取得目前
|
# - 若未提供 prompt(PROMPT 為空),則改呼叫 app/codex_account.js 取得目前
|
||||||
# 登入帳號的 email,寫入 text 輸出後結束(用於驗證身分)。
|
# 登入帳號的 email,寫入 text 輸出後結束(用於驗證身分)。
|
||||||
# 更新日期:2026/06/29 14:18:26
|
# 更新日期:2026/06/29 14:18:26
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -96,8 +96,8 @@ runs:
|
|||||||
fi
|
fi
|
||||||
# 當 PROMPT 為空時,改走「取得登入帳號 email」分支
|
# 當 PROMPT 為空時,改走「取得登入帳號 email」分支
|
||||||
if [ -z "$PROMPT" ]; then
|
if [ -z "$PROMPT" ]; then
|
||||||
# 呼叫 codex_account.py 取得目前登入帳號的 email($GITHUB_ACTION_PATH 為 action 根目錄)
|
# 呼叫 codex_account.js 取得目前登入帳號的 email($GITHUB_ACTION_PATH 為 action 根目錄)
|
||||||
email="$(python3 "$GITHUB_ACTION_PATH/app/codex_account.py")"
|
email="$(node "$GITHUB_ACTION_PATH/app/codex_account.js")"
|
||||||
# 將 email 寫入 text 輸出($GITHUB_OUTPUT 為步驟輸出檔)
|
# 將 email 寫入 text 輸出($GITHUB_OUTPUT 為步驟輸出檔)
|
||||||
echo "text=$email" >> "$GITHUB_OUTPUT"
|
echo "text=$email" >> "$GITHUB_OUTPUT"
|
||||||
# 以 0 正常退出,不再執行 codex exec
|
# 以 0 正常退出,不再執行 codex exec
|
||||||
|
|||||||
Executable
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// 透過 codex app-server 的 JSON-RPC account/read 取得目前登入帳號的 email。
|
||||||
|
//
|
||||||
|
// 這是 TUI `/status` Account 欄位的程式化來源,不解析本地 OAuth token,
|
||||||
|
// 而是由 codex 自身回報登入帳號。將 email 印到 stdout(取不到時印空字串)。
|
||||||
|
|
||||||
|
const { spawn } = require("node:child_process");
|
||||||
|
const readline = require("node:readline");
|
||||||
|
|
||||||
|
const timeoutSeconds = Number(process.argv[2] ?? "25");
|
||||||
|
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
|
||||||
|
console.error("timeout_seconds must be a non-negative number.");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendJson(processHandle, message) {
|
||||||
|
try {
|
||||||
|
processHandle.stdin.write(`${JSON.stringify(message)}\n`);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readAccountEmail() {
|
||||||
|
const processHandle = spawn("codex", ["app-server"], {
|
||||||
|
stdio: ["pipe", "pipe", "ignore"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let settled = false;
|
||||||
|
let timer;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (!processHandle.killed) {
|
||||||
|
processHandle.kill();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.once("exit", cleanup);
|
||||||
|
process.once("SIGINT", () => {
|
||||||
|
cleanup();
|
||||||
|
process.exit(130);
|
||||||
|
});
|
||||||
|
process.once("SIGTERM", () => {
|
||||||
|
cleanup();
|
||||||
|
process.exit(143);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const done = (email = "") => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
cleanup();
|
||||||
|
resolve(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
timer = setTimeout(() => done(""), timeoutSeconds * 1000);
|
||||||
|
|
||||||
|
processHandle.once("error", () => done(""));
|
||||||
|
processHandle.once("exit", () => done(""));
|
||||||
|
processHandle.stdin.once("error", () => done(""));
|
||||||
|
|
||||||
|
const lines = readline.createInterface({
|
||||||
|
input: processHandle.stdout,
|
||||||
|
crlfDelay: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
lines.on("line", (line) => {
|
||||||
|
let message;
|
||||||
|
try {
|
||||||
|
message = JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.id !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = message?.result?.account?.email;
|
||||||
|
done(typeof email === "string" ? email : "");
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
|
method: "initialize",
|
||||||
|
params: { clientInfo: { name: "ci", version: "1.0" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "initialized",
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 2,
|
||||||
|
method: "account/read",
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (!sendJson(processHandle, message)) {
|
||||||
|
done("");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readAccountEmail()
|
||||||
|
.then((email) => {
|
||||||
|
process.stdout.write(`${email}\n`);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
process.stdout.write("\n");
|
||||||
|
});
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"""透過 codex app-server 的 JSON-RPC account/read 取得目前登入帳號的 email。
|
|
||||||
|
|
||||||
這是 TUI `/status` Account 欄位的程式化來源,不解析本地 OAuth token,
|
|
||||||
而是由 codex 自身回報登入帳號。將 email 印到 stdout(取不到時印空字串)。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def read_account_email(timeout_seconds: float = 25.0) -> str:
|
|
||||||
"""透過 codex app-server 的 JSON-RPC account/read 取得目前登入帳號的 email。
|
|
||||||
|
|
||||||
啟動 ``codex app-server`` 子行程,依序送出 initialize / initialized /
|
|
||||||
account/read 三筆 JSON-RPC 訊息,並讀取其回報的登入帳號 email。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
timeout_seconds: 等待 app-server 回應的秒數上限,預設 25.0。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
登入帳號的 email;取不到或逾時時回傳空字串 ""。
|
|
||||||
|
|
||||||
使用情境:
|
|
||||||
在 CI 中驗證登入身分時呼叫。前置條件為 codex CLI 已安裝,
|
|
||||||
且 $HOME/.codex/auth.json 已寫入有效的 OAuth token。
|
|
||||||
"""
|
|
||||||
process = subprocess.Popen(
|
|
||||||
["codex", "app-server"],
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
text=True,
|
|
||||||
bufsize=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
def send(obj):
|
|
||||||
"""將 dict 物件序列化成 JSON 後寫入 app-server 的 stdin 並 flush。"""
|
|
||||||
process.stdin.write(json.dumps(obj) + "\n")
|
|
||||||
process.stdin.flush()
|
|
||||||
|
|
||||||
send({
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "initialize",
|
|
||||||
"params": {"clientInfo": {"name": "ci", "version": "1.0"}},
|
|
||||||
})
|
|
||||||
send({"jsonrpc": "2.0", "method": "initialized", "params": {}})
|
|
||||||
send({"jsonrpc": "2.0", "id": 2, "method": "account/read", "params": {}})
|
|
||||||
|
|
||||||
email = ""
|
|
||||||
deadline = time.time() + timeout_seconds
|
|
||||||
try:
|
|
||||||
while time.time() < deadline:
|
|
||||||
line = process.stdout.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
message = json.loads(line)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if message.get("id") == 2:
|
|
||||||
account = (message.get("result") or {}).get("account") or {}
|
|
||||||
email = account.get("email") or ""
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
process.terminate()
|
|
||||||
|
|
||||||
return email
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print(read_account_email())
|
|
||||||
Reference in New Issue
Block a user