@dsh-edge/dsh-edge-agent 0.0.0-stage → 0.1.2
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/CHANGELOG.md +158 -0
- package/LICENSE +21 -0
- package/README.md +181 -2
- package/config/edge.example.json +179 -0
- package/config/privacy-policy.json +202 -0
- package/cordis.desktop-safe.patch.yml +129 -0
- package/cordis.patch.yml +382 -0
- package/cordis.yml +374 -0
- package/lib/attestation.d.ts +2 -0
- package/lib/attestation.d.ts.map +1 -0
- package/lib/attestation.js +2 -0
- package/lib/audit-log.d.ts +2 -0
- package/lib/audit-log.d.ts.map +1 -0
- package/lib/audit-log.js +2 -0
- package/lib/client.js +87 -0
- package/lib/cloud-llm-adapter.d.ts +2 -0
- package/lib/cloud-llm-adapter.d.ts.map +1 -0
- package/lib/cloud-llm-adapter.js +2 -0
- package/lib/dsh-adapter/attestation.d.ts +42 -0
- package/lib/dsh-adapter/attestation.d.ts.map +1 -0
- package/lib/dsh-adapter/attestation.js +40 -0
- package/lib/dsh-adapter/audit-log.d.ts +139 -0
- package/lib/dsh-adapter/audit-log.d.ts.map +1 -0
- package/lib/dsh-adapter/audit-log.js +376 -0
- package/lib/dsh-adapter/cloud-egress.d.ts +181 -0
- package/lib/dsh-adapter/cloud-egress.d.ts.map +1 -0
- package/lib/dsh-adapter/cloud-egress.js +257 -0
- package/lib/dsh-adapter/cloud-llm-adapter.d.ts +416 -0
- package/lib/dsh-adapter/cloud-llm-adapter.d.ts.map +1 -0
- package/lib/dsh-adapter/cloud-llm-adapter.js +1472 -0
- package/lib/dsh-adapter/contract.d.ts +80 -0
- package/lib/dsh-adapter/contract.d.ts.map +1 -0
- package/lib/dsh-adapter/contract.js +194 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.d.ts +214 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.d.ts.map +1 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.js +409 -0
- package/lib/dsh-adapter/dsh-edge-agent.d.ts +48 -0
- package/lib/dsh-adapter/dsh-edge-agent.d.ts.map +1 -0
- package/lib/dsh-adapter/dsh-edge-agent.js +64 -0
- package/lib/dsh-adapter/edge-config.d.ts +484 -0
- package/lib/dsh-adapter/edge-config.d.ts.map +1 -0
- package/lib/dsh-adapter/edge-config.js +898 -0
- package/lib/dsh-adapter/edge-context-view.d.ts +184 -0
- package/lib/dsh-adapter/edge-context-view.d.ts.map +1 -0
- package/lib/dsh-adapter/edge-context-view.js +483 -0
- package/lib/dsh-adapter/effective-model.d.ts +85 -0
- package/lib/dsh-adapter/effective-model.d.ts.map +1 -0
- package/lib/dsh-adapter/effective-model.js +171 -0
- package/lib/dsh-adapter/etscli-shell-guard.d.ts +244 -0
- package/lib/dsh-adapter/etscli-shell-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/etscli-shell-guard.js +420 -0
- package/lib/dsh-adapter/etscli-tools.d.ts +112 -0
- package/lib/dsh-adapter/etscli-tools.d.ts.map +1 -0
- package/lib/dsh-adapter/etscli-tools.js +393 -0
- package/lib/dsh-adapter/file-logger.d.ts +294 -0
- package/lib/dsh-adapter/file-logger.d.ts.map +1 -0
- package/lib/dsh-adapter/file-logger.js +564 -0
- package/lib/dsh-adapter/function-call-external.d.ts +357 -0
- package/lib/dsh-adapter/function-call-external.d.ts.map +1 -0
- package/lib/dsh-adapter/function-call-external.js +1005 -0
- package/lib/dsh-adapter/host-egress-guard.d.ts +161 -0
- package/lib/dsh-adapter/host-egress-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/host-egress-guard.js +213 -0
- package/lib/dsh-adapter/host-llm-trace.d.ts +198 -0
- package/lib/dsh-adapter/host-llm-trace.d.ts.map +1 -0
- package/lib/dsh-adapter/host-llm-trace.js +344 -0
- package/lib/dsh-adapter/http-api-routes.d.ts +61 -0
- package/lib/dsh-adapter/http-api-routes.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api-routes.js +79 -0
- package/lib/dsh-adapter/http-api-sessions.d.ts +105 -0
- package/lib/dsh-adapter/http-api-sessions.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api-sessions.js +173 -0
- package/lib/dsh-adapter/http-api.d.ts +221 -0
- package/lib/dsh-adapter/http-api.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api.js +1811 -0
- package/lib/dsh-adapter/index.d.ts +62 -0
- package/lib/dsh-adapter/index.d.ts.map +1 -0
- package/lib/dsh-adapter/index.js +97 -0
- package/lib/dsh-adapter/input-guard-messages.d.ts +56 -0
- package/lib/dsh-adapter/input-guard-messages.d.ts.map +1 -0
- package/lib/dsh-adapter/input-guard-messages.js +136 -0
- package/lib/dsh-adapter/input-guard.d.ts +508 -0
- package/lib/dsh-adapter/input-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/input-guard.js +2174 -0
- package/lib/dsh-adapter/intent-prompts.d.ts +77 -0
- package/lib/dsh-adapter/intent-prompts.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-prompts.js +97 -0
- package/lib/dsh-adapter/intent-router.d.ts +710 -0
- package/lib/dsh-adapter/intent-router.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-router.js +2802 -0
- package/lib/dsh-adapter/intent-signals.d.ts +208 -0
- package/lib/dsh-adapter/intent-signals.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-signals.js +391 -0
- package/lib/dsh-adapter/invariant-install.d.ts +16 -0
- package/lib/dsh-adapter/invariant-install.d.ts.map +1 -0
- package/lib/dsh-adapter/invariant-install.js +15 -0
- package/lib/dsh-adapter/invariant.d.ts +16 -0
- package/lib/dsh-adapter/invariant.d.ts.map +1 -0
- package/lib/dsh-adapter/invariant.js +22 -0
- package/lib/dsh-adapter/llm-adapters.d.ts +19 -0
- package/lib/dsh-adapter/llm-adapters.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-adapters.js +16 -0
- package/lib/dsh-adapter/llm-internal-caller.d.ts +92 -0
- package/lib/dsh-adapter/llm-internal-caller.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-internal-caller.js +100 -0
- package/lib/dsh-adapter/llm-max-turns.d.ts +55 -0
- package/lib/dsh-adapter/llm-max-turns.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-max-turns.js +127 -0
- package/lib/dsh-adapter/llm-trace-store.d.ts +131 -0
- package/lib/dsh-adapter/llm-trace-store.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-trace-store.js +205 -0
- package/lib/dsh-adapter/llm-trace.d.ts +450 -0
- package/lib/dsh-adapter/llm-trace.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-trace.js +463 -0
- package/lib/dsh-adapter/local-llm-adapter.d.ts +141 -0
- package/lib/dsh-adapter/local-llm-adapter.d.ts.map +1 -0
- package/lib/dsh-adapter/local-llm-adapter.js +787 -0
- package/lib/dsh-adapter/local-storage.d.ts +96 -0
- package/lib/dsh-adapter/local-storage.d.ts.map +1 -0
- package/lib/dsh-adapter/local-storage.js +198 -0
- package/lib/dsh-adapter/model-router.d.ts +79 -0
- package/lib/dsh-adapter/model-router.d.ts.map +1 -0
- package/lib/dsh-adapter/model-router.js +107 -0
- package/lib/dsh-adapter/ohttp-gateway.d.ts +49 -0
- package/lib/dsh-adapter/ohttp-gateway.d.ts.map +1 -0
- package/lib/dsh-adapter/ohttp-gateway.js +32 -0
- package/lib/dsh-adapter/openai-sse.d.ts +85 -0
- package/lib/dsh-adapter/openai-sse.d.ts.map +1 -0
- package/lib/dsh-adapter/openai-sse.js +462 -0
- package/lib/dsh-adapter/pii-utils.d.ts +96 -0
- package/lib/dsh-adapter/pii-utils.d.ts.map +1 -0
- package/lib/dsh-adapter/pii-utils.js +324 -0
- package/lib/dsh-adapter/plugin-host.d.ts +38 -0
- package/lib/dsh-adapter/plugin-host.d.ts.map +1 -0
- package/lib/dsh-adapter/plugin-host.js +38 -0
- package/lib/dsh-adapter/plugin.d.ts +46 -0
- package/lib/dsh-adapter/plugin.d.ts.map +1 -0
- package/lib/dsh-adapter/plugin.js +100 -0
- package/lib/dsh-adapter/privacy-policy.d.ts +237 -0
- package/lib/dsh-adapter/privacy-policy.d.ts.map +1 -0
- package/lib/dsh-adapter/privacy-policy.js +391 -0
- package/lib/dsh-adapter/prompt-guard.d.ts +152 -0
- package/lib/dsh-adapter/prompt-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/prompt-guard.js +872 -0
- package/lib/dsh-adapter/realm-skill-bridge.d.ts +365 -0
- package/lib/dsh-adapter/realm-skill-bridge.d.ts.map +1 -0
- package/lib/dsh-adapter/realm-skill-bridge.js +1180 -0
- package/lib/dsh-adapter/sanitizer-pipeline.d.ts +140 -0
- package/lib/dsh-adapter/sanitizer-pipeline.d.ts.map +1 -0
- package/lib/dsh-adapter/sanitizer-pipeline.js +431 -0
- package/lib/dsh-adapter/secure-bridge.d.ts +47 -0
- package/lib/dsh-adapter/secure-bridge.d.ts.map +1 -0
- package/lib/dsh-adapter/secure-bridge.js +35 -0
- package/lib/dsh-adapter/session-attribution.d.ts +70 -0
- package/lib/dsh-adapter/session-attribution.d.ts.map +1 -0
- package/lib/dsh-adapter/session-attribution.js +76 -0
- package/lib/dsh-adapter/sessions.d.ts +86 -0
- package/lib/dsh-adapter/sessions.d.ts.map +1 -0
- package/lib/dsh-adapter/sessions.js +179 -0
- package/lib/dsh-adapter/tcm.d.ts +55 -0
- package/lib/dsh-adapter/tcm.d.ts.map +1 -0
- package/lib/dsh-adapter/tcm.js +94 -0
- package/lib/dsh-adapter/td-bus.d.ts +87 -0
- package/lib/dsh-adapter/td-bus.d.ts.map +1 -0
- package/lib/dsh-adapter/td-bus.js +196 -0
- package/lib/dsh-adapter/tool-runner.d.ts +185 -0
- package/lib/dsh-adapter/tool-runner.d.ts.map +1 -0
- package/lib/dsh-adapter/tool-runner.js +271 -0
- package/lib/dsh-adapter/tools.d.ts +118 -0
- package/lib/dsh-adapter/tools.d.ts.map +1 -0
- package/lib/dsh-adapter/tools.js +292 -0
- package/lib/dsh-adapter/types.d.ts +23 -0
- package/lib/dsh-adapter/types.d.ts.map +1 -0
- package/lib/dsh-adapter/types.js +16 -0
- package/lib/dsh-adapter/voice-input.d.ts +30 -0
- package/lib/dsh-adapter/voice-input.d.ts.map +1 -0
- package/lib/dsh-adapter/voice-input.js +34 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.d.ts +152 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.d.ts.map +1 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.js +289 -0
- package/lib/http-api.d.ts +2 -0
- package/lib/http-api.d.ts.map +1 -0
- package/lib/http-api.js +2 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +49 -0
- package/lib/intent-router.d.ts +2 -0
- package/lib/intent-router.d.ts.map +1 -0
- package/lib/intent-router.js +2 -0
- package/lib/llm-adapters.d.ts +2 -0
- package/lib/llm-adapters.d.ts.map +1 -0
- package/lib/llm-adapters.js +2 -0
- package/lib/local-llm-adapter.d.ts +2 -0
- package/lib/local-llm-adapter.d.ts.map +1 -0
- package/lib/local-llm-adapter.js +2 -0
- package/lib/local-storage.d.ts +2 -0
- package/lib/local-storage.d.ts.map +1 -0
- package/lib/local-storage.js +2 -0
- package/lib/model-router.d.ts +2 -0
- package/lib/model-router.d.ts.map +1 -0
- package/lib/model-router.js +2 -0
- package/lib/ohttp-gateway.d.ts +2 -0
- package/lib/ohttp-gateway.d.ts.map +1 -0
- package/lib/ohttp-gateway.js +2 -0
- package/lib/plugin-host.d.ts +2 -0
- package/lib/plugin-host.d.ts.map +1 -0
- package/lib/plugin-host.js +2 -0
- package/lib/privacy-policy.d.ts +2 -0
- package/lib/privacy-policy.d.ts.map +1 -0
- package/lib/privacy-policy.js +2 -0
- package/lib/prompt-guard.d.ts +2 -0
- package/lib/prompt-guard.d.ts.map +1 -0
- package/lib/prompt-guard.js +2 -0
- package/lib/sanitizer-pipeline.d.ts +2 -0
- package/lib/sanitizer-pipeline.d.ts.map +1 -0
- package/lib/sanitizer-pipeline.js +2 -0
- package/lib/secure-bridge.d.ts +2 -0
- package/lib/secure-bridge.d.ts.map +1 -0
- package/lib/secure-bridge.js +2 -0
- package/lib/sessions.d.ts +2 -0
- package/lib/sessions.d.ts.map +1 -0
- package/lib/sessions.js +2 -0
- package/lib/tcm.d.ts +2 -0
- package/lib/tcm.d.ts.map +1 -0
- package/lib/tcm.js +2 -0
- package/lib/td-bus.d.ts +2 -0
- package/lib/td-bus.d.ts.map +1 -0
- package/lib/td-bus.js +2 -0
- package/lib/tools.d.ts +2 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +2 -0
- package/lib/voice-input.d.ts +2 -0
- package/lib/voice-input.d.ts.map +1 -0
- package/lib/voice-input.js +2 -0
- package/package.json +253 -3
- package/presets/agent-preset-edge.json +50 -0
- package/presets/edge.desktop-safe.patch.yml +503 -0
- package/presets/edge.patch.yml +557 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* intent-router — LLM-based intent classification.
|
|
3
|
+
*
|
|
4
|
+
* Uses the local Qwen3-4B (via ctx.llm) to classify user messages as
|
|
5
|
+
* LOCAL (file ops, device control, calculations) or CLOUD (knowledge,
|
|
6
|
+
* chat, creative tasks). Falls back to keyword rules when the LLM
|
|
7
|
+
* call fails (timeout, network error, unclear response).
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-edge-agent/intent-router
|
|
10
|
+
*/
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
12
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
13
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
14
|
+
export declare const name = "intent-router";
|
|
15
|
+
export declare const inject: readonly ["llm", "auditLog", "privacyPolicy"];
|
|
16
|
+
/** Minimal interface our router needs from the audit-log service. */
|
|
17
|
+
interface AuditLike {
|
|
18
|
+
log(entry: {
|
|
19
|
+
level: 'debug' | 'info' | 'warn' | 'error' | 'security';
|
|
20
|
+
kind: string;
|
|
21
|
+
source: string;
|
|
22
|
+
payload: Record<string, unknown>;
|
|
23
|
+
}): Promise<unknown>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* §7.122: 端点是否落在本机。
|
|
27
|
+
*
|
|
28
|
+
* provider 名(`local-llm`)**不代表**落点 —— config/edge.json 的
|
|
29
|
+
* `llm.local.baseUrl` 可以是任何地址,包括公网。operator 要判断
|
|
30
|
+
* 「这轮原文有没有出网」,看的是落点,不是名字。
|
|
31
|
+
*
|
|
32
|
+
* §7.130: 主人裁决——「是否出网」的判据是 **category(哪个池)**,不是端点
|
|
33
|
+
* 落在哪台机。本函数仍然有用(§7.122 的分类器落点披露是备查信息),但它
|
|
34
|
+
* **不参与出网判断**:local 池是 operator 自有设施,端点不在本机也不算出网。
|
|
35
|
+
*/
|
|
36
|
+
export declare function isLoopbackEndpoint(endpoint: string | undefined): boolean;
|
|
37
|
+
export interface Config {
|
|
38
|
+
/** Default realm when classification finds no rule. */
|
|
39
|
+
defaultCategory: 'LOCAL' | 'CLOUD' | 'SANITIZED';
|
|
40
|
+
/** Provider to use for intent classification. */
|
|
41
|
+
classificationProvider: string;
|
|
42
|
+
/** Model id passed alongside `classificationProvider`. */
|
|
43
|
+
classificationModel: string;
|
|
44
|
+
/** Minimum confidence the LLM must express to be trusted.
|
|
45
|
+
* Below this value, the router falls back to keyword rules
|
|
46
|
+
* (fail-open, not REJECT — see Phase B plan). */
|
|
47
|
+
confidenceThreshold: number;
|
|
48
|
+
/** §7.115 (2026-10-25): decision-model-only mode — false = the LLM
|
|
49
|
+
* classifier never runs; a jev skip goes straight to the rules tier +
|
|
50
|
+
* default LOCAL (fail-closed). Default true. */
|
|
51
|
+
llmFallback?: boolean;
|
|
52
|
+
/** Hard cap on a single classify() LLM call. The AbortController fires
|
|
53
|
+
* this many ms after the stream starts; on timeout the call throws
|
|
54
|
+
* and the caller falls through to the keyword/default path (fail-open).
|
|
55
|
+
* Default 4000ms; env DSH_EDGE_INTENT_LLM_TIMEOUT_MS overrides. */
|
|
56
|
+
llmTimeoutMs?: number;
|
|
57
|
+
/** Max number of recent messages to embed as multi-turn context.
|
|
58
|
+
* 0 disables context. Default 3. */
|
|
59
|
+
recentMessagesLimit?: number;
|
|
60
|
+
/** Hard cap on input text length. Past this, the router truncates the
|
|
61
|
+
* prompt input and audits `intent.classify.truncated`. Default 2000;
|
|
62
|
+
* env DSH_EDGE_INTENT_MAX_INPUT overrides. Defense-in-depth against
|
|
63
|
+
* pathologically long prompts overwhelming qwen3-4b. */
|
|
64
|
+
maxInputLength?: number;
|
|
65
|
+
/** Last-resort LOCAL-keyword list used only when privacy-policy is absent
|
|
66
|
+
* or has no matching rule. Default ships EN + CN. Override via env
|
|
67
|
+
* DSH_EDGE_INTENT_FALLBACK_KEYWORDS (comma-separated) to add JA / KO /
|
|
68
|
+
* ES / domain-specific terms without editing source. Replaces the
|
|
69
|
+
* built-in list entirely when set. */
|
|
70
|
+
fallbackKeywords?: string[];
|
|
71
|
+
/** §7.47 (2026-09-12): CLOUD-keyword list matched BEFORE fallbackKeywords.
|
|
72
|
+
* Catches queries that obviously need web/search/online info (新闻/news/
|
|
73
|
+
* 搜索/search/latest/最新/today/今天) and routes them to cloud LLM +
|
|
74
|
+
* web tools — without waiting for the LLM classifier (which can hang
|
|
75
|
+
* on Qwen3-4B). Default ships EN + CN built-in list. Override via env
|
|
76
|
+
* DSH_EDGE_INTENT_CLOUD_KEYWORDS (comma-sep) — appended to built-in.
|
|
77
|
+
* §7.108: the policy file's stages.intent.cloudKeywords ALSO appends
|
|
78
|
+
* (hot-reloadable) — preferred operator surface. */
|
|
79
|
+
cloudKeywords?: string[];
|
|
80
|
+
/** §7.58 (2026-09-16): LOCAL-entity list (密盒/ets/etscli built-in)
|
|
81
|
+
* matched before every keyword layer and used to override a non-LOCAL
|
|
82
|
+
* LLM verdict. Env DSH_EDGE_INTENT_LOCAL_ENTITIES (comma-sep) appends to
|
|
83
|
+
* built-in (NOT replaces), same semantics as cloudKeywords. §7.108:
|
|
84
|
+
* policy-file stages.intent.localEntities also appends (hot-reloadable). */
|
|
85
|
+
localEntities?: string[];
|
|
86
|
+
/** §7.124 (2026-10-27): 家庭设备操作正则层的扩展词表(追加到内置词表)。 */
|
|
87
|
+
deviceCommands?: {
|
|
88
|
+
devices?: string[];
|
|
89
|
+
actions?: string[];
|
|
90
|
+
};
|
|
91
|
+
/** §7.92.3 (2026-09-23) + §7.92.5 (2026-09-23): jev HTTP intent
|
|
92
|
+
* recognizer (port 8001). Toggle — default OFF (opt-in). When ON,
|
|
93
|
+
* jev runs BEFORE the LLM classifier and short-circuits high-
|
|
94
|
+
* confidence / low-risk verdicts. See
|
|
95
|
+
* ~/work/jev-branch/docs/api_reference.md for the upstream contract.
|
|
96
|
+
* §7.92.5 splits the full categories[] taxonomy into ≤16-intent
|
|
97
|
+
* batches (jev v0.3.0 hard cap) and fires them concurrently via
|
|
98
|
+
* Promise.all. Env DSH_EDGE_INTENT_JEV_ENABLED (literal 'true' activates). */
|
|
99
|
+
jevEnabled?: boolean;
|
|
100
|
+
/** §7.92.3: jev base URL. Env DSH_EDGE_INTENT_JEV_URL. */
|
|
101
|
+
jevUrl?: string;
|
|
102
|
+
/** §7.112 (2026-10-25): jev backend kind. 'local' = the ONNX /predict
|
|
103
|
+
* contract (default); 'bailian-decision' = Alibaba's decision-model
|
|
104
|
+
* /systemone structured-decision interface — one call carries the whole
|
|
105
|
+
* taxonomy as choice/score questions (no 16-intent cap, no chitchat
|
|
106
|
+
* markers). Env DSH_EDGE_INTENT_JEV_PROVIDER. */
|
|
107
|
+
jevProvider?: 'local' | 'bailian-decision';
|
|
108
|
+
/** §7.112: decision-model id (bailian-decision only, e.g.
|
|
109
|
+
* 'decision-model-preview'). Env DSH_EDGE_INTENT_JEV_MODEL. */
|
|
110
|
+
jevModel?: string;
|
|
111
|
+
/** §7.112: literal Authorization key (edge.json jev.apiKeyRef →
|
|
112
|
+
* secrets.*). Bailian MaaS requires it; local ONNX never did. */
|
|
113
|
+
jevApiKey?: string;
|
|
114
|
+
/** §7.112: 'raw' = `Authorization: <key>` (org systemone examples);
|
|
115
|
+
* 'bearer' = `Authorization: Bearer <key>` (compatible-mode
|
|
116
|
+
* convention). Default 'raw'. */
|
|
117
|
+
jevAuthScheme?: 'raw' | 'bearer';
|
|
118
|
+
/** §7.114 (2026-10-25): classify path appended to jevUrl. '/systemone'
|
|
119
|
+
* (default) = the MaaS deployments; a local MLX serve of the same
|
|
120
|
+
* contract exposes '/v1/classify' — point jevUrl at its /v1 root and
|
|
121
|
+
* set '/classify'. The decision model is a config-file-bound EXTERNAL
|
|
122
|
+
* service: url/path/model/key all live in edge.json, zero hardcoding. */
|
|
123
|
+
jevPath?: string;
|
|
124
|
+
/** §7.115: directional confidence gate — a LOCAL-leaning decision-model
|
|
125
|
+
* verdict adopts at this floor (default 0.5) instead of paying the LLM
|
|
126
|
+
* round-trip for a second opinion; non-LOCAL (data may leave the device)
|
|
127
|
+
* keeps the strict jevConfidenceThreshold. */
|
|
128
|
+
jevLocalAdoptFloor?: number;
|
|
129
|
+
/** §7.92.3: per-batch jev HTTP timeout in ms (applies to EACH batch
|
|
130
|
+
* fired by the §7.92.5 orchestrator). Env DSH_EDGE_INTENT_JEV_TIMEOUT_MS. */
|
|
131
|
+
jevTimeoutMs?: number;
|
|
132
|
+
/** §7.92.3: minimum jev confidence to adopt. Below → fall through to
|
|
133
|
+
* LLM. Env DSH_EDGE_INTENT_JEV_CONFIDENCE_THRESHOLD. */
|
|
134
|
+
jevConfidenceThreshold?: number;
|
|
135
|
+
/** §7.92.3: jev risk_score gate (≥ this value → REJECT). Env
|
|
136
|
+
* DSH_EDGE_INTENT_JEV_RISK_THRESHOLD. */
|
|
137
|
+
jevRiskThreshold?: number;
|
|
138
|
+
/** §7.92.6 (2026-09-24): circuit-breaker TTL (ms). When all 3
|
|
139
|
+
* jev batches fail (transport / parse / timeout) within one
|
|
140
|
+
* classify() call, the breaker opens and subsequent classify()
|
|
141
|
+
* calls skip jev entirely until TTL elapses. Any successful batch
|
|
142
|
+
* (HTTP 2xx + parse OK, even if action='fallback_llm') closes the
|
|
143
|
+
* breaker immediately. Env DSH_EDGE_INTENT_JEV_BREAKER_TTL_MS. */
|
|
144
|
+
jevBreakerTtlMs?: number;
|
|
145
|
+
/** §7.92.7 (2026-09-24): boot-time `/health` probe (toggle). When
|
|
146
|
+
* true, the service fires a single GET `${jevUrl}/health` (timeout
|
|
147
|
+
* = `jevBootProbeTimeoutMs`, default 2000ms) at `apply()` time. On
|
|
148
|
+
* non-2xx / network error / timeout the breaker is **pre-opened**
|
|
149
|
+
* with full TTL — first classify() short-circuits without paying
|
|
150
|
+
* the ~3 × jevTimeoutMs wall-clock. Default ON; operators set
|
|
151
|
+
* false to skip the probe (e.g. when jev boots lazily alongside
|
|
152
|
+
* dsh-edge and is reliably up by the time chat starts). Env
|
|
153
|
+
* DSH_EDGE_INTENT_JEV_BOOT_PROBE_ENABLED. */
|
|
154
|
+
jevBootProbeEnabled?: boolean;
|
|
155
|
+
/** §7.92.7: boot probe timeout (ms). Smaller than `jevTimeoutMs`
|
|
156
|
+
* (which is per-batch for classify-time use). Env
|
|
157
|
+
* DSH_EDGE_INTENT_JEV_BOOT_PROBE_TIMEOUT_MS. */
|
|
158
|
+
jevBootProbeTimeoutMs?: number;
|
|
159
|
+
/** §7.92.7: interval (ms) between breaker-open `/health` probes.
|
|
160
|
+
* While the breaker is open, a setInterval pings `/health` every
|
|
161
|
+
* N ms (timeout = `jevProbeTimeoutMs`). On 2xx the breaker closes
|
|
162
|
+
* immediately (recovery can be faster than waiting for full TTL).
|
|
163
|
+
* Default 5000ms — frequent enough that recovery feels snappy,
|
|
164
|
+
* infrequent enough to avoid hammering a sick jev. Env
|
|
165
|
+
* DSH_EDGE_INTENT_JEV_BREAKER_PROBE_MS. */
|
|
166
|
+
jevBreakerProbeMs?: number;
|
|
167
|
+
/** §7.92.7: periodic probe timeout (ms). Smaller than `jevTimeoutMs`
|
|
168
|
+
* — we want fast fail on a sick jev rather than waiting the full
|
|
169
|
+
* classify-time budget. Env DSH_EDGE_INTENT_JEV_PROBE_TIMEOUT_MS. */
|
|
170
|
+
jevProbeTimeoutMs?: number;
|
|
171
|
+
/** §7.92.3 (2026-09-23): intent category specs (LOCAL/CLOUD/SANITIZED/
|
|
172
|
+
* REJECT descriptions + jev intent lists). Loaded from
|
|
173
|
+
* `ctx.privacyPolicy.getCategories()` at construction. When set, used
|
|
174
|
+
* as-is (test override path); when absent AND privacyPolicy is missing,
|
|
175
|
+
* falls back to a hardcoded built-in. Same shape as
|
|
176
|
+
* privacy-policy.json#categories: array of {id, description,
|
|
177
|
+
* examples?, jevIntents[]}. Operators extend by editing
|
|
178
|
+
* config/privacy-policy.json (hot-reload picks it up via
|
|
179
|
+
* `policy/loaded` event); no env var override. */
|
|
180
|
+
categories?: import('./privacy-policy.js').IntentCategory[];
|
|
181
|
+
}
|
|
182
|
+
export declare const Config: Schema<Config>;
|
|
183
|
+
export interface Intent {
|
|
184
|
+
category: 'LOCAL' | 'CLOUD' | 'SANITIZED' | 'REJECT';
|
|
185
|
+
confidence: number;
|
|
186
|
+
targetRealm: string;
|
|
187
|
+
rationale: string;
|
|
188
|
+
/**
|
|
189
|
+
* §7.123 (2026-10-27): provenance of `confidence`.
|
|
190
|
+
*
|
|
191
|
+
* `false` means "this number is a PLACEHOLDER, not a reported confidence"
|
|
192
|
+
* — today only the bailian decision-model path produces it: the backend
|
|
193
|
+
* returns a `choice` without a numeric confidence, so the decoder
|
|
194
|
+
* substitutes a synthetic 0.9 ("decisive-by-construction"). A synthetic
|
|
195
|
+
* value must not be able to satisfy an operator's *semantic trust*
|
|
196
|
+
* threshold (stages.intent.semanticCloudMinConfidence), because the
|
|
197
|
+
* operator is trusting a measurement that does not exist.
|
|
198
|
+
*
|
|
199
|
+
* `undefined` = reported (the normal case: LLM classifier, local ONNX
|
|
200
|
+
* jev, rules tier). Only `false` disables the semantic unlock.
|
|
201
|
+
*/
|
|
202
|
+
confidenceReported?: boolean;
|
|
203
|
+
}
|
|
204
|
+
export interface IntentRouterService {
|
|
205
|
+
classify(text: string, context?: {
|
|
206
|
+
recentMessages?: unknown[];
|
|
207
|
+
sessionId?: string;
|
|
208
|
+
}): Promise<Intent>;
|
|
209
|
+
/**
|
|
210
|
+
* §7.107: does this text ask for PUBLIC-WEB information?
|
|
211
|
+
*
|
|
212
|
+
* The operator's ruling: the cloud LLM is only for "需要搜索公网上的信息".
|
|
213
|
+
* This is the signal that unlocks the cloud — a CLOUD verdict from any
|
|
214
|
+
* other tier (a confident local classifier, a caller route) is honoured
|
|
215
|
+
* only when this returns a keyword.
|
|
216
|
+
*
|
|
217
|
+
* The keyword list is the same one the rules tier uses (built-in
|
|
218
|
+
* information-acquisition words + `stages.intent.cloudKeywords`), so the
|
|
219
|
+
* gate and the rules tier can never disagree about what counts as a
|
|
220
|
+
* web need.
|
|
221
|
+
*/
|
|
222
|
+
cloudSignal(text: string): string | undefined;
|
|
223
|
+
resolve(intent: Intent): {
|
|
224
|
+
realm: string;
|
|
225
|
+
config: Record<string, unknown>;
|
|
226
|
+
};
|
|
227
|
+
/** Snapshot of in-memory counters. O(1) read for ops dashboards. */
|
|
228
|
+
getStats(): IntentRouterStats;
|
|
229
|
+
}
|
|
230
|
+
/** Built-in last-resort LOCAL keyword list (EN + CN). Used only when
|
|
231
|
+
* privacy-policy is absent / empty AND `config.fallbackKeywords` is unset.
|
|
232
|
+
* Users override via env `DSH_EDGE_INTENT_FALLBACK_KEYWORDS` (comma-sep).
|
|
233
|
+
*
|
|
234
|
+
* §7.123 (2026-10-27): moved to `./intent-signals.ts` — the strategy DATA
|
|
235
|
+
* (keyword tables, entity tables, the fallback taxonomy) now lives apart
|
|
236
|
+
* from the decision LOGIC in this file. */
|
|
237
|
+
import { LOCAL_KEYWORDS_DEFAULT, CLOUD_KEYWORDS_DEFAULT, LOCAL_ENTITY_KEYWORDS_DEFAULT, JEV_MAX_INTENTS, CATEGORIES_FALLBACK, RULE_CATEGORIES } from './intent-signals.js';
|
|
238
|
+
export { LOCAL_KEYWORDS_DEFAULT, CLOUD_KEYWORDS_DEFAULT, LOCAL_ENTITY_KEYWORDS_DEFAULT, JEV_MAX_INTENTS, CATEGORIES_FALLBACK, RULE_CATEGORIES, };
|
|
239
|
+
export declare class IntentRouterServiceImpl extends Service {
|
|
240
|
+
private readonly defaultCategory;
|
|
241
|
+
private readonly classificationProvider;
|
|
242
|
+
private readonly classificationModel;
|
|
243
|
+
private readonly confidenceThreshold;
|
|
244
|
+
private readonly llmFallback;
|
|
245
|
+
private readonly llmTimeoutMs;
|
|
246
|
+
private readonly recentMessagesLimit;
|
|
247
|
+
private readonly maxInputLength;
|
|
248
|
+
private readonly fallbackKeywords;
|
|
249
|
+
/** §7.47 (2026-09-12): CLOUD keyword list, merged with built-in defaults.
|
|
250
|
+
* Matched in classifyWithRules BEFORE fallbackKeywords. */
|
|
251
|
+
private readonly cloudKeywords;
|
|
252
|
+
/** §7.58 (2026-09-16): LOCAL-entity list, merged with built-in defaults.
|
|
253
|
+
* Checked FIRST in classifyWithRules + as LLM-verdict override guardrail. */
|
|
254
|
+
private readonly localEntities;
|
|
255
|
+
/** §7.124: operator-extended device-command terms (ADDITIVE on top of the
|
|
256
|
+
* built-in vocabulary in ./intent-signals.ts). */
|
|
257
|
+
private readonly deviceCommandOverrides;
|
|
258
|
+
/** §7.92.3 (2026-09-23): jev HTTP intent recognizer (toggle). When
|
|
259
|
+
* true, classify() invokes classifyWithJev() BEFORE classifyWithLlm(). */
|
|
260
|
+
private readonly jevEnabled;
|
|
261
|
+
/** §7.92.3: jev base URL, e.g. `http://127.0.0.1:8001`. */
|
|
262
|
+
private readonly jevUrl;
|
|
263
|
+
/** §7.112: Bailian decision-model backend surface (see Config docs). */
|
|
264
|
+
private readonly jevProvider;
|
|
265
|
+
private readonly jevModel;
|
|
266
|
+
private readonly jevApiKey;
|
|
267
|
+
private readonly jevAuthScheme;
|
|
268
|
+
private readonly jevPath;
|
|
269
|
+
private readonly jevLocalAdoptFloor;
|
|
270
|
+
/** §7.92.3: jev fetch timeout (ms). */
|
|
271
|
+
private readonly jevTimeoutMs;
|
|
272
|
+
/** §7.92.3: min jev confidence to adopt (else fall through to LLM). */
|
|
273
|
+
private readonly jevConfidenceThreshold;
|
|
274
|
+
/** §7.92.3: jev risk_score gate (>= this → REJECT). */
|
|
275
|
+
private readonly jevRiskThreshold;
|
|
276
|
+
/** §7.92.6 (2026-09-24): circuit-breaker TTL (ms). When all 3
|
|
277
|
+
* batches fail, set `jevBreakerOpenUntil = Date.now() + ttl` and
|
|
278
|
+
* classify() short-circuits jev (zero fetch) until that timestamp.
|
|
279
|
+
* Any successful batch closes the breaker (sets to 0). */
|
|
280
|
+
private readonly jevBreakerTtlMs;
|
|
281
|
+
/** §7.92.6: ms-epoch timestamp until which the breaker is open.
|
|
282
|
+
* 0 = closed. Mutable; reset on every Promise.all settle. */
|
|
283
|
+
private jevBreakerOpenUntil;
|
|
284
|
+
/** §7.92.7: boot probe toggle. When true, `start()` fires one
|
|
285
|
+
* `GET ${jevUrl}/health` and pre-opens the breaker on failure. */
|
|
286
|
+
private readonly jevBootProbeEnabled;
|
|
287
|
+
/** §7.92.7: boot probe timeout (ms). */
|
|
288
|
+
private readonly jevBootProbeTimeoutMs;
|
|
289
|
+
/** §7.92.7: interval between breaker-open `/health` probes (ms).
|
|
290
|
+
* Only active while the breaker is open. */
|
|
291
|
+
private readonly jevBreakerProbeMs;
|
|
292
|
+
/** §7.92.7: periodic probe timeout (ms). */
|
|
293
|
+
private readonly jevProbeTimeoutMs;
|
|
294
|
+
/** §7.92.7: setInterval handle for the periodic recovery probe.
|
|
295
|
+
* null = no timer running (breaker closed or probe disabled). */
|
|
296
|
+
private jevBreakerProbeTimer;
|
|
297
|
+
/** §7.92.7: in-flight boot probe promise. Tracked so a test or
|
|
298
|
+
* shutdown hook can `await stop()` without leaving a dangling
|
|
299
|
+
* fetch racing the constructor's first classify() call. */
|
|
300
|
+
private jevBootProbePromise;
|
|
301
|
+
/** §7.92.3 (2026-09-23) + §7.108 (2026-10-24): categories spec — loaded
|
|
302
|
+
* from privacyPolicy.getCategories() at construction AND refreshed on
|
|
303
|
+
* every `policy/loaded` event when the spec came from the policy
|
|
304
|
+
* service (config.categories keeps its explicit-override semantics and
|
|
305
|
+
* is never refreshed). Drives both the LLM prompt (description field
|
|
306
|
+
* per category) AND the jev-intent → category lookup (jevIntents[] per
|
|
307
|
+
* category, flattened into jevIntentMap). Tests inject via
|
|
308
|
+
* `config.categories` to bypass privacy-policy. */
|
|
309
|
+
private categories;
|
|
310
|
+
/** §7.92.3: flat jev-intent-string → category lookup, derived from
|
|
311
|
+
* `this.categories[].jevIntents`. Rebuilt together with categories on
|
|
312
|
+
* policy hot-reload (§7.108). */
|
|
313
|
+
private jevIntentMap;
|
|
314
|
+
private readonly llm;
|
|
315
|
+
private readonly auditLog;
|
|
316
|
+
/** ── classify() 结果缓存(LRU + TTL)───────────────────────────────
|
|
317
|
+
* 只缓存"贵的"路径的最终 verdict(LLM 采纳 / jev 采纳,均过
|
|
318
|
+
* gateCloudVerdict);规则兜底、policy REJECT、jev-reject 都很便宜
|
|
319
|
+
* 且依赖可热重载的状态,不缓存。没有 in-flight 合并 —— 并发同文本
|
|
320
|
+
* 各自走 LLM(保持并发计数语义),只有"完成后"的重复才命中。
|
|
321
|
+
* verdict 对同一文本是确定性的(gate 也是纯文本判定),TTL 只为
|
|
322
|
+
* 让关键词表/策略热重载后的新判定尽快生效。 */
|
|
323
|
+
private static readonly CLASSIFY_CACHE_TTL_MS;
|
|
324
|
+
private static readonly CLASSIFY_CACHE_MAX;
|
|
325
|
+
private readonly classifyCache;
|
|
326
|
+
private classifyCacheKey;
|
|
327
|
+
private classifyCacheGet;
|
|
328
|
+
private classifyCacheSet;
|
|
329
|
+
private readonly stats;
|
|
330
|
+
constructor(ctx: Context, config: Config, auditLog?: AuditLike);
|
|
331
|
+
/**
|
|
332
|
+
* Soft service lookup that works in both Cordis (runtime) and unit tests (bare mock).
|
|
333
|
+
*
|
|
334
|
+
* Cordis context: `ctx.get(name)` is available — use it. Throws when the
|
|
335
|
+
* service is not in the `inject` list, so `get` acts as the gate.
|
|
336
|
+
* Bare mock context (tests): `ctx.get` is undefined — fall back to direct
|
|
337
|
+
* property access `ctx[name]`, which is how tests wire fake services.
|
|
338
|
+
*
|
|
339
|
+
* Either way, a missing service returns `undefined` rather than throwing.
|
|
340
|
+
*/
|
|
341
|
+
private softService;
|
|
342
|
+
private audit;
|
|
343
|
+
/**
|
|
344
|
+
* §7.133: jev 决策模型调用的 trace。
|
|
345
|
+
*
|
|
346
|
+
* jev 走的是**裸 fetch**,不经过 `ctx.llm` —— 所以 adapter 那条路看不到
|
|
347
|
+
* 它。而它恰恰是把用户原文送出设备的那一call(当前配置指向百炼云端
|
|
348
|
+
* decision-model,§7.112/§7.114)。面板若只显示 local-llm 的分类器调用,
|
|
349
|
+
* 就是在显示一件没发生过的事(§7.107 的诚实性纪律)。
|
|
350
|
+
*
|
|
351
|
+
* 所以这里补一路 trace:caller 仍是 `intent-classifier`(它就是意图
|
|
352
|
+
* 分类器),provider 用 `bailian-decision` / `jev` 区分是哪条后端。
|
|
353
|
+
*/
|
|
354
|
+
private traceJevRequest;
|
|
355
|
+
private traceJevResponse;
|
|
356
|
+
private emitJevTrace;
|
|
357
|
+
/**
|
|
358
|
+
* §7.122: 分类器 provider 的**真实落点**。
|
|
359
|
+
*
|
|
360
|
+
* provider 名(`local-llm`)不等于落点 —— config/edge.json 的
|
|
361
|
+
* `llm.local.baseUrl` 可以指向公网。审计要能回答「这轮原文发到哪去了」,
|
|
362
|
+
* 光有 provider 名不够。
|
|
363
|
+
*
|
|
364
|
+
* 解析规则:provider 以 `cloud-` 开头 → cloud 池;否则 local 池。
|
|
365
|
+
* (与 model-router 的 realm 判定同一口径:`local-*` 前缀 = LOCAL 域,
|
|
366
|
+
* 其余按 CLOUD 处理。)
|
|
367
|
+
*/
|
|
368
|
+
private classifierEndpoint;
|
|
369
|
+
/** §7.113 (2026-10-25): the two evidence scalars the Edge tab renders
|
|
370
|
+
* beside the verdict — `keyword` = the regex keyword/entity the rules
|
|
371
|
+
* layer matched (derived from the rationale; empty for semantic
|
|
372
|
+
* verdicts), `semantic` = the chosen category's description sentence,
|
|
373
|
+
* i.e. the "why" the LLM/jev classifier answered with. Both are plain
|
|
374
|
+
* scalars so safePayload passes them (PII-scanned) to the intent panel. */
|
|
375
|
+
private classifyEvidence;
|
|
376
|
+
/** Redact PII from a short preview string before logging. Soft-dep on
|
|
377
|
+
* ctx.piiUtils.redactString. Returns `null` (rather than leaking) if
|
|
378
|
+
* redaction is unavailable — callers should log length-only in that case.
|
|
379
|
+
* Defense-in-depth: audit-log already runs redactObject on every payload,
|
|
380
|
+
* but router-level redaction survives audit-log misconfiguration. */
|
|
381
|
+
private redactPreview;
|
|
382
|
+
/**
|
|
383
|
+
* §7.107 (2026-10-23, operator ruling「只有需要搜索公网上的信息才需要用到
|
|
384
|
+
* cloudllm」): a CLOUD verdict needs a public-web need.
|
|
385
|
+
*
|
|
386
|
+
* The signal is this router's own cloud keyword list (built-in
|
|
387
|
+
* information-acquisition words + `stages.intent.cloudKeywords` + the
|
|
388
|
+
* policy file's `stages.intent.cloudKeywords`, §7.108) — the same list the
|
|
389
|
+
* rules tier matches, so the gate and the rules tier can never disagree
|
|
390
|
+
* about what counts as a web need.
|
|
391
|
+
*
|
|
392
|
+
* The gate is applied to the LLM / jev verdicts ONLY:
|
|
393
|
+
* - the rules tier reaches CLOUD either through a privacy-policy rule
|
|
394
|
+
* (the operator's own statement — honoured as-is) or through a cloud
|
|
395
|
+
* keyword (which IS the signal);
|
|
396
|
+
* - the LLM/jev verdicts are a MODEL's opinion, and a model's opinion
|
|
397
|
+
* is not what decides whether text may leave the device.
|
|
398
|
+
*
|
|
399
|
+
* It lives HERE — at the source of the verdict — and not in the
|
|
400
|
+
* consumer: the verdict is read by two consumers, input-guard
|
|
401
|
+
* (agent/pre-step) and http-api (which writes `agent._edgeRoute` from it
|
|
402
|
+
* and hands that over as an EXPLICIT route that input-guard honours
|
|
403
|
+
* as-is). A gate in the consumer closed only the input-guard path.
|
|
404
|
+
*/
|
|
405
|
+
private gateCloudVerdict;
|
|
406
|
+
classify(text: string, context?: {
|
|
407
|
+
recentMessages?: unknown[];
|
|
408
|
+
sessionId?: string;
|
|
409
|
+
}): Promise<Intent>;
|
|
410
|
+
/** §7.92.5 (2026-09-23): build the full jev `intents[]` payload from
|
|
411
|
+
* `this.categories[]`. Two-phase priority, NO 16-cap (the cap is
|
|
412
|
+
* enforced one level up by `chunkIntents()`):
|
|
413
|
+
*
|
|
414
|
+
* Phase 1: emit every REJECT category's jevIntents first
|
|
415
|
+
* (security priority — must not be dropped)
|
|
416
|
+
* Phase 2: emit all other categories in `categories[]` config order
|
|
417
|
+
* (LOCAL → CLOUD → SANITIZED by default)
|
|
418
|
+
*
|
|
419
|
+
* Dedup by name via a Set<string> so operators who accidentally list
|
|
420
|
+
* the same intent in two categories don't produce a duplicate name
|
|
421
|
+
* (which would confuse jev's own zero-shot classifier).
|
|
422
|
+
*
|
|
423
|
+
* Returns the un-truncated array — the orchestrator batches it. */
|
|
424
|
+
private buildJevIntentsPayload;
|
|
425
|
+
/** §7.92.5 (2026-09-23): single-batch jev fetcher. Extracted from
|
|
426
|
+
* the old monolithic `classifyWithJev()` so the orchestrator can
|
|
427
|
+
* fire N concurrent calls via `Promise.all` and merge the results.
|
|
428
|
+
*
|
|
429
|
+
* Takes the same `text` + `sessionId` as the old function, plus an
|
|
430
|
+
* already-≤16 `intents[]` slice and `chitchatIntents` (defensive
|
|
431
|
+
* marker for jev v0.3.0+). Returns the same shape:
|
|
432
|
+
* - `action: 'reject' | 'adopt' | 'skip'`
|
|
433
|
+
* - `confidence` / `riskScore` / `latencyMs` (always set)
|
|
434
|
+
* - `skipReason` when action='skip' — caller can tell whether to
|
|
435
|
+
* retry the verdict on the next batch (we don't — we just merge)
|
|
436
|
+
*
|
|
437
|
+
* Every audit / counter side-effect for THIS batch fires here, not
|
|
438
|
+
* in the orchestrator, so partial failures are observable per-batch
|
|
439
|
+
* in the JSONL audit stream. */
|
|
440
|
+
private fetchJevBatch;
|
|
441
|
+
/** §7.92.5 (2026-09-23): jev HTTP prefilter — batched orchestrator.
|
|
442
|
+
* Splits the full `categories[].jevIntents[]` payload into ≤16-size
|
|
443
|
+
* batches (jev v0.3.0 hard cap from `head_max_len=256`), fires all
|
|
444
|
+
* batches concurrently via `Promise.all`, and merges the verdicts
|
|
445
|
+
* with a max-confidence pick:
|
|
446
|
+
*
|
|
447
|
+
* 1. Any `reject` action → pick highest `riskScore` (security signal)
|
|
448
|
+
* 2. Else any `adopt` action → pick highest `confidence`; tie-break
|
|
449
|
+
* by earlier batch index (REJECT-priority intents live in
|
|
450
|
+
* batch 0, so the security-first ordering is preserved on ties)
|
|
451
|
+
* 3. Else all skipped → return the first skip result
|
|
452
|
+
*
|
|
453
|
+
* Returns one of:
|
|
454
|
+
* - `action: 'reject'` → `risk_score >= jevRiskThreshold` (security-level)
|
|
455
|
+
* - `action: 'adopt'` → `data.action === 'execute'`, confidence ≥
|
|
456
|
+
* threshold, intent string found in jevIntentMap
|
|
457
|
+
* (mapped to realm)
|
|
458
|
+
* - `action: 'skip'` → any other condition (low-conf, fallback_llm,
|
|
459
|
+
* unrecognized intent, transport / parse error).
|
|
460
|
+
* Caller falls through to classifyWithLlm.
|
|
461
|
+
*
|
|
462
|
+
* Failure handling: every per-batch error path (timeout, network,
|
|
463
|
+
* parse) is audited at warn level and produces `action: 'skip'` for
|
|
464
|
+
* THAT batch — jev being unavailable MUST NOT degrade the existing
|
|
465
|
+
* LLM classification path. This is the same fail-open contract used
|
|
466
|
+
* by classifyWithLlm's classifyWithRules fallback.
|
|
467
|
+
*
|
|
468
|
+
* See ~/work/jev-branch/docs/api_reference.md for the wire spec.
|
|
469
|
+
* Implementation notes:
|
|
470
|
+
* - Uses global `fetch` (Node ≥18). AbortSignal for timeout.
|
|
471
|
+
* - No auth header — jev is local-network by design.
|
|
472
|
+
* - `metadata.session_id` for cross-service trace correlation.
|
|
473
|
+
* - `lang` hardcoded 'zh' (we don't auto-detect; the operator's UI
|
|
474
|
+
* can pass it later if multilingual queries become common). */
|
|
475
|
+
/** §7.112: headers for every jev HTTP surface (predict / health /
|
|
476
|
+
* systemone). Content-Type always; Authorization only when a key is
|
|
477
|
+
* configured — Bailian MaaS requires it, the local ONNX service never
|
|
478
|
+
* did. */
|
|
479
|
+
private jevHeaders;
|
|
480
|
+
/** §7.112 (2026-10-25): Bailian decision-model backend for the jev stage.
|
|
481
|
+
*
|
|
482
|
+
* One POST `${jevUrl}/systemone` replaces the batched /predict flow:
|
|
483
|
+
* the structured-decision interface carries the whole taxonomy at once —
|
|
484
|
+
* questions.intent = choice over categories (criteria = id → description)
|
|
485
|
+
* questions.risk = score over 4 danger levels (→ risk_score 0..1)
|
|
486
|
+
* so there is no 16-intent cap and no chitchat markers. Confidence/risk
|
|
487
|
+
* gates, breaker semantics and audit rows mirror the local flow so
|
|
488
|
+
* classify() cannot tell the backends apart. */
|
|
489
|
+
private classifyWithBailianDecision;
|
|
490
|
+
private classifyWithJev;
|
|
491
|
+
private classifyWithLlm;
|
|
492
|
+
/** §7.58 (2026-09-16): match a LOCAL-entity keyword. CJK entries are
|
|
493
|
+
* substring matches; ASCII entries require non-alphanumeric boundaries
|
|
494
|
+
* (so 'ets' never fires inside "assets"/"settings"). Returns the matched
|
|
495
|
+
* (lower-cased) keyword, or undefined. */
|
|
496
|
+
private matchLocalEntity;
|
|
497
|
+
/** §7.108 (2026-10-24): the policy file's stages.intent signal tables,
|
|
498
|
+
* read LIVE on every classify so a policy hot-reload takes effect on
|
|
499
|
+
* the next turn. Missing service / old policy → undefined (callers
|
|
500
|
+
* fall back to the constructor-built lists). */
|
|
501
|
+
private policyIntentStages;
|
|
502
|
+
/** §7.108: the FULL cloud signal list = built-in + patch config +
|
|
503
|
+
* policy stages.intent.cloudKeywords. ONE definition shared by the
|
|
504
|
+
* rules tier and the §7.107 gate — they can never disagree about what
|
|
505
|
+
* counts as a public-web need. */
|
|
506
|
+
private cloudKeywordsAll;
|
|
507
|
+
/** §7.108: the FULL LOCAL entity list = built-in + patch config + policy. */
|
|
508
|
+
private localEntitiesAll;
|
|
509
|
+
/** §7.108: LOCAL last-resort list — policy stages.intent.fallbackKeywords
|
|
510
|
+
* (non-empty) REPLACES, mirroring the env var's replace semantics. */
|
|
511
|
+
/** §7.124 (2026-10-27): the device-command extension terms = patch config
|
|
512
|
+
* (config/edge.json stages.intent.deviceCommands) + the policy file's
|
|
513
|
+
* stages.intent.deviceCommands (hot-reloadable, read LIVE per classify).
|
|
514
|
+
* Both are ADDITIVE on top of the built-in vocabulary. */
|
|
515
|
+
private deviceCommandTerms;
|
|
516
|
+
private fallbackKeywordsAll;
|
|
517
|
+
/**
|
|
518
|
+
* §7.107: the public-web signal — the keyword that unlocks the cloud.
|
|
519
|
+
*
|
|
520
|
+
* Same list the rules tier matches (built-in information-acquisition words
|
|
521
|
+
* + patch `stages.intent.cloudKeywords` + policy-file
|
|
522
|
+
* `stages.intent.cloudKeywords` (§7.108)); `classifyWithRules` calls this
|
|
523
|
+
* so the gate and the rules tier cannot drift apart.
|
|
524
|
+
*/
|
|
525
|
+
cloudSignal(text: string): string | undefined;
|
|
526
|
+
private classifyWithRules;
|
|
527
|
+
resolve(intent: Intent): {
|
|
528
|
+
realm: string;
|
|
529
|
+
config: Record<string, unknown>;
|
|
530
|
+
};
|
|
531
|
+
/** Snapshot of in-memory counters. Returned object is a copy so callers
|
|
532
|
+
* can't mutate the live state. Counters are process-local — restart
|
|
533
|
+
* zeroes them. */
|
|
534
|
+
/** §7.92.7 (2026-09-24): boot-time health probe. Fires once at
|
|
535
|
+
* `start()` (called from `apply()`). Fire-and-forget — does NOT
|
|
536
|
+
* block plugin init. On 2xx → no-op (the breaker stays closed;
|
|
537
|
+
* classify() proceeds normally). On non-2xx / network error /
|
|
538
|
+
* timeout → pre-opens the breaker for `jevBreakerTtlMs` so the
|
|
539
|
+
* first classify() short-circuits instead of paying
|
|
540
|
+
* 3 × `jevTimeoutMs` wall-clock.
|
|
541
|
+
*
|
|
542
|
+
* Why fire-and-forget: blocking plugin init on a 2s jev probe
|
|
543
|
+
* adds 2s to cold boot, and a chat-less boot (e.g. CI smoke) has
|
|
544
|
+
* no classify() to gate. Race against first classify() is fine
|
|
545
|
+
* because: (1) plugin apply() runs before http-api opens its
|
|
546
|
+
* port, so the earliest chat is several hundred ms away, (2) if
|
|
547
|
+
* classify() runs first it just trips the breaker the normal
|
|
548
|
+
* way (3 batch fails) — same outcome as the probe. */
|
|
549
|
+
start(): void;
|
|
550
|
+
/** §7.92.7: dispose hook. Awaits the in-flight boot probe (if
|
|
551
|
+
* any) and clears the recovery-probe timer. Tests call this to
|
|
552
|
+
* keep the event loop clean between cases; cordis calls this
|
|
553
|
+
* on plugin teardown. */
|
|
554
|
+
stop(): Promise<void>;
|
|
555
|
+
/** §7.92.7: probe jev `/health`. Returns true on HTTP 2xx, false
|
|
556
|
+
* on anything else (network error, timeout, non-2xx). Always
|
|
557
|
+
* resolves — never throws. The `phase` arg distinguishes boot-time
|
|
558
|
+
* vs periodic-recovery audit kinds so ops can tell them apart in
|
|
559
|
+
* the log. */
|
|
560
|
+
private probeJevHealth;
|
|
561
|
+
/** §7.92.7: start the recovery probe setInterval. Idempotent.
|
|
562
|
+
* Called from classifyWithJev() right after `jevBreakerOpenUntil`
|
|
563
|
+
* is set. When the periodic probe lands on 2xx it closes the
|
|
564
|
+
* breaker (and stops itself) — recovery is faster than waiting
|
|
565
|
+
* for full TTL. */
|
|
566
|
+
private startBreakerProbe;
|
|
567
|
+
/** §7.92.7: clear the recovery probe setInterval. Idempotent. */
|
|
568
|
+
private stopBreakerProbe;
|
|
569
|
+
/** §7.92.7 helper: wall-clock spent in the most recent successful
|
|
570
|
+
* probe (used in `intent.jev.recovered` audit payload). The
|
|
571
|
+
* setInterval callback captures `probeStart` in its closure but
|
|
572
|
+
* doesn't surface it cleanly — keep this lightweight by storing
|
|
573
|
+
* the latest observed latency in instance state. */
|
|
574
|
+
private jevLastProbeLatencyMs;
|
|
575
|
+
private jevProbeTimerLatency;
|
|
576
|
+
getStats(): IntentRouterStats;
|
|
577
|
+
}
|
|
578
|
+
/** Map an Intent to a realm/config tuple the rest of the agent loop can consume. */
|
|
579
|
+
export declare function resolveIntent(intent: Intent): {
|
|
580
|
+
realm: string;
|
|
581
|
+
config: Record<string, unknown>;
|
|
582
|
+
};
|
|
583
|
+
/** Parsed shape of the LLM's classification response. */
|
|
584
|
+
interface ClassificationVerdict {
|
|
585
|
+
/** §7.92.3 (2026-09-23): widened from the historical 2-category union
|
|
586
|
+
* (LOCAL | CLOUD) to match the categories spec in privacy-policy.json
|
|
587
|
+
* — the LLM prompt now lists all 4 (LOCAL/CLOUD/SANITIZED/REJECT),
|
|
588
|
+
* so the verdict parser must accept them too. The downstream code
|
|
589
|
+
* (entity guardrail, classifyWithRules fallback, etc.) was already
|
|
590
|
+
* 4-category-aware via `RULE_CATEGORIES`. */
|
|
591
|
+
category: 'LOCAL' | 'CLOUD' | 'SANITIZED' | 'REJECT';
|
|
592
|
+
confidence: number;
|
|
593
|
+
}
|
|
594
|
+
/** In-memory counters exposed via getStats(). Pure observability; the audit
|
|
595
|
+
* log already carries every per-call kind, but counters give operators an
|
|
596
|
+
* O(1) read for dashboards / alerts (rate of timeouts, % sanitized, etc.)
|
|
597
|
+
* without parsing JSONL. */
|
|
598
|
+
export interface IntentRouterStats {
|
|
599
|
+
total: number;
|
|
600
|
+
empty: number;
|
|
601
|
+
truncated: number;
|
|
602
|
+
/** LLM verdict adopted as the final intent. */
|
|
603
|
+
llmHit: number;
|
|
604
|
+
/** TTL/LRU 缓存命中 —— 同文本+同上下文的重复 classify() 不再付
|
|
605
|
+
* LLM/jev 往返("你好" 类闲聊在 §7.107 gate 下尤其常见)。 */
|
|
606
|
+
cacheHit: number;
|
|
607
|
+
/** classifyWithRules adopted — privacy-policy match OR hardcoded keyword. */
|
|
608
|
+
ruleHit: number;
|
|
609
|
+
/** §7.124 (2026-10-27): Tier-0 device-command regex hit — the turn was
|
|
610
|
+
* decided locally by regex, without calling jev or the LLM classifier.
|
|
611
|
+
* Every hit saved one decision-model round-trip (~150ms-6s). */
|
|
612
|
+
deviceCommandHit: number;
|
|
613
|
+
/** No LLM verdict, no keyword → defaultCategory returned. */
|
|
614
|
+
defaultHit: number;
|
|
615
|
+
/** LLM returned below confidence threshold (cause of fallback to rules). */
|
|
616
|
+
lowConfidence: number;
|
|
617
|
+
/** AbortController fired during LLM stream. */
|
|
618
|
+
timeout: number;
|
|
619
|
+
/** LLM threw a non-timeout error (network, parse, etc.). */
|
|
620
|
+
llmError: number;
|
|
621
|
+
/** P1: LLM stream returned text that parseClassificationVerdict could
|
|
622
|
+
* not turn into a verdict (all three tiers failed). Distinct from
|
|
623
|
+
* `llmError` (network / timeout) — the LLM DID respond, the response
|
|
624
|
+
* just wasn't structured. Operators can compare
|
|
625
|
+
* `parseFailure / (llmHit + lowConfidence + parseFailure + llmError)`
|
|
626
|
+
* to gauge parse health. */
|
|
627
|
+
parseFailure: number;
|
|
628
|
+
/** REJECT verdict returned (pre-check or fallback). */
|
|
629
|
+
rejected: number;
|
|
630
|
+
/** Sanitizer applied to CLOUD-classification prompt. */
|
|
631
|
+
sanitized: number;
|
|
632
|
+
/** Sanitizer threw (fail-closed → throws, falls back to classifyWithRules). */
|
|
633
|
+
sanitizeError: number;
|
|
634
|
+
/** CLOUD realm but no sanitizer handle mounted (fail-closed → throws). */
|
|
635
|
+
sanitizeSkipped: number;
|
|
636
|
+
/** §7.92.3 (2026-09-23): jev verdict ADOPTED (path: jev-adopt). Sum
|
|
637
|
+
* with `llmHit + ruleHit + defaultHit + jevHit` to verify all paths
|
|
638
|
+
* hit a final verdict. Compare `jevHit / total` to estimate latency
|
|
639
|
+
* savings from the toggle (every jevHit replaces a 4-30s LLM call
|
|
640
|
+
* with a ~150ms jev call). */
|
|
641
|
+
jevHit: number;
|
|
642
|
+
/** §7.92.3: jev risk-gated REJECT (risk_score >= threshold). Subset
|
|
643
|
+
* of `rejected`. */
|
|
644
|
+
jevReject: number;
|
|
645
|
+
/** §7.92.3: jev SKIPPED to LLM (low-conf / fallback_llm / unmapped).
|
|
646
|
+
* Distinct from jevError (transport failure) — skip means jev
|
|
647
|
+
* worked fine, just deferred to LLM for quality reasons. */
|
|
648
|
+
jevSkip: number;
|
|
649
|
+
/** §7.92.3: jev transport / parse failure. Counts alongside jevSkip
|
|
650
|
+
* in classify()'s tally but operators triage them separately:
|
|
651
|
+
* jevError rate climbing → jev service health issue, jevSkip rate
|
|
652
|
+
* climbing → jev's intent set doesn't match our traffic. */
|
|
653
|
+
jevError: number;
|
|
654
|
+
/** §7.92.3: subset of jevError where AbortSignal fired. */
|
|
655
|
+
jevTimeout: number;
|
|
656
|
+
/** §7.92.3: cumulative jev `data.latency_ms` sum (ms). Use with
|
|
657
|
+
* `getStats()` calls to compute average jev latency without
|
|
658
|
+
* parsing JSONL. */
|
|
659
|
+
jevLatencyMsTotal: number;
|
|
660
|
+
/** §7.92.6 (2026-09-24): circuit-breaker opened N times in this
|
|
661
|
+
* process lifetime. Climbs → jev service is flapping. Pair with
|
|
662
|
+
* jevCircuitSkip to estimate total bypassed latency: each skip
|
|
663
|
+
* avoided ~3 × jevTimeoutMs = 7500ms worst-case wall-clock. */
|
|
664
|
+
jevCircuitOpen: number;
|
|
665
|
+
/** §7.92.6: classify() calls that returned early because breaker
|
|
666
|
+
* was open. Total latency saved ≈ jevCircuitSkip × 7500ms. */
|
|
667
|
+
jevCircuitSkip: number;
|
|
668
|
+
/** §7.92.7 (2026-09-24): boot probe counters. jevBootProbeOk
|
|
669
|
+
* climbs when the startup `GET /health` returned 2xx; jevBootProbeFail
|
|
670
|
+
* climbs when it failed (breaker pre-opened, first classify()
|
|
671
|
+
* short-circuits). Operators compare these to gauge whether jev
|
|
672
|
+
* is consistently unhealthy at boot. */
|
|
673
|
+
jevBootProbeOk: number;
|
|
674
|
+
jevBootProbeFail: number;
|
|
675
|
+
/** §7.92.7: periodic recovery probe counters. jevProbeAttempts =
|
|
676
|
+
* total `/health` pings fired while the breaker was open;
|
|
677
|
+
* jevRecoveredProbe = times a probe succeeded and closed the
|
|
678
|
+
* breaker (recovery was faster than waiting for full TTL).
|
|
679
|
+
* Healthy ratio ≈ jevRecoveredProbe / jevProbeAttempts. */
|
|
680
|
+
jevProbeAttempts: number;
|
|
681
|
+
jevRecoveredProbe: number;
|
|
682
|
+
/** G9: 10-bucket histogram of LLM confidence values × (accepted | rejected).
|
|
683
|
+
* Bucket key is `String(Math.floor(c * 10))` clamped to [0,9]. Use
|
|
684
|
+
* `getStats()` to obtain a deep copy. Empty until the first parsed
|
|
685
|
+
* verdict — timeouts / errors / unclear responses are not recorded. */
|
|
686
|
+
confidenceHistogram: {
|
|
687
|
+
[bucket: string]: {
|
|
688
|
+
accepted: number;
|
|
689
|
+
rejected: number;
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Fence-tolerant parser for `{ category, confidence }` JSON the LLM returns.
|
|
695
|
+
* Falls back to: extract the first {...} block → regex token match for
|
|
696
|
+
* `category` + a best-effort confidence float from the surrounding text.
|
|
697
|
+
*/
|
|
698
|
+
export declare function parseClassificationVerdict(raw: string): ClassificationVerdict | null;
|
|
699
|
+
export declare function coerceClassification(obj: unknown): ClassificationVerdict | null;
|
|
700
|
+
/** Best-effort extract a text payload from a recent-message object.
|
|
701
|
+
* Handles dsh-llm's `{content: [{type:'text', text}]}`, plain `{text}`,
|
|
702
|
+
* `{message: string}`, or anything stringifiable. Returns '' on failure. */
|
|
703
|
+
export declare function extractMessageText(m: unknown): string;
|
|
704
|
+
declare module '@deepseek-ai/cordis' {
|
|
705
|
+
interface Context {
|
|
706
|
+
intentRouter: IntentRouterServiceImpl;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
export declare function apply(ctx: Context, config: Config): Promise<() => Promise<void>>;
|
|
710
|
+
//# sourceMappingURL=intent-router.d.ts.map
|