Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ frontend/out/
*.spec.tsx
__tests__/
WEBUI_DEVELOPMENT_GUIDE.md

# Rust FFI build artifacts (v2 wreq-ffi)
wreq-ffi/target/
wreq-ffi/include/wreq_ffi.h
wreq-ffi/Cargo.lock
135 changes: 110 additions & 25 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,91 @@
FROM node:22-bookworm AS frontend-builder
FROM --platform=$BUILDPLATFORM node:22-bookworm AS frontend-builder

WORKDIR /frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend ./
RUN npm run build

FROM golang:1.22-bookworm AS builder
FROM --platform=$BUILDPLATFORM rust:1.86-bookworm AS rust-builder
ARG BUILDPLATFORM
ARG TARGETPLATFORM
ARG TARGETARCH
ARG TARGETOS=linux
WORKDIR /src

RUN apt-get update -o Acquire::Retries=5 \
&& apt-get install -y -o Acquire::Retries=5 --no-install-recommends \
cmake perl build-essential libclang-dev clang lld file \
gcc-x86-64-linux-gnu g++-x86-64-linux-gnu \
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
&& rm -rf /var/lib/apt/lists/*

RUN set -eux; \
case "${TARGETARCH}" in \
amd64) RUST_TARGET=x86_64-unknown-linux-gnu ;; \
arm64) RUST_TARGET=aarch64-unknown-linux-gnu ;; \
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
esac; \
rustup target add "${RUST_TARGET}"; \
echo "${RUST_TARGET}" > /tmp/rust_target

ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc \
CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc \
CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++ \
AR_x86_64_unknown_linux_gnu=x86_64-linux-gnu-ar \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \
CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ \
AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar

ENV CARGO_TARGET_DIR=/cargo-target

COPY wreq-ffi ./wreq-ffi

RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/cargo-target,id=cargo-target-${TARGETARCH},sharing=locked \
set -eux; \
RUST_TARGET=$(cat /tmp/rust_target); \
case "${TARGETARCH}" in \
amd64) CC=x86_64-linux-gnu-gcc; CXX=x86_64-linux-gnu-g++; AR=x86_64-linux-gnu-ar ;; \
arm64) CC=aarch64-linux-gnu-gcc; CXX=aarch64-linux-gnu-g++; AR=aarch64-linux-gnu-ar ;; \
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
esac; \
export CC CXX AR; \
echo "rust-builder toolchain: TARGETARCH=${TARGETARCH} RUST_TARGET=${RUST_TARGET} CC=${CC} CXX=${CXX} AR=${AR}"; \
echo "rust-builder diag: BUILDPLATFORM=${BUILDPLATFORM} TARGETPLATFORM=${TARGETPLATFORM} TARGETARCH=${TARGETARCH} RUST_TARGET=${RUST_TARGET} host=$(uname -m)"; \
cd wreq-ffi; \
mkdir -p include; \
touch src/lib.rs; \
cargo build --release --target "${RUST_TARGET}"; \
test -f include/wreq_ffi.h; \
mkdir -p /out; \
cp "${CARGO_TARGET_DIR}/${RUST_TARGET}/release/libwreq_ffi.a" /out/; \
cp include/wreq_ffi.h /out/; \
FIRST_MEMBER=$(ar t /out/libwreq_ffi.a | head -1); \
AFILE=$(ar p /out/libwreq_ffi.a "$FIRST_MEMBER" | file -); \
echo "rust-builder: first member ($FIRST_MEMBER) of /out/libwreq_ffi.a => ${AFILE}"; \
case "${TARGETARCH}" in \
amd64) echo "${AFILE}" | grep -q 'x86-64' || { echo "FATAL: /out/libwreq_ffi.a is not x86-64 (TARGETARCH=amd64). This usually means a cache mount got mixed up; try: docker buildx prune -af" >&2; exit 1; } ;; \
arm64) echo "${AFILE}" | grep -q 'aarch64' || { echo "FATAL: /out/libwreq_ffi.a is not aarch64 (TARGETARCH=arm64). This usually means a cache mount got mixed up; try: docker buildx prune -af" >&2; exit 1; } ;; \
esac; \
echo "rust-builder: arch verified for TARGETARCH=${TARGETARCH}"

FROM --platform=$BUILDPLATFORM golang:1.22-bookworm AS builder
ARG BUILDPLATFORM
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH

RUN apt-get update -o Acquire::Retries=5 \
&& apt-get install -y -o Acquire::Retries=5 --no-install-recommends \
file \
gcc-x86-64-linux-gnu g++-x86-64-linux-gnu \
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /src
ARG TARGETOS=linux
ARG TARGETARCH=amd64
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
Expand All @@ -20,37 +95,47 @@ COPY cmd ./cmd
COPY internal ./internal
COPY static ./static
COPY --from=frontend-builder /frontend/out /src/static/admin
COPY --from=rust-builder /out/libwreq_ffi.a /src/wreq-ffi/target/release/libwreq_ffi.a
COPY --from=rust-builder /out/wreq_ffi.h /src/wreq-ffi/include/wreq_ffi.h

RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
test -f ./cmd/notion2api/main.go \
&& CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -v -trimpath -ldflags="-s -w" -o /out/notion2api ./cmd/notion2api
set -eux; \
case "${TARGETARCH}" in \
amd64) CC=x86_64-linux-gnu-gcc; CXX=x86_64-linux-gnu-g++ ;; \
arm64) CC=aarch64-linux-gnu-gcc; CXX=aarch64-linux-gnu-g++ ;; \
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
esac; \
echo "go-builder diag: BUILDPLATFORM=${BUILDPLATFORM} TARGETPLATFORM=${TARGETPLATFORM} TARGETARCH=${TARGETARCH} CC=${CC} host=$(uname -m)"; \
FIRST_MEMBER=$(ar t /src/wreq-ffi/target/release/libwreq_ffi.a | head -1); \
AFILE=$(ar p /src/wreq-ffi/target/release/libwreq_ffi.a "$FIRST_MEMBER" | file -); \
echo "go-builder: first member ($FIRST_MEMBER) of libwreq_ffi.a => ${AFILE}"; \
case "${TARGETARCH}" in \
amd64) echo "${AFILE}" | grep -q 'x86-64' || { echo "FATAL: libwreq_ffi.a in builder stage is not x86-64; rust-builder produced wrong arch or COPY layer is stale. Run: docker buildx prune -af" >&2; exit 1; } ;; \
arm64) echo "${AFILE}" | grep -q 'aarch64' || { echo "FATAL: libwreq_ffi.a in builder stage is not aarch64; rust-builder produced wrong arch or COPY layer is stale. Run: docker buildx prune -af" >&2; exit 1; } ;; \
esac; \
test -f ./cmd/notion2api/main.go; \
CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} CC=${CC} CXX=${CXX} \
go build -v -trimpath -tags wreq_ffi \
-ldflags="-s -w" \
-o /out/notion2api ./cmd/notion2api

FROM mcr.microsoft.com/playwright:v1.58.0-noble
FROM node:22-bookworm-slim

ENV TZ=Asia/Shanghai
ENV PLAYWRIGHT_CHROME_BIN=/opt/playwright-helper/chrome
ENV CHROME_BIN=/opt/playwright-helper/chrome
ENV CHROMIUM_BIN=/opt/playwright-helper/chrome
ENV NODE_PATH=/opt/playwright-helper/node_modules
ENV NODE_PATH=/opt/notion2api-helper/node_modules
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata curl tini \
&& rm -rf /var/lib/apt/lists/* \
&& browser_bin="" \
&& for candidate in /ms-playwright/chromium-*/chrome-linux64/chrome /ms-playwright/chromium-*/chrome-linux/chrome; do \
if [ -x "$candidate" ]; then browser_bin="$candidate"; break; fi; \
done \
&& test -n "$browser_bin" \
&& mkdir -p /opt/playwright-helper /app/config /app/data/notion_accounts /app/static \
&& ln -sf "$browser_bin" "$PLAYWRIGHT_CHROME_BIN" \
&& "$PLAYWRIGHT_CHROME_BIN" --version

