@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 @@
|
|
|
1
|
+
{"version":3,"file":"openai-sse.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/openai-sse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openai-sse — shared helpers for talking to OpenAI-compatible
|
|
3
|
+
* `/chat/completions` endpoints. Both `cloud-llm-adapter` (DeepSeek)
|
|
4
|
+
* and `local-llm-adapter` (`mode: 'real'`) drive their wire envelope
|
|
5
|
+
* and SSE parse through this module so a wire-protocol change lands
|
|
6
|
+
* in one place.
|
|
7
|
+
*
|
|
8
|
+
* Wire shape:
|
|
9
|
+
* POST {baseUrl}/chat/completions
|
|
10
|
+
* { model, messages, stream: true, tools?, temperature?, max_tokens? }
|
|
11
|
+
* response: `text/event-stream` of `data: {...}` frames ending in
|
|
12
|
+
* `data: [DONE]`. The adapter translates frames into the DSH
|
|
13
|
+
* `StreamChunk` union (block-start, text-delta, reasoning-delta,
|
|
14
|
+
* tool-call-delta, block-end, usage, finish).
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-edge-agent/openai-sse
|
|
17
|
+
*/
|
|
18
|
+
import { attributionHeaders, LlmError, ToolCallId as brandCallId } from '@deepseek-ai/dsh-llm';
|
|
19
|
+
/**
|
|
20
|
+
* §7.105 (2026-10-21): an empty tool-call id is FATAL, not cosmetic.
|
|
21
|
+
*
|
|
22
|
+
* OpenAI-compatible endpoints may omit the tool-call `id` entirely (some
|
|
23
|
+
* llama.cpp / vLLM builds send `tool_calls` deltas with no `id` field) or
|
|
24
|
+
* send `id: ""`. The host persists that id verbatim into the v4 session log,
|
|
25
|
+
* and the log validator rejects the WHOLE FILE on the next load
|
|
26
|
+
* ("tool call id requires a nonempty string") — so ONE bad tool call makes
|
|
27
|
+
* the entire session history unloadable, not just the affected turn.
|
|
28
|
+
*
|
|
29
|
+
* Never let an empty id escape this module. The synthesized id is stored in
|
|
30
|
+
* the per-stream `toolCalls` map, which keeps it stable across every delta
|
|
31
|
+
* of one call and identical to the final `block-end` block, so the host's
|
|
32
|
+
* assistant message advertises exactly one id for the call.
|
|
33
|
+
*/
|
|
34
|
+
function synthesizeToolCallId(wireIndex) {
|
|
35
|
+
return `call_local_${wireIndex}_${Math.random().toString(36).slice(2, 10)}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pick the first candidate that is a NONEMPTY string, else `fallback`.
|
|
39
|
+
* `??` is not enough: an empty string survives `??` but is an invalid
|
|
40
|
+
* OpenAI `id` / `tool_call_id`.
|
|
41
|
+
*/
|
|
42
|
+
function firstNonEmptyId(candidates, fallback) {
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
if (typeof candidate === 'string' && candidate !== '')
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
/** Build the wire body for `POST /chat/completions`. */
|
|
50
|
+
export function buildChatCompletionsBody(options, defaultModel, temperature,
|
|
51
|
+
// §7.87.5 (2026-09-19): inject `chat_template_kwargs={enable_thinking: false}`
|
|
52
|
+
// for thinking models (qwen3.5 / QwQ / DeepSeek-R1). Default OFF — production
|
|
53
|
+
// wants fast non-reasoning answers; users can opt-in via env var.
|
|
54
|
+
enableThinking = false,
|
|
55
|
+
// §7.106 (2026-10-23): also send the standard `reasoning_effort:"none"` —
|
|
56
|
+
// the dialect OLLAMA's /v1 layer actually honors (probe-verified on the
|
|
57
|
+
// user's <内网主机>:11434: chat_template_kwargs AND top-level `think:false`
|
|
58
|
+
// are both silently dropped there; reasoning_effort:none removed
|
|
59
|
+
// message.reasoning and took completion 46→6 tokens). Without it the
|
|
60
|
+
// 256-token model-detect call has its budget eaten by reasoning and
|
|
61
|
+
// returns empty content (finish=length → json-parse-failed → fail-open).
|
|
62
|
+
// Only the local pool sets this — unknown-field-strict servers stay on
|
|
63
|
+
// the old dialect alone.
|
|
64
|
+
alsoReasoningEffort = false) {
|
|
65
|
+
const wireMessages = [];
|
|
66
|
+
if (options.system !== undefined && options.system.length > 0) {
|
|
67
|
+
wireMessages.push({ role: 'system', content: options.system });
|
|
68
|
+
}
|
|
69
|
+
// §7.105 (2026-10-21): resolve tool-call ids UP FRONT so an assistant
|
|
70
|
+
// tool-call and its tool-result always carry the SAME id on the wire.
|
|
71
|
+
//
|
|
72
|
+
// `??` lets an EMPTY id through (it is not nullish), and a tool-call id
|
|
73
|
+
// that is empty on the wire is rejected upstream. It is also the id the
|
|
74
|
+
// host persists into the session log — the v4 log validator rejects the
|
|
75
|
+
// WHOLE file on an empty tool-call id ("tool call id requires a nonempty
|
|
76
|
+
// string"), so one empty id makes an entire session fail to load.
|
|
77
|
+
//
|
|
78
|
+
// The blocks are deep-frozen at runtime, so the resolved id is carried in a
|
|
79
|
+
// side map instead of being written back. Results always follow their call
|
|
80
|
+
// in conversation order, so a FIFO queue pairs an id-less result with the
|
|
81
|
+
// id-less call it answers.
|
|
82
|
+
const resolvedIds = new Map();
|
|
83
|
+
const pendingEmptyCallIds = [];
|
|
84
|
+
for (const message of options.messages) {
|
|
85
|
+
const blocks = (Array.isArray(message.content) ? message.content : []);
|
|
86
|
+
for (const block of blocks) {
|
|
87
|
+
if (block.type === 'tool-call') {
|
|
88
|
+
const existing = firstNonEmptyId([block.toolCallId, block.callId, block.id], '');
|
|
89
|
+
if (existing !== '') {
|
|
90
|
+
resolvedIds.set(block, existing);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const synthesized = `call_${Math.random().toString(36).slice(2)}`;
|
|
94
|
+
resolvedIds.set(block, synthesized);
|
|
95
|
+
pendingEmptyCallIds.push(synthesized);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (block.type === 'tool-result') {
|
|
99
|
+
const existing = firstNonEmptyId([block.toolCallId, block.callId, block.id], '');
|
|
100
|
+
if (existing !== '') {
|
|
101
|
+
resolvedIds.set(block, existing);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
resolvedIds.set(block, pendingEmptyCallIds.shift() ?? `call_${Math.random().toString(36).slice(2)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const message of options.messages) {
|
|
110
|
+
// Cordis messages use content-block arrays. We need to translate:
|
|
111
|
+
// - text blocks → concatenated text content
|
|
112
|
+
// - tool-call blocks → OpenAI `tool_calls` field (assistant messages)
|
|
113
|
+
// - tool-result blocks → separate `role: 'tool'` messages with `tool_call_id`
|
|
114
|
+
const blocks = (Array.isArray(message.content) ? message.content : []);
|
|
115
|
+
// §7.119 (2026-10-26): DSH 的**规范** tool-result 形状是「消息级」的 ——
|
|
116
|
+
// { role:'tool', toolCallId:'call_x', source:{kind:'tool',callId:'call_x'},
|
|
117
|
+
// content:[{type:'text', text:'…'}] }
|
|
118
|
+
// (上游 `createToolResultMessage()` 造的就是这个形状,见
|
|
119
|
+
// @deepseek-ai/dsh-llm lib/index.js。)
|
|
120
|
+
//
|
|
121
|
+
// 旧实现只认 content 里 `type==='tool-result'` 的块,于是把上面这条消息
|
|
122
|
+
// 当成普通消息处理:tool_call_id 直接丢失 → §7.106 的配对守卫(下面
|
|
123
|
+
// 205-215 行)判定它「找不到配对的 call」→ 降级成
|
|
124
|
+
// role:'user', content:"[tool-result (unreferenced) lost its matching
|
|
125
|
+
// call; output:]\n<真实输出>"
|
|
126
|
+
// 也就是说**模型的每一个工具结果都是以「丢失配对」的 user 文本进去的**,
|
|
127
|
+
// 而 assistant 侧的 tool_call 从来没被真正应答过。
|
|
128
|
+
//
|
|
129
|
+
// 实测:真实会话里 100% 的 tool/result 事件都是这个形状(77/77)——
|
|
130
|
+
// 整条工具结果通道是坏的,不是偶发。
|
|
131
|
+
//
|
|
132
|
+
// 这里补一条消息级分支:能取到 id 就按真正的 `role:'tool'` + `tool_call_id`
|
|
133
|
+
// 下发,取不到才回落到下面的块级路径(保持旧行为)。
|
|
134
|
+
if (message.role === 'tool') {
|
|
135
|
+
const messageId = firstNonEmptyId([
|
|
136
|
+
message.toolCallId,
|
|
137
|
+
message.source?.callId,
|
|
138
|
+
message.source?.toolCallId,
|
|
139
|
+
], '');
|
|
140
|
+
if (messageId !== '') {
|
|
141
|
+
const toolText = typeof message.content === 'string'
|
|
142
|
+
? message.content
|
|
143
|
+
: extractText(blocks);
|
|
144
|
+
wireMessages.push({ role: 'tool', tool_call_id: messageId, content: toolText });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const textParts = [];
|
|
149
|
+
const toolCalls = [];
|
|
150
|
+
const toolResults = [];
|
|
151
|
+
for (const block of blocks) {
|
|
152
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
153
|
+
textParts.push(block.text);
|
|
154
|
+
}
|
|
155
|
+
else if (block.type === 'tool-call') {
|
|
156
|
+
toolCalls.push({
|
|
157
|
+
id: resolvedIds.get(block) ?? firstNonEmptyId([block.toolCallId, block.callId, block.id], `call_${Math.random().toString(36).slice(2)}`),
|
|
158
|
+
type: 'function',
|
|
159
|
+
function: {
|
|
160
|
+
name: String(block.name ?? ''),
|
|
161
|
+
arguments: typeof block.arguments === 'string' ? block.arguments : JSON.stringify(block.arguments ?? {}),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else if (block.type === 'tool-result') {
|
|
166
|
+
toolResults.push({
|
|
167
|
+
id: resolvedIds.get(block) ?? firstNonEmptyId([block.toolCallId, block.callId, block.id], ''),
|
|
168
|
+
content: typeof block.content === 'string' ? block.content : extractText(block.content),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// reasoning blocks are intentionally skipped for the wire format
|
|
172
|
+
}
|
|
173
|
+
// Tool results become separate `role: 'tool'` messages
|
|
174
|
+
if (toolResults.length > 0) {
|
|
175
|
+
for (const tr of toolResults) {
|
|
176
|
+
wireMessages.push({ role: 'tool', tool_call_id: tr.id, content: tr.content });
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Build the wire message with appropriate fields
|
|
181
|
+
const wireMsg = { role: message.role };
|
|
182
|
+
wireMsg.content = textParts.length > 0 ? textParts.join('') : (toolCalls.length > 0 ? null : '');
|
|
183
|
+
if (toolCalls.length > 0) {
|
|
184
|
+
wireMsg.tool_calls = toolCalls;
|
|
185
|
+
}
|
|
186
|
+
wireMessages.push(wireMsg);
|
|
187
|
+
}
|
|
188
|
+
// §7.106 (2026-10-23) pairing guard: every `role:'tool'` message must
|
|
189
|
+
// reference an assistant tool_call that EXISTS IN THIS BODY. Strict
|
|
190
|
+
// gateways (minimax 2013 "tool result's tool id() not found") enforce the
|
|
191
|
+
// referential integrity DeepSeek silently tolerates. Broken histories come
|
|
192
|
+
// from the ollama-era models' empty-id tool calls (§7.105 class): a result
|
|
193
|
+
// whose call was pruned out of the window, or whose FIFO-synthesized id
|
|
194
|
+
// was never claimed by an assistant message, is an orphan no upstream will
|
|
195
|
+
// accept. Demote orphans to plain user text — the tool OUTPUT survives for
|
|
196
|
+
// the model to read, only the dangling reference goes away.
|
|
197
|
+
{
|
|
198
|
+
const claimedCallIds = new Set();
|
|
199
|
+
for (const m of wireMessages) {
|
|
200
|
+
const tcs = m.tool_calls;
|
|
201
|
+
if (Array.isArray(tcs)) {
|
|
202
|
+
for (const tc of tcs) {
|
|
203
|
+
if (typeof tc.id === 'string' && tc.id.length > 0)
|
|
204
|
+
claimedCallIds.add(tc.id);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (let i = 0; i < wireMessages.length; i++) {
|
|
209
|
+
const m = wireMessages[i];
|
|
210
|
+
if (m.role !== 'tool')
|
|
211
|
+
continue;
|
|
212
|
+
const id = typeof m.tool_call_id === 'string' ? m.tool_call_id : '';
|
|
213
|
+
if (id.length > 0 && claimedCallIds.has(id))
|
|
214
|
+
continue;
|
|
215
|
+
const text = typeof m.content === 'string' ? m.content : '';
|
|
216
|
+
wireMessages[i] = {
|
|
217
|
+
role: 'user',
|
|
218
|
+
content: `[tool-result ${id.length > 0 ? id : '(unreferenced)'} lost its matching call; output:]\n${text}`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const body = {
|
|
223
|
+
// CRITICAL: use the adapter's configured `defaultModel` (the real upstream
|
|
224
|
+
// model name, e.g. "deepseek-chat"), NOT `options.model` which is the Cordis
|
|
225
|
+
// provider route ID (e.g. "cloud-llm"). The route ID is meaningless
|
|
226
|
+
// to the upstream API — only the adapter knows its own model name.
|
|
227
|
+
model: defaultModel,
|
|
228
|
+
messages: wireMessages,
|
|
229
|
+
stream: true,
|
|
230
|
+
};
|
|
231
|
+
if (options.tools !== undefined && options.tools.length > 0) {
|
|
232
|
+
body.tools = options.tools.map((tool) => ({
|
|
233
|
+
type: 'function',
|
|
234
|
+
function: {
|
|
235
|
+
name: tool.name,
|
|
236
|
+
description: tool.description,
|
|
237
|
+
parameters: tool.parameters,
|
|
238
|
+
},
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
if (temperature !== undefined)
|
|
242
|
+
body.temperature = temperature;
|
|
243
|
+
if (options.maxTokens !== undefined)
|
|
244
|
+
body.max_tokens = options.maxTokens;
|
|
245
|
+
// §7.87.5 (2026-09-19): disable chain-of-thought for thinking-capable models
|
|
246
|
+
// (qwen3.5 / QwQ / DeepSeek-R1). Without this, max_tokens gets burned on
|
|
247
|
+
// reasoning and `content` returns "" (finish_reason="length"). Pass-through
|
|
248
|
+
// is a no-op for non-thinking servers — they ignore unknown kwargs.
|
|
249
|
+
if (!enableThinking) {
|
|
250
|
+
body.chat_template_kwargs = { enable_thinking: false };
|
|
251
|
+
if (alsoReasoningEffort)
|
|
252
|
+
body.reasoning_effort = 'none';
|
|
253
|
+
}
|
|
254
|
+
return body;
|
|
255
|
+
}
|
|
256
|
+
/** Pull the text out of a content-block array into one concatenated string. */
|
|
257
|
+
export function extractText(blocks) {
|
|
258
|
+
return blocks
|
|
259
|
+
.filter((block) => block.type === 'text' && typeof block.text === 'string')
|
|
260
|
+
.map((block) => block.text)
|
|
261
|
+
.join('');
|
|
262
|
+
}
|
|
263
|
+
/** Read a `Response` body as an SSE stream and yield finished frames. */
|
|
264
|
+
export async function* iterateSseFrames(response, onError) {
|
|
265
|
+
if (!response.ok || response.body === null) {
|
|
266
|
+
const text = await response.text().catch(() => '');
|
|
267
|
+
throw new LlmError(`OpenAI-compatible SSE: HTTP ${response.status} from ${response.url}: ${text.slice(0, 512)}`, 'PROVIDER_ERROR');
|
|
268
|
+
}
|
|
269
|
+
const stream = response.body;
|
|
270
|
+
const reader = stream.getReader();
|
|
271
|
+
const decoder = new TextDecoder('utf-8');
|
|
272
|
+
let buffer = '';
|
|
273
|
+
try {
|
|
274
|
+
for (;;) {
|
|
275
|
+
const { value, done } = await reader.read();
|
|
276
|
+
if (done)
|
|
277
|
+
break;
|
|
278
|
+
buffer += decoder.decode(value, { stream: true });
|
|
279
|
+
const events = splitSseEvents(buffer);
|
|
280
|
+
buffer = events.remainder;
|
|
281
|
+
for (const event of events.events) {
|
|
282
|
+
const chunk = parseSseEvent(event);
|
|
283
|
+
if (chunk !== undefined)
|
|
284
|
+
yield chunk;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
onError(error.message ?? String(error));
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
reader.releaseLock();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Translate a `text-delta` or `tool-call-delta` SSE event into a DSH
|
|
297
|
+
* `StreamChunk`. Returns `null` for `finish` events — the caller
|
|
298
|
+
* tracks the finish reason separately and emits the terminal `finish`
|
|
299
|
+
* through `terminalChunks` once the SSE stream closes.
|
|
300
|
+
*/
|
|
301
|
+
export function sseEventToChunk(event, toolCalls) {
|
|
302
|
+
if (event.kind === 'text-delta') {
|
|
303
|
+
return { chunk: { type: 'text-delta', index: 0, text: event.text }, sawText: true, sawToolCall: false };
|
|
304
|
+
}
|
|
305
|
+
if (event.kind === 'tool-call-delta') {
|
|
306
|
+
const current = toolCalls.get(event.index) ?? { id: '', name: '', arguments: '' };
|
|
307
|
+
// §7.105 (2026-10-21): the upstream `id` may be absent or empty. A
|
|
308
|
+
// tool-call id must be a NONEMPTY string — the host's v4 session-log
|
|
309
|
+
// validator rejects the entire log on an empty id ("tool call id requires
|
|
310
|
+
// a nonempty string"), which is how one local-llm tool call made a whole
|
|
311
|
+
// session fail to load. Prefer the upstream id when it has one; otherwise
|
|
312
|
+
// synthesize a stable id for this wire index and remember it, so every
|
|
313
|
+
// later delta of the same call and the final `block-end` agree.
|
|
314
|
+
if (event.id !== undefined && event.id !== '')
|
|
315
|
+
current.id = event.id;
|
|
316
|
+
else if (current.id === '')
|
|
317
|
+
current.id = synthesizeToolCallId(event.index);
|
|
318
|
+
if (event.name !== undefined)
|
|
319
|
+
current.name = event.name;
|
|
320
|
+
current.arguments += event.argumentsDelta;
|
|
321
|
+
toolCalls.set(event.index, current);
|
|
322
|
+
const idForChunk = current.id;
|
|
323
|
+
// CRITICAL: dsh-session's `snapshotJsonValue` rejects chunks with `undefined`
|
|
324
|
+
// fields (throws "carries non-JSON-serializable data"). When DeepSeek splits
|
|
325
|
+
// a tool call across multiple deltas, intermediate chunks omit `name` (and
|
|
326
|
+
// on the very first chunk the local accumulator's `name` is still `''`).
|
|
327
|
+
// Strip `undefined` so the chunk survives lossless JSON validation.
|
|
328
|
+
// §7.58 (2026-09-16): the chunk index MUST equal the index `terminalChunks`
|
|
329
|
+
// later uses for the matching `block-end` — both are `wire index + 1`
|
|
330
|
+
// (index 0 belongs to text). They used to disagree (delta = wire index 0,
|
|
331
|
+
// block-end = 1-based sequential): the host `BlockAssembler` keys partials
|
|
332
|
+
// by index, so the delta-accumulated call and the block-end landed in two
|
|
333
|
+
// different blocks → one persisted assistant message with the SAME
|
|
334
|
+
// tool-call duplicated under a single shared id, and the next replay got
|
|
335
|
+
// DeepSeek 400 "Duplicate value for 'tool_call_id' … in message[N]"
|
|
336
|
+
// (session 24303d6a turn 2, 2026-09-15). Only text-less tool turns were
|
|
337
|
+
// affected — when text came first, it squatted index 0 and the colliding
|
|
338
|
+
// delta was harmlessly merged into the text partial.
|
|
339
|
+
const chunk = {
|
|
340
|
+
type: 'tool-call-delta',
|
|
341
|
+
index: event.index + 1,
|
|
342
|
+
id: brandCallId(idForChunk),
|
|
343
|
+
argumentsDelta: event.argumentsDelta,
|
|
344
|
+
};
|
|
345
|
+
if (event.name !== undefined)
|
|
346
|
+
chunk.name = event.name;
|
|
347
|
+
return {
|
|
348
|
+
chunk,
|
|
349
|
+
sawText: false,
|
|
350
|
+
sawToolCall: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
/** Extract the finish reason out of a `finish` event. */
|
|
356
|
+
export function sseFinishReason(event) {
|
|
357
|
+
return event.kind === 'finish' ? event.finishReason : undefined;
|
|
358
|
+
}
|
|
359
|
+
/** Emit the terminal chunks: text `block-end`, sorted tool-call `block-end`s, then `finish`. */
|
|
360
|
+
export function* terminalChunks(sawText, assembledText, toolCalls, finishKind, sawToolCall) {
|
|
361
|
+
if (sawText) {
|
|
362
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: assembledText } };
|
|
363
|
+
}
|
|
364
|
+
// CRITICAL: Tool-call block-ends must use indices that don't collide with text (index 0).
|
|
365
|
+
// The BlockAssembler uses chunk.index to track blocks, so text and tool-calls must have
|
|
366
|
+
// distinct indices. §7.58: the mapping is EXACTLY `wire index + 1` — the same one
|
|
367
|
+
// sseEventToChunk applies to live tool-call-delta chunks — so each call's delta
|
|
368
|
+
// partial and its block-end land in the SAME block slot. Sequential numbering
|
|
369
|
+
// (`toolCallIndex++`) instead would diverge whenever wire indices have gaps and
|
|
370
|
+
// double the tool-calls whenever a delta partial was already open (see the
|
|
371
|
+
// duplicate-id incident documented at the tool-call-delta yield above).
|
|
372
|
+
for (const [wireIndex, call] of [...toolCalls.entries()].sort((a, b) => a[0] - b[0])) {
|
|
373
|
+
yield {
|
|
374
|
+
type: 'block-end',
|
|
375
|
+
index: wireIndex + 1,
|
|
376
|
+
block: {
|
|
377
|
+
type: 'tool-call',
|
|
378
|
+
// §7.105 (2026-10-21): the block-end id is the one the host persists
|
|
379
|
+
// into `assistant/message`, so it must never be empty. `sseEventToChunk`
|
|
380
|
+
// already synthesized one when the upstream omitted it; this guard
|
|
381
|
+
// covers a map populated by any other path.
|
|
382
|
+
id: brandCallId((call.id !== '' ? call.id : synthesizeToolCallId(wireIndex))),
|
|
383
|
+
name: call.name,
|
|
384
|
+
arguments: call.arguments,
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
yield {
|
|
389
|
+
type: 'finish',
|
|
390
|
+
reason: finishKind === 'length'
|
|
391
|
+
? { kind: 'max-tokens' }
|
|
392
|
+
: finishKind === 'tool-calls' || sawToolCall
|
|
393
|
+
? { kind: 'tool-calls' }
|
|
394
|
+
: { kind: 'stop' },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
export function splitSseEvents(buffer) {
|
|
398
|
+
const events = [];
|
|
399
|
+
let idx = buffer.indexOf('\n\n');
|
|
400
|
+
let cursor = 0;
|
|
401
|
+
while (idx !== -1) {
|
|
402
|
+
events.push(buffer.slice(cursor, idx));
|
|
403
|
+
cursor = idx + 2;
|
|
404
|
+
idx = buffer.indexOf('\n\n', cursor);
|
|
405
|
+
}
|
|
406
|
+
return { events, remainder: buffer.slice(cursor) };
|
|
407
|
+
}
|
|
408
|
+
export function parseSseEvent(raw) {
|
|
409
|
+
const lines = raw.split('\n');
|
|
410
|
+
for (const line of lines) {
|
|
411
|
+
if (!line.startsWith('data:'))
|
|
412
|
+
continue;
|
|
413
|
+
const payload = line.slice(5).trim();
|
|
414
|
+
if (payload.length === 0 || payload === '[DONE]')
|
|
415
|
+
continue;
|
|
416
|
+
let parsed;
|
|
417
|
+
try {
|
|
418
|
+
parsed = JSON.parse(payload);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const record = parsed;
|
|
424
|
+
const choices = record.choices;
|
|
425
|
+
if (!Array.isArray(choices) || choices.length === 0)
|
|
426
|
+
continue;
|
|
427
|
+
const choice = choices[0];
|
|
428
|
+
const delta = choice.delta;
|
|
429
|
+
const content = delta?.content;
|
|
430
|
+
if (typeof content === 'string' && content.length > 0) {
|
|
431
|
+
return { kind: 'text-delta', text: content };
|
|
432
|
+
}
|
|
433
|
+
const toolCalls = delta?.tool_calls;
|
|
434
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
435
|
+
const first = toolCalls[0];
|
|
436
|
+
const fn = first.function;
|
|
437
|
+
const index = typeof first.index === 'number' ? first.index : 0;
|
|
438
|
+
return {
|
|
439
|
+
kind: 'tool-call-delta',
|
|
440
|
+
index,
|
|
441
|
+
id: typeof first.id === 'string' ? first.id : undefined,
|
|
442
|
+
name: typeof fn?.name === 'string' ? fn.name : undefined,
|
|
443
|
+
argumentsDelta: typeof fn?.arguments === 'string' ? fn.arguments : '',
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const finishReason = choice.finish_reason;
|
|
447
|
+
if (typeof finishReason === 'string') {
|
|
448
|
+
if (finishReason === 'tool_calls') {
|
|
449
|
+
return { kind: 'finish', finishReason: 'tool-calls' };
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
kind: 'finish',
|
|
453
|
+
finishReason: finishReason === 'length' ? 'length' : 'stop',
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
/** Default attribution headers every request should carry. */
|
|
460
|
+
export function defaultAttributionHeaders() {
|
|
461
|
+
return attributionHeaders();
|
|
462
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pii-utils — 共享的 PII 检测与脱敏工具
|
|
3
|
+
*
|
|
4
|
+
* 本模块是**全项目 PII 正则的唯一事实源**(`PII_PATTERNS`)。
|
|
5
|
+
* 消费方:
|
|
6
|
+
* - `privacy-policy.ts` 的 `DEFAULT_POLICY.sanitizationRules`(直接复用
|
|
7
|
+
* `PII_PATTERNS`,不再各写一份字面量)
|
|
8
|
+
* - `sanitizer-pipeline.ts` 经由 `privacyPolicy.getSanitizationRules()`
|
|
9
|
+
* 消费 pattern 字符串做**可逆**脱敏
|
|
10
|
+
* - `prompt-guard` / `audit-log` / `local-storage` 经由本模块的
|
|
11
|
+
* `redactString` / `redactObject` 做**不可逆**掩码
|
|
12
|
+
*
|
|
13
|
+
* 两条链路共用同一批 pattern,只有替换语义不同(占位符 vs 掩码),
|
|
14
|
+
* 因此 pattern 必须集中定义,禁止在别处重复书写。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-edge-agent/pii-utils
|
|
17
|
+
*/
|
|
18
|
+
export type PIIName = 'id-card' | 'bank-card' | 'phone' | 'email' | 'api-key' | 'ip-address' | 'mac-address' | 'passport' | 'license-plate' | 'credential';
|
|
19
|
+
/**
|
|
20
|
+
* PII 正则字面量(唯一事实源)。
|
|
21
|
+
*
|
|
22
|
+
* 以 `RegExp` source 语法书写(不含分隔符与 flags),便于同时用于
|
|
23
|
+
* `new RegExp(pattern, 'gi')`(脱敏管线)与本模块的 `RegExp` 实例。
|
|
24
|
+
*
|
|
25
|
+
* 规则顺序见 `PII_ORDER` —— 顺序是语义的一部分,长规则必须先跑。
|
|
26
|
+
*/
|
|
27
|
+
export declare const PII_PATTERNS: Record<PIIName, string>;
|
|
28
|
+
/**
|
|
29
|
+
* 规则求值顺序。长规则优先(id-card → bank-card → phone),防止短规则
|
|
30
|
+
* 在更长的数字串内部误命中。
|
|
31
|
+
*
|
|
32
|
+
* 注意:顺序生效的前提是 `redactString` / `sanitize` 采用**累进匹配**
|
|
33
|
+
* (每条规则在上一条的产出上继续匹配),否则顺序没有意义。
|
|
34
|
+
*/
|
|
35
|
+
export declare const PII_ORDER: PIIName[];
|
|
36
|
+
/** 各 PII 类型的数据分级,与 privacy-policy 的 dataClassifications 对应。 */
|
|
37
|
+
export declare const PII_DATA_CLASS: Record<PIIName, 'confidential' | 'secret'>;
|
|
38
|
+
/** 不可逆掩码函数(仅用于日志/存储,不参与上云脱敏)。 */
|
|
39
|
+
export declare const PII_MASKS: Record<PIIName, (match: string) => string>;
|
|
40
|
+
export interface PIIRule {
|
|
41
|
+
name: PIIName;
|
|
42
|
+
pattern: RegExp;
|
|
43
|
+
mask: (match: string) => string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 10 种 PII 规则,由 `PII_PATTERNS` + `PII_MASKS` 按 `PII_ORDER` 组装。
|
|
47
|
+
* 与 privacy-policy 的 sanitizationRules 共用同一批 pattern 字面量。
|
|
48
|
+
*/
|
|
49
|
+
export declare const PII_RULES: PIIRule[];
|
|
50
|
+
/**
|
|
51
|
+
* 对单个字符串应用所有 PII 规则,返回脱敏后的字符串和脱敏明细。
|
|
52
|
+
*
|
|
53
|
+
* 累进匹配:每条规则在**上一条规则的产出**(`result`)上继续匹配,而不是
|
|
54
|
+
* 回到原始 `text`。否则后序规则会命中前序规则已经掩码掉的区间,导致
|
|
55
|
+
* `result.replace` 静默 no-op、而 `matches` 里却记了一笔不存在的脱敏
|
|
56
|
+
* (2026-08-30 修复)。
|
|
57
|
+
*
|
|
58
|
+
* 同一取值在文本中出现多次时一次性全部替换,且只计一笔,避免计数虚高。
|
|
59
|
+
*/
|
|
60
|
+
export declare function redactString(text: string, rules?: PIIRule[]): {
|
|
61
|
+
redacted: string;
|
|
62
|
+
matches: Array<{
|
|
63
|
+
type: string;
|
|
64
|
+
masked: string;
|
|
65
|
+
}>;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 深度遍历对象,对所有字符串值应用 PII 脱敏。
|
|
69
|
+
* 返回新对象(不修改原对象)。
|
|
70
|
+
*/
|
|
71
|
+
export declare function redactObject<T>(obj: T, rules?: PIIRule[]): {
|
|
72
|
+
redacted: T;
|
|
73
|
+
count: number;
|
|
74
|
+
};
|
|
75
|
+
export declare function normalizeForPII(text: string): {
|
|
76
|
+
normalized: string;
|
|
77
|
+
/** `charMap[i]` = 规范化后第 i 个字符在原文本 `text` 中的源位置。
|
|
78
|
+
* 严格单调递增(规范化要么 1:0 删,要么 1:1 改,从不插入字符)。
|
|
79
|
+
* 长度 = normalized.length。 */
|
|
80
|
+
charMap: number[];
|
|
81
|
+
/** 规范化是否实际改变了文本(便于审计/forensics)。 */
|
|
82
|
+
changed: boolean;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* 把 `normalizeForPII()` 返回的 charMap + normalized 上的命中区间
|
|
86
|
+
* `[start, end)` 翻回 `text` 上的对应区间。
|
|
87
|
+
*
|
|
88
|
+
* 用法:`sliceOriginal(text, charMap, start, end)` → 用户原始子串
|
|
89
|
+
* (含被丢弃的空白、被转换的中文数字)。
|
|
90
|
+
*
|
|
91
|
+
* 注意:返回字符串的字符数可能 > (end - start),因为 whitespace
|
|
92
|
+
* 不在 charMap 里但仍在原文本里。
|
|
93
|
+
*/
|
|
94
|
+
export declare function sliceOriginal(text: string, charMap: number[], start: number, end: number): string;
|
|
95
|
+
export declare function containsCredential(text: string): boolean;
|
|
96
|
+
//# sourceMappingURL=pii-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pii-utils.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/pii-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,OAAO,GACf,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,GACvD,YAAY,GAAG,aAAa,GAAG,UAAU,GAAG,eAAe,GAAG,YAAY,CAAA;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CA8BhD,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,OAAO,EAW9B,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,QAAQ,CAWrE,CAAA;AAED,iCAAiC;AACjC,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAwBhE,CAAA;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;CAChC;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,EAAE,OAAO,EAI5B,CAAA;AAEH;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,OAAO,EAAc,GAC3B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAwBxE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,GAAG,EAAE,CAAC,EACN,KAAK,GAAE,OAAO,EAAc,GAC3B;IAAE,QAAQ,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAuBhC;AAsED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB;;kCAE8B;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAA;CACjB,CAmDA;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,CASR;AAUD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD"}
|