From d35ee15680bc374da85b42e2f0f9847e36a2e8d0 Mon Sep 17 00:00:00 2001 From: fakeminjun7321 Date: Thu, 3 Sep 2026 19:47:37 +0900 Subject: [PATCH] =?UTF-8?q?=EC=88=98=EC=A0=95:=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=85=EB=A0=A5=20=EA=B2=BD=EA=B3=84=EC=99=80=20?= =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4=20=ED=91=9C=EC=8B=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=ED=95=98=EA=B3=A0=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 37 +++++++ README.md | 16 +++ docs/assistant-backend.md | 9 +- next.config.ts | 15 ++- open-next.config.ts | 5 +- package-lock.json | 6 +- package.json | 2 +- src/app/api/assistant/route.ts | 16 +-- src/components/Footer.tsx | 6 +- src/components/Nav.tsx | 46 ++++----- src/components/assistant/AssistantClient.tsx | 16 +-- src/components/assistant/types.ts | 28 ++--- src/lib/assistant/request.ts | 28 +++++ src/lib/assistant/security.ts | 3 + src/lib/assistant/workers-ai.ts | 2 + src/lib/site-metadata.ts | 37 +++++++ tests/assistant-boundaries.test.ts | 101 +++++++++++++++++++ tests/site-metadata.test.ts | 29 ++++++ 18 files changed, 317 insertions(+), 85 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 src/lib/site-metadata.ts create mode 100644 tests/assistant-boundaries.test.ts create mode 100644 tests/site-metadata.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cc34007 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: 테스트 및 Worker 빌드 + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + NEXT_TELEMETRY_DISABLED: "1" + WRANGLER_SEND_METRICS: "false" + CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV: "false" + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "24" + cache: npm + - run: npm ci + - run: npm test + - run: npm run lint + - run: npx next typegen + - run: npx tsc --noEmit + - run: npm run build diff --git a/README.md b/README.md index 6ce879c..7323cb2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ npm run dev Open . English is served without a prefix and Korean is served under `/ko`. +`next dev` does not initialize Cloudflare bindings, so local development does not +require Cloudflare authentication or invoke remote Workers AI. The assistant +uses its deterministic fallback without the live inference configuration. + ## OMM AI assistant `/assistant` and `/ko/assistant` provide a constrained OMM command selector. @@ -36,6 +40,18 @@ npx tsc --noEmit npm run build ``` +`npm run build` runs the Next.js build once through OpenNext and produces the +Cloudflare Worker bundle in `.open-next/`. + +Pull requests and pushes to `main` run tests, lint, type checking and the Worker +build in GitHub Actions. The workflow uses no Cloudflare credentials and does +not deploy or call Workers AI. + +The navigation badge reads the published `omm-model` version from PyPI. It is +hidden if that request fails; the development version on GitHub `main` is not +presented as a release. The footer commit is embedded at build time from +Cloudflare Workers/Pages, GitHub Actions, Vercel, or the local Git checkout. + To compare the website command manifest against a trusted current local OMM checkout without network access: diff --git a/docs/assistant-backend.md b/docs/assistant-backend.md index d93fc35..a25bc02 100644 --- a/docs/assistant-backend.md +++ b/docs/assistant-backend.md @@ -2,7 +2,7 @@ The assistant is a constrained command selector, not a general chatbot. The browser sends `{ locale, question, turnCount }`. The server narrows the -question to at most five entries derived from the existing command docs. A +question to at most eight entries derived from the existing command docs. A Workers AI model may return only one candidate `commandId` or `clarify`. Options, examples, risk labels, links, and shell text always come from the static OMM command catalog. @@ -10,6 +10,8 @@ static OMM command catalog. ## Runtime contract - `question`: 1–480 Unicode code points. +- Request bodies are bounded while streaming, including uploads without a + `Content-Length` header. Invalid or interrupted uploads receive HTTP 400. - `turnCount`: integer 0–2 (three browser questions at most). - Model output: at most 48 completion tokens, non-streaming, temperature 0, five-second application timeout, no retry. @@ -20,7 +22,8 @@ static OMM command catalog. - `429`, capacity, timeout, model, binding, database, and invalid-JSON failures fall back to ordinary static command search without a retry. -`wrangler.jsonc` contains only the Workers AI binding: +`wrangler.jsonc` declares the Workers AI binding alongside the D1 and OpenNext +bindings: ```json "ai": { "binding": "AI" } @@ -28,6 +31,8 @@ static OMM command catalog. The route reads the binding through OpenNext's `getCloudflareContext()` and calls `env.AI.run()`. No browser-visible API key exists. +`next dev` does not initialize Cloudflare bindings, so it neither starts a remote preview +session nor invokes paid inference during ordinary local development. ## Model choice diff --git a/next.config.ts b/next.config.ts index 180deba..25a39cf 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,11 +1,16 @@ import type { NextConfig } from "next"; +import { execFileSync } from "node:child_process"; + +import { buildCommitSha } from "./src/lib/site-metadata"; const nextConfig: NextConfig = { - /* config options here */ + // Cloudflare bindings are supplied by the Worker. Plain `next dev` uses the + // assistant route's deterministic fallback without opening remote sessions. + env: { + OMM_BUILD_SHA: buildCommitSha(process.env, () => + execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }), + ), + }, }; export default nextConfig; - -if (process.env.NODE_ENV === "development") { - import('@opennextjs/cloudflare').then(m => m.initOpenNextCloudflareForDev()); -} diff --git a/open-next.config.ts b/open-next.config.ts index 77f2953..5ed7bee 100644 --- a/open-next.config.ts +++ b/open-next.config.ts @@ -7,9 +7,8 @@ const cloudflareConfig = { incrementalCache: staticAssetsIncrementalCache, enableCacheInterception: true, }), - // Without this, `opennextjs-cloudflare build` re-runs the project's own - // "build" npm script (`next build && opennextjs-cloudflare build`), - // recursing into itself forever. + // Keep the framework build explicit: the package's "build" script invokes + // OpenNext, so falling back to that script would recurse into itself. buildCommand: "next build", }; diff --git a/package-lock.json b/package-lock.json index 25a22ab..bf2bf8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10128,9 +10128,9 @@ } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", diff --git a/package.json b/package.json index c5f8de8..659c629 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "build": "next build && opennextjs-cloudflare build", + "build": "opennextjs-cloudflare build", "dev": "next dev", "start": "next start", "lint": "eslint", diff --git a/src/app/api/assistant/route.ts b/src/app/api/assistant/route.ts index a748bfa..f5e0829 100644 --- a/src/app/api/assistant/route.ts +++ b/src/app/api/assistant/route.ts @@ -4,13 +4,10 @@ import { D1AssistantStore, type D1DatabaseLike, } from "../../../lib/assistant/budget"; -import { parseAssistantRequestText } from "../../../lib/assistant/request"; +import { parseAssistantRequest } from "../../../lib/assistant/request"; import { rateLimitIdentity } from "../../../lib/assistant/security"; import { answerAssistantQuestion } from "../../../lib/assistant/service"; -import { - ASSISTANT_LIMITS, - type AssistantResponse, -} from "../../../lib/assistant/types"; +import type { AssistantResponse } from "../../../lib/assistant/types"; import type { WorkersAiBinding } from "../../../lib/assistant/workers-ai"; type AssistantEnv = CloudflareEnv & { @@ -42,14 +39,7 @@ function json(result: AssistantResponse, status = 200): Response { } export async function POST(request: Request): Promise { - const declaredLength = Number(request.headers.get("content-length")); - if ( - Number.isFinite(declaredLength) && - declaredLength > ASSISTANT_LIMITS.maxBodyCharacters * 4 - ) { - return json(invalidRequest(), 400); - } - const parsed = parseAssistantRequestText(await request.text()); + const parsed = await parseAssistantRequest(request); if (!parsed.ok) return json(invalidRequest(), 400); let env: AssistantEnv | undefined; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index d3bc164..a2273db 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -65,10 +65,8 @@ function Column({ export default function Footer({ locale }: { locale: Locale }) { const t = getDictionary(locale).footer; - /* DIRECTION.md §4.7: the bottom row carries the build's real commit - short-SHA. Vercel injects it at build time; locally it is absent, and the - row then renders repo + license only rather than a fake placeholder. */ - const sha = process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7); + // next.config resolves the Cloudflare/CI/local commit while building. + const sha = process.env.OMM_BUILD_SHA?.slice(0, 7); return (