Files
PiEgg 7a9bc23084 ✨ Feature(gui): configure server upload secret in settings (#1451)
* ✨ Feature(gui): configure server upload secret in settings

PicGo Core already supports authenticating `POST /upload` with a shared secret (`Authorization: Bearer <secret>`, `X-PicGo-Secret` or `?secret=`), resolved from `PICGO_SERVER_SECRET` or `settings.server.secret`, but the GUI offered no way to set it. Add a masked secret field with a show/hide toggle to the PicGo Server dialog, next to host and port.

The dialog saves the trimmed value as `settings.server.secret`; an empty value disables authentication. Saving already restarts the server, and Core reads the secret from config on `listen()`, so the main process needs no change and `PICGO_SERVER_SECRET` still takes precedence. The secret value is never added to the settings search index.

* 🐛 Fix(gui): keep server secret when normalizing settings

normalizeSettingsConfig only copied host, port and enable from `settings.server`, so after a reload the renderer state had no secret: the server dialog reopened with an empty secret field, and saving it again (e.g. after changing the port) would have silently cleared the secret and disabled authentication. Keep `secret` (falling back to an empty string) and cover it with a test.

* 📝 Docs: document renderer config normalizer whitelist in AGENTS.md

The server secret was dropped from renderer state because normalizeSettingsConfig only copies listed fields, and the server dialog re-saves the whole object. Record the rule for future changes: when adding a persisted config field, update the shared types, renderer state type, default value and normalizer, and add a round-trip normalizer test. Also give `settings.server.secret` an explicit empty default to follow that rule.

* 📝 Docs: require English PRs and squash merges in AGENTS.md
2026-10-05 20:26:32 +08:00

74 lines
12 KiB
Markdown

# Repository Guidelines
## Project Structure & Module Organization
PicGo is an Electron + Vue 3 desktop client. Source lives in `src/`: `src/main` for main-process and IPC logic, `src/renderer` for Vue views, and `src/universal` for shared helpers (`types/`, `events/constants.ts`). `background.ts` wires Electron Builder. Static assets and locale YAML files stay in `public/` (add languages under `public/i18n/`), while `docs/` hosts user-facing guides. Automation scripts live in `scripts/`, and legacy tests sit under `test/unit` (Karma) and `test/e2e` (Spectron).
## Build, Test, and Development Commands
- `pnpm install` — install dependencies; `npm install` is unsupported. Only run this when the user explicitly asks/coordinates it.
- Always add/remove dependencies with `pnpm` (never edit package.json versions by hand then install).
- `pnpm dev` — electron-vite dev server for main/preload/renderer.
- `pnpm build` — electron-vite build outputs to `dist/main`, `dist/preload`, `dist/renderer`; `pnpm preview` for preview mode.
- Packaging config lives in `electron-builder.yml` (read by electron-builder via package.json `build` field/extraResources); set `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` if downloads are slow.
- `pnpm lint` / `pnpm lint:fix` — run or auto-fix ESLint (Standard, TypeScript, Vue rules).
- `pnpm lint:dpdm` — fail fast on circular dependencies in `src/`.
- `pnpm check` — run `tsc` + `lint` (run once before finishing a task).
- Before completing a task, always run `pnpm check` and resolve any issues it reports.
- i18n type files are auto-generated by the Vite `i18nTypesPlugin` when `public/i18n/*.yml` changes. Do not add or rely on a manual `gen-i18n` step.
## Coding Style & Naming Conventions
Follow ESLint Standard defaults: two-space indentation, single quotes, trailing commas where allowed, and no stray semicolons. Author new modules in TypeScript. Keep renderer files browser-safe; route Node APIs through IPC helpers such as `src/main/events/picgoCoreIPC.ts`. Name Vue components in PascalCase (`UploadPanel.vue`) and use camelCase for utilities. Centralize IPC event names inside `src/universal/events/constants.ts`, and store enums/types under `src/universal/types/` so they stay reusable. Static assets are served from `public/` and resolved via `getStaticPath`/`getStaticFileUrl` (`src/universal/utils/staticPath.ts`); avoid using `__static` directly.
Static assets are served from `public/`. In the main process use `getStaticPath`/`getStaticFileUrl` (`src/universal/utils/staticPath.ts`). In the renderer, place assets under `public/` and resolve them via `import.meta.env.BASE_URL + filename` (helper: `src/renderer/utils/static.ts`); do not rely on `__static` in renderer code.
- Do not use `as any` under any circumstances; keep typings explicit and safe.
- Avoid `as any` in tests as well; build concrete typed stubs (e.g., `IpcMainInvokeEvent`) instead.
- Do not prefix method calls with `void` (e.g. use `store?.refreshPicBeds()` rather than `void store?.refreshPicBeds()`).
- Do not write `void someMethod()` or `void object.method()` anywhere in the codebase. If you need fire-and-forget behavior, use an `async` callback with `await`, or handle errors explicitly with `try/catch` and logging instead of swallowing them.
- Because the renderer uses React Compiler, do not add `useMemo` or `useCallback` by default. Prefer plain values and inline functions unless a specific API requires stable identity or there is a proven performance issue.
- For Zustand store state/actions, prefer reading them directly in the consuming component with `useAppStore` / `useStore` instead of passing them down through unnecessary prop layers. If a child can access the needed store value or action itself, do not thread it through parent props.
- If a child component only needs a store action (for example `providerStoreActions.toggleExpanded`, `settingsStoreActions.setSearchValue`, or `galleryStoreActions.setViewMode`), do not pass that store action through props. Import and use the action directly inside the child component.
- Renderer Zustand stores must follow the project store architecture:
- `src/renderer/store/app-store.ts` is for true global renderer state only.
- Feature/page-local UI state must live under `src/renderer/store/<feature>/` (for example `src/renderer/store/gallery/store.ts`, `src/renderer/store/gallery/actions.ts`), not inside `app-store.ts`.
- Do not add an extra nested `store/` directory like `src/renderer/store/gallery/store/store.ts`.
- Zustand state and actions must be separated:
- Do not define state-mutating actions inside `create()`.
- Keep store files focused on state shape and initial state.
- Put global actions in `src/renderer/store/app-actions.ts`.
- Put feature actions in `src/renderer/store/<feature>/actions.ts`.
- Actions must update state via `useXxxStore.setState(...)`.
- Follow strict IPC boundaries for Zustand actions:
- Pure IPC/service calls without Zustand state changes should call the adapter/service directly from the component or helper, not through a Zustand action.
- Flows that combine IPC/service work with Zustand state updates must live in actions files.
- Keep server state and client state separated:
- TanStack Query should own server state: remote API data, loading/error/stale status, refetching, cache, and request dedupe.
- Zustand should own client/UI state: selected source, selected ids, filters, view mode, panel open state, and other local user intent.
- Connect the two with ids, query params, and local UI state (for example `albumSource`, `typeFilter`, or `searchValue`), but do not mirror query response data back into Zustand.
- If server state invalidates a local UI choice (for example a user becomes non-paid while `albumSource` is cloud), use a small effect/action to correct the local UI state instead of storing the whole server response in Zustand.
- When updating nested Zustand state (especially config-like objects), use `zustand/middleware/immer`; do not reintroduce deep `...state` spread chains for nested updates.
- Components must consume Zustand state through auto-generated selectors (for example `useAppStore.use.appConfig()`), not by destructuring the whole store or writing ad-hoc hook selectors in components.
- Renderer-side shared constants (for example responsive breakpoints, UI timing values, fixed dimensions, thresholds, and repeated literal values used across components) should be centralized in `src/renderer/utils/consts.ts` instead of being hardcoded inline in components. When a new renderer constant may be reused or affects shared behavior, add it there first.
- Renderer-side date/time formatting should use `dayjs` and shared format constants from `src/renderer/utils/consts.ts` (for example `DEFAULT_DATE_TIME_FORMAT`) instead of `Intl.DateTimeFormat` or ad-hoc inline format strings.
- Enum-like object constants declared with `as const` (for example status maps, option maps, and value registries) must use PascalCase names, not camelCase. Prefer names like `PicGoCloudRequestStatusValues`, `SettingsAppearanceValues`, or `AppPlatformValues`.
- If a renderer → main request mutates persisted config/state without using `saveConfig`, call `notifyAppConfigUpdated()` in main to inform renderers.
- Renderer config normalization is a whitelist: `normalizeAppConfig` / `normalizeSettingsConfig` in `src/renderer/store/utils.ts` copy only the fields they list (for example `settings.server` → `host`, `port`, `enable`, `secret`), and anything not listed is silently dropped from renderer state after every reload. When adding a new persisted config field, update all of: the shared types (`src/universal/types/types.d.ts` / `view.d.ts`), the renderer state type (e.g. `SettingsConfigState` in `src/renderer/components/main/settings/utils.ts`), its default value, and the normalizer. Settings dialogs re-save the whole object (e.g. `settings.server`), so a field missing from the normalizer is not just invisible in the UI but gets overwritten with an empty value on the next save. Add a round-trip test (raw config → normalizer → field still present) next to `src/__tests__/renderer/store/settings-normalize.spec.ts`; a component test that feeds drafts directly will not catch this.
- Prefer enums over union types for discrete value sets (e.g., encryption methods). Avoid introducing new string literal union types.
- Renderer page/component styles should prefer Tailwind utility classes; avoid adding new Vue `<style>` blocks unless there's no reasonable Tailwind equivalent.
- New renderer ↔ main request/response APIs should be implemented via RPC routes (see `src/main/events/rpc/routes/system.ts`) with `RPCRouter` + `IRPCActionType` rather than adding ad-hoc IPC modules (e.g. `picgoCloudIPC`).
- For request/response semantics in renderer, prefer `invokeRPC` (backed by `ipcMain.handle(RPC_ACTIONS, ...)` in `src/main/events/rpc/index.ts`).
- **AlertDialog async confirm buttons**: Do not use `AlertDialogAction` for confirm buttons that perform async operations (API calls, etc.), because `AlertDialogAction` auto-closes the dialog on click regardless of `event.preventDefault()`. Use a plain `<Button>` instead, manage `open` state manually, and close the dialog only after the async operation completes (success or error). See `src/renderer/components/main/gallery/gallery-delete-dialog.tsx` for reference.
- **Nullish coalescing for optional checks**: Prefer `(value?.field ?? fallback) > 0` over verbose `value !== null && value !== undefined && typeof value.field === 'number' && value.field > 0` chains. Use TypeScript's optional chaining (`?.`) and nullish coalescing (`??`) to keep boolean checks concise.
## Testing Guidelines
Place renderer unit specs in `test/unit/specs` with the `.spec.js` suffix; Karma picks them up via `require.context`. Run them with `npx karma start test/unit/karma.conf.js --single-run` and ensure new renderer folders are covered. Spectron e2e cases live in `test/e2e/specs`; build first (`pnpm build`), then run `npx mocha test/e2e/index.js` so Spectron can launch `dist/electron/main.js`. Document any test data, IPC stubs, or fixtures you add to keep suites reproducible.
## Commit & Pull Request Guidelines
Commits follow the PicGo conventional preset enforced by Husky (`pnpm lint:dpdm` + Commitlint). Stage your changes and run `pnpm cz` to craft messages that pass CI. Pull requests should explain the change, link related issues, and attach UI screenshots or recordings. Note how you validated the work (dev server, build, Karma, Spectron) and call out migration or configuration steps reviewers must perform.
- Write pull request titles, descriptions, and review comments in English, even when the conversation with the maintainer is in another language.
- Merge pull requests with squash merge (`gh pr merge <number> --squash`), using the PR title as the squash commit subject. Do not use merge commits or rebase merges, and only merge after all required CI checks have passed.
## Internationalization Tips
Add locales by creating `public/i18n/<locale>.yml`, exposing its `LANG_DISPLAY_LABEL`, and registering it in `src/universal/i18n/index.ts`. Typed i18n declarations are generated automatically from `public/i18n/en.yml` into `src/universal/types/i18n.d.ts` and `src/renderer/i18n/i18next.d.ts`.
- Any user-facing copy (UI text, error messages, warnings, prompts, tips, notifications, etc.) MUST use i18n keys. Do not hardcode strings in code.
- Renderer: use `$T('KEY')` from `src/renderer/i18n/index.ts`.
- Main process: use `T('KEY')` from `src/main/i18n/index.ts`.
- Add new keys to all locales under `public/i18n/` (at least `en.yml`, `zh-CN.yml`, `zh-TW.yml`). The Vite i18n types plugin will regenerate the shared declarations automatically.