Imported from AlyceSingle/STCreative-Workshop (
AGENTS.md). Install upstream withnpx skills add AlyceSingle/STCreative-Workshop. Copyright stays with the author.
AGENTS.md — SillyTavern Creative Workshop
Coding-agent instructions for this repository. Read this file in full before making any changes.
Project Overview
A SillyTavern worldbook workshop platform with Discord OAuth2 login. Users browse/subscribe to worldbook entry packs; subscriptions are injected directly into SillyTavern via a companion extension.
| Layer | Stack |
|---|---|
| Backend | Node.js 20, Express 5, better-sqlite3, Passport + passport-discord |
| Frontend | Vue 3 (<script setup>), Vite 8, Pinia 3, Vue Router 4, Tailwind CSS v4 |
| Database | SQLite (WAL mode) — auto-created at backend/db/stories.db |
| Auth | Discord OAuth2 only; session cookie (connect.sid); no JWT |
| Extension | st-extension/ — SillyTavern extension that opens the workshop in a popup and proxies TavernHelper calls |
Not a monorepo — backend/ and frontend/ are independent Node projects with separate package.json and node_modules/. No root-level package.json.
Directory Structure
STCreativeWorkshop/
├── backend/
│ ├── db/init.js # SQLite schema + getDb() singleton; migrations at bottom
│ ├── middleware/auth.js # requireAuth middleware
│ ├── routes/
│ │ ├── auth.js # /auth/* Discord OAuth2 + /auth/me
│ │ ├── stories.js # /api/stories CRUD (legacy)
│ │ ├── tags.js # /api/tags (legacy)
│ │ ├── workshop.js # /api/workshop — workshops, packs, entries, like/subscribe
│ │ ├── creator.js # /api/creator — creator applications
│ │ └── admin.js # /api/admin — admin panel operations
│ ├── .env # Real secrets — NEVER commit
│ ├── .env.example # Env var template
│ └── server.js # Express entry point
├── frontend/src/
│ ├── api/ # API 层 — 按路由模块化封装 HTTP 请求
│ │ ├── auth.js # /auth/* 认证相关 API
│ │ ├── stories.js # /api/stories 故事相关 API(legacy)
│ │ ├── workshop.js # /api/workshop 工坊、模组、条目相关 API
│ │ ├── admin.js # /api/admin 管理后台 API
│ │ ├── creator.js # /api/creator 创作者申请 API
│ │ └── index.js # 统一导出所有 API 模块
│ ├── utils/
│ │ └── request.js # Axios 封装,提供 get/post/put/delete/patch 方法
│ ├── components/ # 组件按功能分组
│ │ ├── common/ # 通用基础组件
│ │ │ ├── ConfirmModal.vue # 确认弹窗
│ │ │ ├── CustomSelect.vue # 自定义下拉选择
│ │ │ ├── SearchSelect.vue # 带搜索的下拉选择
│ │ │ ├── Toast.vue # 消息提示单项
│ │ │ └── ToastContainer.vue # 消息提示容器
│ │ ├── layout/ # 布局组件
│ │ │ └── Navbar.vue # 导航栏
│ │ ├── cards/ # 业务卡片组件
│ │ │ ├── StoryCard.vue # 故事卡片
│ │ │ ├── WorkshopPackCard.vue # 模组卡片
│ │ │ └── WorkshopEntryCard.vue # 条目卡片
│ │ ├── modals/ # 业务弹窗组件
│ │ │ ├── ImportLocalEntriesModal.vue # 本地条目导入弹窗
│ │ │ ├── PackSubscribeModal.vue # 模组订阅确认弹窗
│ │ │ └── PackUpdateModal.vue # 模组更新弹窗
│ │ └── filters/ # 筛选/选择组件
│ │ ├── CharacterSelector.vue # 角色卡选择器
│ │ └── TagFilter.vue # 标签筛选器
│ ├── config/sections.js # TAG_GROUPS, DEFAULT_TAGS, localStorage worldbook helpers
│ ├── router/index.js # Vue Router; auth guard redirects to { name: 'workshop' }
│ ├── stores/ # Pinia: auth.js, stories.js, workshop.js
│ ├── views/ # HomeView, WorkshopView, WorkshopPackDetail, WorkshopPackEditor,
│ │ # WorkshopEntryEditor, WorkshopCreate, WorkshopEdit,
│ │ # ProfileView, CreatorApplyView, AdminView
│ ├── style.css # Tailwind v4 + @layer component classes
│ └── main.js
├── st-extension/ # SillyTavern extension (vanilla JS, no build step)
│ ├── index.js # Extension entry; opens popup, relays TavernHelper calls
│ ├── manifest.json
│ └── style.css
└── @types/ # Global JS type stubs
Build & Dev Commands
Backend
cd backend
npm install
npm run dev # nodemon server.js — auto-restart, port 3000
npm start # node server.js — production
# Syntax check (no test runner exists):
node --check server.js
node --check routes/workshop.js
node --check db/init.js
Frontend
cd frontend
npm install
npm run dev # Vite dev server — port 5173, proxies /api & /auth → localhost:3000
npm run build # Production build → frontend/dist/
npm run preview # Preview built output locally
There are no tests and no linter. Do not reference npm test or npm run lint.
To add tests, install vitest in the relevant sub-project and add "test": "vitest" to its scripts.
Environment Variables (backend/.env)
| Variable | Description |
|---|---|
DISCORD_CLIENT_ID |
Discord app client ID |
DISCORD_CLIENT_SECRET |
Discord app client secret |
DISCORD_REDIRECT_URI |
Must match Discord portal exactly |
SESSION_SECRET |
express-session signing secret |
FRONTEND_URL |
e.g. http://localhost:5173 in dev |
PORT |
Backend port (default 3000) |
NODE_ENV |
development or production |
HTTP_PROXY |
Optional: http://127.0.0.1:10808 for CN proxy |
HTTP_PROXY patches passport-discord via discordStrategy._oauth2.setAgent(agent) using https-proxy-agent v5 (CJS).
Do not upgrade https-proxy-agent to v6+ — it is ESM-only and will break require().
Code Style
Language & Modules
- Backend: CommonJS only —
require()/module.exports. Neverimport/export. - Frontend: ES Modules —
import/exportonly. - No TypeScript — plain
.js/.vuefiles. - All UI text and code comments must be in Chinese (zh-CN).
Backend Import Order
- Node built-ins (
path,querystring) - Third-party packages (
express,passport,better-sqlite3) - Local modules (
../db/init,../middleware/auth)
请注意,不要使用emoji符号代替图标,请使用svg
Vue Components
- Always
<script setup>Composition API — never Options API. - SFC block order:
<script setup>→<template>→<style>. - Use
defineProps()/defineEmits(). Place reusable logic insrc/composables/. - Import order inside
<script setup>: Vue core → vue-router/pinia → local components → stores → config/utils.
Naming Conventions
| Thing | Convention | Example |
|---|---|---|
| Vue components | PascalCase | WorkshopPackCard.vue |
| Vue views | PascalCase + View suffix |
HomeView.vue |
| Pinia stores | camelCase file + use*Store export |
useWorkshopStore |
| Backend route files | camelCase | workshop.js |
| DB columns | snake_case | author_id, created_at |
| JS vars/functions | camelCase | fetchPacks, workshopSlug |
| Constants | UPPER_SNAKE_CASE | TAG_GROUPS, PORT |
Design Language
- Background:
#FFFBF0(cream). Primary:#F97316(orange). Danger:#EF4444. - Fonts:
'Fredoka'for headings/titles,'Nunito'for body/labels. - Hand-drawn aesthetic:
border-radius: 16px,box-shadow: 3px 3px 0 <color>, dashed borders. - Define new reusable styles in
src/style.cssunder@layer components. Avoid long inlineclassstrings.
Error Handling
Backend:
- Wrap all DB operations in
try/catch. 500→res.status(500).json({ error: '服务器内部错误' })401→res.status(401).json({ error: 'Unauthorized', message: '请先登录' })400→res.status(400).json({ error: '<具体中文描述>' })- Log with:
console.error('[Module] description:', err)
Frontend:
- Store actions catch all errors and set a
errorref. Neverthrowfrom a store action. - Views display
store.erroras user-facing Chinese messages.
Database Access
- All DB access is synchronous (
better-sqlite3). Use prepared statements with?placeholders. - Call
getDb()at the top of each route handler — never cachedbat module scope in routes. - Multi-step writes use
db.transaction(). - Schema changes go at the bottom of
db/init.jsinsidetry/catchmigration guards.
API Conventions
- API prefix:
/api/. Auth prefix:/auth/. - List:
{ data: [...], pagination: { page, limit, total, totalPages } } - Single item:
{ data: { ... } } - Errors:
{ error: '<Chinese message>' }
Tags
- Only preset tags from
TAG_GROUPSinconfig/sections.jsare allowed — no custom input. DEFAULT_TAGSis the flat array for validation or legacy use.
API Layer Architecture
前端 API 层位于 src/api/,按后端路由模块化组织:
src/api/
├── auth.js # 认证:login, logout, fetchMe, checkLogin
├── stories.js # 故事 CRUD(legacy)
├── workshop.js # 工坊、模组、条目的所有操作
├── admin.js # 管理后台:用户管理、申请审核、工坊审批
├── creator.js # 创作者申请状态查询和提交
└── index.js # 统一导出
使用规范:
- 所有 HTTP 请求必须通过
src/api/模块,禁止在 views/stores 中直接使用fetch或axios。 src/utils/request.js封装 Axios,提供get/post/put/delete/patch方法,自动处理错误响应。- Store 调用 API 方法,View 调用 Store action 或直接调用 API(简单场景)。
- API 方法返回后端响应数据(已解析 JSON),错误时抛出带有
message属性的 Error 对象。
示例:
// store/workshop.js
import workshopApi from '@/api/workshop'
async function fetchPacks(page, options) {
packsLoading.value = true
try {
const json = await workshopApi.fetchPacks(page, options)
packs.value = json.packs
pagination.value = json.pagination
} catch (err) {
error.value = err.message || '获取模组列表失败'
} finally {
packsLoading.value = false
}
}
Key Architecture Notes
- Routing — home vs workshop:
{ name: 'home' }is/(landing page).{ name: 'workshop' }is/workshop(pack browser). Back buttons inProfileViewandWorkshopCreatemust go to{ name: 'home' }. - WorkshopView default:
workshopSlugdefaults tonull(show all workshops). Do not default to'steampunk'. - Workshop worldbook: Each workshop has a
worldbookfield (its default target worldbook name). Users can override it per-slug inlocalStorageviagetWorldbookName(slug)/saveWorldbookName(slug, name). The "恢复默认" button resets tocurrentWorkshop.worldbook. - Production:
NODE_ENV=production→ Express servesfrontend/dist/as static files with SPA fallback (/*splat→index.html). - Vite base path:
vite.config.jssetsbase: '/'(root path). All built assets are served from root in production. - SillyTavern integration:
workshop.jsstore detectswindow.SillyTavern(direct iframe) andwindow.opener(extension popup). Subscribe/unsubscribe callswindow.TavernHelperAPI to read/write worldbook entries. Gracefully no-ops outside ST. - ST extension (
st-extension/): Vanilla JS, no build step. Opens the workshop site in a popup and relaysTavernHelperAPI calls from the popup back into ST. Edit and test manually. https-proxy-agentmust stay at v5 (CJS). v6+ is ESM-only.- Known bug:
WorkshopEntryCard.vuecallsrouter.push({ name: 'workshop-edit', ... })— the correct route name isworkshop-entry-edit. Fix when touching that component.