RUN mkdir -p /opt/playwright-helper \
&& cd /opt/playwright-helper \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \
&& mkdir -p /opt/notion2api-helper /app/config /app/data/notion_accounts /app/static

RUN cd /opt/notion2api-helper \
&& npm init -y >/dev/null 2>&1 \
&& npm install --omit=dev --no-package-lock playwright-core@1.58.0 \
&& test -d "$NODE_PATH/playwright-core" \
&& npm install --omit=dev --no-package-lock node-wreq@2.2.1 \
&& test -d "$NODE_PATH/node-wreq" \
&& npm cache clean --force >/dev/null 2>&1

COPY --from=builder /out/notion2api /app/notion2api
Expand Down
82 changes: 69 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@

### 本地运行

```powershell
Set-Location 'E:\WorkSpace\sub2api\chatgpt_register\Nation2API'
& 'D:\Go\bin\go.exe' run .\cmd\notion2api --config .\config.example.json
```bash
go run ./cmd/notion2api --config ./config.example.json
```

### 本地构建

```powershell
Set-Location 'E:\WorkSpace\sub2api\chatgpt_register\Nation2API'
& 'D:\Go\bin\go.exe' build .\cmd\notion2api
```bash
go build ./cmd/notion2api
```

## Docker 部署
Expand All @@ -47,17 +45,75 @@ docker compose -f docker-compose.prod.yml up -d --build
- Health:`http://127.0.0.1:8787/healthz`
- WebUI:`http://127.0.0.1:8787/admin`

