# AGENTS GUIDE Vue 3 + Vite 8 SPA with Element Plus. Keep changes small, follow existing patterns, avoid refactors during fixes. ## Quick Facts - Framework: Vue 3 (Options API) - Build tool: Vite 8 - UI: Element Plus 2 (icons via `@element-plus/icons-vue`) - Router: Vue Router 4 (`createWebHistory`, base from `import.meta.env.BASE_URL`) - Node: 24.x - Regression tests use Node’s built-in test runner with the existing Vite loader; no additional test dependency ## Commands | Command | Description | |---------|-------------| | `yarn install` | Install dependencies | | `yarn dev` | Start dev server (host: 0.0.0.0) | | `yarn build` | Production build | | `yarn preview` | Preview production build locally | | `yarn lint` | ESLint check | | `node --test tests/subscription-workflows.test.mjs` | Subscription workflow regression tests | ## CI / Workflows - **build.yml**: triggers on push/PR to `master` and `dev` — runs `yarn install --frozen-lockfile` + `yarn lint` + Node regression tests + `yarn build`, then uploads `dist/` as artifact (7-day retention) - **docker-build-push.yml**: triggers on push to `master` — builds and pushes multi-arch image (`linux/amd64`, `linux/arm64`) to `careywong/subweb:latest` ## Repository Layout ``` src/ ├── main.js # App bootstrap, plugin registration, Vue mount ├── App.vue ├── router/index.js # Vue Router (history mode) ├── views/Subconverter.vue # Main page ├── components/ │ ├── ConfigUploadDialog.vue # Config upload dialog │ ├── UrlParseDialog.vue # URL parse dialog │ └── SvgIcon/index.vue # SVG icon wrapper component ├── composables/ │ ├── useSubscription.js # Form defaults + URL generation and parsing rules │ ├── useSubscriptionForm.js # Reactive form state + addCustomParam + saveSubUrl │ └── useGeneratedLinks.js # Generated long/short link association + pending requests ├── services/ │ ├── backendService.js # BackendService.getBackendVersion() │ ├── shortUrlService.js # ShortUrlService.generateShortUrl(), resolveUrl() │ └── configUploadService.js # Upload + copy attempt, returns { url, copied } ├── config/ │ ├── constants.js # CONSTANTS (env-backed, DEFAULT_CLIENT_TYPE='clash') │ ├── client-types.js # CLIENT_TYPES map (display label → target value) │ └── remote-configs.js # REMOTE_CONFIGS grouped options array ├── utils/ │ ├── storage.js # getLocalStorageItem / setLocalStorageItem (TTL-based) │ ├── validators.js # validateSubUrl → { valid, message } | validateForm → boolean │ ├── formatters.js # formatVersion, formatErrorMessage, processSubUrl │ ├── clipboard.js # copyText (Clipboard API + execCommand fallback) │ └── search.js # Backend autocomplete search helper ├── plugins/ # setupXxx(app) registrations (element-plus, axios, device) └── icons/ ├── index.js # Registers SVG sprite └── svg/ # SVG source files (e.g., github.svg) services/ # Docker Compose stack (subweb + myurls + redis) ``` ## Key Modules ### `src/config/constants.js` All values read from `import.meta.env` with `VITE_` prefix. Key constants: - `DEFAULT_BACKEND` — appends `/sub?` to `VITE_SUBCONVERTER_DEFAULT_BACKEND` - `DEFAULT_CLIENT_TYPE` — hardcoded `'clash'` - `SHORT_URL_API`, `CONFIG_UPLOAD_API`, `PROJECT`, `BOT_LINK`, etc. ### `src/composables/useSubscriptionForm.js` Returns a plain object merged into `data()` via spread. Form defaults come from `createSubscriptionForm()` in `useSubscription.js`. Default mode is advanced (`advanced: '2'`). `form.udp` is `null` when unspecified, or `true`/`false` when explicitly set. ### `src/composables/useSubscription.js` `useSubscription(defaultBackend)` returns `makeUrl(form, advanced, customParams)` and synchronous `parseUrl(url)`. Generation owns backend selection and source normalization; invalid required fields produce an empty string. Parsing returns `{ success: true, form, customParams }` or `{ success: false, message }` without mutating existing state. Apply the returned form only on success. Missing Boolean options retain the existing import behavior (`false`); explicit UDP false differs from absence. ### `src/composables/useGeneratedLinks.js` Store `useGeneratedLinks()` in `data()` and invoke its methods through that reactive object. `setLongUrl(url)` invalidates the old short link and pending results. `shorten(generateShortUrl)` ignores obsolete results and errors. `importUrl` selects the matching short link or the current long link. ### `src/services/` Classes expose static methods. Axios operations take `$axios` first. `ShortUrlService.resolveUrl(input, fetchUrl = fetch)` owns redirect expansion; `target` query parameters identify direct conversion links. Upload response shape: `{ code: 0, data: { url }, msg }`; `uploadConfig($axios, content, copyText)` returns `{ url, copied }` and preserves the URL if copying fails. Short URL response shape: `{ Code: 1, ShortUrl, Message }`. See [Error handling](#error-handling) for notification ownership. Silent failures remain acceptable for `getBackendVersion`. ### `src/utils/storage.js` TTL stored inside the JSON value as `{ setTime, ttl, expire, value }`. `expire` checked on every read; expired entries are removed automatically. TTL value comes from `VITE_CACHE_TTL` env var. ## Code Style - Indentation: 2 spaces - Quotes: single quotes preferred - Semicolons: none. ESLint’s `semi: 0` disables enforcement, so review new code for this convention - Vue component names: single-word allowed (`vue/multi-word-component-names: off`) - `no-console` / `no-debugger`: error in production, off in dev - ESLint extends: `plugin:vue/vue3-essential`, `eslint:recommended` - Parser: `@babel/eslint-parser` with `requireConfigFile: false` ## Imports & Modules - ES modules (`import`/`export`) throughout - Absolute alias `@` maps to `src/` (see `vite.config.js`) - Import order: core libs → local config/utils → services → components - Dynamic imports only for route lazy-load ## Vue Patterns - Options API everywhere; do not introduce Composition API or `