fix(system): retired heartbeat rows no longer alarm on status page
- GET /system/health splits processes into current instances (hosts with
a beat in the last 10 min, newest per host+kind, not stopped) and
retiredProcesses[]; 45s stale and level only consider current ones
- heartbeat writer marks stopped_at on graceful exit (migration 0043),
upsert clears it; pure api role now stops its writer on SIGTERM
- scheduled job system.heartbeat_cleanup (maintain, 1h) deletes rows
without a beat for 24h and samples older than 7 days
- status page: per-role alarms ("io worker 45 秒没有心跳"), collapsible
retired instances; glossary/runbook updated
- bump all members to 0.2.2, CHANGELOG [0.2.2]
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fa261eb6cd
commit
b4c9b1ead2
32 files changed
+747
-91
No files matched your search
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* camofox-browser 适配器(BrowserProvider 首个实现)。
|
||||
* 对照:jo-inc/camofox-browser master openapi.json(info.version 1.17.0)与 README,仅读文档,未连接真实实例。
|
||||
* 对照:jo-inc/camofox-browser v1.17.0(commit 389c996)openapi.json、README 与 server.js;2026-09-28 对真实容器核验,
|
||||
* 结果见 docs/runbook/browser.md「真机核验」。
|
||||
*
|
||||
* 上游模型与本项目合同的映射:
|
||||
* - camofox userId = 一个隔离 BrowserContext(cookies/storage),本项目用 profileKey(站点/账号)充当;
|
||||
@@ -9,10 +10,10 @@
|
||||
* 认证:README/OpenAPI 为 `Authorization: Bearer <CAMOFOX_ACCESS_KEY>`(设置后除 /health 外全部路由要求);
|
||||
* 凭据每次请求经 BrowserSecretResolver 解出,不缓存、不入日志、不进错误信息。
|
||||
*
|
||||
* 已实现(仅对 mock 与上游文档验证):health、capabilities、会话开/关/列举、标签页增删与对账、navigate(白名单/超时/重试)、
|
||||
* 已实现:health、capabilities、会话开/关/列举、标签页增删与对账、navigate(白名单/超时/重试)、
|
||||
* 后退/前进/刷新、snapshot(脱敏)、screenshot、click/type/select/scroll/press、extract、waitForDownload、
|
||||
* 内存版控制权租约与串行锁。会话/标签/租约的持久化与对账由服务层(apps/server modules/browser)以数据库为准。
|
||||
* 未核验的上游行为(坐标点击、无目标输入、downloads 字段形状)见 docs/runbook/browser.md。
|
||||
* 与文档不同的实测行为(截图直接返回 image/png、click 不接受坐标、焦点输入需 mode=keyboard、downloads 字段名)已按实测适配。
|
||||
*/
|
||||
import type {
|
||||
BrowserCapabilities,
|
||||
@@ -718,6 +719,13 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
await this.withProfile(s, async () => {
|
||||
this.assertActor(s, ref.as);
|
||||
const tab = this.tabOf(s, ref);
|
||||
// 上游 click/type 只认 ref/selector(1.17.0 实测不接受 {x, y}):坐标先经 evaluate 标记该点元素,再按选择器操作,
|
||||
// 这样仍走上游完整的鼠标事件序列与下载守卫,而不是页面内合成 click
|
||||
if (typeof payload.x === "number" && typeof payload.y === "number") {
|
||||
payload.selector = await this.pointSelector(s, tab, payload.x, payload.y);
|
||||
delete payload.x;
|
||||
delete payload.y;
|
||||
}
|
||||
const path = `/tabs/${encodeURIComponent(tab.upstreamTabId)}/${action}`;
|
||||
const res = await this.request("POST", path, {
|
||||
body: { userId: s.session.profileKey, ...payload },
|
||||
@@ -732,6 +740,22 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
});
|
||||
}
|
||||
|
||||
/** 视口坐标 → 唯一属性选择器(POST /tabs/{id}/evaluate + elementFromPoint);坐标处无元素报 bad_request */
|
||||
private async pointSelector(s: SessionState, tab: BrowserTab, x: number, y: number): Promise<string> {
|
||||
const nonce = this.newId().replace(/[^a-z0-9]/gi, "").slice(0, 16);
|
||||
const expression = `(() => { const e = document.elementFromPoint(${x}, ${y}); if (!e) return false;
|
||||
for (const o of document.querySelectorAll("[data-umt-pt]")) o.removeAttribute("data-umt-pt");
|
||||
e.setAttribute("data-umt-pt", "${nonce}"); return true; })()`;
|
||||
const path = `/tabs/${encodeURIComponent(tab.upstreamTabId)}/evaluate`;
|
||||
const res = await this.request("POST", path, {
|
||||
body: { userId: s.session.profileKey, expression },
|
||||
timeoutMs: s.budget.timeoutMs,
|
||||
});
|
||||
const out = await this.json(res, path);
|
||||
if (out.result !== true) throw new BrowserError("bad_request", "该坐标处没有可操作的元素");
|
||||
return `[data-umt-pt="${nonce}"]`;
|
||||
}
|
||||
|
||||
/** 点击可能跳转或改动页面:之后旧引用作废 */
|
||||
async click(ref: TabRef, target: ElementTarget): Promise<void> {
|
||||
await this.act(
|
||||
@@ -765,7 +789,8 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
ref,
|
||||
"type",
|
||||
(s) => ({
|
||||
...(target ? this.assertTarget(s, target) : {}),
|
||||
// 无目标 = 输入到当前焦点:上游 mode=fill 必须带 ref/selector,焦点输入要 mode=keyboard(1.17.0 实测)
|
||||
...(target ? this.assertTarget(s, target) : { mode: "keyboard" }),
|
||||
text,
|
||||
...(input.clear !== undefined ? { clear: input.clear } : {}),
|
||||
...(input.submit !== undefined ? { submit: input.submit } : {}),
|
||||
@@ -875,7 +900,13 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
if (s.seenDownloads.has(key)) continue;
|
||||
const status = str(d.status) ?? str(d.state);
|
||||
if (status && !["completed", "complete", "done", "finished"].includes(status)) continue;
|
||||
const b64 = str(d.data) ?? str(d.base64) ?? str(d.content);
|
||||
// 1.17.0 实测字段:{ id, url, suggestedFilename, mimeType, bytes, createdAt, failure, dataBase64 | dataSkipped | readError }
|
||||
if (d.failure) {
|
||||
s.seenDownloads.add(key);
|
||||
throw new BrowserError("upstream", "浏览器下载失败");
|
||||
}
|
||||
if (d.dataSkipped) throw new BrowserError("budget_exceeded", `下载产物超出字节预算(${opts.maxBytes})`);
|
||||
const b64 = str(d.dataBase64) ?? str(d.data) ?? str(d.base64) ?? str(d.content);
|
||||
const size = int(d.size) ?? int(d.bytes) ?? null;
|
||||
if (size !== null && size > opts.maxBytes)
|
||||
throw new BrowserError("budget_exceeded", `下载产物超出字节预算(${opts.maxBytes})`);
|
||||
|
||||
Reference in new issue
Block a user