Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
with:
python-version: "3.11"

- name: Install Python test dependencies
run: python -m pip install pytest

- name: Install dependencies
run: npm ci

Expand Down
21 changes: 21 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ flowchart TD
| Goal | Command |
| --- | --- |
| Run locally | `npm install && npm run dev` |
| Seed a playable demo | `npm run demo:seed && npm run dev` |
| Verify locally | `npm run lint && npm test && npm run build && pytest tests/python -q` |
| Export one remembered entry skill for Codex | `npm run skills:export -- --target codex --mode aggregator-only` |
| Export full bundle for Codex | `npm run skills:export -- --target codex --mode full-bundle` |
Expand All @@ -78,6 +79,26 @@ Defaults:
- Codex installs to `~/.codex/skills` unless `CODEX_HOME` is set
- Claude installs to `~/.claude/skills` unless `CLAUDE_HOME` is set

## Quick Start

```bash
npm install
npm run dev
```

Open `http://localhost:3000`, or the local port printed by Next.js.

To see the studio with useful first-run content:

```bash
npm run demo:seed
npm run dev
```

The seed command creates one idempotent local demo project, reference sample,
style card, and queued planning job. Running it again will not duplicate the
same demo.

## Top-level Skill

If you only want to remember one skill name, use:
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ flowchart TD
| 目标 | 命令 |
| --- | --- |
| 本地开发运行 | `npm install && npm run dev` |
| 预置一个可试玩 demo | `npm run demo:seed && npm run dev` |
| 本地完整验证 | `npm run lint && npm test && npm run build && pytest tests/python -q` |
| 为 Codex 导出一个总入口 skill | `npm run skills:export -- --target codex --mode aggregator-only` |
| 为 Codex 导出完整 skill bundle | `npm run skills:export -- --target codex --mode full-bundle` |
Expand All @@ -96,6 +97,15 @@ npm run dev

打开 `http://localhost:3000`,或者 Next.js 启动时显示的本地端口。

想先看一个有内容的仪表盘,可以运行:

```bash
npm run demo:seed
npm run dev
```

这个命令会创建一个幂等的本地示例项目、参考素材、风格卡和排队任务。重复运行不会重复插入同一个 demo。

推荐第一轮使用顺序:

1. 打开 `/wizard/new-novel`
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"build": "next build",
"start": "next start",
"lint": "eslint",
"demo:seed": "tsx scripts/seed-demo.ts",
"skills:export": "tsx scripts/export-skills.ts",
"skills:install": "tsx scripts/install-skills.ts",
"test": "vitest run",
Expand Down
9 changes: 9 additions & 0 deletions scripts/seed-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { seedDemoData } from "../src/lib/server/demo-seed";

function main() {
const result = seedDemoData();

console.log(JSON.stringify(result, null, 2));
}

main();
78 changes: 78 additions & 0 deletions src/lib/server/demo-seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
createDraftJob,
createProject,
createReferenceWork,
createStyleProfile,
listProjects,
} from "./db";

const DEMO_PROJECT_TITLE = "雾港债火";

type ProjectRow = {
id: number;
title: string;
};

export function seedDemoData() {
const existingProject = (listProjects() as ProjectRow[]).find(
(project) => project.title === DEMO_PROJECT_TITLE,
);

if (existingProject) {
return {
created: false,
message: "Demo data already exists.",
projectId: existingProject.id,
title: existingProject.title,
};
}

const project = createProject({
genre: "都市奇幻 / 悬疑",
premise:
"负债的夜班修表师在雾港听见旧钟里的亡者留言,被迫用七天查清一场债火案。",
title: DEMO_PROJECT_TITLE,
});

const reference = createReferenceWork({
creatorLabel: "demo sample",
projectId: project.id,
sourceLabel: "demo://fog-harbor-opening",
sourceType: "excerpt",
title: "雾港开场节奏样本",
});

const styleProfile = createStyleProfile({
antiPatterns: ["解释动机过早", "段尾连续抽象总结", "对白缺少动作承接"],
metrics: {
averageSentenceLength: 15.8,
dialogueRatio: 0.31,
hookDensity: 0.42,
},
name: "雾港悬压推进卡",
referenceWorkId: reference.id,
summary:
"短场景切换、物件线索推进、对白后接动作反应,章末保留一个可验证谜面。",
});

const job = createDraftJob({
jobType: "plan",
payload: {
chapter: 1,
focus: "建立债火案、旧钟留言和主角七天倒计时。",
styleProfileId: styleProfile.id,
},
projectId: project.id,
status: "queued",
});

return {
created: true,
jobId: job.id,
message: "Demo data created.",
projectId: project.id,
referenceId: reference.id,
styleProfileId: styleProfile.id,
title: project.title,
};
}
48 changes: 48 additions & 0 deletions tests/ts/demo-seed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

async function loadDemoModules() {
vi.resetModules();
return {
dashboard: await import("@/lib/server/dashboard"),
db: await import("@/lib/server/db"),
demoSeed: await import("@/lib/server/demo-seed"),
};
}

describe("demo seed data", () => {
afterEach(async () => {
const { db } = await loadDemoModules();
db.resetDatabaseForTests();
delete process.env.XIAOSHUO_HOME;
});

test("creates an idempotent first-run project, style card, and queued job", async () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "xiaoshuo-demo-seed-"));
process.env.XIAOSHUO_HOME = home;

const { dashboard, demoSeed } = await loadDemoModules();

const first = demoSeed.seedDemoData();
const second = demoSeed.seedDemoData();
const snapshot = dashboard.getDashboardSnapshot();

expect(first).toMatchObject({
created: true,
title: "雾港债火",
});
expect(second).toMatchObject({
created: false,
projectId: first.projectId,
});
expect(snapshot.metrics).toMatchObject({
projects: 1,
queuedJobs: 1,
references: 1,
styleProfiles: 1,
});
expect(snapshot.projects[0]?.title).toBe("雾港债火");
expect(snapshot.styleProfiles[0]?.name).toBe("雾港悬压推进卡");
});
});
Loading