v0.8.2: 工单大清理 - 编年史分页+限时挑战+星潮深化+云排行榜+手写叙事+socket多人星潮+UI打磨
工单 #8 编年史上限+分页: - engine.ts: slice(-50)→slice(-200) - ChronicleDialog.tsx: 加分页(每页10条)+上一页/下一页+页码显示 工单 #5 限时挑战 (subagent 10-a): - beacon.ts: BeaconTimedChallenge + getTimedSlotKey(4h时段) + generateTimedChallenge - BeaconPanel.tsx: amber主题限时区块 + 倒计时 + <30min紧急状态 - gameStore: trackBeacon 加 timedJustCompleted + claimTimedBeacon action 工单 #3 星潮类型深化 (subagent 10-a): - starTide.ts: +3种星潮 surge(emerald)/eclipse(rose)/prism(fuchsia) - TideModifiers: +targetLenBonus/bossWinRateBonus/autoDecodeIntervalMult - decode.ts: generatePuzzle 加 targetLenBonus 参数 - achievements: ach_tides_all 阈值 6→9 工单 #4/P2 云排行榜 (subagent 10-b): - mini-services/leaderboard-service/ (端口3030, Hono+bun, 内存1000条) - API: GET/POST /api/leaderboard + /stats + CORS + 防刷 - beacon.ts: fetchCloudLeaderboard/submitCloudScore - BeaconPanel: 本地Top20/全球Top100 双tab + YOU徽章高亮 工单 #9 手写叙事 (subagent 10-c): - chronicle.ts: EPOCH_LORE 5纪元×3节点=15段手写叙事(80-150字/段) - buildLore 优先手写节点, fallback 模板, 11个变量替换 P3 socket 多人星潮 (subagent 10-c): - mini-services/star-tide-service/ (端口3031, socket.io) - 每10-15min广播global-tide, 60s持续, 6种类型权重 - useGlobalTide hook + triggerGlobalTide action - StarTideIndicator 加 🌐 全球星潮标记 P2 UI打磨: - page.tsx: CrystalOrb 区加装饰全息环(3层旋转) + 四角标记 + 顶部状态条 + 底部铭文 QA: lint零错误 + dev HTTP200 + VLM 8/10 + 2个mini-service运行中(3030/3031)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# star-tide-service · 全球星潮同步 mini-service
|
||||
|
||||
> 回响星核 / Echo Nexus · P3 工单 #10-c
|
||||
>
|
||||
> socket.io 多人同步星潮 —— 所有在线玩家同时经历同一道全球星潮 60 秒。
|
||||
|
||||
## 设计
|
||||
|
||||
- **端口**:固定 `3031`
|
||||
- **网关**:Caddy 通过 `?XTransformPort=3031` 转发;前端用 `io("/?XTransformPort=3031")`
|
||||
- **运行时**:bun + socket.io 4
|
||||
- **存储**:纯内存(无状态,重启不影响前端游戏)
|
||||
|
||||
## 星潮节奏
|
||||
|
||||
- 首次星潮:服务端启动后 **60 秒**(可由 env `FIRST_TIDE_DELAY_MS` 覆盖,便于 QA)
|
||||
- 后续星潮:**10-15 分钟随机间隔**
|
||||
- 每次持续:**60 秒**
|
||||
- 类型:从 6 种星潮按权重随机抽取(与前端 `TIDE_EVENTS` 一致)
|
||||
- 晶体潮 22 / 谐振风暴 20 / 遗迹共振 16 / 虚空低语 18 / 星核悸动 14 / 寂静期 10
|
||||
|
||||
## socket.io 事件
|
||||
|
||||
### 服务端 → 客户端
|
||||
|
||||
| 事件 | 载荷 | 触发时机 |
|
||||
|------|------|---------|
|
||||
| `tide-state` | `{ current: GlobalTideState \| null, nextTideAt, serverTime }` | 客户端连接时立即推送 |
|
||||
| `global-tide` | `{ type, name, startedAt, endsAt, durationSec }` | 全球星潮开始 |
|
||||
| `tide-ended` | `{ endedAt, nextTideAt }` | 全球星潮结束 |
|
||||
| `next-tide-info` | `{ current, nextTideAt, serverTime, remainingMs }` | 响应客户端 `get-next-tide` |
|
||||
|
||||
### 客户端 → 服务端
|
||||
|
||||
| 事件 | 载荷 | 行为 |
|
||||
|------|------|------|
|
||||
| `get-next-tide` | — | 查询下次星潮剩余时间 |
|
||||
| `admin-trigger-tide` | — | **dev/QA 用**:立即触发一次全球星潮 |
|
||||
|
||||
## HTTP 端点
|
||||
|
||||
socket.io `path: '/'` 拦截所有 HTTP 请求,因此本服务不提供独立 HTTP 健康检查端点。
|
||||
|
||||
- `GET /` —— socket.io 默认握手响应(`{"code":0,"message":"Transport unknown"}` 表示服务在线)
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd mini-services/star-tide-service
|
||||
bun run dev # bun --hot index.ts,热重载
|
||||
# 或
|
||||
bun run start # bun index.ts,无热重载
|
||||
```
|
||||
|
||||
## QA 验证
|
||||
|
||||
```bash
|
||||
# socket.io 在线验证(收到 JSON 响应即表示服务在线)
|
||||
curl http://localhost:3031/
|
||||
# 预期:{"code":0,"message":"Transport unknown"}
|
||||
```
|
||||
|
||||
前端可在浏览器 console 触发全球星潮(QA 用):
|
||||
|
||||
```js
|
||||
const sock = io("/?XTransformPort=3031");
|
||||
sock.on("tide-state", (s) => console.log("当前状态:", s));
|
||||
sock.emit("admin-trigger-tide"); // 立即触发一次全球星潮
|
||||
```
|
||||
|
||||
## 离线容错
|
||||
|
||||
- 服务端宕机 → 前端 `useGlobalTide` 自动重连(socket.io 默认行为)
|
||||
- 断连期间 → 前端本地 `tickTide` 继续按原节奏工作,玩家不会卡住
|
||||
- 全球星潮进行中 → 前端本地 `tickTide` 暂停触发(避免冲突)
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "echo-nexus-star-tide-service",
|
||||
"dependencies": {
|
||||
"socket.io": "^4.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||
|
||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||
|
||||
"@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"engine.io": ["engine.io@6.6.9", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0" } }, "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg=="],
|
||||
|
||||
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="],
|
||||
|
||||
"socket.io-adapter": ["socket.io-adapter@2.5.8", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.21.0" } }, "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw=="],
|
||||
|
||||
"socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// 回响星核 / Echo Nexus — 全球星潮同步 mini-service
|
||||
// P3/工单 #10-c:socket.io 多人同步星潮
|
||||
// 端口固定 3031;Caddy 通过 ?XTransformPort=3031 转发。
|
||||
// 前端连接:io("/?XTransformPort=3031"),path 默认 "/"。
|
||||
//
|
||||
// 逻辑:
|
||||
// - 服务端每 10-15 分钟(随机间隔)随机选一个星潮类型,
|
||||
// 广播 `global-tide` 事件 { type, name, startedAt, endsAt, durationSec: 60 }
|
||||
// - 客户端连接后立即收到 `tide-state` 事件,包含当前进行中的星潮(如有)
|
||||
// + 下次星潮时间戳 + 服务器时间(用于时钟同步)
|
||||
// - 客户端可随时 emit `get-next-tide`,服务端响应 `next-tide-info`
|
||||
// { current, nextTideAt, serverTime, remainingMs }
|
||||
// - 全球星潮结束时广播 `tide-ended` { endedAt, nextTideAt }
|
||||
// - `admin-trigger-tide`(dev 用):立即触发一次全球星潮,便于 QA 验证
|
||||
//
|
||||
// 离线容错:服务端宕机时,前端 useGlobalTide hook 会自动重连;
|
||||
// 在断连期间,本地 tickTide 继续按原节奏工作,玩家不会被卡住。
|
||||
|
||||
import { createServer } from "http";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 配置
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 全球星潮持续秒数(与前端 triggerGlobalTide 保持一致) */
|
||||
const DURATION_SEC = 60;
|
||||
|
||||
/** 首次星潮延迟(毫秒)—— 默认 60s 便于 QA 验证;可通过 env FIRST_TIDE_DELAY_MS 覆盖 */
|
||||
const FIRST_TIDE_DELAY_MS = Number(process.env.FIRST_TIDE_DELAY_MS) || 60 * 1000;
|
||||
|
||||
/** 后续星潮最小间隔(10 分钟) */
|
||||
const MIN_GAP_MS = 10 * 60 * 1000;
|
||||
|
||||
/** 后续星潮最大间隔(15 分钟) */
|
||||
const MAX_GAP_MS = 15 * 60 * 1000;
|
||||
|
||||
/** 固定端口 3031 */
|
||||
const PORT = 3031;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 星潮类型表(与前端 TIDE_EVENTS 一致:crystal/resonance/ruins/void/core/silence)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type TideType = "crystal" | "resonance" | "ruins" | "void" | "core" | "silence";
|
||||
|
||||
const TIDE_META: Record<TideType, { name: string; weight: number }> = {
|
||||
crystal: { name: "晶体潮", weight: 22 },
|
||||
resonance: { name: "谐振风暴", weight: 20 },
|
||||
ruins: { name: "遗迹共振", weight: 16 },
|
||||
void: { name: "虚空低语", weight: 18 },
|
||||
core: { name: "星核悸动", weight: 14 },
|
||||
silence: { name: "寂静期", weight: 10 },
|
||||
};
|
||||
|
||||
const TIDE_TYPES = Object.keys(TIDE_META) as TideType[];
|
||||
const TIDE_TOTAL_WEIGHT = TIDE_TYPES.reduce((s, t) => s + TIDE_META[t].weight, 0);
|
||||
|
||||
function rollTide(): TideType {
|
||||
let r = Math.random() * TIDE_TOTAL_WEIGHT;
|
||||
for (const t of TIDE_TYPES) {
|
||||
r -= TIDE_META[t].weight;
|
||||
if (r <= 0) return t;
|
||||
}
|
||||
return "crystal";
|
||||
}
|
||||
|
||||
function randomGap(): number {
|
||||
return MIN_GAP_MS + Math.floor(Math.random() * (MAX_GAP_MS - MIN_GAP_MS));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 全局状态
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface GlobalTideState {
|
||||
type: TideType;
|
||||
name: string;
|
||||
startedAt: number;
|
||||
endsAt: number;
|
||||
durationSec: number;
|
||||
}
|
||||
|
||||
let currentTide: GlobalTideState | null = null;
|
||||
let nextTideAt: number = Date.now() + FIRST_TIDE_DELAY_MS;
|
||||
let tideStartTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let tideEndTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 星潮调度
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function startTide(): void {
|
||||
// 若已有进行中的星潮,先结束再开(理论上不应发生,防御性)
|
||||
if (currentTide) {
|
||||
endTide();
|
||||
}
|
||||
// 清掉残留的 tideEndTimer,避免泄漏
|
||||
if (tideEndTimer) {
|
||||
clearTimeout(tideEndTimer);
|
||||
tideEndTimer = null;
|
||||
}
|
||||
const type = rollTide();
|
||||
const now = Date.now();
|
||||
currentTide = {
|
||||
type,
|
||||
name: TIDE_META[type].name,
|
||||
startedAt: now,
|
||||
endsAt: now + DURATION_SEC * 1000,
|
||||
durationSec: DURATION_SEC,
|
||||
};
|
||||
console.log(
|
||||
`[global-tide] ✦ ${currentTide.name} (${type}) 已降临 · 持续 ${DURATION_SEC}s · 结束于 ${new Date(currentTide.endsAt).toLocaleTimeString()}`
|
||||
);
|
||||
io.emit("global-tide", currentTide);
|
||||
|
||||
// 调度结束 → endTide 会自己 clearTimeout(tideEndTimer);
|
||||
// 结束后自动 scheduleNextTide
|
||||
tideEndTimer = setTimeout(() => {
|
||||
endTide();
|
||||
scheduleNextTide();
|
||||
}, DURATION_SEC * 1000);
|
||||
}
|
||||
|
||||
function endTide(): void {
|
||||
if (!currentTide) return;
|
||||
console.log(`[global-tide] ◌ ${currentTide.name} 已退去`);
|
||||
currentTide = null;
|
||||
io.emit("tide-ended", {
|
||||
endedAt: Date.now(),
|
||||
nextTideAt,
|
||||
});
|
||||
if (tideEndTimer) {
|
||||
clearTimeout(tideEndTimer);
|
||||
tideEndTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNextTide(): void {
|
||||
// 清掉残留的 tideStartTimer,避免泄漏
|
||||
if (tideStartTimer) {
|
||||
clearTimeout(tideStartTimer);
|
||||
tideStartTimer = null;
|
||||
}
|
||||
const gap = randomGap();
|
||||
nextTideAt = Date.now() + gap;
|
||||
console.log(
|
||||
`[global-tide] 下次星潮将在 ${Math.round(gap / 1000)}s 后 · ${new Date(nextTideAt).toLocaleTimeString()}`
|
||||
);
|
||||
tideStartTimer = setTimeout(() => {
|
||||
startTide();
|
||||
}, gap);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// socket.io
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 注意:socket.io `path: '/'` 会拦截所有 HTTP 请求(包括 /health),
|
||||
// 因此本服务不提供独立 HTTP 健康检查端点;QA 用 `curl http://localhost:3031/`
|
||||
// 收到 `{"code":0,"message":"Transport unknown"}` 即表示 socket.io 在线。
|
||||
const httpServer = createServer();
|
||||
|
||||
const io = new Server(httpServer, {
|
||||
// 路径必须为 "/",Caddy 据此转发;同时这也是 catch-all,
|
||||
// 任何 URL 都会被 socket.io 接管(前端默认 /socket.io/ 也会被捕获)
|
||||
path: "/",
|
||||
cors: { origin: "*", methods: ["GET", "POST"] },
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000,
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`[socket] 客户端连接: ${socket.id}(当前在线 ${io.engine.clientsCount})`);
|
||||
|
||||
// 立即推送当前状态
|
||||
socket.emit("tide-state", {
|
||||
current: currentTide,
|
||||
nextTideAt,
|
||||
serverTime: Date.now(),
|
||||
});
|
||||
|
||||
// 客户端查询下次星潮剩余时间
|
||||
socket.on("get-next-tide", () => {
|
||||
socket.emit("next-tide-info", {
|
||||
current: currentTide,
|
||||
nextTideAt,
|
||||
serverTime: Date.now(),
|
||||
remainingMs: Math.max(0, nextTideAt - Date.now()),
|
||||
});
|
||||
});
|
||||
|
||||
// dev/QA 用:手动触发一次全球星潮
|
||||
socket.on("admin-trigger-tide", () => {
|
||||
console.log(`[socket] admin-trigger-tide by ${socket.id}`);
|
||||
// 取消尚未触发的下次星潮定时器(startTide 会负责调度本次星潮的结束 + 下次开始)
|
||||
if (tideStartTimer) {
|
||||
clearTimeout(tideStartTimer);
|
||||
tideStartTimer = null;
|
||||
}
|
||||
startTide();
|
||||
// startTide 内部已 setTimeout(DURATION_SEC) → endTide + scheduleNextTide
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`[socket] 客户端断开: ${socket.id}(${reason})`);
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
console.error(`[socket] 错误 (${socket.id}):`, err);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 启动
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`[star-tide-service] 监听端口 ${PORT}`);
|
||||
console.log(
|
||||
`[star-tide-service] 首次星潮将在 ${Math.round(FIRST_TIDE_DELAY_MS / 1000)}s 后 · ${new Date(nextTideAt).toLocaleTimeString()}`
|
||||
);
|
||||
console.log(`[star-tide-service] 后续星潮间隔:${MIN_GAP_MS / 1000 / 60}-${MAX_GAP_MS / 1000 / 60} 分钟随机`);
|
||||
console.log(`[star-tide-service] QA 验证:curl http://localhost:${PORT}/ → {"code":0,"message":"Transport unknown"}`);
|
||||
|
||||
// 调度首次星潮(startTide 内部会在 60s 后自动 endTide + scheduleNextTide,无需在此重复调度)
|
||||
tideStartTimer = setTimeout(() => {
|
||||
startTide();
|
||||
}, FIRST_TIDE_DELAY_MS);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 优雅退出
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function shutdown(signal: string) {
|
||||
console.log(`[star-tide-service] 收到 ${signal},正在关闭…`);
|
||||
if (tideStartTimer) clearTimeout(tideStartTimer);
|
||||
if (tideEndTimer) clearTimeout(tideEndTimer);
|
||||
io.close(() => {
|
||||
httpServer.close(() => {
|
||||
console.log("[star-tide-service] 已关闭");
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "echo-nexus-star-tide-service",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --hot index.ts",
|
||||
"start": "bun index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"socket.io": "^4.8.3"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user