oh-my-agent-web 0.3.0 → 0.5.0
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 +108 -450
- package/dist/server/approval/read-only.js +107 -0
- package/dist/server/approval/rules.js +85 -0
- package/dist/server/approval/scope.js +150 -0
- package/dist/server/brand.js +2 -0
- package/dist/server/cli-config.js +52 -0
- package/dist/server/cli-info.js +101 -0
- package/dist/server/cli.js +304 -0
- package/dist/server/engine/builtin.js +84 -0
- package/dist/server/engine/catalog.js +8 -0
- package/dist/server/engine/codex-approvals.js +45 -0
- package/dist/server/engine/codex-map.js +298 -0
- package/dist/server/engine/codex-sessions.js +159 -0
- package/dist/server/engine/codex-transport.js +49 -0
- package/dist/server/engine/codex.js +314 -0
- package/dist/server/engine/dsh-history.js +195 -0
- package/dist/server/engine/dsh-log.js +245 -0
- package/dist/server/engine/dsh-map.js +221 -0
- package/dist/server/engine/dsh-sessions.js +207 -0
- package/dist/server/engine/dsh-transport.js +37 -0
- package/dist/server/engine/dsh.js +284 -0
- package/dist/server/engine/fake.js +231 -0
- package/dist/server/engine/jsonrpc-stdio.js +241 -0
- package/dist/server/engine/model-registry-reload.js +115 -0
- package/dist/server/engine/narrate-guidance.js +22 -0
- package/dist/server/engine/opencode-map.js +499 -0
- package/dist/server/engine/opencode-projects.js +61 -0
- package/dist/server/engine/opencode-sessions.js +195 -0
- package/dist/server/engine/opencode-transport.js +48 -0
- package/dist/server/engine/opencode.js +394 -0
- package/dist/server/engine/pi-events.js +148 -0
- package/dist/server/engine/pi-plugins.js +310 -0
- package/dist/server/engine/pi-rename.js +14 -0
- package/dist/server/engine/pi-sdk.js +503 -0
- package/dist/server/engine/pi-session-model.js +23 -0
- package/dist/server/engine/pi-subagent-tool.js +166 -0
- package/dist/server/engine/pi-subagents.js +372 -0
- package/dist/server/engine/pi-thinking.js +34 -0
- package/dist/server/engine/pi-transcript.js +225 -0
- package/dist/server/engine/pi-version.js +11 -0
- package/dist/server/engine/port.js +1 -0
- package/dist/server/engine/registry.js +47 -0
- package/dist/server/engine/scratch.js +13 -0
- package/dist/server/engine/subagent-guidance.js +22 -0
- package/dist/server/engine/suggestion-guidance.js +9 -0
- package/dist/server/engine/switchable.js +22 -0
- package/dist/server/engine/wiring.js +166 -0
- package/dist/server/files/dir-routes.js +40 -0
- package/dist/server/files/file-search.js +63 -0
- package/dist/server/files/file-service.js +232 -0
- package/dist/server/files/files-routes.js +210 -0
- package/dist/server/files/git-info.js +361 -0
- package/dist/server/http/auth-routes.js +99 -0
- package/dist/server/http/auth.js +191 -0
- package/dist/server/http/aux-routes.js +40 -0
- package/dist/server/http/compress.js +77 -0
- package/dist/server/http/health-and-static.js +33 -0
- package/dist/server/http/live-routes.js +113 -0
- package/dist/server/http/login-page.js +200 -0
- package/dist/server/index.js +363 -520
- package/dist/server/markers/actions.js +262 -0
- package/dist/server/mcp/mcp-auth.js +184 -0
- package/dist/server/mcp/mcp-command.js +94 -0
- package/dist/server/mcp/mcp-import.js +290 -0
- package/dist/server/mcp/mcp-market-sources.js +192 -0
- package/dist/server/mcp/mcp-market-types.js +5 -0
- package/dist/server/mcp/mcp-market.js +175 -0
- package/dist/server/mcp/mcp-probe.js +134 -0
- package/dist/server/mcp/mcp-routes.js +70 -0
- package/dist/server/mcp/mcp-store.js +286 -0
- package/dist/server/mcp/pi-mcp-config.js +280 -0
- package/dist/server/plugins/plugin-routes.js +21 -0
- package/dist/server/protocol-parse.js +262 -0
- package/dist/server/protocol-settings.js +1 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/providers/catalog-store.js +85 -0
- package/dist/server/providers/catalog-writes.js +197 -0
- package/dist/server/providers/catalog.js +77 -0
- package/dist/server/providers/codex-models.js +96 -0
- package/dist/server/providers/codex-test.js +124 -0
- package/dist/server/providers/enabled-models.js +118 -0
- package/dist/server/providers/import-models.js +69 -0
- package/dist/server/providers/import-routes.js +203 -0
- package/dist/server/providers/model-test.js +52 -0
- package/dist/server/providers/probe.js +64 -0
- package/dist/server/providers/provider-handlers.js +223 -0
- package/dist/server/providers/provider-store.js +359 -0
- package/dist/server/providers/request-headers.js +7 -0
- package/dist/server/providers/subscription-routes.js +181 -0
- package/dist/server/providers/validate.js +241 -0
- package/dist/server/push/push-routes.js +114 -0
- package/dist/server/push/push.js +120 -0
- package/dist/server/session/approval-bridge.js +103 -0
- package/dist/server/session/attachment-resolve.js +21 -0
- package/dist/server/session/client-view.js +82 -0
- package/dist/server/session/conversation-create.js +65 -0
- package/dist/server/session/delete-session.js +84 -0
- package/dist/server/session/engine-switch.js +108 -0
- package/dist/server/session/lock.js +82 -0
- package/dist/server/session/model-controller.js +119 -0
- package/dist/server/session/notices.js +31 -0
- package/dist/server/session/pi-opener.js +123 -0
- package/dist/server/session/pi-refresh.js +51 -0
- package/dist/server/session/pi-sessions-provider.js +46 -0
- package/dist/server/session/pi-sessions.js +272 -0
- package/dist/server/session/refresh-actions.js +42 -0
- package/dist/server/session/rename-session.js +23 -0
- package/dist/server/session/runtime-services.js +28 -0
- package/dist/server/session/runtime-types.js +1 -0
- package/dist/server/session/runtime.js +380 -0
- package/dist/server/session/session-stats.js +91 -0
- package/dist/server/session/settings.js +367 -0
- package/dist/server/session/snapshot-for.js +7 -0
- package/dist/server/session/stats-refresher.js +50 -0
- package/dist/server/session/subagents.js +116 -0
- package/dist/server/session/switch-conversation.js +86 -0
- package/dist/server/session/throttle.js +14 -0
- package/dist/server/session/turn-events.js +126 -0
- package/dist/server/session/turn-finish.js +79 -0
- package/dist/server/session/turn-markers.js +26 -0
- package/dist/server/session/turn-queue.js +111 -0
- package/dist/server/session/turn-send.js +108 -0
- package/dist/server/session/turn-watchdog.js +77 -0
- package/dist/server/sessions/session-routes.js +17 -0
- package/dist/server/skills/skill-install.js +186 -0
- package/dist/server/skills/skill-routes.js +134 -0
- package/dist/server/skills/skill-store.js +163 -0
- package/dist/server/skills/skillhub.js +214 -0
- package/dist/server/skills/zip.js +121 -0
- package/dist/server/snapshot/fan-out.js +10 -0
- package/dist/server/snapshot/messages.js +75 -0
- package/dist/server/snapshot/sender.js +62 -0
- package/dist/server/snapshot/state.js +202 -0
- package/dist/server/subagents/profile-store.js +287 -0
- package/dist/server/terminal/terminal-routes.js +176 -0
- package/dist/server/terminal/terminal-session.js +140 -0
- package/dist/server/uploads/upload-routes.js +64 -0
- package/dist/server/uploads/upload-store.js +115 -0
- package/dist/server/util/data-dir.js +36 -0
- package/dist/server/util/env.js +146 -0
- package/dist/server/util/ids.js +28 -0
- package/dist/server/util/log-file.js +67 -0
- package/dist/server/util/log.js +52 -0
- package/dist/server/util/origin.js +75 -0
- package/dist/server/util/paths.js +60 -0
- package/dist/server/util/pricing-index.js +121 -0
- package/dist/server/util/pricing.js +214 -0
- package/dist/server/util/startup.js +34 -0
- package/dist/server/util/upstream-version.js +140 -0
- package/dist/server/util/usage.js +19 -0
- package/dist/server/util/version.js +50 -0
- package/dist/server/ws/handle-message.js +107 -0
- package/package.json +50 -63
- package/web/dist/apple-touch-icon-precomposed.png +0 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/abnfDiagram-WEYOIA3C-VUKLHHSn.js +1 -0
- package/web/dist/assets/arc-BeUkaHhk.js +1 -0
- package/web/dist/assets/architecture-WOLXFQ4H-B9I4s0NV.js +1 -0
- package/web/dist/assets/architectureDiagram-ZHVYKT53-CaEkLhfq.js +36 -0
- package/web/dist/assets/array-BifhSqXX.js +1 -0
- package/web/dist/assets/blockDiagram-RJAE7QA3-IR49UNE_.js +140 -0
- package/web/dist/assets/c4Diagram-XDPPPE54-B-81CloB.js +38 -0
- package/web/dist/assets/channel-B_mTTjGt.js +1 -0
- package/web/dist/assets/chunk-2E7WHVMD-CaWVF7qv.js +88 -0
- package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web/dist/assets/chunk-4EMNKR2T-BvXf2tq-.js +1 -0
- package/web/dist/assets/chunk-55XCU76K-Bz_d2e2D.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web/dist/assets/chunk-7S6XSTFO-B_SPMiU8.js +2 -0
- package/web/dist/assets/chunk-AQDTSKZB-Cen-_xej.js +72 -0
- package/web/dist/assets/chunk-DSNLSHJZ-DEw1kFwj.js +129 -0
- package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/web/dist/assets/chunk-HAGFOTED-BwpOOcw8.js +62 -0
- package/web/dist/assets/chunk-HNE4Q36P-BjCwi2uW.js +1 -0
- package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/web/dist/assets/chunk-NQOKRVIF-WcqhWFS5.js +213 -0
- package/web/dist/assets/chunk-UHW7PFBP-quDvQby8.js +1 -0
- package/web/dist/assets/chunk-VQ5W62QI-DZBwmhzq.js +1 -0
- package/web/dist/assets/chunk-WQQL4CDK-wYyHYuui.js +10 -0
- package/web/dist/assets/chunk-XXDRQBXY-DfJzC7nt.js +1 -0
- package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web/dist/assets/chunk-YZO67HFG-CWLraGk_.js +1 -0
- package/web/dist/assets/chunk-ZAUX2LTX-ChBUt_FJ.js +1 -0
- package/web/dist/assets/chunk-ZCHMQSOR-C57UFT60.js +2 -0
- package/web/dist/assets/classDiagram-v2-4MYWR3UA-Bo4i7SNY.js +217 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-CK360NdH.js +1 -0
- package/web/dist/assets/cynefin-EF2NZ3EQ-DlXFjIFm.js +1 -0
- package/web/dist/assets/cynefinDiagram-WF24UDRP-zvZIqA1c.js +62 -0
- package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/web/dist/assets/dagre-NK4GIZVX-CnKhCu-v.js +4 -0
- package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/web/dist/assets/diagram-CBK3MWTC-3QTR8Fjx.js +24 -0
- package/web/dist/assets/diagram-I5XIIV2D-D9lSd7z6.js +24 -0
- package/web/dist/assets/diagram-ODIL2UCW-DWJD6lPM.js +30 -0
- package/web/dist/assets/diagram-PFBEJUXE-CWJmoUg4.js +3 -0
- package/web/dist/assets/diagram-RJA3I4VV-D1SZHQcQ.js +41 -0
- package/web/dist/assets/diagram-TYUNFSNR-DCCjGi1x.js +200 -0
- package/web/dist/assets/dist-VOk_Iv17.js +1 -0
- package/web/dist/assets/ebnfDiagram-V4P3TTPT-CQLdqEiW.js +1 -0
- package/web/dist/assets/elk-QONTBCTQ-Dp2pEp6b.js +27 -0
- package/web/dist/assets/erDiagram-KYQIGQAA-BFGNoFQ-.js +99 -0
- package/web/dist/assets/eventmodeling-K75KTNOO-CFfAemGw.js +1 -0
- package/web/dist/assets/flowDiagram-IUGLLWBU-DmSp3cbW.js +1 -0
- package/web/dist/assets/ganttDiagram-R7GPNTJY-vK3jPGDq.js +292 -0
- package/web/dist/assets/gitGraph-VSP46ZUC-rXBgHTdq.js +1 -0
- package/web/dist/assets/gitGraphDiagram-PDYL7XHY-CBbGhago.js +106 -0
- package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/web/dist/assets/index-DBOF_OJe.js +104 -0
- package/web/dist/assets/index-nlY_-OaR.css +1 -0
- package/web/dist/assets/info-OHQRW6UA-Cd9vp-6C.js +1 -0
- package/web/dist/assets/infoDiagram-MVDRKAX5-Ce-Sg_qu.js +2 -0
- package/web/dist/assets/init-C-OQMol4.js +1 -0
- package/web/dist/assets/ishikawaDiagram-HPEHV2GE-DWlmR6BY.js +70 -0
- package/web/dist/assets/journeyDiagram-BOSHW7O7-DmDG97ae.js +139 -0
- package/web/dist/assets/kanban-definition-FNSA4HC5-1Z4sDrgV.js +89 -0
- package/web/dist/assets/katex-B_JcIwqj.js +257 -0
- package/web/dist/assets/line-DGM1-9KW.js +1 -0
- package/web/dist/assets/linear-B6hyeGHu.js +1 -0
- package/web/dist/assets/mermaid-parser.core-D1HmdkA4.js +7 -0
- package/web/dist/assets/mermaid.core-DeUJrhO1.js +44 -0
- package/web/dist/assets/mindmap-definition-6OY57UG3-C3-FByy8.js +98 -0
- package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/web/dist/assets/packet-JDAUHWVQ-BoOr7DaW.js +1 -0
- package/web/dist/assets/path-fybaL0A-.js +1 -0
- package/web/dist/assets/pegDiagram-ECIUJNBX-Bn7m75TZ.js +1 -0
- package/web/dist/assets/pie-XZMESJXO-CGuMTDBp.js +1 -0
- package/web/dist/assets/pieDiagram-GNMPY74T-DHhaPwZe.js +39 -0
- package/web/dist/assets/quadrantDiagram-D57K4MC3-DxTgFHLK.js +7 -0
- package/web/dist/assets/radar-ABXABTNO-BiKxmALL.js +1 -0
- package/web/dist/assets/railroad-I3PHUGI6-BKYufgg4.js +1 -0
- package/web/dist/assets/railroad-abnf-LNEFI6M7-DmOrjlyL.js +1 -0
- package/web/dist/assets/railroad-ebnf-DWYD2UWJ-BgMyVDd1.js +1 -0
- package/web/dist/assets/railroad-peg-JR7BVNR7-BN6NL8S-.js +1 -0
- package/web/dist/assets/railroadDiagram-Y72NRBL7-BsbA-9gP.js +1 -0
- package/web/dist/assets/requirementDiagram-EQYSW5RA-HUw4YqWd.js +84 -0
- package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/web/dist/assets/sankeyDiagram-SY24JHO5-Dx1MOFRu.js +40 -0
- package/web/dist/assets/sequenceDiagram-BLEHNHQU-C0-Rzg3g.js +169 -0
- package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/web/dist/assets/src-CuHygcpj.js +1 -0
- package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnEICGtg.js +291 -0
- package/web/dist/assets/swimlanes-B3ENRKFQ-CwDRCHZD.js +1 -0
- package/web/dist/assets/swimlanesDiagram-AYQRV4AK-DZ_aAXmG.js +8 -0
- package/web/dist/assets/timeline-definition-JXYKGLGB-C4BVpgzc.js +120 -0
- package/web/dist/assets/treeView-D4JQ5SDB-BBsvEMP3.js +1 -0
- package/web/dist/assets/treemap-SAJKECNS-BYw6cxC8.js +1 -0
- package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkOXLgwP.js +328 -0
- package/web/dist/assets/vennDiagram-Q3Y3IDCW-Kgjk_n6B.js +34 -0
- package/web/dist/assets/wardley-7MLQ67FV-BGVObhvJ.js +1 -0
- package/web/dist/assets/wardleyDiagram-IVAIDKWA-i97uD6tL.js +78 -0
- package/web/dist/assets/xychartDiagram-4URNHDU4-awJi5TfJ.js +7 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicon.svg +20 -8
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-maskable-512.png +0 -0
- package/web/dist/index.html +31 -24
- package/web/dist/manifest.webmanifest +17 -0
- package/web/dist/maskable.svg +25 -0
- package/web/dist/sw.js +40 -0
- package/bin/oh-my-agent-web-ps.mjs +0 -203
- package/bin/oh-my-agent-web-ws.mjs +0 -782
- package/bin/oh-my-agent-web.mjs +0 -321
- package/bin/update-check.mjs +0 -142
- package/dist/server/codex-client.js +0 -310
- package/dist/server/codex-supervisor.js +0 -446
- package/dist/server/fs-service.js +0 -144
- package/dist/server/request-inspector.js +0 -458
- package/dist/shared/protocol.js +0 -112
- package/scripts/manage-server.mjs +0 -155
- package/scripts/omaw.mjs +0 -74
- package/shared/protocol.ts +0 -228
- package/web/dist/assets/index-5LJ5fRcO.js +0 -42
- package/web/dist/assets/index-QiDfPKFo.css +0 -1
- package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillHub(skillhub.tencent.com / api.skillhub.cn)技能市场。
|
|
3
|
+
*
|
|
4
|
+
* 接口来自参考实现 `/Users/dds/data/git/oh-my-skills`(`src/core/hub.ts` 与
|
|
5
|
+
* `src/core/skillhub-install.ts`),这里只保留 OMAW 需要的部分,并补上两个参考实现没有的细节:
|
|
6
|
+
*
|
|
7
|
+
* - **列表**:`GET https://api.skillhub.cn/api/skills?page=&pageSize=&sortBy=score&order=desc&keyword=&category=`
|
|
8
|
+
* 返回 `{ code, data: { skills: [...], total } }`;字段名很花(`name`/`slug`/`namespace`/
|
|
9
|
+
* `ownerName`/`downloads`/`stars`/`description_zh`…),这里统一收敛成一个结构。
|
|
10
|
+
* - **下载**:`GET https://api.skillhub.cn/api/v1/download?slug=<slug>` → 一个 zip,
|
|
11
|
+
* `SKILL.md` 一般在根目录,也可能套一层目录。
|
|
12
|
+
* - **分类**:接口不给分类清单,参考实现是写死的 8 个 key(截图里那排就是它们)。
|
|
13
|
+
*
|
|
14
|
+
* 缓存:搜索结果在进程内存里放 5 分钟(同参数直接命中)。技能库首页是「打开面板就看」,
|
|
15
|
+
* 每次翻面板都打一次外网既慢又没必要。
|
|
16
|
+
*/
|
|
17
|
+
import { extractZip, skillRootOf } from "./zip.js";
|
|
18
|
+
import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
const BASE = "https://api.skillhub.cn";
|
|
22
|
+
const SEARCH_TTL_MS = 5 * 60_000;
|
|
23
|
+
const TIMEOUT_MS = 20_000;
|
|
24
|
+
/** 分类清单:与 SkillHub 前端的 key 一致(接口不返回分类表) */
|
|
25
|
+
export const HUB_CATEGORIES = [
|
|
26
|
+
{ key: "", label: "全部" },
|
|
27
|
+
{ key: "ai-intelligence", label: "AI 智能" },
|
|
28
|
+
{ key: "developer-tools", label: "开发工具" },
|
|
29
|
+
{ key: "productivity", label: "效率提升" },
|
|
30
|
+
{ key: "data-analysis", label: "数据分析" },
|
|
31
|
+
{ key: "content-creation", label: "内容创作" },
|
|
32
|
+
{ key: "security-compliance", label: "安全合规" },
|
|
33
|
+
{ key: "communication-collaboration", label: "通讯协作" },
|
|
34
|
+
];
|
|
35
|
+
function str(value) {
|
|
36
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
37
|
+
}
|
|
38
|
+
function num(value) {
|
|
39
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
40
|
+
}
|
|
41
|
+
function record(value) {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
43
|
+
? value
|
|
44
|
+
: null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 条目自己带的分类(`dev-programming` 这种)跟筛选用的 key 是**两套词表**:
|
|
48
|
+
* 筛选那 8 个 key 是 SkillHub 前端的侧栏分类(接口会做别名映射,实测每个 key 命中数不同),
|
|
49
|
+
* 条目上的 `category` 则是更细的 12 类。两套都要有中文名,否则卡片上会直接露英文 key。
|
|
50
|
+
*/
|
|
51
|
+
const ITEM_CATEGORY_LABELS = {
|
|
52
|
+
"dev-programming": "开发编程",
|
|
53
|
+
"knowledge-management": "知识管理",
|
|
54
|
+
"life-service": "生活服务",
|
|
55
|
+
"office-efficiency": "办公效率",
|
|
56
|
+
"design-media": "设计媒体",
|
|
57
|
+
professional: "专业服务",
|
|
58
|
+
education: "教育学习",
|
|
59
|
+
"content-creation": "内容创作",
|
|
60
|
+
"data-analysis": "数据分析",
|
|
61
|
+
"ai-agent": "AI 智能体",
|
|
62
|
+
"business-ops": "商业运营",
|
|
63
|
+
"it-ops-security": "IT 运维安全",
|
|
64
|
+
};
|
|
65
|
+
function categoryLabel(key) {
|
|
66
|
+
if (key === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return ITEM_CATEGORY_LABELS[key] ?? HUB_CATEGORIES.find((item) => item.key === key)?.label ?? key;
|
|
69
|
+
}
|
|
70
|
+
/** 一条原始记录 → HubSkill(字段名各家不一,按优先级取第一个非空的) */
|
|
71
|
+
function toHubSkill(raw) {
|
|
72
|
+
const slug = str(raw.slug) ?? str(raw.id);
|
|
73
|
+
const name = str(raw.name) ?? str(raw.title);
|
|
74
|
+
if (slug === undefined || name === undefined)
|
|
75
|
+
return null;
|
|
76
|
+
const namespace = record(raw.namespace);
|
|
77
|
+
const category = str(raw.category);
|
|
78
|
+
return {
|
|
79
|
+
slug,
|
|
80
|
+
name,
|
|
81
|
+
...((str(raw.ownerName) ?? str(namespace?.handle) ?? str(raw.upstream_owner_login))
|
|
82
|
+
? { author: str(raw.ownerName) ?? str(namespace?.handle) ?? str(raw.upstream_owner_login) }
|
|
83
|
+
: {}),
|
|
84
|
+
description: str(raw.description_zh) ?? str(raw.description) ?? str(raw.summary) ?? "",
|
|
85
|
+
...(category === undefined ? {} : { category, categoryLabel: categoryLabel(category) }),
|
|
86
|
+
...(num(raw.downloads) === undefined ? {} : { downloads: num(raw.downloads) }),
|
|
87
|
+
...(num(raw.stars) === undefined ? {} : { stars: num(raw.stars) }),
|
|
88
|
+
...(str(raw.version) === undefined ? {} : { version: str(raw.version) }),
|
|
89
|
+
...(str(raw.homepage) === undefined ? {} : { homepage: str(raw.homepage) }),
|
|
90
|
+
...(str(raw.iconUrl) === undefined ? {} : { iconUrl: str(raw.iconUrl) }),
|
|
91
|
+
...(str(raw.source) === undefined ? {} : { source: str(raw.source) }),
|
|
92
|
+
...(raw.verified === true ? { verified: true } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function createSkillHub(options = {}) {
|
|
96
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
97
|
+
const cache = new Map();
|
|
98
|
+
return {
|
|
99
|
+
async search(input) {
|
|
100
|
+
const page = Math.max(1, Math.floor(input.page ?? 1));
|
|
101
|
+
const pageSize = Math.max(1, Math.min(50, Math.floor(input.pageSize ?? 24)));
|
|
102
|
+
const query = (input.query ?? "").trim();
|
|
103
|
+
const category = (input.category ?? "").trim();
|
|
104
|
+
const key = `${query}\0${category}\0${page}\0${pageSize}`;
|
|
105
|
+
const hit = cache.get(key);
|
|
106
|
+
if (input.refresh !== true && hit !== undefined && Date.now() - hit.at < SEARCH_TTL_MS)
|
|
107
|
+
return hit.result;
|
|
108
|
+
const url = new URL(`${BASE}/api/skills`);
|
|
109
|
+
url.searchParams.set("page", String(page));
|
|
110
|
+
url.searchParams.set("pageSize", String(pageSize));
|
|
111
|
+
url.searchParams.set("sortBy", "score");
|
|
112
|
+
url.searchParams.set("order", "desc");
|
|
113
|
+
if (query !== "")
|
|
114
|
+
url.searchParams.set("keyword", query);
|
|
115
|
+
if (category !== "")
|
|
116
|
+
url.searchParams.set("category", category);
|
|
117
|
+
const response = await fetchImpl(url.toString(), {
|
|
118
|
+
headers: { accept: "application/json", "user-agent": "oh-my-agent-web" },
|
|
119
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok)
|
|
122
|
+
throw new Error(`SkillHub 返回 HTTP ${response.status}`);
|
|
123
|
+
const payload = (await response.json());
|
|
124
|
+
const data = record(record(payload)?.data);
|
|
125
|
+
const list = Array.isArray(data?.skills) ? data.skills : [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const skills = [];
|
|
128
|
+
for (const item of list) {
|
|
129
|
+
const recordItem = record(item);
|
|
130
|
+
if (recordItem === null)
|
|
131
|
+
continue;
|
|
132
|
+
const skill = toHubSkill(recordItem);
|
|
133
|
+
// SkillHub 同一个技能会出现多条(不同版本/命名空间):按 slug 只留第一条,
|
|
134
|
+
// 否则界面里同一个技能排两遍,React 的 key 还会撞(重复 key 的渲染结果不可控)
|
|
135
|
+
if (skill === null || seen.has(skill.slug))
|
|
136
|
+
continue;
|
|
137
|
+
seen.add(skill.slug);
|
|
138
|
+
skills.push(skill);
|
|
139
|
+
}
|
|
140
|
+
const total = num(data?.total);
|
|
141
|
+
const result = {
|
|
142
|
+
skills,
|
|
143
|
+
page,
|
|
144
|
+
pageSize,
|
|
145
|
+
...(total === undefined ? {} : { total }),
|
|
146
|
+
hasMore: total === undefined ? skills.length === pageSize : page * pageSize < total,
|
|
147
|
+
};
|
|
148
|
+
cache.set(key, { at: Date.now(), result });
|
|
149
|
+
return result;
|
|
150
|
+
},
|
|
151
|
+
async fetchPackage(slug) {
|
|
152
|
+
const response = await fetchImpl(`${BASE}/api/v1/download?slug=${encodeURIComponent(slug)}`, {
|
|
153
|
+
headers: { accept: "application/zip", "user-agent": "oh-my-agent-web" },
|
|
154
|
+
signal: AbortSignal.timeout(60_000),
|
|
155
|
+
});
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
// 4xx 基本都是对面仓库里没有这一版的包(实测有技能 302 到 COS 后 404),
|
|
158
|
+
// 不是我们的参数错了 —— 把话说清楚,别让人去查自己的配置
|
|
159
|
+
throw new Error(response.status === 404
|
|
160
|
+
? "下载技能包失败:对方仓库里这一版的包不存在(SkillHub 数据问题,换一个技能或稍后再试)"
|
|
161
|
+
: `下载技能包失败(HTTP ${response.status})`);
|
|
162
|
+
}
|
|
163
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
164
|
+
const workDir = await mkdtemp(join(tmpdir(), "omaw-skill-"));
|
|
165
|
+
const cleanup = () => rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
166
|
+
try {
|
|
167
|
+
const files = await extractZip(buffer, workDir);
|
|
168
|
+
const sub = skillRootOf(files);
|
|
169
|
+
if (sub === null)
|
|
170
|
+
throw new Error("技能包里没有 SKILL.md");
|
|
171
|
+
const dir = sub === "" ? workDir : join(workDir, sub);
|
|
172
|
+
await stat(join(dir, "SKILL.md"));
|
|
173
|
+
return { dir, cleanup };
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
await cleanup();
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/** 干净的技能目录名:只留字母数字与 `-_.`,并去掉开头那个点 */
|
|
183
|
+
export function safeSkillDirName(raw) {
|
|
184
|
+
const cleaned = raw
|
|
185
|
+
.trim()
|
|
186
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
187
|
+
.replace(/^\.+/, "")
|
|
188
|
+
.replace(/-{2,}/g, "-")
|
|
189
|
+
.replace(/^-+|-+$/g, "");
|
|
190
|
+
return cleaned === "" ? "skill" : cleaned.slice(0, 80);
|
|
191
|
+
}
|
|
192
|
+
/** 目录里是不是一个技能(有 SKILL.md 或 .SKILL.md.disabled) */
|
|
193
|
+
export async function hasSkillFile(dir) {
|
|
194
|
+
for (const name of ["SKILL.md", ".SKILL.md.disabled"]) {
|
|
195
|
+
const ok = await stat(join(dir, name))
|
|
196
|
+
.then((info) => info.isFile())
|
|
197
|
+
.catch(() => false);
|
|
198
|
+
if (ok)
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
/** 列一个目录下所有「是技能」的子目录名 */
|
|
204
|
+
export async function listSkillDirs(root) {
|
|
205
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
206
|
+
const names = [];
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
209
|
+
continue;
|
|
210
|
+
if (await hasSkillFile(join(root, entry.name)))
|
|
211
|
+
names.push(entry.name);
|
|
212
|
+
}
|
|
213
|
+
return names.sort();
|
|
214
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一个**够用就好**的 ZIP 解包器:把 SkillHub 下发的技能包解到临时目录。
|
|
3
|
+
*
|
|
4
|
+
* 为什么自己写而不装 `adm-zip`:这里只需要「读中央目录 → 逐个 inflate」这一件事
|
|
5
|
+
* (ADR-0008 的依赖预算),而 `node:zlib` 自带 `inflateRaw`,四十行就够。
|
|
6
|
+
*
|
|
7
|
+
* 安全边界(解压是典型的高危动作,一条都不能省):
|
|
8
|
+
* - 只认 `store`(0)与 `deflate`(8)两种方法,别的直接拒;
|
|
9
|
+
* - 每个条目名先归一化:绝对路径、`..`、反斜杠(Windows 路径穿越)、符号链接一律拒绝;
|
|
10
|
+
* - 单文件 8MB、总量 64MB 上限 —— 一个「技能包」不该比这更大,防 zip bomb;
|
|
11
|
+
* - 只写普通文件与目录,不还原权限位、不还原时间(技能包不需要)。
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
14
|
+
import { dirname, join, normalize, sep } from "node:path";
|
|
15
|
+
import { inflateRawSync } from "node:zlib";
|
|
16
|
+
const MAX_ENTRY_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
18
|
+
/** 读中央目录:条目清单 + 名字(ZIP64 / 加密包直接拒,技能包用不上) */
|
|
19
|
+
function readCentralDirectory(buffer) {
|
|
20
|
+
// 找 EOCD(末尾 22 字节起,注释最多 64KB)
|
|
21
|
+
let eocd = -1;
|
|
22
|
+
for (let offset = buffer.length - 22; offset >= Math.max(0, buffer.length - 22 - 0xffff); offset -= 1) {
|
|
23
|
+
if (buffer.readUInt32LE(offset) === 0x06054b50) {
|
|
24
|
+
eocd = offset;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (eocd < 0)
|
|
29
|
+
throw new Error("不是 ZIP 文件(找不到目录结尾)");
|
|
30
|
+
const count = buffer.readUInt16LE(eocd + 10);
|
|
31
|
+
const directoryOffset = buffer.readUInt32LE(eocd + 16);
|
|
32
|
+
if (count === 0xffff || directoryOffset === 0xffffffff)
|
|
33
|
+
throw new Error("不支持 ZIP64 包");
|
|
34
|
+
const entries = [];
|
|
35
|
+
let offset = directoryOffset;
|
|
36
|
+
for (let index = 0; index < count; index += 1) {
|
|
37
|
+
if (offset + 46 > buffer.length || buffer.readUInt32LE(offset) !== 0x02014b50) {
|
|
38
|
+
throw new Error("ZIP 目录损坏");
|
|
39
|
+
}
|
|
40
|
+
const flags = buffer.readUInt16LE(offset + 8);
|
|
41
|
+
if ((flags & 0x1) !== 0)
|
|
42
|
+
throw new Error("不支持加密的 ZIP 包");
|
|
43
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
44
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
45
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
46
|
+
const name = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
|
|
47
|
+
entries.push({
|
|
48
|
+
name,
|
|
49
|
+
method: buffer.readUInt16LE(offset + 10),
|
|
50
|
+
compressedSize: buffer.readUInt32LE(offset + 20),
|
|
51
|
+
uncompressedSize: buffer.readUInt32LE(offset + 24),
|
|
52
|
+
localHeaderOffset: buffer.readUInt32LE(offset + 42),
|
|
53
|
+
});
|
|
54
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
55
|
+
}
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
/** 条目名 → 安全的相对路径(不安全的直接抛,不静默跳过:坏包就该整个失败) */
|
|
59
|
+
function safeRelativePath(name) {
|
|
60
|
+
if (name.endsWith("/"))
|
|
61
|
+
return null; // 目录条目:由文件的父目录创建
|
|
62
|
+
if (name.includes("\\"))
|
|
63
|
+
throw new Error(`包里出现 Windows 反斜杠路径:${name}`);
|
|
64
|
+
if (name.startsWith("/") || /^[A-Za-z]:/.test(name))
|
|
65
|
+
throw new Error(`包里出现绝对路径:${name}`);
|
|
66
|
+
const normalized = normalize(name);
|
|
67
|
+
if (normalized.startsWith("..") || normalized.includes(`${sep}..${sep}`) || normalized === "..") {
|
|
68
|
+
throw new Error(`包里出现越界路径:${name}`);
|
|
69
|
+
}
|
|
70
|
+
return normalized;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 解到 `destination`,返回解出来的相对文件清单。
|
|
74
|
+
* 调用方负责建临时目录、用完删掉(见 `skill-install.ts`)。
|
|
75
|
+
*/
|
|
76
|
+
export async function extractZip(buffer, destination) {
|
|
77
|
+
const entries = readCentralDirectory(buffer);
|
|
78
|
+
const written = [];
|
|
79
|
+
let total = 0;
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
const relative = safeRelativePath(entry.name);
|
|
82
|
+
if (relative === null)
|
|
83
|
+
continue;
|
|
84
|
+
if (entry.uncompressedSize > MAX_ENTRY_BYTES)
|
|
85
|
+
throw new Error(`包里的 ${entry.name} 太大(>8MB)`);
|
|
86
|
+
total += entry.uncompressedSize;
|
|
87
|
+
if (total > MAX_TOTAL_BYTES)
|
|
88
|
+
throw new Error("包解压后太大(>64MB)");
|
|
89
|
+
// 跳到本地文件头,跳过它自己的名字与扩展字段
|
|
90
|
+
const header = entry.localHeaderOffset;
|
|
91
|
+
if (header + 30 > buffer.length || buffer.readUInt32LE(header) !== 0x04034b50) {
|
|
92
|
+
throw new Error(`ZIP 条目头损坏:${entry.name}`);
|
|
93
|
+
}
|
|
94
|
+
const nameLength = buffer.readUInt16LE(header + 26);
|
|
95
|
+
const extraLength = buffer.readUInt16LE(header + 28);
|
|
96
|
+
const start = header + 30 + nameLength + extraLength;
|
|
97
|
+
const raw = buffer.subarray(start, start + entry.compressedSize);
|
|
98
|
+
let content;
|
|
99
|
+
if (entry.method === 0)
|
|
100
|
+
content = raw;
|
|
101
|
+
else if (entry.method === 8)
|
|
102
|
+
content = inflateRawSync(raw);
|
|
103
|
+
else
|
|
104
|
+
throw new Error(`不支持的压缩方式 ${entry.method}:${entry.name}`);
|
|
105
|
+
if (content.length > MAX_ENTRY_BYTES)
|
|
106
|
+
throw new Error(`包里的 ${entry.name} 解压后太大`);
|
|
107
|
+
const target = join(destination, relative);
|
|
108
|
+
await mkdir(dirname(target), { recursive: true });
|
|
109
|
+
await writeFile(target, content);
|
|
110
|
+
written.push(relative.split(sep).join("/"));
|
|
111
|
+
}
|
|
112
|
+
return written;
|
|
113
|
+
}
|
|
114
|
+
/** 解出来的文件里,`SKILL.md` 所在的那一层(技能包有时会套一层目录) */
|
|
115
|
+
export function skillRootOf(files) {
|
|
116
|
+
const direct = files.find((file) => file.toLowerCase() === "skill.md");
|
|
117
|
+
if (direct !== undefined)
|
|
118
|
+
return "";
|
|
119
|
+
const nested = files.find((file) => /^[^/]+\/skill\.md$/i.test(file));
|
|
120
|
+
return nested === undefined ? null : nested.slice(0, nested.indexOf("/"));
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function fanOutSnapshot(snapshot, targets) {
|
|
2
|
+
const shared = JSON.stringify({ type: "event", event: "snapshot", snapshot });
|
|
3
|
+
for (const socket of targets.sockets) {
|
|
4
|
+
if (socket.readyState === socket.OPEN)
|
|
5
|
+
socket.send(shared);
|
|
6
|
+
}
|
|
7
|
+
for (const [clientId, sink] of targets.sinks) {
|
|
8
|
+
sink(JSON.stringify({ type: "event", event: "snapshot", snapshot: targets.snapshotFor(clientId) }));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息块的纯函数工具(ADR-0012:消息由块构成)。
|
|
3
|
+
*
|
|
4
|
+
* 全部是纯函数(不改入参),便于单测覆盖「文本追加 / 工具卡状态流转 / 摘录截断」。
|
|
5
|
+
*/
|
|
6
|
+
import { TOOL_EXCERPT_CHARS } from "../protocol.js";
|
|
7
|
+
export function createMessage(id, role, ts) {
|
|
8
|
+
return { id, role, ts, blocks: [] };
|
|
9
|
+
}
|
|
10
|
+
/** 追加文本:接到最后一个文本块后面(保持「说一句 → 调工具 → 再说一句」的交错顺序) */
|
|
11
|
+
export function appendText(message, text) {
|
|
12
|
+
const blocks = [...message.blocks];
|
|
13
|
+
const last = blocks[blocks.length - 1];
|
|
14
|
+
if (last && last.kind === "text")
|
|
15
|
+
blocks[blocks.length - 1] = { kind: "text", text: last.text + text };
|
|
16
|
+
else
|
|
17
|
+
blocks.push({ kind: "text", text });
|
|
18
|
+
return { ...message, blocks };
|
|
19
|
+
}
|
|
20
|
+
export function startTool(message, input) {
|
|
21
|
+
const block = {
|
|
22
|
+
kind: "tool",
|
|
23
|
+
toolCallId: input.toolCallId,
|
|
24
|
+
name: input.name,
|
|
25
|
+
argsSummary: input.argsSummary,
|
|
26
|
+
// 摘要已经够短就不重复带一份:界面靠「有没有 argsFull」判断要不要显示「完整命令」
|
|
27
|
+
...(input.argsFull && input.argsFull !== input.argsSummary ? { argsFull: input.argsFull } : {}),
|
|
28
|
+
status: "running",
|
|
29
|
+
outputExcerpt: "",
|
|
30
|
+
truncated: false,
|
|
31
|
+
startedAt: input.startedAt,
|
|
32
|
+
};
|
|
33
|
+
return { ...message, blocks: [...message.blocks, block] };
|
|
34
|
+
}
|
|
35
|
+
/** 摘录预算(ADR-0004 §5):超限截断并打标记,而不是把 200KB 塞进快照 */
|
|
36
|
+
export function truncateExcerpt(output, limit = TOOL_EXCERPT_CHARS) {
|
|
37
|
+
if (output.length <= limit)
|
|
38
|
+
return { outputExcerpt: output, truncated: false };
|
|
39
|
+
return { outputExcerpt: output.slice(0, limit), truncated: true };
|
|
40
|
+
}
|
|
41
|
+
export function finishTool(message, input) {
|
|
42
|
+
const { outputExcerpt, truncated } = truncateExcerpt(input.output);
|
|
43
|
+
const blocks = message.blocks.map((block) => {
|
|
44
|
+
if (block.kind !== "tool" || block.toolCallId !== input.toolCallId)
|
|
45
|
+
return block;
|
|
46
|
+
return {
|
|
47
|
+
...block,
|
|
48
|
+
status: input.status,
|
|
49
|
+
outputExcerpt,
|
|
50
|
+
truncated,
|
|
51
|
+
endedAt: input.endedAt,
|
|
52
|
+
...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
return { ...message, blocks };
|
|
56
|
+
}
|
|
57
|
+
export function appendError(message, code, text) {
|
|
58
|
+
return { ...message, blocks: [...message.blocks, { kind: "error", code, message: text }] };
|
|
59
|
+
}
|
|
60
|
+
/** 快照里最后一条消息的纯文本(给状态栏/调试用) */
|
|
61
|
+
export function messageText(message) {
|
|
62
|
+
return message.blocks
|
|
63
|
+
.map((block) => block.kind === "text" ? block.text : block.kind === "error" ? `[${block.code}] ${block.message}` : "")
|
|
64
|
+
.join("");
|
|
65
|
+
}
|
|
66
|
+
/** 追加思考文本(与正文分开:正文说结论,思考说过程) */
|
|
67
|
+
export function appendThinking(message, text) {
|
|
68
|
+
const blocks = [...message.blocks];
|
|
69
|
+
const last = blocks[blocks.length - 1];
|
|
70
|
+
if (last && last.kind === "thinking")
|
|
71
|
+
blocks[blocks.length - 1] = { kind: "thinking", text: last.text + text };
|
|
72
|
+
else
|
|
73
|
+
blocks.push({ kind: "thinking", text });
|
|
74
|
+
return { ...message, blocks };
|
|
75
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const DEFAULT_WINDOW_MS = 60;
|
|
2
|
+
export function createSender(options) {
|
|
3
|
+
const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
|
|
4
|
+
const stats = {
|
|
5
|
+
sentSnapshots: 0,
|
|
6
|
+
coalescedSnapshots: 0,
|
|
7
|
+
skippedSnapshots: 0,
|
|
8
|
+
sentEvents: 0,
|
|
9
|
+
sentReplies: 0,
|
|
10
|
+
};
|
|
11
|
+
let pending = null;
|
|
12
|
+
let lastSentAt = Number.NEGATIVE_INFINITY;
|
|
13
|
+
let busy = false;
|
|
14
|
+
const sendNow = () => {
|
|
15
|
+
if (!pending)
|
|
16
|
+
return false;
|
|
17
|
+
options.send({ kind: "snapshot", snapshot: pending });
|
|
18
|
+
pending = null;
|
|
19
|
+
stats.sentSnapshots += 1;
|
|
20
|
+
return true;
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
pushSnapshot(snapshot) {
|
|
24
|
+
if (pending)
|
|
25
|
+
stats.coalescedSnapshots += 1;
|
|
26
|
+
pending = snapshot;
|
|
27
|
+
},
|
|
28
|
+
pushEvent(event) {
|
|
29
|
+
options.send({ kind: "event", event });
|
|
30
|
+
stats.sentEvents += 1;
|
|
31
|
+
},
|
|
32
|
+
pushReply(reply) {
|
|
33
|
+
options.send({ kind: "reply", reply });
|
|
34
|
+
stats.sentReplies += 1;
|
|
35
|
+
},
|
|
36
|
+
tick(now) {
|
|
37
|
+
if (!pending)
|
|
38
|
+
return;
|
|
39
|
+
if (busy) {
|
|
40
|
+
stats.skippedSnapshots += 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (now - lastSentAt < windowMs)
|
|
44
|
+
return;
|
|
45
|
+
if (sendNow())
|
|
46
|
+
lastSentAt = now;
|
|
47
|
+
},
|
|
48
|
+
setBusy(next) {
|
|
49
|
+
busy = next;
|
|
50
|
+
},
|
|
51
|
+
flush(now) {
|
|
52
|
+
if (sendNow())
|
|
53
|
+
lastSentAt = now;
|
|
54
|
+
},
|
|
55
|
+
stats() {
|
|
56
|
+
return { ...stats };
|
|
57
|
+
},
|
|
58
|
+
hasPendingSnapshot() {
|
|
59
|
+
return pending !== null;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|