contactsheet 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1187,3 +1187,34 @@ body:not([data-sidebar="off"]) #cs-hud { left: calc(var(--cs-sb-w) + 16px); }
1187
1187
  box-shadow: 0 0 0 3px var(--cs-accent), 0 0 0 6px var(--cs-accent-32);
1188
1188
  z-index: 4;
1189
1189
  }
1190
+
1191
+ /* ---------- dev server 僵死横幅(全局,盖在一切之上但不拦事件之外的区域) ---------- */
1192
+ .cs-health {
1193
+ position: fixed;
1194
+ top: 12px;
1195
+ left: 50%;
1196
+ transform: translateX(-50%);
1197
+ z-index: var(--cs-z-overlay);
1198
+ display: flex;
1199
+ align-items: baseline;
1200
+ gap: var(--cs-sp-4);
1201
+ max-width: min(720px, 86vw);
1202
+ padding: var(--cs-sp-4) var(--cs-sp-6);
1203
+ border: 1px solid var(--cs-danger);
1204
+ border-radius: var(--cs-r-3);
1205
+ background: color-mix(in srgb, var(--cs-danger) 12%, var(--cs-s1));
1206
+ color: var(--cs-fg);
1207
+ font: var(--cs-fs-sm)/1.5 var(--cs-ff-sans);
1208
+ box-shadow: var(--cs-e3);
1209
+ /* 入场:@starting-style 由 transition 接住,无 JS 时序(MOTION-013);进入用 ease-out(MOTION-006) */
1210
+ transition: opacity var(--cs-dur-3) var(--cs-ease-out), translate var(--cs-dur-3) var(--cs-ease-out);
1211
+ }
1212
+ @starting-style {
1213
+ .cs-health { opacity: 0; translate: 0 -8px; }
1214
+ }
1215
+ .cs-health b { color: var(--cs-danger); font-weight: var(--cs-fw-bold); white-space: nowrap; }
1216
+ .cs-health span { color: var(--cs-fg-2); }
1217
+ @media (prefers-reduced-motion: reduce) {
1218
+ .cs-health { transition: opacity var(--cs-dur-3) var(--cs-ease-out); }
1219
+ @starting-style { .cs-health { translate: none; } }
1220
+ }
package/dist/cli.js CHANGED
@@ -1147,6 +1147,56 @@ async function pushToSession(cfg, text2, pid) {
1147
1147
  return { ok: true, pid: target.pid, name: target.name };
1148
1148
  }
1149
1149
 
1150
+ // src/server/health.ts
1151
+ var INTERVAL_MS = Number(process.env.CS_HEALTH_INTERVAL_MS ?? 2e4);
1152
+ var TIMEOUT_MS = Number(process.env.CS_HEALTH_TIMEOUT_MS ?? 1e4);
1153
+ var FAILS_TO_TRIP = 2;
1154
+ var current = { ok: true };
1155
+ function currentHealth() {
1156
+ return current;
1157
+ }
1158
+ function startHealthMonitor(cfg, broadcast2) {
1159
+ let fails = 0;
1160
+ let everOk = false;
1161
+ let timer = null;
1162
+ const probe = async () => {
1163
+ let failDetail = null;
1164
+ try {
1165
+ const r = await fetch(`${cfg.target}/__cs/registry`, { signal: AbortSignal.timeout(TIMEOUT_MS) });
1166
+ if (r.ok || r.status === 404) {
1167
+ everOk = true;
1168
+ fails = 0;
1169
+ if (!current.ok) {
1170
+ current = { ok: true, lastOkAt: Date.now() };
1171
+ broadcast2({ type: "health", ok: true });
1172
+ } else {
1173
+ current = { ok: true, lastOkAt: Date.now() };
1174
+ }
1175
+ return;
1176
+ }
1177
+ failDetail = `SSR \u63A2\u6D4B\u8FD4\u56DE HTTP ${r.status}`;
1178
+ } catch (err) {
1179
+ const e = err;
1180
+ failDetail = e.name === "TimeoutError" ? `SSR \u63A2\u6D4B ${TIMEOUT_MS / 1e3}s \u65E0\u54CD\u5E94` : `\u8FDE\u4E0D\u4E0A(${e.cause?.code ?? "network"})`;
1181
+ }
1182
+ if (!everOk) return;
1183
+ fails++;
1184
+ if (fails >= FAILS_TO_TRIP && current.ok) {
1185
+ current = { ok: false, detail: failDetail ?? "\u672A\u77E5", lastOkAt: current.lastOkAt };
1186
+ broadcast2({ type: "health", ok: false, detail: current.detail });
1187
+ console.warn(`[contactsheet] \u26A0 \u76EE\u6807 ${cfg.target} \u7591\u4F3C\u50F5\u6B7B:${current.detail}(\u9759\u6001\u53EF\u80FD\u4ECD 200)\u3002\u753B\u5E03\u5DF2\u6302\u6A2A\u5E45\u3002`);
1188
+ }
1189
+ };
1190
+ timer = setInterval(() => void probe(), INTERVAL_MS);
1191
+ timer.unref();
1192
+ void probe();
1193
+ return {
1194
+ close() {
1195
+ if (timer) clearInterval(timer);
1196
+ }
1197
+ };
1198
+ }
1199
+
1150
1200
  // src/server/sse.ts
1151
1201
  var clients = /* @__PURE__ */ new Set();
1152
1202
  var heartbeat = null;
@@ -1383,6 +1433,10 @@ async function handleApi(cfg, req, res, pathname) {
1383
1433
  const rel = await saveRef(cfg, body.name || "ref.png", body.dataBase64);
1384
1434
  return sendJson(res, 200, { path: rel });
1385
1435
  }
1436
+ if (pathname === "/__cs/api/health") {
1437
+ if (method !== "GET") return sendText(res, 405, "method not allowed");
1438
+ return sendJson(res, 200, currentHealth());
1439
+ }
1386
1440
  if (pathname === "/__cs/api/context") {
1387
1441
  if (method !== "GET") return sendText(res, 405, "method not allowed");
1388
1442
  try {
@@ -1559,6 +1613,11 @@ async function startServer(cfg) {
1559
1613
  res.destroy();
1560
1614
  }
1561
1615
  });
1616
+ proxy.on("proxyReq", (proxyReq, _req, res) => {
1617
+ res.on("close", () => {
1618
+ if (!res.writableEnded) proxyReq.destroy();
1619
+ });
1620
+ });
1562
1621
  proxy.on("proxyRes", () => proxyLog.alive());
1563
1622
  proxy.on("open", () => proxyLog.alive());
1564
1623
  let mcp = null;
@@ -1594,9 +1653,11 @@ async function startServer(cfg) {
1594
1653
  warn(`\u76D1\u542C\u5728 ${host}:${port} \u2014\u2014 \u753B\u5E03\u65E0\u9274\u6743,\u540C\u7F51\u7EDC\u7684\u4EFB\u4F55\u4EBA\u90FD\u80FD\u8BFB\u4F60\u7684\u6279\u6CE8\u3001\u5E76\u5411\u4F60\u7684 Claude Code \u4F1A\u8BDD\u63A8\u9001\u6D88\u606F\u3002\u53EA\u5728\u53EF\u4FE1\u7F51\u7EDC\u8FD9\u4E48\u505A\u3002`)
1595
1654
  );
1596
1655
  }
1656
+ const health = startHealthMonitor(cfg, broadcast);
1597
1657
  return {
1598
1658
  port,
1599
1659
  async close() {
1660
+ health.close();
1600
1661
  closeAll();
1601
1662
  proxyLog.close();
1602
1663
  proxy.close();