## 代理与 Resin 粘性代理

### 代理模式

`proxy_mode` 支持:

- `off`:关闭代理
- `env`:从环境变量读取(优先 `N2A_*`)
- `http`:固定 HTTP 代理
- `https`:按协议拆分 HTTP/HTTPS 代理
- `socks5`:SOCKS5/SOCKS5H 代理
- `resin_forward`:Resin 粘性代理转发

### 环境变量优先级(`proxy_mode=env`)

HTTPS 请求优先顺序:

1. `N2A_PROXY_HTTPS_URL`
2. `N2A_UPSTREAM_PROXY_HTTPS_URL`
3. `N2A_PROXY_URL`
4. `N2A_UPSTREAM_PROXY_URL`
5. `HTTPS_PROXY` / `https_proxy`
6. `ALL_PROXY` / `all_proxy`

HTTP 请求优先顺序:

1. `N2A_PROXY_HTTP_URL`
2. `N2A_UPSTREAM_PROXY_HTTP_URL`
3. `N2A_PROXY_URL`
4. `N2A_UPSTREAM_PROXY_URL`
5. `HTTP_PROXY` / `http_proxy`
6. `ALL_PROXY` / `all_proxy`

也可以直接用环境变量覆盖配置文件中的代理字段:

- `N2A_PROXY_MODE`
- `N2A_PROXY_URL`
- `N2A_PROXY_HTTP_URL`
- `N2A_PROXY_HTTPS_URL`
- `N2A_RESIN_ENABLED`
- `N2A_RESIN_URL`
- `N2A_RESIN_PLATFORM`
- `N2A_RESIN_MODE`

### Resin 粘性代理(按账号隔离)

每个账号都可以独立设置粘性身份:

- `accounts[].sticky_proxy_account`:显式设置粘性账号名(推荐)
- 未设置时会回退到邮箱派生值

当启用 `resin_forward` 时:

- 代理认证用户名格式:`<resin_platform>.<sticky_proxy_account>`
- 密码使用 `resin_url` 中 token
- 请求会附带 `X-Resin-Account` 头

## 配置说明

建议优先检查这些字段:

- `api_key`:OpenAI 兼容接口密钥
- `admin.password`:WebUI 登录密码
- `upstream_base_url`:上游站点地址
- `upstream_origin`:上游请求 `Origin`
- `accounts`:账号池配置
- `active_account`:默认激活账号
- `storage.sqlite_path`:SQLite 数据库路径
- `upstream_base_url` / `upstream_origin`
- `proxy_mode` / `proxy_url` / `proxy_http_url` / `proxy_https_url`
- `resin_enabled` / `resin_url` / `resin_platform` / `resin_mode`
- `accounts[*].sticky_proxy_account`
- `accounts` / `active_account`
- `storage.sqlite_path`

可直接参考:

Expand All @@ -67,8 +123,8 @@ docker compose -f docker-compose.prod.yml up -d --build
## 使用建议

- 首次启动后先访问 `/admin`,确认账号、配置和连通性是否正常
- 常规本地使用直接运行二进制或 `go run` 即可
- 需要容器化部署时优先使用 Docker Compose
- 修改管理台前端后需执行 `npm --prefix ./frontend run build:static`
- 调整会话延续与存储时,建议同步检查 `internal/app/sqlite_store.go` 的 schema 与迁移兼容性

