-
Notifications
You must be signed in to change notification settings - Fork 3.6k
fix(weixin-bridge): make the Weixin bridge runnable and simplify its Quick Start #6170
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
base: main
Are you sure you want to change the base?
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,75 @@ | ||
| #!/usr/bin/env bash | ||
| # Start a local Codewhale runtime and the Weixin bridge in one terminal. | ||
| # | ||
| # Generates a shared CODEWHALE_RUNTIME_TOKEN, starts `codewhale serve --http` in | ||
| # the background, waits for it to answer /health, then runs the bridge in the | ||
| # foreground. Both processes share the generated token, so no copy/paste. | ||
| # | ||
| # Ctrl-C stops both. Override defaults with the env vars below: | ||
| # CODEWHALE_RUNTIME_PORT runtime port (default 7878) | ||
| # CODEWHALE_RUNTIME_TOKEN reuse an existing token (default: generated) | ||
| # WEIXIN_ALLOW_UNLISTED first-pairing mode (default true) | ||
| # WEIXIN_STATE_DIR state directory (default: ./.state) | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| bridge_dir="$(cd "$script_dir/.." && pwd)" | ||
|
|
||
| port="${CODEWHALE_RUNTIME_PORT:-7878}" | ||
| runtime_url="http://127.0.0.1:${port}" | ||
|
|
||
| if [[ -z "${CODEWHALE_RUNTIME_TOKEN:-}" ]]; then | ||
| CODEWHALE_RUNTIME_TOKEN="$(openssl rand -hex 32)" | ||
| echo "Generated CODEWHALE_RUNTIME_TOKEN for this session." | ||
| fi | ||
| export CODEWHALE_RUNTIME_TOKEN | ||
|
|
||
| export CODEWHALE_RUNTIME_URL="$runtime_url" | ||
| export WEIXIN_ALLOW_UNLISTED="${WEIXIN_ALLOW_UNLISTED:-true}" | ||
|
Contributor
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. |
||
| export WEIXIN_STATE_DIR="${WEIXIN_STATE_DIR:-$bridge_dir/.state}" | ||
|
|
||
| runtime_pid="" | ||
|
|
||
| cleanup() { | ||
| if [[ -n "$runtime_pid" ]] && kill -0 "$runtime_pid" 2>/dev/null; then | ||
| echo "" | ||
| echo "Stopping runtime (pid $runtime_pid)..." | ||
| kill "$runtime_pid" 2>/dev/null || true | ||
| wait "$runtime_pid" 2>/dev/null || true | ||
| fi | ||
| } | ||
| trap cleanup EXIT INT TERM | ||
|
|
||
| echo "Starting runtime on $runtime_url ..." | ||
| codewhale serve --http \ | ||
| --host 127.0.0.1 \ | ||
| --port "$port" \ | ||
| --auth-token "$CODEWHALE_RUNTIME_TOKEN" & | ||
| runtime_pid=$! | ||
|
|
||
| # Wait for /health before handing over to the bridge, so the first pairing | ||
| # message does not race a runtime that has not bound its port yet. | ||
| for _ in $(seq 1 60); do | ||
| if curl -fsS "$runtime_url/health" >/dev/null 2>&1; then | ||
|
Contributor
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. |
||
| break | ||
| fi | ||
| if ! kill -0 "$runtime_pid" 2>/dev/null; then | ||
| echo "Runtime exited before becoming healthy." >&2 | ||
| exit 1 | ||
| fi | ||
| sleep 0.5 | ||
| done | ||
|
|
||
| if ! curl -fsS "$runtime_url/health" >/dev/null 2>&1; then | ||
| echo "Runtime did not become healthy at $runtime_url/health within 30s." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Runtime is healthy. Starting Weixin bridge..." | ||
| echo "Allow-unlisted (first pairing): $WEIXIN_ALLOW_UNLISTED" | ||
| echo "State dir: $WEIXIN_STATE_DIR" | ||
| echo "" | ||
|
|
||
| cd "$bridge_dir" | ||
| node src/index.mjs | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ import { | |
| activeTurnBlock, | ||
| helpText, | ||
| } from "./lib.mjs"; | ||
| import { renderQrToText } from "./qr.mjs"; | ||
| import { ThreadStore as CoreThreadStore } from "../../bridge-core/src/lib.mjs"; | ||
|
|
||
| // ============================================================================ | ||
|
|
@@ -163,9 +164,15 @@ const config = { | |
| stateDir: | ||
| weixinEnv("WEIXIN_STATE_DIR") || | ||
| "/var/lib/codewhale-weixin-bot-bridge", | ||
| // Defaults inside stateDir rather than to a second absolute path: setting | ||
| // only WEIXIN_STATE_DIR must not leave the thread map pointing at /var/lib, | ||
| // which fails with EACCES on every incoming message and silently drops it. | ||
| threadMapPath: | ||
| weixinEnv("WEIXIN_THREAD_MAP_PATH") || | ||
| "/var/lib/codewhale-weixin-bot-bridge/thread-map.json", | ||
| path.join( | ||
| weixinEnv("WEIXIN_STATE_DIR") || "/var/lib/codewhale-weixin-bot-bridge", | ||
| "thread-map.json" | ||
| ), | ||
| maxReplyChars: Number(weixinEnv("WEIXIN_MAX_REPLY_CHARS") || 3500), | ||
| longPollTimeoutMs: Number( | ||
| weixinEnv("WEIXIN_LONGPOLL_TIMEOUT_MS") || 35000 | ||
|
|
@@ -708,6 +715,9 @@ async function loadSyncBuf(stateDir) { | |
|
|
||
| async function saveSyncBuf(stateDir, buf) { | ||
| const p = resolveSyncBufPath(stateDir); | ||
| // The state dir may not exist yet on a first run whose first persisted write | ||
| // is the poll cursor rather than account.json. | ||
| await fs.mkdir(path.dirname(p), { recursive: true, mode: 0o700 }); | ||
| const tmp = `${p}.tmp`; | ||
| await fs.writeFile(tmp, buf, { mode: 0o600 }); | ||
| await fs.rename(tmp, p); | ||
|
|
@@ -870,8 +880,26 @@ async function main() { | |
| console.log(`Runtime: ${config.runtimeUrl}`); | ||
| console.log(`Workspace: ${config.workspace}`); | ||
| console.log(`State dir: ${config.stateDir}`); | ||
| console.log(`Thread map: ${config.threadMapPath}`); | ||
|
|
||
| // 初始化 ThreadStore | ||
| // 初始化 ThreadStore。`open()` 只读,真正的写入发生在第一条消息到达时; | ||
| // 那时失败会被 getUpdates 的 catch 吞掉,表现为“微信没有回应”。所以这里 | ||
| // 先建目录并真实写一次探针文件,把问题在启动时就暴露出来。 | ||
| try { | ||
| const dir = path.dirname(config.threadMapPath); | ||
| await fs.mkdir(dir, { recursive: true, mode: 0o700 }); | ||
| const probe = path.join(dir, ".write-probe"); | ||
| await fs.writeFile(probe, "", { mode: 0o600 }); | ||
| await fs.rm(probe, { force: true }); | ||
|
Comment on lines
+889
to
+893
Contributor
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. 🟡 State directory escapes startup probe When Learn moreThe bridge writes the thread map to Example: Set Recommended fix: Probe both Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| } catch (error) { | ||
| console.error( | ||
| `Thread map directory is not writable: ${path.dirname(config.threadMapPath)} (${error.message})` | ||
| ); | ||
| console.error( | ||
| "Set WEIXIN_STATE_DIR (or WEIXIN_THREAD_MAP_PATH) to a writable directory." | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| threadStore = await ThreadStore.open(config.threadMapPath); | ||
|
|
||
| // 尝试加载已有账号 | ||
|
|
@@ -888,6 +916,13 @@ async function main() { | |
|
|
||
| const { qrcodeUrl, sessionKey } = await getLoginQR(); | ||
| console.log("请用微信扫描以下二维码登录:"); | ||
| // Render the login URL as a scannable terminal QR. The URL is printed too, | ||
| // so a terminal that mangles the half-block glyphs still has a way through. | ||
| try { | ||
| if (qrcodeUrl) console.log(renderQrToText(qrcodeUrl)); | ||
| } catch (error) { | ||
| console.warn(`Could not render QR in terminal: ${error.message}`); | ||
| } | ||
| console.log(qrcodeUrl); | ||
| console.log(""); | ||
|
|
||
|
|
||
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.
🟡 Two-terminal startup requires root
A non-root user running
npm startleavesWEIXIN_STATE_DIRunder/var/lib. The startup probe gets EACCES and exits before login.Was this helpful? React with 👍 or 👎 to provide feedback.