docs: 部署脚本与项目文档
- scripts/ 部署与构建脚本 - README/.gitignore 等
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp/
|
||||
.pnp.js
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
dist-ssr/
|
||||
*.local
|
||||
|
||||
# Cache
|
||||
.vite/
|
||||
.cache/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor / OS
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# Supabase generated types
|
||||
supabase/.temp/
|
||||
@@ -0,0 +1,15 @@
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
*.cache
|
||||
|
||||
# Misc
|
||||
coverage/
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,112 @@
|
||||
# PineAgents Web
|
||||
|
||||
Static site (Vite + React) for the PineAgents product — the public web display port (PineAgentsWeb) for the PineAgents OPC 超级个体平台. Built output is served with a minimal Node server that supports SPA fallback (e.g. direct access to `/docs/channels`).
|
||||
|
||||
> 项目身份:`pineagents-web`。页面内视觉/文案仍沿用既有 QwenPaw 素材,未做品牌改造。
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- pnpm (recommended) or npm
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# or
|
||||
npm install
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
# or
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Dev server runs at `http://localhost:5173` (or the next free port).
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
# or
|
||||
npm run build
|
||||
```
|
||||
|
||||
Output is in `dist/`.
|
||||
|
||||
## Preview (local only)
|
||||
|
||||
- **Vite preview** (quick local check):
|
||||
`pnpm run preview`
|
||||
|
||||
- **Production-style server** (same as prod, no PM2):
|
||||
`pnpm run preview:prod`
|
||||
Serves `dist/` at `http://localhost:8088` with SPA fallback.
|
||||
|
||||
## Production with PM2
|
||||
|
||||
Use PM2 to run the preview server in production: auto-restart on crash, logs, and easy start/stop.
|
||||
|
||||
### 1. Install PM2 globally (one-time)
|
||||
|
||||
```bash
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
If the script runs without global PM2, it will try to install it automatically.
|
||||
|
||||
### 2. Build and start
|
||||
|
||||
```bash
|
||||
cd website
|
||||
pnpm run build
|
||||
pm2 start ecosystem.config.cjs
|
||||
```
|
||||
|
||||
Or from repo root (install + build + PM2 start/reload):
|
||||
|
||||
```bash
|
||||
bash scripts/website_build.sh
|
||||
```
|
||||
|
||||
Or use the helper script (installs PM2 if missing, then starts):
|
||||
|
||||
```bash
|
||||
bash scripts/start.sh
|
||||
```
|
||||
|
||||
Default port: **8088**. Override with `PORT=3000 pm2 start ecosystem.config.cjs` or by editing `ecosystem.config.cjs`.
|
||||
|
||||
### 3. PM2 commands
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------------------------- | ----------------------------------------- |
|
||||
| `pm2 status` | List apps and status |
|
||||
| `pm2 logs pineagents-web` | Stream stdout/stderr logs |
|
||||
| `pm2 restart pineagents-web` | Restart the app |
|
||||
| `pm2 reload ecosystem.config.cjs --update-env` | Reload with latest config (zero-downtime) |
|
||||
| `pm2 stop pineagents-web` | Stop the app |
|
||||
| `pm2 delete pineagents-web` | Remove from PM2 (stop + delete) |
|
||||
|
||||
### 4. After code/build changes
|
||||
|
||||
Rebuild then reload so PM2 serves the new `dist/`:
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
pm2 reload ecosystem.config.cjs --update-env
|
||||
```
|
||||
|
||||
Or from repo root:
|
||||
|
||||
```bash
|
||||
bash scripts/website_build.sh
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
- **Port**: Set in `ecosystem.config.cjs` (`env.PORT` / `args`) or env `PORT`. Default `8088`.
|
||||
- **App name**: `pineagents-web` in `ecosystem.config.cjs` (used in `pm2 logs/restart`).
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build search-index.json from docs/*.zh.md and *.en.md for client-side search.
|
||||
* Run before vite build so dist gets the index.
|
||||
*/
|
||||
import { readdir, readFile, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const docsDir = join(__dirname, "..", "public", "docs");
|
||||
const outPath = join(__dirname, "..", "public", "search-index.json");
|
||||
|
||||
const EXCERPT_LEN = 800;
|
||||
|
||||
function slugifyHeading(text) {
|
||||
const s = text
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-zA-Z0-9_\-\u4e00-\u9fa5]/g, "");
|
||||
return s || "section";
|
||||
}
|
||||
|
||||
function stripMarkdownForExcerpt(md) {
|
||||
return md
|
||||
.replace(/<!--[\s\S]*?-->/g, " ")
|
||||
.replace(/<svg[\s\S]*?<\/svg>/gi, " ")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/^#+\s+.+$/gm, "")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/[*_`#]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseDoc(md) {
|
||||
const lines = md.split("\n");
|
||||
let title = "";
|
||||
const headings = [];
|
||||
let body = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const h2 = /^##\s+(.+)$/.exec(line);
|
||||
const h3 = /^###\s+(.+)$/.exec(line);
|
||||
if (i === 0 && line.startsWith("# ")) {
|
||||
title = line
|
||||
.slice(2)
|
||||
.replace(/#+\s*$/, "")
|
||||
.trim();
|
||||
continue;
|
||||
}
|
||||
if (h2) {
|
||||
const text = h2[1].replace(/#+\s*$/, "").trim();
|
||||
headings.push({ level: 2, text, id: slugifyHeading(text) });
|
||||
continue;
|
||||
}
|
||||
if (h3) {
|
||||
const text = h3[1].replace(/#+\s*$/, "").trim();
|
||||
headings.push({ level: 3, text, id: slugifyHeading(text) });
|
||||
continue;
|
||||
}
|
||||
if (!title && line.trim()) title = line.replace(/#+\s*$/, "").trim();
|
||||
body.push(line);
|
||||
}
|
||||
|
||||
const fullBody = body.join("\n");
|
||||
const excerpt = stripMarkdownForExcerpt(fullBody).slice(0, EXCERPT_LEN);
|
||||
|
||||
return { title: title || "Untitled", headings, excerpt };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const files = await readdir(docsDir);
|
||||
const entries = [];
|
||||
|
||||
for (const f of files) {
|
||||
const zhMatch = f.match(/^(.+)\.zh\.md$/);
|
||||
const enMatch = f.match(/^(.+)\.en\.md$/);
|
||||
const lang = zhMatch ? "zh" : enMatch ? "en" : null;
|
||||
const slug = zhMatch?.[1] ?? enMatch?.[1];
|
||||
if (!lang || !slug) continue;
|
||||
|
||||
const path = join(docsDir, f);
|
||||
const md = await readFile(path, "utf-8");
|
||||
const { title, headings, excerpt } = parseDoc(md);
|
||||
entries.push({
|
||||
slug,
|
||||
lang,
|
||||
title,
|
||||
headings: headings.map((h) => ({ text: h.text, id: h.id })),
|
||||
excerpt,
|
||||
});
|
||||
}
|
||||
|
||||
await writeFile(outPath, JSON.stringify(entries), "utf-8");
|
||||
console.log(`Wrote ${entries.length} doc entries to ${outPath}`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* After vite build, copy index.html into docs/ and docs/<slug>/ so that
|
||||
* static hosts (e.g. GitHub Pages) serve the SPA for each route without
|
||||
* needing a Node server. Must match DOC_SLUGS in src/pages/Docs.tsx.
|
||||
*/
|
||||
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const distDir = join(__dirname, "..", "dist");
|
||||
|
||||
const DOC_SLUGS = [
|
||||
"intro",
|
||||
"quickstart",
|
||||
"console",
|
||||
"channels",
|
||||
"skills",
|
||||
"memory",
|
||||
"compact",
|
||||
"commands",
|
||||
"heartbeat",
|
||||
"config",
|
||||
"backup",
|
||||
"cli",
|
||||
"creator",
|
||||
"community",
|
||||
"contributing",
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const indexHtml = await readFile(join(distDir, "index.html"), "utf-8");
|
||||
const BLOG_SLUGS = [
|
||||
"introducing-qwenpaw-driver",
|
||||
"qwenpaw-developer-day-collection",
|
||||
"play-with-qwenpaw-pet",
|
||||
];
|
||||
const paths = [
|
||||
"docs",
|
||||
"docs/search",
|
||||
...DOC_SLUGS.map((s) => `docs/${s}`),
|
||||
"blog",
|
||||
...BLOG_SLUGS.map((s) => `blog/${s}`),
|
||||
];
|
||||
for (const p of paths) {
|
||||
const out = join(distDir, p, "index.html");
|
||||
await mkdir(dirname(out), { recursive: true });
|
||||
await writeFile(out, indexHtml);
|
||||
}
|
||||
console.log("[spa-fallback-pages] Wrote index.html for /, /docs, /docs/*");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user