## 开源协议

Expand Down
8 changes: 7 additions & 1 deletion cmd/notion2api/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package main

import "notion2api/internal/app"
import (
"log"

"notion2api/internal/app"
"notion2api/internal/wreq"
)

func main() {
log.Printf("notion2api: wreq backend = %s", wreq.Version())
app.Main()
}
60 changes: 41 additions & 19 deletions config.example.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
{
"probe_json": "C:\\Users\\GALIAIS\\AppData\\Local\\Temp\\TestAdminAccountManualImportCreatesSessionFiles722969220\\001\\manual_example_com\\probe.json",
"probe_json": "probe_files/default/probe.json",
"host": "127.0.0.1",
"port": 8791,
"api_key": "notion2api-local-key",
"port": 8787,
"api_key": "change-me-openai-key",
"upstream_base_url": "https://www.notion.so",
"upstream_origin": "https://www.notion.so",
"upstream_host_header": "",
"upstream_tls_server_name": "",
"upstream_use_env_proxy": false,
"proxy_mode": "off",
"proxy_url": "",
"proxy_http_url": "",
"proxy_https_url": "",
"resin_enabled": false,
"resin_url": "",
"resin_platform": "Default",
"resin_mode": "forward",
"model_id": "auto",
"default_model": "auto",
"active_account": "manual@example.com",
"active_account": "",
"timeout_sec": 180,
"poll_interval_sec": 1.5,
"poll_max_rounds": 40,
Expand Down Expand Up @@ -60,24 +71,35 @@
},
"accounts": [
{
"email": "manual@example.com",
"probe_json": "C:\\Users\\GALIAIS\\AppData\\Local\\Temp\\TestAdminAccountManualImportCreatesSessionFiles722969220\\001\\manual_example_com\\probe.json",
"profile_dir": "C:\\Users\\GALIAIS\\AppData\\Local\\Temp\\TestAdminAccountManualImportCreatesSessionFiles722969220\\001\\manual_example_com",
"storage_state_path": "C:\\Users\\GALIAIS\\AppData\\Local\\Temp\\TestAdminAccountManualImportCreatesSessionFiles722969220\\001\\manual_example_com\\storage_state.json",
"pending_state_path": "C:\\Users\\GALIAIS\\AppData\\Local\\Temp\\TestAdminAccountManualImportCreatesSessionFiles722969220\\001\\manual_example_com\\pending_login.json",
"user_id": "user-1",
"user_name": "manual",
"space_id": "space-1",
"space_name": "manual's Space",
"client_version": "23.13.test",
"status": "expired",
"last_error": "open C:\\Users\\GALIAIS\\AppData\\Local\\Temp\\TestAdminAccountManualImportCreatesSessionFiles722969220\\001\\manual_example_com\\probe.json: The system cannot find the path specified.",
"last_login_at": "2026-03-22T14:30:44+08:00"
"email": "alice@example.com",
"probe_json": "probe_files/notion_accounts/alice_example_com/probe.json",
"profile_dir": "probe_files/notion_accounts/alice_example_com",
"storage_state_path": "probe_files/notion_accounts/alice_example_com/storage_state.json",
"pending_state_path": "probe_files/notion_accounts/alice_example_com/pending_login.json",
"proxy_mode": "resin_forward",
"sticky_proxy_account": "alice",
"resin_enabled": true,
"resin_url": "http://127.0.0.1:2260/your-resin-token",
"resin_platform": "Default",
"resin_mode": "forward",
"priority": 100,
"disabled": false
},
{
"email": "bob@example.com",
"probe_json": "probe_files/notion_accounts/bob_example_com/probe.json",
"profile_dir": "probe_files/notion_accounts/bob_example_com",
"storage_state_path": "probe_files/notion_accounts/bob_example_com/storage_state.json",
"pending_state_path": "probe_files/notion_accounts/bob_example_com/pending_login.json",
"proxy_mode": "socks5",
"proxy_url": "socks5://127.0.0.1:1080",
"priority": 50,
"disabled": false
}
],
"model_aliases": {
"gpt52": "gpt-5.2",
"gpt54": "gpt-5.4"
"gpt54": "gpt-5.4",
"gpt52": "gpt-5.2"
}
}

Loading
Loading