新增 PM2 部署設定與生產用靜態伺服器
摘要: 加入 ecosystem.config.cjs(web + watch 兩個 app)與零依賴靜態伺服器 server/serve.mjs,並將監看伺服器改為僅綁 127.0.0.1。 根本原因: 先前只能用 vite dev server 開發模式跑,沒有常駐部署方式; 且 watch.mjs 預設綁所有介面,transcript 資料有暴露到區網的風險。 影響: pm2 start ecosystem.config.cjs 即可常駐: - claude-office-web:5180,綁 0.0.0.0,區網可看(模擬模式) - claude-office-watch:5179,僅 127.0.0.1,實況模式限本機瀏覽器 修法: - server/serve.mjs:零依賴靜態服務 dist/,SPA fallback, assets 走 immutable 快取 - watch.mjs listen 加上 127.0.0.1 - README 補 PM2 部署章節(含 Windows 開機自啟說明) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,28 @@ session transcript(JSONL),只讀取新增的位元組,轉譯成事件推播給
|
|||||||
| 專案 15 秒沒動靜 | 待命;5 分鐘沒動靜 → 離線(Zzz) |
|
| 專案 15 秒沒動靜 | 待命;5 分鐘沒動靜 → 離線(Zzz) |
|
||||||
|
|
||||||
注意:監看伺服器會讀取本機的 Claude Code 對話記錄(含程式碼與提示詞),
|
注意:監看伺服器會讀取本機的 Claude Code 對話記錄(含程式碼與提示詞),
|
||||||
**只綁 localhost、不要對外開放**;如需團隊共用請自行加上驗證。
|
**只綁 127.0.0.1、不要對外開放**;如需團隊共用請自行加上驗證。
|
||||||
|
|
||||||
|
## PM2 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # 產出 dist/
|
||||||
|
pm2 start ecosystem.config.cjs # 啟動 web(5180)+ watch(5179)
|
||||||
|
pm2 save # 記住程序清單
|
||||||
|
```
|
||||||
|
|
||||||
|
| App | Port | 綁定 | 說明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `claude-office-web` | 5180 | 0.0.0.0 | `server/serve.mjs` 靜態服務 dist/(區網手機可看,模擬模式) |
|
||||||
|
| `claude-office-watch` | 5179 | 127.0.0.1 | transcript 監看,僅本機(實況模式只在本機瀏覽器可用) |
|
||||||
|
|
||||||
|
常用指令:`pm2 logs claude-office-web`、`pm2 restart claude-office-web`、
|
||||||
|
`pm2 delete claude-office-web claude-office-watch`。
|
||||||
|
更新版本:`git pull && npm run build && pm2 restart all`(靜態檔重讀即可,
|
||||||
|
其實只有 server/ 變動才需要 restart)。
|
||||||
|
|
||||||
|
Windows 開機自啟:PM2 原生 `pm2 startup` 不支援 Windows,可另裝
|
||||||
|
[`pm2-windows-startup`](https://www.npmjs.com/package/pm2-windows-startup) 或用工作排程器執行 `pm2 resurrect`。
|
||||||
|
|
||||||
## 架構
|
## 架構
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "claude-office-web",
|
||||||
|
script: "server/serve.mjs",
|
||||||
|
cwd: __dirname,
|
||||||
|
env: { PORT: 5180 },
|
||||||
|
max_restarts: 5,
|
||||||
|
restart_delay: 3000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-office-watch",
|
||||||
|
script: "server/watch.mjs",
|
||||||
|
cwd: __dirname,
|
||||||
|
env: { PORT: 5179 },
|
||||||
|
max_restarts: 5,
|
||||||
|
restart_delay: 3000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Minimal static server for the production build (dist/).
|
||||||
|
* Zero dependencies. SPA fallback to index.html.
|
||||||
|
* Binds 0.0.0.0 so the office is viewable from the LAN;
|
||||||
|
* the transcript watcher stays localhost-only.
|
||||||
|
*/
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const PORT = Number(process.env.PORT || 5180);
|
||||||
|
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "dist");
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".js": "text/javascript",
|
||||||
|
".css": "text/css",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".png": "image/png",
|
||||||
|
".ico": "image/x-icon",
|
||||||
|
".json": "application/json",
|
||||||
|
".woff2": "font/woff2",
|
||||||
|
".map": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const urlPath = decodeURIComponent((req.url || "/").split("?")[0]);
|
||||||
|
let filePath = path.normalize(path.join(ROOT, urlPath));
|
||||||
|
if (!filePath.startsWith(ROOT)) {
|
||||||
|
res.writeHead(403);
|
||||||
|
res.end("forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stat = await fs.stat(filePath).catch(() => null);
|
||||||
|
if (!stat || stat.isDirectory()) {
|
||||||
|
filePath = path.join(ROOT, "index.html");
|
||||||
|
}
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const data = await fs.readFile(filePath);
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": MIME[ext] || "application/octet-stream",
|
||||||
|
"Cache-Control": urlPath.startsWith("/assets/")
|
||||||
|
? "public, max-age=31536000, immutable"
|
||||||
|
: "no-cache",
|
||||||
|
});
|
||||||
|
res.end(data);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end("server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, "0.0.0.0", () => {
|
||||||
|
console.log(`✳ Claude Office web http://localhost:${PORT} (serving dist/)`);
|
||||||
|
});
|
||||||
+2
-1
@@ -253,7 +253,8 @@ const server = createServer((req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await pollOnce(true);
|
await pollOnce(true);
|
||||||
server.listen(PORT, () => {
|
// Transcripts are sensitive — never expose beyond this machine.
|
||||||
|
server.listen(PORT, "127.0.0.1", () => {
|
||||||
console.log(`✳ Claude Office watcher`);
|
console.log(`✳ Claude Office watcher`);
|
||||||
console.log(` watching ${PROJECTS_ROOT}`);
|
console.log(` watching ${PROJECTS_ROOT}`);
|
||||||
console.log(` projects ${projects.size} active in last ${ACTIVE_WINDOW_DAYS} days`);
|
console.log(` projects ${projects.size} active in last ${ACTIVE_WINDOW_DAYS} days`);
|
||||||
|
|||||||
Reference in New Issue
Block a user