-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(env): support boe/pre/online environment switching #2006
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| #!/usr/bin/env bash | ||
| # Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # larkenv —— 一个脚本搞定 lark-cli 的 boe / pre / online 环境切换。 | ||
| # | ||
| # ./env/larkenv setup 编译 + 安装 + 自动配 PATH(只需跑这一次) | ||
| # larkenv init boe <app-id> 一条龙:配 app(不弹选择)+ 固定 user 身份 + 登录 | ||
| # larkenv init boe 同上,但走交互式选 app | ||
| # larkenv login boe 只补登录(app 已配好时用),自动出二维码 | ||
| # larkenv qr <登录URL> 手动把某个 URL 转成终端二维码 | ||
| # larkenv boe <任何 lark-cli 命令> | ||
| # larkenv pre <任何 lark-cli 命令> | ||
| # larkenv online <任何 lark-cli 命令> | ||
| # | ||
| # 可选: | ||
| # LARK_LANE=<泳道> larkenv boe ... 指定泳道(注入 X-TT-ENV 头) | ||
| # LARK_CLI_SHOW_LOGID=1 larkenv ... 打印所有请求的 x-tt-logid(默认只在出错时打) | ||
| # | ||
| # 三个环境的配置和登录态完全隔离在 ~/.lark-cli-env/config/{boe,pre,online},互不影响。 | ||
| set -euo pipefail | ||
|
|
||
| BIN_DIR="${LARK_CLI_ENV_BIN:-$HOME/.local/bin}" | ||
| CONFIG_ROOT="$HOME/.lark-cli-env" | ||
| BIN="$BIN_DIR/lark-cli-env" | ||
|
|
||
| usage() { | ||
| # Print the header comment block (from the title line to the first | ||
| # non-comment line), stripped of its leading "# ". | ||
| awk 'NR<5{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0" | ||
| exit "${1:-0}" | ||
| } | ||
|
|
||
| # ---------- setup:编译 + 安装 + 配 PATH ---------- | ||
| do_setup() { | ||
| local repo_root | ||
| repo_root="$(cd "$(dirname "$0")/.." && pwd)" | ||
|
|
||
| echo "==> 编译 lark-cli ..." | ||
| (cd "$repo_root" && ./build.sh) | ||
|
|
||
| echo "==> 安装到 $BIN_DIR ..." | ||
| mkdir -p "$BIN_DIR" "$CONFIG_ROOT/config" | ||
| cp "$repo_root/lark-cli" "$BIN" | ||
| cp "$repo_root/env/larkenv" "$BIN_DIR/larkenv" | ||
| chmod +x "$BIN" "$BIN_DIR/larkenv" | ||
|
|
||
| # 幂等地把 BIN_DIR 加进 shell rc | ||
| local rc marker='# added by lark-cli env/larkenv' | ||
| case "${SHELL##*/}" in | ||
| zsh) rc="$HOME/.zshrc" ;; | ||
| bash) rc="$HOME/.bashrc" ;; | ||
| *) rc="" ;; | ||
| esac | ||
| if [ -n "$rc" ] && ! grep -qF "$marker" "$rc" 2>/dev/null; then | ||
| printf '\n%s\nexport PATH="%s:$PATH"\n' "$marker" "$BIN_DIR" >>"$rc" | ||
| echo "==> 已把 $BIN_DIR 写入 $rc" | ||
| fi | ||
|
|
||
| # NOTE: brace the expansions below — the surrounding full-width punctuation | ||
| # would otherwise be swallowed into the variable name by bash. | ||
| # Plain `[ … ] && x=y` would return non-zero on the else path and trip set -e. | ||
| local reload="新开一个终端" | ||
| if [ -n "$rc" ]; then | ||
| reload="新开一个终端(或 source ${rc})" | ||
| fi | ||
|
|
||
| cat <<EOF | ||
|
|
||
| 安装完成。${reload},然后: | ||
|
|
||
| larkenv init boe # 首次:配 app + 固定 user 身份 + 扫码登录 | ||
| larkenv init pre <上面拿到的 app-id> # 其他环境复用同一个 app,不用重配 | ||
| larkenv login boe # 登录态失效时,只补登录 | ||
|
|
||
| larkenv boe base +table-list --base-token <t> | ||
| larkenv pre wiki +node-get --token <url> | ||
| larkenv online drive +search --query xxx | ||
|
|
||
| EOF | ||
| } | ||
|
|
||
| # ---------- 环境变量注入 ---------- | ||
| apply_env() { | ||
| export LARKSUITE_CLI_CONFIG_DIR="$CONFIG_ROOT/config/$1" | ||
| case "$1" in | ||
| boe) | ||
| export LARKSUITE_CLI_ENDPOINT_DOMAIN="feishu-boe.cn" | ||
| # boe 是内网域名,必须绕开公司外部 relay 代理 | ||
| export LARK_CLI_NO_PROXY=1 | ||
| ;; | ||
| pre) | ||
| export LARKSUITE_CLI_ENDPOINT_DOMAIN="feishu-pre.cn" | ||
| ;; | ||
| online) | ||
| # 不覆盖 endpoint,走开源默认 feishu.cn / larksuite.com;仅隔离配置目录 | ||
| ;; | ||
| esac | ||
| # Append rather than overwrite, so a caller-supplied LARKSUITE_CLI_EXTRA_HEADERS | ||
| # (e.g. "x-use-boe: 1") survives alongside LARK_LANE. | ||
| if [ -n "${LARK_LANE:-}" ]; then | ||
| if [ -n "${LARKSUITE_CLI_EXTRA_HEADERS:-}" ]; then | ||
| export LARKSUITE_CLI_EXTRA_HEADERS="${LARKSUITE_CLI_EXTRA_HEADERS}; X-TT-ENV: $LARK_LANE" | ||
| else | ||
| export LARKSUITE_CLI_EXTRA_HEADERS="X-TT-ENV: $LARK_LANE" | ||
| fi | ||
| fi | ||
| mkdir -p "$LARKSUITE_CLI_CONFIG_DIR" | ||
| } | ||
|
|
||
| # json_field extracts a top-level string field from a JSON object on stdin. | ||
| json_field() { | ||
| if command -v jq >/dev/null 2>&1; then | ||
| jq -r --arg k "$1" '.[$k] // empty' | ||
| else | ||
| python3 -c 'import json,sys; print(json.load(sys.stdin).get(sys.argv[1],""))' "$1" | ||
| fi | ||
| } | ||
|
|
||
| # do_login runs the device flow but renders the verification URL as a terminal | ||
| # QR code before polling. Scanning with your own Feishu app avoids authorizing | ||
| # as whichever account the default browser happens to be signed in as — the | ||
| # usual cause of "用户登录态无效 (20033)" on a shared machine. | ||
| do_login() { | ||
| local json url code | ||
| json="$("$BIN" auth login --recommend --no-wait --json)" | ||
| url="$(printf '%s' "$json" | json_field verification_url)" | ||
| code="$(printf '%s' "$json" | json_field device_code)" | ||
|
|
||
| if [ -z "$url" ] || [ -z "$code" ]; then | ||
| echo "无法解析登录信息,原始输出:" >&2 | ||
| printf '%s\n' "$json" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| echo "" >&2 | ||
| echo "用你自己的手机飞书扫下面的码(别用浏览器,容易串成别人的账号):" >&2 | ||
| echo "" >&2 | ||
| "$BIN" auth qrcode --ascii "$url" | ||
| echo "" >&2 | ||
| echo "扫不了的话再用链接:$url" >&2 | ||
| echo "" >&2 | ||
|
|
||
| "$BIN" auth login --device-code "$code" | ||
| } | ||
|
|
||
| require_bin() { | ||
| [ -x "$BIN" ] || { | ||
| echo "还没安装,先在仓库根目录跑一次:./env/larkenv setup" >&2 | ||
| exit 1 | ||
| } | ||
| } | ||
|
|
||
| # ---------- 入口 ---------- | ||
| [ $# -ge 1 ] || usage 1 | ||
|
|
||
| case "$1" in | ||
| setup) | ||
| do_setup | ||
| ;; | ||
| qr) | ||
| # Renders the device-flow URL as a terminal QR code. The CLI never opens a | ||
| # browser itself, so scanning with your own Feishu app is the way to avoid | ||
| # authorizing as whichever account the default browser happens to hold. | ||
| [ $# -eq 2 ] || { | ||
| echo "用法: larkenv qr <登录URL>" >&2 | ||
| exit 1 | ||
| } | ||
| require_bin | ||
| exec "$BIN" auth qrcode --ascii "$2" | ||
| ;; | ||
| init) | ||
| # With an app id, config init runs fully non-interactively — no app-selection | ||
| # prompt at all. boe/pre can reuse the online app id, so there is usually no | ||
| # reason to register a separate app per environment. | ||
| case $# in | ||
| 2 | 3) ;; | ||
| *) | ||
| echo "用法: larkenv init <boe|pre|online> [app-id]" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
| require_bin | ||
| apply_env "$2" | ||
|
|
||
| if [ $# -eq 3 ]; then | ||
| printf 'App Secret (输入不回显): ' >&2 | ||
| stty -echo 2>/dev/null || true | ||
| read -r secret | ||
| stty echo 2>/dev/null || true | ||
| printf '\n' >&2 | ||
| # --app-secret-stdin keeps the secret out of the process list. | ||
| printf '%s' "$secret" | "$BIN" config init --app-id "$3" --app-secret-stdin | ||
| unset secret | ||
|
Comment on lines
+187
to
+194
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Preserve the secret and always restore terminal echo.
🤖 Prompt for AI Agents |
||
| else | ||
| echo "提示:已有 app 时可直接 'larkenv init $2 <app-id>' 跳过选择流程。" >&2 | ||
| "$BIN" config init | ||
| fi | ||
|
|
||
| # 固定成 user 身份,之后所有命令都不用再带 --as user。 | ||
| "$BIN" config default-as user | ||
| do_login | ||
| ;; | ||
| login) | ||
| # 单独补登录:app 配好了、只是授权失败或过期时用这个,不用重跑 init。 | ||
| [ $# -eq 2 ] || { | ||
| echo "用法: larkenv login <boe|pre|online>" >&2 | ||
| exit 1 | ||
| } | ||
| require_bin | ||
| apply_env "$2" | ||
| do_login | ||
| ;; | ||
| boe | pre | online) | ||
| require_bin | ||
| env_name="$1" | ||
| shift | ||
| apply_env "$env_name" | ||
| exec "$BIN" "$@" | ||
| ;; | ||
| -h | --help | help) | ||
| usage 0 | ||
| ;; | ||
| *) | ||
| echo "未知命令 '$1'(可用: boe / pre / online / setup / init / login / qr)" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package cmdutil | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "os" | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "github.com/larksuite/cli/internal/transport" | ||
| ) | ||
|
|
||
| // DebugHeadersEnv, when set to a truthy value, makes DebugHeaderTransport dump | ||
| // the outbound request line and headers to stderr. It sits at the innermost | ||
| // position of the transport chain, so what it prints is what actually goes on | ||
| // the wire — after every other layer has contributed its headers. | ||
| const DebugHeadersEnv = "LARK_CLI_DEBUG_HEADERS" | ||
|
|
||
| // sensitiveHeaders are redacted in the dump; their presence is still reported. | ||
| var sensitiveHeaders = map[string]bool{ | ||
| "authorization": true, | ||
| "x-lark-mcp-uat": true, | ||
| "x-lark-mcp-tat": true, | ||
| "cookie": true, | ||
| "x-larksuite-key": true, | ||
| } | ||
|
Comment on lines
+23
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Redact A request carrying that standard credential header is printed verbatim when debug logging is enabled, contradicting the redaction guarantee. Add it to 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| // DebugHeaderTransport dumps outbound request headers when DebugHeadersEnv is set. | ||
| type DebugHeaderTransport struct { | ||
| Base http.RoundTripper | ||
| Out io.Writer // defaults to os.Stderr | ||
| } | ||
|
|
||
| func (t *DebugHeaderTransport) base() http.RoundTripper { | ||
| if t.Base != nil { | ||
| return t.Base | ||
| } | ||
| return transport.Fallback() | ||
| } | ||
|
|
||
| func (t *DebugHeaderTransport) out() io.Writer { | ||
| if t.Out != nil { | ||
| return t.Out | ||
| } | ||
| return os.Stderr | ||
| } | ||
|
|
||
| func debugHeadersEnabled() bool { | ||
| v := strings.TrimSpace(os.Getenv(DebugHeadersEnv)) | ||
| return v != "" && v != "0" | ||
| } | ||
|
|
||
| // RoundTrip implements http.RoundTripper. | ||
| func (t *DebugHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| if !debugHeadersEnabled() { | ||
| return t.base().RoundTrip(req) | ||
| } | ||
|
|
||
| var b strings.Builder | ||
| fmt.Fprintf(&b, "[lark-cli] --> %s %s\n", req.Method, req.URL.String()) | ||
|
|
||
| keys := make([]string, 0, len(req.Header)) | ||
| for k := range req.Header { | ||
| keys = append(keys, k) | ||
| } | ||
| sort.Strings(keys) | ||
| for _, k := range keys { | ||
| val := strings.Join(req.Header[k], ", ") | ||
| if sensitiveHeaders[strings.ToLower(k)] { | ||
| val = fmt.Sprintf("<redacted, %d chars>", len(val)) | ||
| } | ||
| fmt.Fprintf(&b, "[lark-cli] %s: %s\n", k, val) | ||
| } | ||
| fmt.Fprint(t.out(), b.String()) | ||
|
|
||
| return t.base().RoundTrip(req) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package cmdutil | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "os" | ||
| "strings" | ||
|
|
||
| "github.com/larksuite/cli/internal/transport" | ||
| ) | ||
|
|
||
| // ExtraHeadersEnv is the environment variable carrying extra request headers, | ||
| // formatted as "Key: Value" pairs separated by ";" | ||
| // (e.g. "X-TT-ENV: boe_lane; X-Other: 1"). Primarily used to select a | ||
| // boe/pre swimlane via X-TT-ENV. | ||
| const ExtraHeadersEnv = "LARKSUITE_CLI_EXTRA_HEADERS" | ||
|
|
||
| // parseExtraHeaders parses an ExtraHeadersEnv value into a header map. | ||
| // Entries without a colon are skipped. | ||
| func parseExtraHeaders(raw string) map[string]string { | ||
| out := map[string]string{} | ||
| for _, part := range strings.Split(raw, ";") { | ||
| i := strings.Index(part, ":") | ||
| if i < 0 { | ||
| continue | ||
| } | ||
| k := strings.TrimSpace(part[:i]) | ||
| v := strings.TrimSpace(part[i+1:]) | ||
| if k != "" { | ||
| out[k] = v | ||
| } | ||
| } | ||
| return out | ||
|
Comment on lines
+22
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Reject malformed extra-header configuration instead of silently changing it. Malformed segments and blank keys are currently discarded, so a misspelled lane header can send requests without the requested routing. Return a typed validation error and test that failure.
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
|
|
||
| // EnvHeaderTransport is an http.RoundTripper that injects headers declared in | ||
| // ExtraHeadersEnv into every request. It is a no-op when the variable is unset. | ||
| type EnvHeaderTransport struct { | ||
| Base http.RoundTripper | ||
| } | ||
|
|
||
| func (t *EnvHeaderTransport) base() http.RoundTripper { | ||
| if t.Base != nil { | ||
| return t.Base | ||
| } | ||
| return transport.Fallback() | ||
| } | ||
|
|
||
| // RoundTrip implements http.RoundTripper. | ||
| func (t *EnvHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| headers := parseExtraHeaders(os.Getenv(ExtraHeadersEnv)) | ||
| if len(headers) == 0 { | ||
| return t.base().RoundTrip(req) | ||
| } | ||
| req = req.Clone(req.Context()) | ||
| for k, v := range headers { | ||
| req.Header.Set(k, v) | ||
| } | ||
| return t.base().RoundTrip(req) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Reject unsupported environment names.
initandloginaccept arbitrary values here. For example,init ../../tmpescapesCONFIG_ROOT, whileinit stagingsilently uses online endpoints. Restrict this function toboe,pre, andonline, returning a failure for anything else.🤖 Prompt for AI Agents