berry-agent 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
package/dist/sdk/http.js
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sdk/http — SDK HTTP+SSE 传输面(批 13e-2;03 §10.6 HTTP+SSE 件承载条 +
|
|
3
|
+
* 批 13e 落码定形注①②⑤;批 18a-1' 路由扩展位 + 多监听扩形——2026-09-07
|
|
4
|
+
* webui 插件化批)。
|
|
5
|
+
*
|
|
6
|
+
* **一核多流**(定形注②):daemon 全程单一协议核(createSdkBackend 自持
|
|
7
|
+
* decideApproval/onSubscribed 两面),出站 sink 两相位——
|
|
8
|
+
* - 请求作用域(栈顶收集器):POST / SSE 建立期 handleRequest 同步执行,应答
|
|
9
|
+
* 帧(ack/entries/sessions/decide-result/hello/replay-end/error)定向回触发
|
|
10
|
+
* 请求;handleRequest 全同步 ⇒ 栈式作用域零交织(Node 单线程 + 同步受理);
|
|
11
|
+
* - 活体相位(栈空):pushEvent/heartbeatTick 的帧按 sessionId 扇出至订阅
|
|
12
|
+
* SSE 流——**订阅生命周期与真观众同步**(定形注⑤):流关闭即退订
|
|
13
|
+
* (core.unsubscribe),ask fail-closed 判据〔无订阅者即 cancel〕与传输面
|
|
14
|
+
* 真观众恒一致;POST-only 调用方(prompt 后未开 SSE)的审批即时保守收场。
|
|
15
|
+
*
|
|
16
|
+
* 端点面(定形注①):五 POST/GET + GET /v1/events(SSE = hello 的 HTTP 承载
|
|
17
|
+
* 位——sessionId/after/noDelta 入查询参,重放→replay-end→直播与 stdio 同核
|
|
18
|
+
* 同件)。POST 体 = 载荷去 verb 形(端点即动词——本层注入 verb 后经 channels
|
|
19
|
+
* schema 单源深校验,零第二套载荷形)。
|
|
20
|
+
*
|
|
21
|
+
* **路由扩展位(18a-1'——03 §10.6 路由扩展位段)**:sdk HTTP 面开注册器
|
|
22
|
+
* SdkRouteRegistrar——webui SPA 面 / issue webhook 挂点等 core: 件路由注册于
|
|
23
|
+
* 此(件零自持 node:http 监听;过渡期〔批 12f core: 注册表前〕host 装配序直
|
|
24
|
+
* 调 options.routes / handle.register——「禁用 = 装配不接线」诚实注记,不虚
|
|
25
|
+
* 称可禁用)。路由匹配序 = `/v1/*` 保留字(前缀圈占——未中六端点即 404 不
|
|
26
|
+
* fallthrough)→ 具名扩展路由 → catch-all → 404。gate 执法序倒转:**路由匹配
|
|
27
|
+
* 先行 → per-route auth 档**——未匹配路径一律 404 不要求凭证(安全面行为变化,
|
|
28
|
+
* routes.test.ts 显式回归锁留痕)。
|
|
29
|
+
*
|
|
30
|
+
* 三防线挂点(10.4 同律 + 差异面④⑤):鉴权恒在场(token 预置或进程内生成,
|
|
31
|
+
* Bearer 判走 verifyTokenConstantTime 常时比对——18a-1' 自字符串直比升常时,
|
|
32
|
+
* 有意行为增强注记);Host 白名单 + Origin 硬防线面级先行适用于一切路由(含
|
|
33
|
+
* 扩展位路由),仅 TCP 监听(sock = 本地文件权限信任边界——无 DNS 无浏览器,
|
|
34
|
+
* 防线不适用);启动断言 judgeListenConfig 复核(非回环 TCP 无预置凭证
|
|
35
|
+
* fail-closed——TCP×N 一票非回环无凭证即整面拒启)。
|
|
36
|
+
*
|
|
37
|
+
* **loopbackOnly 挂载规则**(真源 03 §10.6):loopbackOnly 路由在非回环 TCP
|
|
38
|
+
* 监听器不挂载(404 同未注册 + start 时 warn 一行);回环 TCP 与 sock 监听器
|
|
39
|
+
* 挂全路由。
|
|
40
|
+
*
|
|
41
|
+
* SSE 纪律(定形注⑤):帧形 `data: <单行 JSON>\n\n` + 30s 注释行 ping;写侧
|
|
42
|
+
* 看门狗 90s 判死 reap(读侧判死不可实施);背压期可丢载荷 per-stream
|
|
43
|
+
* shedding(droppable 谓词注入——/v1/events 用 isDroppableFrame 线协议判据、
|
|
44
|
+
* openSse 消费面自定;线控帧缓冲不弃)。面级流账(openStreams)统一收口:
|
|
45
|
+
* stop() 全流 close 含扩展路由 SSE 流。
|
|
46
|
+
*/
|
|
47
|
+
import { unlink } from 'node:fs/promises';
|
|
48
|
+
import { createServer } from 'node:http';
|
|
49
|
+
import { connect } from 'node:net';
|
|
50
|
+
import { createSdkBackend, isDroppableFrame, SDK_PROTOCOL_VERSION, validateSdkRequest } from '../channels/index.js';
|
|
51
|
+
import { generateToken, isLoopbackHost, judgeHostHeader, judgeListenConfig, originAllowed, verifyTokenConstantTime, } from './security.js';
|
|
52
|
+
import { SDK_AUTH_HEADER, SDK_HTTP_ENDPOINTS, SDK_PROTOCOL_HEADER, SDK_ROUTE_OPEN_PURPOSES, SDK_SSE_PING_INTERVAL_MS, SDK_SSE_WRITE_TIMEOUT_MS, } from './types.js';
|
|
53
|
+
/** 请求体上限缺省(10 MiB——/v1/* prompt 内容护栏;扩展路由 per-route bodyLimitBytes 覆盖) */
|
|
54
|
+
const DEFAULT_BODY_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
55
|
+
/** Bearer 前缀(鉴权头形——Bearer ∪ cookie 双通道的 Bearer 腿) */
|
|
56
|
+
const BEARER_PREFIX = 'Bearer ';
|
|
57
|
+
/** 路由通配段字面(catch-all 尾——SPA fallback 承载位) */
|
|
58
|
+
const WILDCARD_SEGMENT = '*';
|
|
59
|
+
/** `/v1` 保留字前缀(SDK 六动词圈占——扩展路由注册即拒 + 匹配侧不 fallthrough) */
|
|
60
|
+
const V1_RESERVED = '/v1';
|
|
61
|
+
/** 错误帧 → HTTP 状态码映射(定形注⑤——错误帧原样作响应体,未列码缺省 400) */
|
|
62
|
+
const HTTP_STATUS_BY_CODE = {
|
|
63
|
+
SDK_PROTOCOL_MISMATCH: 400,
|
|
64
|
+
SDK_DECODE: 400,
|
|
65
|
+
SDK_CURSOR_INVALID: 400,
|
|
66
|
+
SESSION_NOT_FOUND: 404,
|
|
67
|
+
SESSION_CLOSED: 409,
|
|
68
|
+
SDK_MESSAGE_CONFLICT: 409,
|
|
69
|
+
SDK_OVERLOADED: 503,
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* 注册期校验 + 编译(fail-loud——一切非法形构造/注册即抛,不留到请求期):
|
|
73
|
+
* path 段式(`:name` 参数段 + 至多一枚 `*` 通配尾);`/v1` 前缀保留字;
|
|
74
|
+
* open 档 purpose 射界闭集(探活/鉴权换证/静态壳三类语义位——代码可执法)。
|
|
75
|
+
*/
|
|
76
|
+
function compileRoute(descriptor) {
|
|
77
|
+
const { method, path } = descriptor;
|
|
78
|
+
// 单 `*` = 吞一切(SPA fallback 承载位);其余必须 `/` 开头
|
|
79
|
+
if (path !== WILDCARD_SEGMENT && !path.startsWith('/')) {
|
|
80
|
+
throw new Error(`路由 path 须以 / 开头(或单独 * 吞一切):${method} ${path}`);
|
|
81
|
+
}
|
|
82
|
+
// /v1 前缀保留字——SDK 六动词圈占,注册即拒(03 §10.6「/v1/ 前缀系保留字 fail-loud」)
|
|
83
|
+
if (path === V1_RESERVED || path.startsWith(`${V1_RESERVED}/`)) {
|
|
84
|
+
throw new Error(`路由 path /v1 前缀系 SDK 六动词保留字——注册即拒(03 §10.6):${method} ${path}`);
|
|
85
|
+
}
|
|
86
|
+
const segments = path === WILDCARD_SEGMENT ? [WILDCARD_SEGMENT] : path.slice(1).split('/');
|
|
87
|
+
if (segments.some((seg) => seg === '')) {
|
|
88
|
+
throw new Error(`路由 path 空段非法:${method} ${path}`);
|
|
89
|
+
}
|
|
90
|
+
// 通配 `*`:至多一枚且须尾段(中段通配歧义形拒)
|
|
91
|
+
const starIndex = segments.indexOf(WILDCARD_SEGMENT);
|
|
92
|
+
if (starIndex !== -1 &&
|
|
93
|
+
(starIndex !== segments.length - 1 || segments.indexOf(WILDCARD_SEGMENT, starIndex + 1) !== -1)) {
|
|
94
|
+
throw new Error(`路由 path 通配 * 至多一枚且须尾段:${method} ${path}`);
|
|
95
|
+
}
|
|
96
|
+
for (const seg of segments) {
|
|
97
|
+
if (seg.startsWith(':')) {
|
|
98
|
+
if (!/^[A-Za-z0-9_]+$/.test(seg.slice(1))) {
|
|
99
|
+
throw new Error(`路由参数名非法(字母数字下划线):${method} ${path}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (seg !== WILDCARD_SEGMENT && /[:*]/.test(seg)) {
|
|
103
|
+
throw new Error(`路由段含保留字符(: / *):${method} ${path}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// open 档射界枚举钉死:purpose 闭集越界即拒(注册期白名单校验——非仅冷读审读)
|
|
107
|
+
if (typeof descriptor.auth === 'object' &&
|
|
108
|
+
descriptor.auth !== null &&
|
|
109
|
+
descriptor.auth.mode === 'open' &&
|
|
110
|
+
!SDK_ROUTE_OPEN_PURPOSES.includes(descriptor.auth.purpose)) {
|
|
111
|
+
throw new Error(`open 档 purpose 越界(${String(descriptor.auth.purpose)})——合法语义位闭集:${SDK_ROUTE_OPEN_PURPOSES.join(' / ')}(探活/鉴权换证/静态壳)`);
|
|
112
|
+
}
|
|
113
|
+
return { descriptor, segments, key: `${method} ${path}` };
|
|
114
|
+
}
|
|
115
|
+
/** 单路由匹配(method 先判 + 段式比对;catch-all 尾吞余段原样进 wildcard) */
|
|
116
|
+
function matchRoute(route, method, pathSegments) {
|
|
117
|
+
if (route.descriptor.method !== method)
|
|
118
|
+
return undefined;
|
|
119
|
+
const segs = route.segments;
|
|
120
|
+
const starIndex = segs.indexOf(WILDCARD_SEGMENT);
|
|
121
|
+
if (starIndex === -1) {
|
|
122
|
+
// 具名路由:段数全等 + 逐段比对(`:name` 段提取参数)
|
|
123
|
+
if (segs.length !== pathSegments.length)
|
|
124
|
+
return undefined;
|
|
125
|
+
const params = {};
|
|
126
|
+
for (let i = 0; i < segs.length; i++) {
|
|
127
|
+
const pattern = segs[i];
|
|
128
|
+
const value = pathSegments[i];
|
|
129
|
+
if (pattern.startsWith(':'))
|
|
130
|
+
params[pattern.slice(1)] = value;
|
|
131
|
+
else if (pattern !== value)
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return { route, params, wildcard: undefined };
|
|
135
|
+
}
|
|
136
|
+
// catch-all:star 前缀段全等 + 余段 ≥ 0(余段原样 join 进 wildcard——SPA fallback 承载位)
|
|
137
|
+
if (pathSegments.length < starIndex)
|
|
138
|
+
return undefined;
|
|
139
|
+
const params = {};
|
|
140
|
+
for (let i = 0; i < starIndex; i++) {
|
|
141
|
+
const pattern = segs[i];
|
|
142
|
+
const value = pathSegments[i];
|
|
143
|
+
if (pattern.startsWith(':'))
|
|
144
|
+
params[pattern.slice(1)] = value;
|
|
145
|
+
else if (pattern !== value)
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return { route, params, wildcard: pathSegments.slice(starIndex).join('/') };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 单 SSE 流(面级 SSE 基建单源——/v1/events 与 openSse helper 共用一形)。
|
|
152
|
+
* 写侧自治:背压期 shedding 可丢载荷(droppable 谓词注入——/v1/events 用线协议
|
|
153
|
+
* 判据、扩展面自定)+ 看门狗 90s 判死 reap;ping 注释行保活。close 幂等
|
|
154
|
+
* (连接 close / 看门狗 / 宿主 stop 三路共用)。
|
|
155
|
+
*/
|
|
156
|
+
class SseStream {
|
|
157
|
+
res;
|
|
158
|
+
onEnd;
|
|
159
|
+
now;
|
|
160
|
+
droppable;
|
|
161
|
+
backedUp = false;
|
|
162
|
+
backedUpSince;
|
|
163
|
+
ended = false;
|
|
164
|
+
pingTimer;
|
|
165
|
+
constructor(res,
|
|
166
|
+
/** 流终结一次性回调(面级流账自摘 + /v1/events 另挂 deregister——face 注入) */
|
|
167
|
+
onEnd, now,
|
|
168
|
+
/** 背压可丢判据(缺席 = 不 shedding——关键帧永不丢档) */
|
|
169
|
+
droppable) {
|
|
170
|
+
this.res = res;
|
|
171
|
+
this.onEnd = onEnd;
|
|
172
|
+
this.now = now;
|
|
173
|
+
this.droppable = droppable;
|
|
174
|
+
// SSE 开流即 200(此后错误只能走帧——SSE 状态码位已用尽,错误帧即词面)
|
|
175
|
+
res.writeHead(200, {
|
|
176
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
177
|
+
'cache-control': 'no-cache',
|
|
178
|
+
connection: 'keep-alive',
|
|
179
|
+
'x-accel-buffering': 'no', // 代理禁缓冲(SSE 语义钉死)
|
|
180
|
+
});
|
|
181
|
+
// 头即发(flushHeaders——writeHead 只记账,头缓到首写才发;零首帧静默流
|
|
182
|
+
// 〔webui events 连接即当下、无 hello 帧〕头会挂到 30s ping。18a-2' 揭出
|
|
183
|
+
// 的真缺陷,回归锁 routes.test「静默流」例)
|
|
184
|
+
res.flushHeaders();
|
|
185
|
+
res.on('close', () => this.end());
|
|
186
|
+
res.on('drain', () => {
|
|
187
|
+
this.backedUp = false;
|
|
188
|
+
this.backedUpSince = undefined;
|
|
189
|
+
});
|
|
190
|
+
this.pingTimer = setInterval(() => this.pingTick(), SDK_SSE_PING_INTERVAL_MS);
|
|
191
|
+
}
|
|
192
|
+
/** 帧写入(载荷整体单次 JSON.stringify 单行——帧合成钉死,禁手工拼帧) */
|
|
193
|
+
write(data) {
|
|
194
|
+
if (this.ended)
|
|
195
|
+
return;
|
|
196
|
+
if (this.backedUp && this.droppable?.(data) === true)
|
|
197
|
+
return;
|
|
198
|
+
this.send(`data: ${JSON.stringify(data)}\n\n`);
|
|
199
|
+
}
|
|
200
|
+
/** 宿主收场腿(stop() 全流收口——幂等) */
|
|
201
|
+
close() {
|
|
202
|
+
this.end();
|
|
203
|
+
}
|
|
204
|
+
pingTick() {
|
|
205
|
+
if (this.ended)
|
|
206
|
+
return;
|
|
207
|
+
// 写侧看门狗:背压持续超窗即判死 reap(ping 写不动的流已不可救)
|
|
208
|
+
if (this.backedUp &&
|
|
209
|
+
this.backedUpSince !== undefined &&
|
|
210
|
+
this.now() - this.backedUpSince > SDK_SSE_WRITE_TIMEOUT_MS) {
|
|
211
|
+
this.end();
|
|
212
|
+
this.res.destroy();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!this.send(': ping\n\n'))
|
|
216
|
+
this.markBackpressure();
|
|
217
|
+
}
|
|
218
|
+
/** 写一行块;写异常即 reap(同步写失败 = 流已坏死) */
|
|
219
|
+
send(chunk) {
|
|
220
|
+
if (this.ended)
|
|
221
|
+
return false;
|
|
222
|
+
try {
|
|
223
|
+
const ok = this.res.write(chunk);
|
|
224
|
+
if (!ok)
|
|
225
|
+
this.markBackpressure();
|
|
226
|
+
return ok;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
this.end();
|
|
230
|
+
this.res.destroy();
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
markBackpressure() {
|
|
235
|
+
if (!this.backedUp) {
|
|
236
|
+
this.backedUp = true;
|
|
237
|
+
this.backedUpSince = this.now();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** 终结(幂等):停 ping → onEnd 一次性(连接 close / 看门狗 / 宿主 stop 三路共用)→ res 收线(服务端主动 EOF——stop 收口位 server.close() 不挂死) */
|
|
241
|
+
end() {
|
|
242
|
+
if (this.ended)
|
|
243
|
+
return;
|
|
244
|
+
this.ended = true;
|
|
245
|
+
clearInterval(this.pingTimer);
|
|
246
|
+
this.onEnd();
|
|
247
|
+
if (!this.res.writableEnded)
|
|
248
|
+
this.res.end();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** 请求体超限标记(readBody 返回位——413 语义由调用方应答) */
|
|
252
|
+
class BodyTooLarge extends Error {
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 读请求体(限幅护栏 + **排空纪律**——03 §10.6 helper `readBody` 条款:超帽
|
|
256
|
+
* 应答永不早于请求体收完,防连接池 RST 连坐——超限后继续消费流至尽才报)。
|
|
257
|
+
*/
|
|
258
|
+
async function readBody(req, limitBytes) {
|
|
259
|
+
const chunks = [];
|
|
260
|
+
let size = 0;
|
|
261
|
+
let exceeded = false;
|
|
262
|
+
for await (const chunk of req) {
|
|
263
|
+
if (exceeded)
|
|
264
|
+
continue; // 已超帽:继续排空不记账(应答等体收完)
|
|
265
|
+
size += chunk.length;
|
|
266
|
+
if (size > limitBytes) {
|
|
267
|
+
exceeded = true;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
chunks.push(chunk);
|
|
271
|
+
}
|
|
272
|
+
if (exceeded)
|
|
273
|
+
throw new BodyTooLarge(`请求体超限(> ${limitBytes} 字节)`);
|
|
274
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
275
|
+
}
|
|
276
|
+
/** 错误帧应答(码→状态映射;帧形与线协议错误帧同构——词汇零第二套) */
|
|
277
|
+
function sendError(res, status, code, message) {
|
|
278
|
+
if (res.writableEnded)
|
|
279
|
+
return;
|
|
280
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
281
|
+
res.end(JSON.stringify({ kind: 'error', code, message }));
|
|
282
|
+
}
|
|
283
|
+
/** 单次 listen(sock 路径形)——error 上抛、listening 回调即就绪 */
|
|
284
|
+
function listenOnPath(server, socketPath) {
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
server.once('error', reject);
|
|
287
|
+
server.listen(socketPath, () => resolve());
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** 单次 listen(TCP 形)——host/port 形参序与 node 语义一致 */
|
|
291
|
+
function listenOnTcp(server, host, port) {
|
|
292
|
+
return new Promise((resolve, reject) => {
|
|
293
|
+
server.once('error', reject);
|
|
294
|
+
server.listen(port, host, () => resolve());
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/** 探活 Unix sock:连得上 = 在场 daemon;连不上 = 死迹 */
|
|
298
|
+
function probeSocket(socketPath) {
|
|
299
|
+
return new Promise((resolve) => {
|
|
300
|
+
const sock = connect(socketPath);
|
|
301
|
+
sock.once('connect', () => {
|
|
302
|
+
sock.destroy();
|
|
303
|
+
resolve(true);
|
|
304
|
+
});
|
|
305
|
+
sock.once('error', () => resolve(false));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/** Bearer 剥取(authorization 头 → token 值;非 Bearer 形 = undefined) */
|
|
309
|
+
function bearerTokenOf(req) {
|
|
310
|
+
const raw = req.headers[SDK_AUTH_HEADER];
|
|
311
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
312
|
+
if (typeof value !== 'string' || !value.startsWith(BEARER_PREFIX))
|
|
313
|
+
return undefined;
|
|
314
|
+
return value.slice(BEARER_PREFIX.length);
|
|
315
|
+
}
|
|
316
|
+
/** cookie 值剥取(`name=value; other=x` 分号分段——EventSource 结构性无 Authorization 位的浏览器腿) */
|
|
317
|
+
function cookieValueOf(req, name) {
|
|
318
|
+
const raw = req.headers.cookie;
|
|
319
|
+
if (typeof raw !== 'string' || raw === '')
|
|
320
|
+
return undefined;
|
|
321
|
+
for (const part of raw.split(';')) {
|
|
322
|
+
const trimmed = part.trim();
|
|
323
|
+
const eq = trimmed.indexOf('=');
|
|
324
|
+
if (eq === -1)
|
|
325
|
+
continue;
|
|
326
|
+
if (trimmed.slice(0, eq) === name)
|
|
327
|
+
return trimmed.slice(eq + 1);
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 造 SDK HTTP 面(一核多流 + 路由扩展位)。监听器族多监听并存:sock 可选 +
|
|
333
|
+
* TCP 单形/数组(daemon/前台装配位 13e-3 与本件测试消费);扩展路由两注册
|
|
334
|
+
* 入口——options.routes(构造期)与 handle.register(晚注册,返摘除 fn)。
|
|
335
|
+
*/
|
|
336
|
+
export function createSdkHttpFace(options) {
|
|
337
|
+
// 启动断言复核(13e-3 宿主先拒——本面再核:直造面也不留非回环无凭证档)
|
|
338
|
+
const judged = judgeListenConfig(options.config);
|
|
339
|
+
if (!judged.ok)
|
|
340
|
+
throw new Error(judged.reason);
|
|
341
|
+
// 鉴权恒在场:预置 token 在场即用,缺席自足生成进程内一次性(披露面宿主出)
|
|
342
|
+
const token = options.config.token === undefined || options.config.token === '' ? generateToken() : options.config.token;
|
|
343
|
+
const bodyLimit = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
344
|
+
const warn = options.warn ?? (() => { });
|
|
345
|
+
const heartbeatIntervalMs = options.coreOptions?.heartbeatIntervalMs ?? 5_000;
|
|
346
|
+
const now = options.coreOptions?.now ?? Date.now;
|
|
347
|
+
// —— 一核多流出站路由(定形注②)——
|
|
348
|
+
/** 活体扇出账(sessionId → 订阅流集——订阅生命周期与真观众同步) */
|
|
349
|
+
const streamsBySession = new Map();
|
|
350
|
+
/** 面级流账(一切 SSE 流含扩展路由 openSse 流——stop() 统一收口) */
|
|
351
|
+
const openStreams = new Set();
|
|
352
|
+
/** 请求作用域栈(handleRequest 同步期非空——应答帧定向回触发请求) */
|
|
353
|
+
const scopeStack = [];
|
|
354
|
+
const sink = {
|
|
355
|
+
write: (frame) => {
|
|
356
|
+
const scope = scopeStack[scopeStack.length - 1];
|
|
357
|
+
if (scope !== undefined)
|
|
358
|
+
return scope(frame); // 请求作用域:收集器恒可写(小量)
|
|
359
|
+
// 活体相位:按 sessionId 扇出(无观众即弃——帧词汇里活体帧恒携会话)
|
|
360
|
+
const sid = frame.sessionId;
|
|
361
|
+
if (typeof sid === 'string' && sid !== '') {
|
|
362
|
+
for (const stream of streamsBySession.get(sid) ?? [])
|
|
363
|
+
stream.write(frame);
|
|
364
|
+
}
|
|
365
|
+
return true; // 活体相位恒收(per-stream shedding 自治——核级队列不背压)
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
const handle = createSdkBackend({ ...options.bridge, sink }, options.coreOptions);
|
|
369
|
+
const core = handle.core;
|
|
370
|
+
/** 末流退订(流撤即撤核内订阅——ask fail-closed 判据与真观众同步) */
|
|
371
|
+
const deregister = (sessionId, stream) => {
|
|
372
|
+
const set = streamsBySession.get(sessionId);
|
|
373
|
+
if (set === undefined)
|
|
374
|
+
return;
|
|
375
|
+
set.delete(stream);
|
|
376
|
+
if (set.size === 0) {
|
|
377
|
+
streamsBySession.delete(sessionId);
|
|
378
|
+
core.unsubscribe(sessionId);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
/** 请求作用域受理(收集器进栈 → 核受理 → 出栈——零交织窗口) */
|
|
382
|
+
const handleScoped = (request) => {
|
|
383
|
+
const collected = [];
|
|
384
|
+
const collector = (frame) => {
|
|
385
|
+
collected.push(frame);
|
|
386
|
+
return true;
|
|
387
|
+
};
|
|
388
|
+
scopeStack.push(collector);
|
|
389
|
+
try {
|
|
390
|
+
core.handleRequest(request);
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
scopeStack.pop();
|
|
394
|
+
}
|
|
395
|
+
return collected;
|
|
396
|
+
};
|
|
397
|
+
/** POST 应答映射(错误帧→状态码;interrupt 无应答档 = 204 空体——定形注①) */
|
|
398
|
+
const respondCollected = (res, collected, verb) => {
|
|
399
|
+
const first = collected[0];
|
|
400
|
+
if (first === undefined) {
|
|
401
|
+
if (verb === 'interrupt') {
|
|
402
|
+
res.writeHead(204);
|
|
403
|
+
res.end();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// 应答帧缺席 = 核闭合态静默档(宿主 close() 后无因残行)——不留半开档
|
|
407
|
+
sendError(res, 503, 'SDK_OVERLOADED', '协议核已闭合——请求不受理(面将随宿主收场)');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (first.kind === 'error') {
|
|
411
|
+
sendError(res, HTTP_STATUS_BY_CODE[first.code] ?? 400, first.code, first.message);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
415
|
+
res.end(JSON.stringify(first));
|
|
416
|
+
};
|
|
417
|
+
/** POST 端点共腿:读体 → 注入 verb 深校验(schema 单源)→ 作用域受理 → 应答映射 */
|
|
418
|
+
const postVerb = async (req, res, verb) => {
|
|
419
|
+
let body;
|
|
420
|
+
try {
|
|
421
|
+
body = await readBody(req, bodyLimit);
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
if (err instanceof BodyTooLarge) {
|
|
425
|
+
sendError(res, 413, 'SDK_DECODE', err.message);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
let parsed;
|
|
431
|
+
try {
|
|
432
|
+
parsed = JSON.parse(body);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
sendError(res, 400, 'SDK_DECODE', '请求体非合法 JSON');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
439
|
+
sendError(res, 400, 'SDK_DECODE', '请求体须为 JSON 对象');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// 端点即动词:传输层注入 verb(体携带的 verb 被覆写——端点权威)后同源深校验
|
|
443
|
+
const validated = validateSdkRequest({ ...parsed, verb });
|
|
444
|
+
if (!validated.ok) {
|
|
445
|
+
sendError(res, 400, 'SDK_DECODE', validated.reason);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const collected = handleScoped(validated.value);
|
|
449
|
+
// prompt 落定后观众同步:该会话零 SSE 流即撤核内自动订阅(POST-only 调用方
|
|
450
|
+
// 无直播腿——保留空订阅只会让 ask 打入无人区挂死;撤后 fail-closed 即时
|
|
451
|
+
// cancel。SSE 已开则保留——ack 帧携句柄定位会话)
|
|
452
|
+
const first = collected[0];
|
|
453
|
+
if (verb === 'prompt' && first?.kind === 'ack') {
|
|
454
|
+
if ((streamsBySession.get(first.sessionId)?.size ?? 0) === 0)
|
|
455
|
+
core.unsubscribe(first.sessionId);
|
|
456
|
+
}
|
|
457
|
+
respondCollected(res, collected, verb);
|
|
458
|
+
};
|
|
459
|
+
/** SSE 建立位(hello 的 HTTP 承载——定形注①):查询参三值 → 流内三步衔接 */
|
|
460
|
+
const getEvents = (_req, res, url) => {
|
|
461
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
462
|
+
if (sessionId === null || sessionId === '') {
|
|
463
|
+
sendError(res, 400, 'SDK_DECODE', 'sessionId 查询参必填(无会话订阅的 SSE 流恒空载——与 stdio 无会话 hello 档分立)');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let after;
|
|
467
|
+
const afterRaw = url.searchParams.get('after');
|
|
468
|
+
if (afterRaw !== null) {
|
|
469
|
+
after = Number(afterRaw);
|
|
470
|
+
if (!Number.isInteger(after)) {
|
|
471
|
+
sendError(res, 400, 'SDK_DECODE', `after 查询参非整数:${afterRaw}`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
let noDelta;
|
|
476
|
+
const noDeltaRaw = url.searchParams.get('noDelta');
|
|
477
|
+
if (noDeltaRaw !== null) {
|
|
478
|
+
if (noDeltaRaw !== 'true' && noDeltaRaw !== 'false') {
|
|
479
|
+
sendError(res, 400, 'SDK_DECODE', `noDelta 查询参非布尔(true/false):${noDeltaRaw}`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
noDelta = noDeltaRaw === 'true';
|
|
483
|
+
}
|
|
484
|
+
// 建立期作用域 = 流本体(hello/entries/replay-end/未决 ask 重推直入流);
|
|
485
|
+
// 版本已在 gate 层验过——hello 协议核内校验恒过。背压 shedding 走线协议
|
|
486
|
+
// 判据(isDroppableFrame——纯活体帧可丢、线控帧缓冲不弃;unknown 域收敛
|
|
487
|
+
// 转型 = /v1/events 流载荷恒 SdkWireFrame 的件内不变式)
|
|
488
|
+
const stream = new SseStream(res, () => {
|
|
489
|
+
openStreams.delete(stream);
|
|
490
|
+
deregister(sessionId, stream);
|
|
491
|
+
}, now, (data) => isDroppableFrame(data));
|
|
492
|
+
openStreams.add(stream);
|
|
493
|
+
const collector = (frame) => {
|
|
494
|
+
stream.write(frame);
|
|
495
|
+
return true;
|
|
496
|
+
};
|
|
497
|
+
scopeStack.push(collector);
|
|
498
|
+
try {
|
|
499
|
+
core.handleRequest({
|
|
500
|
+
verb: 'hello',
|
|
501
|
+
protocolVersion: SDK_PROTOCOL_VERSION,
|
|
502
|
+
sessionId,
|
|
503
|
+
...(after !== undefined ? { after } : {}),
|
|
504
|
+
...(noDelta !== undefined ? { noDelta } : {}),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
scopeStack.pop();
|
|
509
|
+
}
|
|
510
|
+
// 订阅落定才挂扇出;error 档(会话不存在/游标非法)帧已入流——收流即终结
|
|
511
|
+
if (!core.isSubscribed(sessionId)) {
|
|
512
|
+
stream.close();
|
|
513
|
+
res.end();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
let set = streamsBySession.get(sessionId);
|
|
517
|
+
if (set === undefined) {
|
|
518
|
+
set = new Set();
|
|
519
|
+
streamsBySession.set(sessionId, set);
|
|
520
|
+
}
|
|
521
|
+
set.add(stream);
|
|
522
|
+
};
|
|
523
|
+
/* ---------------- 路由扩展位实装(03 §10.6 路由扩展位段) ---------------- */
|
|
524
|
+
/** 具名扩展路由(无通配尾——匹配序第二档) */
|
|
525
|
+
const namedRoutes = [];
|
|
526
|
+
/** catch-all 扩展路由(通配尾——匹配序第三档,具名全 miss 后才轮到) */
|
|
527
|
+
const catchAllRoutes = [];
|
|
528
|
+
/** 注册键账(`${method} ${path}` 双键唯一——重复注册 fail-loud) */
|
|
529
|
+
const routeKeys = new Set();
|
|
530
|
+
/** 注册(两入口共享实现):校验编译 → 入表;返摘除 fn(摘后 404 同未注册、键释放可重注册) */
|
|
531
|
+
const register = (descriptor) => {
|
|
532
|
+
const compiled = compileRoute(descriptor);
|
|
533
|
+
if (routeKeys.has(compiled.key)) {
|
|
534
|
+
throw new Error(`路由重复注册(method+path 双键唯一):${compiled.key}——注册期 fail-loud`);
|
|
535
|
+
}
|
|
536
|
+
routeKeys.add(compiled.key);
|
|
537
|
+
const table = compiled.segments.includes(WILDCARD_SEGMENT) ? catchAllRoutes : namedRoutes;
|
|
538
|
+
table.push(compiled);
|
|
539
|
+
return () => {
|
|
540
|
+
routeKeys.delete(compiled.key);
|
|
541
|
+
const at = table.indexOf(compiled);
|
|
542
|
+
if (at !== -1)
|
|
543
|
+
table.splice(at, 1);
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
for (const descriptor of options.routes ?? [])
|
|
547
|
+
register(descriptor);
|
|
548
|
+
/** 扩展路由匹配(挂载规则内含:非回环监听器对 loopbackOnly 路由不挂载——404 同未注册) */
|
|
549
|
+
const matchExtension = (method, pathSegments, loopback) => {
|
|
550
|
+
// 两遍序:具名先行 → catch-all(匹配序条款「具名扩展路由 → catch-all」)
|
|
551
|
+
for (const route of namedRoutes) {
|
|
552
|
+
if (route.descriptor.loopbackOnly === true && !loopback)
|
|
553
|
+
continue; // 挂载规则
|
|
554
|
+
const match = matchRoute(route, method, pathSegments);
|
|
555
|
+
if (match !== undefined)
|
|
556
|
+
return match;
|
|
557
|
+
}
|
|
558
|
+
for (const route of catchAllRoutes) {
|
|
559
|
+
if (route.descriptor.loopbackOnly === true && !loopback)
|
|
560
|
+
continue;
|
|
561
|
+
const match = matchRoute(route, method, pathSegments);
|
|
562
|
+
if (match !== undefined)
|
|
563
|
+
return match;
|
|
564
|
+
}
|
|
565
|
+
return undefined;
|
|
566
|
+
};
|
|
567
|
+
/** per-route 鉴权档执法(四值——token 闸位;Host/Origin 已面级先行) */
|
|
568
|
+
const routeAuthOk = (req, auth) => {
|
|
569
|
+
// open / self 档跳面 token 闸(open = 射界已注册期钉死;self = 件侧自验——issue webhook HMAC 位)
|
|
570
|
+
if (auth === 'self')
|
|
571
|
+
return true;
|
|
572
|
+
if (typeof auth === 'object' && auth !== null) {
|
|
573
|
+
if (auth.mode === 'open')
|
|
574
|
+
return true;
|
|
575
|
+
if (auth.mode === 'token-or-cookie') {
|
|
576
|
+
// Bearer ∪ cookie 双通道(恒时比对——sha256 摘要恒长 + timingSafeEqual)
|
|
577
|
+
const bearer = bearerTokenOf(req);
|
|
578
|
+
if (bearer !== undefined && verifyTokenConstantTime(bearer, token))
|
|
579
|
+
return true;
|
|
580
|
+
const cookie = cookieValueOf(req, auth.cookie);
|
|
581
|
+
return cookie !== undefined && verifyTokenConstantTime(cookie, token);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// 'token' 档(Bearer 单通道)
|
|
585
|
+
const bearer = bearerTokenOf(req);
|
|
586
|
+
return bearer !== undefined && verifyTokenConstantTime(bearer, token);
|
|
587
|
+
};
|
|
588
|
+
/** 扩展路由 ctx 构造(面级 helper 单源——params/wildcard 匹配期绑定) */
|
|
589
|
+
const makeRouteCtx = (match) => ({
|
|
590
|
+
params: match.params,
|
|
591
|
+
wildcard: match.wildcard,
|
|
592
|
+
openSse: (res, opts) => {
|
|
593
|
+
const stream = new SseStream(res, () => {
|
|
594
|
+
openStreams.delete(stream);
|
|
595
|
+
}, now, opts?.droppable);
|
|
596
|
+
openStreams.add(stream);
|
|
597
|
+
return stream;
|
|
598
|
+
},
|
|
599
|
+
readBody: async (req) => {
|
|
600
|
+
try {
|
|
601
|
+
const body = await readBody(req, match.route.descriptor.bodyLimitBytes ?? bodyLimit);
|
|
602
|
+
return { ok: true, body };
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
if (err instanceof BodyTooLarge)
|
|
606
|
+
return { ok: false, status: 413, message: err.message };
|
|
607
|
+
throw err;
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
verifyToken: (value) => verifyTokenConstantTime(value, token),
|
|
611
|
+
});
|
|
612
|
+
/* ---------------- 三防线闸门 + 版本闸(/v1/* 族——13e 定形序维持) ---------------- */
|
|
613
|
+
/** /v1/* 闸门(13e-2 定形序:token → Host/Origin——18a-1' 仅 token 判升常时比对) */
|
|
614
|
+
const gate = (req, sec) => {
|
|
615
|
+
const bearer = bearerTokenOf(req);
|
|
616
|
+
if (bearer === undefined || !verifyTokenConstantTime(bearer, token)) {
|
|
617
|
+
return {
|
|
618
|
+
ok: false,
|
|
619
|
+
status: 401,
|
|
620
|
+
code: 'SDK_UNAUTHORIZED',
|
|
621
|
+
message: '鉴权失败:Authorization 头须为 Bearer 形携实效 token',
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
if (sec.label === 'tcp') {
|
|
625
|
+
const host = judgeHostHeader(req.headers.host, sec.bindHost);
|
|
626
|
+
if (!host.ok)
|
|
627
|
+
return { ok: false, status: 403, code: 'SDK_FORBIDDEN', message: `Host 防线拒:${host.reason}` };
|
|
628
|
+
if (!originAllowed(req.headers.origin, sec.bindHost, sec.listenPort)) {
|
|
629
|
+
return { ok: false, status: 403, code: 'SDK_FORBIDDEN', message: 'Origin 防线拒:非同源请求(10.4③)' };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return { ok: true };
|
|
633
|
+
};
|
|
634
|
+
/** 版本闸门(定形注①——每请求 X-SDK-Protocol:缺席/不符同拒;扩展路由免——浏览器无连接级握手位) */
|
|
635
|
+
const versionGate = (req) => {
|
|
636
|
+
const raw = req.headers[SDK_PROTOCOL_HEADER];
|
|
637
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
638
|
+
if (value === undefined)
|
|
639
|
+
return { ok: false, reason: `版本头 ${SDK_PROTOCOL_HEADER} 缺席(每请求必携)` };
|
|
640
|
+
const version = Number(value);
|
|
641
|
+
if (version !== SDK_PROTOCOL_VERSION) {
|
|
642
|
+
return { ok: false, reason: `线协议版本不符:服务端 ${SDK_PROTOCOL_VERSION} / 调用方 ${value}` };
|
|
643
|
+
}
|
|
644
|
+
return { ok: true };
|
|
645
|
+
};
|
|
646
|
+
/** Host/Origin 面级防线(扩展路由族先行位——适用于一切路由,仅 TCP 监听执法) */
|
|
647
|
+
const extensionLineDefense = (req, sec) => {
|
|
648
|
+
if (sec.label !== 'tcp')
|
|
649
|
+
return { ok: true };
|
|
650
|
+
const host = judgeHostHeader(req.headers.host, sec.bindHost);
|
|
651
|
+
if (!host.ok)
|
|
652
|
+
return { ok: false, reason: `Host 防线拒:${host.reason}` };
|
|
653
|
+
if (!originAllowed(req.headers.origin, sec.bindHost, sec.listenPort)) {
|
|
654
|
+
return { ok: false, reason: 'Origin 防线拒:非同源请求(10.4③)' };
|
|
655
|
+
}
|
|
656
|
+
return { ok: true };
|
|
657
|
+
};
|
|
658
|
+
/**
|
|
659
|
+
* 路由分发(匹配序 + gate 序倒转——03 §10.6 路由扩展位段):
|
|
660
|
+
* 1. `/v1/*` 保留字圈占(六端点命中走 13e 定形序 gate → versionGate →
|
|
661
|
+
* isClosed → verb;未中即 404 **不 fallthrough**——保留字系面级圈占,
|
|
662
|
+
* 防 SPA catch-all 吞掉程序面打错路径);
|
|
663
|
+
* 2. 扩展路由匹配(具名 → catch-all;挂载规则内含);
|
|
664
|
+
* 3. 未匹配一律 404 **不要求凭证**(gate 序倒转——安全面行为变化,回归锁
|
|
665
|
+
* routes.test.ts「未匹配 404 免凭证」例留痕);
|
|
666
|
+
* 4. 扩展命中:Host/Origin 面级先行(TCP 监听)→ per-route auth 档 →
|
|
667
|
+
* isClosed 503 同律 → handler(异常面上抛监听器 catch → 500)。
|
|
668
|
+
*/
|
|
669
|
+
const dispatch = async (req, res, sec) => {
|
|
670
|
+
const url = new URL(req.url ?? '/', 'http://localhost.local'); // 基座仅为相对形合法——只取 pathname/query
|
|
671
|
+
const pathname = url.pathname;
|
|
672
|
+
const method = req.method ?? 'GET';
|
|
673
|
+
const routeKey = `${method} ${pathname}`;
|
|
674
|
+
const isV1 = pathname === V1_RESERVED || pathname.startsWith(`${V1_RESERVED}/`);
|
|
675
|
+
// —— 第一档:/v1/* 保留字(圈占——未中六端点即 404,不 fallthrough 扩展路由)——
|
|
676
|
+
const v1Matched = routeKey === `POST ${SDK_HTTP_ENDPOINTS.prompt}` ||
|
|
677
|
+
routeKey === `POST ${SDK_HTTP_ENDPOINTS.interrupt}` ||
|
|
678
|
+
routeKey === `POST ${SDK_HTTP_ENDPOINTS.decide}` ||
|
|
679
|
+
routeKey === `POST ${SDK_HTTP_ENDPOINTS.entries}` ||
|
|
680
|
+
routeKey === `GET ${SDK_HTTP_ENDPOINTS.sessions}` ||
|
|
681
|
+
routeKey === `GET ${SDK_HTTP_ENDPOINTS.events}`;
|
|
682
|
+
if (isV1) {
|
|
683
|
+
if (!v1Matched) {
|
|
684
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
685
|
+
res.end('not found');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// 13e 定形序:gate(token 常时比对 + Host/Origin)→ versionGate → isClosed
|
|
689
|
+
const g = gate(req, sec);
|
|
690
|
+
if (!g.ok) {
|
|
691
|
+
sendError(res, g.status, g.code, g.message);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const v = versionGate(req);
|
|
695
|
+
if (!v.ok) {
|
|
696
|
+
sendError(res, 400, 'SDK_PROTOCOL_MISMATCH', v.reason);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (core.isClosed) {
|
|
700
|
+
sendError(res, 503, 'SDK_OVERLOADED', '协议核已闭合——请求不受理(面将随宿主收场)');
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
switch (routeKey) {
|
|
704
|
+
case `POST ${SDK_HTTP_ENDPOINTS.prompt}`:
|
|
705
|
+
await postVerb(req, res, 'prompt');
|
|
706
|
+
return;
|
|
707
|
+
case `POST ${SDK_HTTP_ENDPOINTS.interrupt}`:
|
|
708
|
+
await postVerb(req, res, 'interrupt');
|
|
709
|
+
return;
|
|
710
|
+
case `POST ${SDK_HTTP_ENDPOINTS.decide}`:
|
|
711
|
+
await postVerb(req, res, 'decide');
|
|
712
|
+
return;
|
|
713
|
+
case `POST ${SDK_HTTP_ENDPOINTS.entries}`:
|
|
714
|
+
await postVerb(req, res, 'getEntries');
|
|
715
|
+
return;
|
|
716
|
+
case `GET ${SDK_HTTP_ENDPOINTS.sessions}`: {
|
|
717
|
+
// 零载荷动词:注入 verb 直受理(sessions 帧即应答)
|
|
718
|
+
const collected = handleScoped({ verb: 'sessions' });
|
|
719
|
+
respondCollected(res, collected, 'sessions');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
case `GET ${SDK_HTTP_ENDPOINTS.events}`:
|
|
723
|
+
getEvents(req, res, url);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// —— 第二/三档:扩展路由(具名 → catch-all;挂载规则 = 非回环监听器不挂 loopbackOnly)——
|
|
728
|
+
const pathSegments = pathname === '/' ? [] : pathname.slice(1).split('/');
|
|
729
|
+
const match = matchExtension(method, pathSegments, sec.loopback);
|
|
730
|
+
// —— 404 先行(gate 序倒转):未匹配一律 404,不要求凭证、防线不执法于未注册路径 ——
|
|
731
|
+
if (match === undefined) {
|
|
732
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
733
|
+
res.end('not found');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
// 扩展命中:Host/Origin 面级先行(一切路由)→ per-route auth 档 → isClosed 503 同律
|
|
737
|
+
const line = extensionLineDefense(req, sec);
|
|
738
|
+
if (!line.ok) {
|
|
739
|
+
res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
|
|
740
|
+
res.end(line.reason);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (!routeAuthOk(req, match.route.descriptor.auth)) {
|
|
744
|
+
res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8' });
|
|
745
|
+
res.end('unauthorized');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (core.isClosed) {
|
|
749
|
+
res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
|
|
750
|
+
res.end('service unavailable');
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
await match.route.descriptor.handler(req, res, makeRouteCtx(match));
|
|
754
|
+
};
|
|
755
|
+
// —— 监听器族(sock 可选 + TCP 单/多;安全档与挂载规则随监听器分账)——
|
|
756
|
+
const servers = [];
|
|
757
|
+
let sockServer;
|
|
758
|
+
let sockOwned = false;
|
|
759
|
+
if (options.config.socketPath !== undefined && options.config.socketPath !== '') {
|
|
760
|
+
const sockSecurity = { label: 'sock', loopback: true }; // sock = 本地文件权限信任边界,挂全路由
|
|
761
|
+
sockServer = createServer((req, res) => {
|
|
762
|
+
void dispatch(req, res, sockSecurity).catch((err) => {
|
|
763
|
+
warn(`sock 面请求处理异常:${err instanceof Error ? err.message : String(err)}`);
|
|
764
|
+
if (!res.writableEnded) {
|
|
765
|
+
res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
766
|
+
res.end('internal error');
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
servers.push({ server: sockServer, security: sockSecurity });
|
|
771
|
+
}
|
|
772
|
+
const tcpSpecs = options.config.tcp === undefined
|
|
773
|
+
? []
|
|
774
|
+
: Array.isArray(options.config.tcp)
|
|
775
|
+
? [...options.config.tcp]
|
|
776
|
+
: [options.config.tcp];
|
|
777
|
+
for (const spec of tcpSpecs) {
|
|
778
|
+
const security = {
|
|
779
|
+
label: 'tcp',
|
|
780
|
+
loopback: isLoopbackHost(spec.host), // 挂载规则:回环 TCP 挂全路由;非回环对 loopbackOnly 不挂载
|
|
781
|
+
bindHost: spec.host,
|
|
782
|
+
listenPort: spec.port,
|
|
783
|
+
};
|
|
784
|
+
const server = createServer((req, res) => {
|
|
785
|
+
void dispatch(req, res, security).catch((err) => {
|
|
786
|
+
warn(`tcp 面请求处理异常:${err instanceof Error ? err.message : String(err)}`);
|
|
787
|
+
if (!res.writableEnded) {
|
|
788
|
+
res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
789
|
+
res.end('internal error');
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
servers.push({ server, security });
|
|
794
|
+
}
|
|
795
|
+
/** 监听器对 createServer 面具(监听前占位——start 时真正 listen) */
|
|
796
|
+
const tcpServers = servers.filter((entry) => entry.security.label === 'tcp');
|
|
797
|
+
let heartbeatTimer;
|
|
798
|
+
return {
|
|
799
|
+
backend: handle.backend,
|
|
800
|
+
core,
|
|
801
|
+
token,
|
|
802
|
+
register,
|
|
803
|
+
start: async () => {
|
|
804
|
+
// —— 挂载规则 warn(start 时一行——非回环 TCP × loopbackOnly 路由在场)——
|
|
805
|
+
const hasLoopbackOnly = namedRoutes.some((route) => route.descriptor.loopbackOnly === true) ||
|
|
806
|
+
catchAllRoutes.some((route) => route.descriptor.loopbackOnly === true);
|
|
807
|
+
for (const { security } of tcpServers) {
|
|
808
|
+
if (hasLoopbackOnly && !security.loopback) {
|
|
809
|
+
warn(`loopbackOnly 扩展路由在非回环 TCP 监听器(${security.bindHost}:${security.listenPort})不挂载——404 同未注册(03 §10.6 挂载规则)`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
let socketPath;
|
|
813
|
+
if (sockServer !== undefined && options.config.socketPath !== undefined) {
|
|
814
|
+
// sock 监听(陈旧死迹清后重试;活迹 = 在场 daemon——真占用上抛宿主裁)
|
|
815
|
+
try {
|
|
816
|
+
await listenOnPath(sockServer, options.config.socketPath);
|
|
817
|
+
}
|
|
818
|
+
catch (err) {
|
|
819
|
+
const errno = err.code;
|
|
820
|
+
if (errno !== 'EADDRINUSE')
|
|
821
|
+
throw err;
|
|
822
|
+
if (await probeSocket(options.config.socketPath))
|
|
823
|
+
throw err; // 在场 daemon——宿主裁 HOST_DATA_DIR_BUSY
|
|
824
|
+
await unlink(options.config.socketPath).catch(() => { }); // 死迹清迹
|
|
825
|
+
await listenOnPath(sockServer, options.config.socketPath);
|
|
826
|
+
}
|
|
827
|
+
sockOwned = true;
|
|
828
|
+
socketPath = options.config.socketPath;
|
|
829
|
+
}
|
|
830
|
+
const tcpInfo = [];
|
|
831
|
+
for (const { server, security } of tcpServers) {
|
|
832
|
+
await listenOnTcp(server, security.bindHost, security.listenPort);
|
|
833
|
+
const addr = server.address();
|
|
834
|
+
const actualPort = typeof addr === 'object' && addr !== null ? addr.port : security.listenPort;
|
|
835
|
+
// 实绑口回填(port 0 内核指派后配置口 0 作废——Origin 同源判据用真口;
|
|
836
|
+
// 18a-2' webui 迁移揭出的 18a-1' 真缺陷,回归锁 routes.test「实绑口」例)
|
|
837
|
+
security.listenPort = actualPort;
|
|
838
|
+
tcpInfo.push({ host: security.bindHost, port: actualPort });
|
|
839
|
+
}
|
|
840
|
+
// 心跳装配驱动(线核零自驱时钟——13b 纪律;drain 无位:活体相位 sink 恒收)
|
|
841
|
+
heartbeatTimer = setInterval(() => core.heartbeatTick(), heartbeatIntervalMs);
|
|
842
|
+
return { ...(socketPath !== undefined ? { socketPath } : {}), tcp: tcpInfo };
|
|
843
|
+
},
|
|
844
|
+
stop: async () => {
|
|
845
|
+
if (heartbeatTimer !== undefined)
|
|
846
|
+
clearInterval(heartbeatTimer);
|
|
847
|
+
heartbeatTimer = undefined;
|
|
848
|
+
// 全流收口(客户端读至 EOF;面级流账统一 close 含扩展路由 openSse 流;
|
|
849
|
+
// /v1/events 流的 deregister 经 onEnd 逐流走清账)
|
|
850
|
+
for (const stream of openStreams)
|
|
851
|
+
stream.close();
|
|
852
|
+
openStreams.clear();
|
|
853
|
+
streamsBySession.clear();
|
|
854
|
+
handle.dispose(); // 在飞 ask 保守 cancel + core.close
|
|
855
|
+
const closeServer = (server) => new Promise((resolve) => {
|
|
856
|
+
server.close(() => resolve());
|
|
857
|
+
});
|
|
858
|
+
await Promise.all(servers.map((entry) => closeServer(entry.server)));
|
|
859
|
+
// 自己的 sock 足迹随关清(不碰他面活迹)
|
|
860
|
+
if (sockOwned && options.config.socketPath !== undefined) {
|
|
861
|
+
await unlink(options.config.socketPath).catch(() => { });
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
};
|
|
865
|
+
}
|