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
38 changes: 28 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
# Private runtime configuration
NUXT_BASE_URL=http://127.0.0.1:3000
NUXT_SECRET=
NUXT_BETTER_AUTH_SECRET=

# OpenAI
# Telegram
NUXT_TELEGRAM_BOT_TOKEN=
NUXT_TELEGRAM_BOT_USERNAME=AskLunaChatbot
NUXT_TELEGRAM_OAUTH_CLIENT_ID=
NUXT_TELEGRAM_OAUTH_CLIENT_SECRET=
NUXT_TELEGRAM_WEBHOOK_SECRET=

# Github
NUXT_GITHUB_CLIENT_ID=
NUXT_GITHUB_CLIENT_SECRET=

# Google
NUXT_GOOGLE_CLIENT_ID=
NUXT_GOOGLE_CLIENT_SECRET=

# Cloudflare
NUXT_CLOUDFLARE_CLIENT_ID=
NUXT_CLOUDFLARE_CLIENT_SECRET=

# AI
NUXT_OPENAI_API_KEY=
NUXT_OPENAI_MODEL=gpt-5.6-luna
NUXT_OPENAI_BASE_URL=
Expand All @@ -12,12 +31,11 @@ NUXT_OPENAI_MODEL_CAPACITY=text,image,audio,
# Ollama web tools
NUXT_OLLAMA_API_KEY=

# Telegram
NUXT_TELEGRAM_BOT_TOKEN=
NUXT_TELEGRAM_BOT_USERNAME=AskLunaChatbot
NUXT_TELEGRAM_OAUTH_CLIENT_ID=
NUXT_TELEGRAM_OAUTH_CLIENT_SECRET=
NUXT_TELEGRAM_WEBHOOK_SECRET=
# Email verification (Resend)
NUXT_EMAIL_ENABLE=false
NUXT_RESEND_API_KEY=
NUXT_RESEND_EMAIL=
NUXT_RESEND_NAME=Luna

# Public runtime configuration
NUXT_PUBLIC_SITE_URL=https://luna.htu.me
# Public URL for set-webhook
NUXT_PUBLIC_SITE_URL=http://127.0.0.1:3000
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ logs
next-env.d.ts

# cahce
.nuxtrc
.stylelintcache

