contactsheet 0.1.3 → 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.
- package/README.md +19 -2
- package/dist/canvas/app.js +525 -459
- package/dist/canvas/app.js.map +4 -4
- package/dist/canvas/style.css +31 -0
- package/dist/cli.js +95 -8
- package/dist/cli.js.map +4 -4
- package/package.json +1 -1
package/dist/canvas/style.css
CHANGED
|
@@ -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;
|
|
@@ -1581,18 +1640,30 @@ async function startServer(cfg) {
|
|
|
1581
1640
|
s.on("close", () => sockets.delete(s));
|
|
1582
1641
|
});
|
|
1583
1642
|
const host = cfg.host ?? "127.0.0.1";
|
|
1584
|
-
|
|
1643
|
+
let twin = null;
|
|
1644
|
+
const port = await listenWithFallback(server, cfg, host, (t) => {
|
|
1645
|
+
twin = t;
|
|
1646
|
+
t.on("connection", (sock) => {
|
|
1647
|
+
sockets.add(sock);
|
|
1648
|
+
sock.on("close", () => sockets.delete(sock));
|
|
1649
|
+
});
|
|
1650
|
+
});
|
|
1585
1651
|
if (host !== "127.0.0.1" && host !== "localhost") {
|
|
1586
1652
|
console.warn(
|
|
1587
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`)
|
|
1588
1654
|
);
|
|
1589
1655
|
}
|
|
1656
|
+
const health = startHealthMonitor(cfg, broadcast);
|
|
1590
1657
|
return {
|
|
1591
1658
|
port,
|
|
1592
1659
|
async close() {
|
|
1660
|
+
health.close();
|
|
1593
1661
|
closeAll();
|
|
1594
1662
|
proxyLog.close();
|
|
1595
1663
|
proxy.close();
|
|
1664
|
+
if (twin) {
|
|
1665
|
+
await new Promise((r) => twin.close(() => r()));
|
|
1666
|
+
}
|
|
1596
1667
|
await new Promise((resolve2) => {
|
|
1597
1668
|
const fallback = setTimeout(resolve2, 2e3);
|
|
1598
1669
|
for (const s of sockets) s.destroy();
|
|
@@ -1674,18 +1745,34 @@ async function occupiedBySameProject(cfg, port) {
|
|
|
1674
1745
|
return false;
|
|
1675
1746
|
}
|
|
1676
1747
|
}
|
|
1677
|
-
async function listenWithFallback(server, cfg, host) {
|
|
1678
|
-
const
|
|
1748
|
+
async function listenWithFallback(server, cfg, host, onTwin) {
|
|
1749
|
+
const listenOnce = (srv, port, h) => new Promise((resolve2, reject) => {
|
|
1679
1750
|
const onErr = (err) => {
|
|
1680
|
-
if (err.code === "EADDRINUSE") resolve2(
|
|
1751
|
+
if (err.code === "EADDRINUSE") resolve2("busy");
|
|
1752
|
+
else if (err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT") resolve2("nofam");
|
|
1681
1753
|
else reject(err);
|
|
1682
1754
|
};
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
resolve2(
|
|
1755
|
+
srv.once("error", onErr);
|
|
1756
|
+
srv.listen(port, h, () => {
|
|
1757
|
+
srv.off("error", onErr);
|
|
1758
|
+
resolve2("ok");
|
|
1687
1759
|
});
|
|
1688
1760
|
});
|
|
1761
|
+
const tryListen = async (port) => {
|
|
1762
|
+
const main2 = await listenOnce(server, port, host);
|
|
1763
|
+
if (main2 !== "ok") return false;
|
|
1764
|
+
if (host !== "127.0.0.1") return true;
|
|
1765
|
+
const t = http.createServer(server.listeners("request")[0]);
|
|
1766
|
+
for (const l of server.listeners("upgrade")) t.on("upgrade", l);
|
|
1767
|
+
const r = await listenOnce(t, port, "::1");
|
|
1768
|
+
if (r === "ok") {
|
|
1769
|
+
onTwin(t);
|
|
1770
|
+
return true;
|
|
1771
|
+
}
|
|
1772
|
+
if (r === "nofam") return true;
|
|
1773
|
+
await new Promise((res) => server.close(() => res()));
|
|
1774
|
+
return false;
|
|
1775
|
+
};
|
|
1689
1776
|
if (await tryListen(cfg.port)) return cfg.port;
|
|
1690
1777
|
if (await occupiedBySameProject(cfg, cfg.port)) {
|
|
1691
1778
|
console.log(
|