# wrangler files
Expand Down
3 changes: 2 additions & 1 deletion .nuxtrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
setups.nuxt-skill-hub="0.1.1"
setups.nuxt-skill-hub="0.1.1"
setups.@nuxtjs/better-auth="0.3.2"
11 changes: 5 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- `app/` is the application-wide layer (the repository's `0.app` equivalent). It contains shared components, shared composables, the root Nuxt app entry, and global styles. Feature code belongs to the relevant layer.
- `shared/` follows the Nuxt 4 convention: `shared/types/` and `shared/utils/` (top level) are auto-imported into app and server; everything else, such as `shared/schema/`, is imported through the `#shared/*` alias. Shared code must not import Vue or Nitro runtime APIs, and chat-domain logic belongs in its owning layer rather than here.
- `layers/00.site/` contains the page framework, default layout, public home, site navigation, error page, and legal pages.
- `layers/01.auth/` contains login/user routes, auth middleware, Better Auth client/server code, and `/api/auth/**`.
- `layers/01.auth/` contains login/user routes and Nuxt Better Auth client/server configuration. The module mounts `/api/auth/**`.
- `layers/02.illust/` contains the illustration list/detail routes, components, resolver, and types.
- `layers/03.fan/` contains fan, sticker, and wallpaper routes, components, composables, utilities, and types.
- `layers/04.changelog/` contains changelog routes and components.
Expand All @@ -14,17 +14,17 @@
- `layers/07.bot/` contains Telegram webhook routes and Telegram channel parsing/runtime. It may depend on `#layers/chat/*`, but chat must not depend on bot.
- Local layers are auto-discovered from `layers/`; do not register them with root `extends`. Numeric prefixes control priority, with later names taking precedence: `07.bot` is higher than `00.site`. Each layer needs `nuxt.config.ts` with `$meta.name` set to its unprefixed alias.
- Type-only modules live in each layer's `types/`; cloud storage access (KV/Blob) lives in `storage/` folders, never `utils/`.
- `server/db/schema/auth.gen.ts` is generated by `bun run auth:generate`; after changing Better Auth configuration, regenerate it and never edit the generated file manually.
- `layers/01.auth/server/utils/` follows the Better Auth convention: it holds only the auth instance (`auth.ts`) and its plugins.
- `layers/01.auth/server/api/auth/` mounts Better Auth; `server/db/schema/` is the source of truth for the NuxtHub database; generated SQLite migrations live in `server/db/migrations/`.
- Nuxt Better Auth generates its NuxtHub schema from `layers/01.auth/server/auth.config.ts`; restart Nuxt after changing schema-affecting plugins.
- `layers/01.auth/server/utils/` holds authentication provider and hook logic used by the layer's server auth config.
- `server/db/schema/` is the source of truth for application-owned NuxtHub tables; generated SQLite migrations live in `server/db/migrations/`.
- `scripts/` contains standalone Telegram operations scripts (`bot:poll`, `bot:set-webhook`); the production webhook URL is `/api/telegram/webhook`.
- `tests/` contains root HTTP E2E tests; pure domain tests live beside their owning layer in `layers/*/tests/`. `content/` and `public/` hold root-level content and media.

## Development Commands

Use Bun 1.4 (`bun`/`bunx`) and keep the lockfile in sync.

Development server is running on `127.0.0.1:3000`. Make sure not to start duplicate development servers.
Development server is running on `127.0.0.1:3000`(open this instead of `localhost`). Make sure not to start duplicate development servers.

```sh
bun install
Expand All @@ -34,7 +34,6 @@ bun run test # Run Vitest unit and HTTP E2E suites
bun run test:watch # Watch tests during development
bun run build # Build the Cloudflare Durable Objects target
bun run db:generate # Generate NuxtHub migrations from the schema
bun run auth:generate # Regenerate the Better Auth Drizzle schema
bun run db:migrate # Apply pending NuxtHub migrations
bun run db:sql # Run NuxtHub database SQL operations
bun run preview # Preview the built Cloudflare worker locally
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- `app/`:root app 入口、全局 CSS 和 Cursor composable
- `server/db/`:NuxtHub D1 schema 和迁移
- `layers/00.site/`:页面框架、首页、法律页面和网站导航
- `layers/01.auth/`:登录、用户页面、认证 middleware、Better Auth 和 `/api/auth/**`
- `layers/01.auth/`:登录、用户页面和 Nuxt Better Auth 配置;模块挂载 `/api/auth/**`
- `layers/02.illust/`:插画列表和详情
- `layers/03.fan/`:fan、贴纸和壁纸
- `layers/04.changelog/`:更新日志
Expand All @@ -36,7 +36,7 @@ bun install
cp .env.example .env
```

在 `.env` 中设置 `NUXT_SECRET`,例如:
在 `.env` 中设置 `NUXT_BETTER_AUTH_SECRET`,例如:

```bash
openssl rand -base64 32
Expand Down Expand Up @@ -79,7 +79,7 @@ bun run db:generate

`bun run db:migrate` 和 `bun run db:sql` 保留为 NuxtHub 官方数据库运维命令,不承载旧项目数据转换。

Better Auth 服务端和客户端配置位于 `layers/01.auth/`。数据库 schema 仍位于 `server/db/schema/`,认证 KV adapter 位于 auth layer 的 `server/storage/`。
Better Auth 服务端和客户端配置位于 `layers/01.auth/`。认证表由 Nuxt Better Auth 根据该 layer 的 server config 自动生成;业务数据库 schema 仍位于 `server/db/schema/`。

## Configuration

Expand Down
120 changes: 0 additions & 120 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,131 +4,11 @@
@source '../../../content/**/*';
@source '../../../layers/**/*.{vue,ts}';

@import 'tailwindcss';
@import '@nuxt/ui';

@theme {
--font-sans:
'Quicksand', 'Noto Sans SC', ui-sans-serif, system-ui, sans-serif;
}

:root {
--font-sans:
'Quicksand', 'Noto Sans SC', ui-sans-serif, system-ui, sans-serif;
--ui-radius: 0.5rem;
--ui-color-primary-50: oklch(0.971 0.014 343.198);
--ui-color-primary-100: oklch(0.971 0.014 343.198);
--ui-color-primary-200: oklch(0.948 0.028 342.258);
--ui-color-primary-300: oklch(0.899 0.061 343.231);
--ui-color-primary-400: oklch(0.823 0.12 346.018);
--ui-color-primary-500: oklch(0.718 0.202 349.761);
--ui-color-primary-600: oklch(0.656 0.241 354.308);
--ui-color-primary-700: oklch(0.592 0.249 0.584);
--ui-color-primary-800: oklch(0.525 0.223 3.958);
--ui-color-primary-900: oklch(0.459 0.187 3.815);
--ui-color-primary-950: oklch(0.408 0.153 2.432);
--ui-color-secondary-50: oklch(0.969 0.016 293.756);
--ui-color-secondary-100: oklch(0.969 0.016 293.756);
--ui-color-secondary-200: oklch(0.943 0.029 294.588);
--ui-color-secondary-300: oklch(0.894 0.057 293.283);
--ui-color-secondary-400: oklch(0.811 0.111 293.571);
--ui-color-secondary-500: oklch(0.702 0.183 293.541);
--ui-color-secondary-600: oklch(0.606 0.25 292.717);
--ui-color-secondary-700: oklch(0.541 0.281 293.009);
--ui-color-secondary-800: oklch(0.491 0.27 292.581);
--ui-color-secondary-900: oklch(0.432 0.232 292.759);
--ui-color-secondary-950: oklch(0.38 0.189 293.745);
--ui-color-success-50: oklch(0.986 0.031 120.757);
--ui-color-success-100: oklch(0.986 0.031 120.757);
--ui-color-success-200: oklch(0.967 0.067 122.328);
--ui-color-success-300: oklch(0.938 0.127 124.321);
--ui-color-success-400: oklch(0.897 0.196 126.665);
--ui-color-success-500: oklch(0.841 0.238 128.85);
--ui-color-success-600: oklch(0.768 0.233 130.85);
--ui-color-success-700: oklch(0.648 0.2 131.684);
--ui-color-success-800: oklch(0.532 0.157 131.589);
--ui-color-success-900: oklch(0.453 0.124 130.933);
--ui-color-success-950: oklch(0.405 0.101 131.063);
--ui-color-info-50: oklch(0.984 0.019 200.873);
--ui-color-info-100: oklch(0.984 0.019 200.873);
--ui-color-info-200: oklch(0.956 0.045 203.388);
--ui-color-info-300: oklch(0.917 0.08 205.041);
--ui-color-info-400: oklch(0.865 0.127 207.078);
--ui-color-info-500: oklch(0.789 0.154 211.53);
--ui-color-info-600: oklch(0.715 0.143 215.221);
--ui-color-info-700: oklch(0.609 0.126 221.723);
--ui-color-info-800: oklch(0.52 0.105 223.128);
--ui-color-info-900: oklch(0.45 0.085 224.283);
--ui-color-info-950: oklch(0.398 0.07 227.392);
--ui-color-warning-50: oklch(0.987 0.026 102.212);
--ui-color-warning-100: oklch(0.987 0.026 102.212);
--ui-color-warning-200: oklch(0.973 0.071 103.193);
--ui-color-warning-300: oklch(0.945 0.129 101.54);
--ui-color-warning-400: oklch(0.905 0.182 98.111);
--ui-color-warning-500: oklch(0.852 0.199 91.936);
--ui-color-warning-600: oklch(0.795 0.184 86.047);
--ui-color-warning-700: oklch(0.681 0.162 75.834);
--ui-color-warning-800: oklch(0.554 0.135 66.442);
--ui-color-warning-900: oklch(0.476 0.114 61.907);
--ui-color-warning-950: oklch(0.421 0.095 57.708);
--ui-color-error-50: oklch(0.969 0.015 12.422);
--ui-color-error-100: oklch(0.969 0.015 12.422);
--ui-color-error-200: oklch(0.941 0.03 12.58);
--ui-color-error-300: oklch(0.892 0.058 10.001);
--ui-color-error-400: oklch(0.81 0.117 11.638);
--ui-color-error-500: oklch(0.712 0.194 13.428);
--ui-color-error-600: oklch(0.645 0.246 16.439);
--ui-color-error-700: oklch(0.586 0.253 17.585);
--ui-color-error-800: oklch(0.514 0.222 16.935);
--ui-color-error-900: oklch(0.455 0.188 13.697);
--ui-color-error-950: oklch(0.41 0.159 10.272);
}

.dark {
--ui-radius: 1rem;
--ui-text-dimmed: var(--ui-color-neutral-400);
--ui-text-muted: var(--ui-color-neutral-300);
--ui-text-toned: var(--ui-color-neutral-200);
--ui-text: var(--ui-color-neutral-100);
--ui-text-inverted: var(--ui-color-neutral-950);
--ui-bg: var(--ui-color-neutral-950);
--ui-bg-muted: var(--ui-color-neutral-900);
--ui-bg-elevated: var(--ui-color-neutral-900);
--ui-bg-accented: var(--ui-color-neutral-800);
--ui-border: var(--ui-color-neutral-700);
--ui-border-muted: var(--ui-color-neutral-800);
--ui-border-accented: var(--ui-color-neutral-600);
--ui-color-primary-50: oklch(0.977 0.017 320.058);
--ui-color-primary-100: oklch(0.977 0.017 320.058);
--ui-color-primary-200: oklch(0.952 0.037 318.852);
--ui-color-primary-300: oklch(0.903 0.076 319.62);
--ui-color-primary-400: oklch(0.833 0.145 321.434);
--ui-color-primary-500: oklch(0.74 0.238 322.16);
--ui-color-primary-600: oklch(0.667 0.295 322.15);
--ui-color-primary-700: oklch(0.591 0.293 322.896);
--ui-color-primary-800: oklch(0.518 0.253 323.949);
--ui-color-primary-900: oklch(0.452 0.211 324.591);
--ui-color-primary-950: oklch(0.401 0.17 325.612);
--ui-color-secondary-50: oklch(0.984 0.019 200.873);
--ui-color-secondary-100: oklch(0.984 0.019 200.873);
--ui-color-secondary-200: oklch(0.956 0.045 203.388);
--ui-color-secondary-300: oklch(0.917 0.08 205.041);
--ui-color-secondary-400: oklch(0.865 0.127 207.078);
--ui-color-secondary-500: oklch(0.789 0.154 211.53);
--ui-color-secondary-600: oklch(0.715 0.143 215.221);
--ui-color-secondary-700: oklch(0.609 0.126 221.723);
--ui-color-secondary-800: oklch(0.52 0.105 223.128);
--ui-color-secondary-900: oklch(0.45 0.085 224.283);
--ui-color-secondary-950: oklch(0.398 0.07 227.392);
--ui-color-info-50: oklch(0.969 0.016 293.756);
--ui-color-info-100: oklch(0.969 0.016 293.756);
--ui-color-info-200: oklch(0.943 0.029 294.588);
--ui-color-info-300: oklch(0.894 0.057 293.283);
--ui-color-info-400: oklch(0.811 0.111 293.571);
--ui-color-info-500: oklch(0.702 0.183 293.541);
--ui-color-info-600: oklch(0.606 0.25 292.717);
--ui-color-info-700: oklch(0.541 0.281 293.009);
--ui-color-info-800: oklch(0.491 0.27 292.581);
--ui-color-info-900: oklch(0.432 0.232 292.759);
--ui-color-info-950: oklch(0.38 0.189 293.745);
}
7 changes: 7 additions & 0 deletions app/composables/useCursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ const cursorDefinitions = {
grabbing: { file: 'hand_closed.svg', x: 16, y: 16, fallback: 'grabbing' },
zoomIn: { file: 'zoom_in.svg', x: 14, y: 14, fallback: 'zoom-in' },
zoomOut: { file: 'zoom_out.svg', x: 14, y: 14, fallback: 'zoom-out' },
resizeHorizontal: {
file: 'resize_horizontal.svg',
x: 16,
y: 16,
fallback: 'ew-resize'
},
crosshair: { file: 'cross_small.svg', x: 16, y: 16, fallback: 'crosshair' },
wait: { file: 'busy_hourglass.svg', x: 16, y: 16, fallback: 'wait' },
notAllowed: {
Expand All @@ -24,6 +30,7 @@ const cursorSelectors: ReadonlyArray<readonly [CursorName, string]> = [
['zoomIn', '.cursor-zoom-in'],
['zoomOut', '.cursor-zoom-out'],
['crosshair', '.cursor-crosshair'],
['resizeHorizontal', '.cursor-ew-resize'],
['wait', '.cursor-wait'],
[
'text',
Expand Down
3 changes: 3 additions & 0 deletions app/composables/useSound.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
bind as bindCuelume,
play as playCuelume,
setEnabled as setCuelumeEnabled,
setVolume as setCuelumeVolume,
Expand All @@ -19,6 +20,8 @@ export function useSound() {
setCuelumeEnabled(enabled.value)
setCuelumeVolume(volume.value)

onMounted(() => bindCuelume())

function play(name: Readonly<SoundName>, options?: Readonly<ISoundOptions>) {
if (!enabled.value) return
playCuelume(name, options)
Expand Down
Loading