dsh-dashcam 1.12.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ // ingress 审计接线:把 DSH 的「发往模型的请求」与「进入会话的输入」摘要进账本。
2
+ //
3
+ // 事件名已按本机安装的 DSH(resources/app.asar)核对:
4
+ // - 请求出口:`llm/stream` waterfall,载荷就是完整的请求对象
5
+ // `{ provider, model, messages, tools, sessionId, signal }`(dsh-llm 的
6
+ // `streamWithRegistration` 里调用 `ctx.waterfall(this, 'llm/stream', options, ...)`)。
7
+ // `agent/request` 虽然还在,但源码注释明确「cannot change request messages」,
8
+ // 且发生在 messages 派生之前,不能用于取证。
9
+ // - 消息入口:`session/event` 里的 `user/message`(与幽灵报告读取的会话日志同源)。
10
+ // `agent/inbox/spliced` 是投递事件,条目可能随后被取消;`user/message` 是
11
+ // 真正进面的消息,更适合当 inbound 基准。
12
+ //
13
+ // 设计边界:
14
+ // - 只记元数据(role / len / head80 / sha256 / 附件身份),不落正文;
15
+ // - 不做判定、不拦截、不抛错:任何记录异常都不影响请求;
16
+ // - 两个事件名都可用 config.ingress.requestEvent / inboundEvent 覆盖;
17
+ // 接不上时由宿主写 telemetry/ingress-mount 说明监听了什么,不静默关闭。
18
+ import { createRequire } from 'node:module';
19
+ import { createHash } from 'node:crypto';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const dashcam = require('../../src/index.js');
23
+
24
+ const DEFAULT_REQUEST_EVENT = 'llm/stream';
25
+ const DEFAULT_INBOUND_EVENT = 'session/event';
26
+
27
+ function sha256(value) {
28
+ return createHash('sha256').update(value).digest('hex');
29
+ }
30
+
31
+ function pickSessionId(payload) {
32
+ if (!payload || typeof payload !== 'object') return 'unknown';
33
+ return payload.sessionId
34
+ || payload.sessionID
35
+ || payload.id
36
+ || (payload.session && payload.session.id)
37
+ || (payload.header && payload.header.id)
38
+ || 'unknown';
39
+ }
40
+
41
+ function pickMessages(payload) {
42
+ if (!payload || typeof payload !== 'object') return null;
43
+ const candidates = [
44
+ payload.messages,
45
+ payload.request && payload.request.messages,
46
+ payload.conversation && payload.conversation.messages,
47
+ payload.payload && payload.payload.messages,
48
+ ];
49
+ for (const candidate of candidates) {
50
+ if (Array.isArray(candidate)) return candidate;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function pickTools(payload) {
56
+ if (!payload || typeof payload !== 'object') return null;
57
+ const candidates = [
58
+ payload.tools,
59
+ payload.request && payload.request.tools,
60
+ payload.payload && payload.payload.tools,
61
+ ];
62
+ for (const candidate of candidates) {
63
+ if (Array.isArray(candidate)) {
64
+ // 工具定义也可能很长:只记数量与整体指纹,不逐条落账。
65
+ return { count: candidate.length, hash: `sha256:${sha256(JSON.stringify(candidate))}` };
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /** 把 LLM 请求归一化成 recorder 需要的最小形状;形状不认识就返回 null。 */
72
+ export function normalizeRequestPayload(payload) {
73
+ const messages = pickMessages(payload);
74
+ if (!messages) return null;
75
+ return {
76
+ sessionId: pickSessionId(payload),
77
+ requestId: payload.requestId || payload.requestID || null,
78
+ provider: payload.provider || payload.providerId || null,
79
+ model: payload.model || payload.modelId || null,
80
+ messages,
81
+ tools: pickTools(payload),
82
+ coverage: payload.coverage || null,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * 会话事件 → 入站条目。只认 `user/message`,其余事件(tool/result、
88
+ * developer/message、assistant/message)不是「用户进来的输入」。
89
+ */
90
+ export function normalizeSessionEvent(session, event) {
91
+ if (!event || typeof event !== 'object' || event.type !== 'user/message') return null;
92
+ const message = event.data;
93
+ if (!message || typeof message !== 'object') return null;
94
+ const content = message.content !== undefined ? message.content : message.text;
95
+ if (content === undefined || content === null) return null;
96
+ return {
97
+ sessionId: pickSessionId(session),
98
+ messageId: message.id || message.messageId || null,
99
+ kind: (message.source && message.source.kind) || message.kind || 'user',
100
+ content,
101
+ };
102
+ }
103
+
104
+ /** 兼容自定义 inboundEvent:载荷可能是单条、数组,或 { entries: [...] }。 */
105
+ export function normalizeInboundPayload(payload) {
106
+ if (!payload) return [];
107
+ const entries = Array.isArray(payload)
108
+ ? payload
109
+ : (Array.isArray(payload.entries) ? payload.entries : [payload]);
110
+ // 事件载荷通常在顶层带 sessionId,条目里不一定有:先继承顶层,再让条目覆盖。
111
+ const parentSessionId = pickSessionId(payload);
112
+ return entries
113
+ .map((entry) => {
114
+ if (!entry || typeof entry !== 'object') return null;
115
+ const message = entry.message && typeof entry.message === 'object' ? entry.message : entry;
116
+ const content = message.content !== undefined ? message.content : message.text;
117
+ if (content === undefined || content === null) return null;
118
+ if (typeof content === 'string' && content.length === 0) return null;
119
+ const entrySessionId = pickSessionId(entry);
120
+ const messageSessionId = pickSessionId(message);
121
+ return {
122
+ sessionId: entrySessionId !== 'unknown'
123
+ ? entrySessionId
124
+ : (messageSessionId !== 'unknown' ? messageSessionId : parentSessionId),
125
+ messageId: message.id || message.messageId || entry.messageId || null,
126
+ kind: (message.source && message.source.kind)
127
+ || (entry.source && entry.source.kind)
128
+ || message.kind
129
+ || 'user',
130
+ content,
131
+ };
132
+ })
133
+ .filter(Boolean);
134
+ }
135
+
136
+ /**
137
+ * 挂载 ingress 记录器并订阅请求/入站事件。
138
+ * 返回 { enabled, dispose, recorder, requestEvent, inboundEvent }。
139
+ */
140
+ export function attachIngress(ctx, {
141
+ ledger,
142
+ policy = {},
143
+ plugin = 'dsh-dashcam-host',
144
+ pluginVersion = null,
145
+ config = {},
146
+ log = console.warn,
147
+ } = {}) {
148
+ const settings = config && typeof config === 'object' ? config : {};
149
+ if (settings.enabled === false || !ledger) {
150
+ return {
151
+ enabled: false,
152
+ requestEvent: null,
153
+ inboundEvent: null,
154
+ recorder: null,
155
+ dispose() {},
156
+ };
157
+ }
158
+
159
+ const recorder = dashcam.createIngressRecorder({
160
+ ledger,
161
+ policy,
162
+ plugin,
163
+ pluginVersion,
164
+ log: (message) => log(message),
165
+ });
166
+ const requestEvent = settings.requestEvent || DEFAULT_REQUEST_EVENT;
167
+ const inboundEvent = settings.inboundEvent || DEFAULT_INBOUND_EVENT;
168
+
169
+ // waterfall 监听器必须返回 next();记录本身同步完成,不 await 流。
170
+ const onRequest = (payload, next) => {
171
+ try {
172
+ const normalized = normalizeRequestPayload(payload);
173
+ if (normalized) {
174
+ recorder.recordRequest(normalized);
175
+ // 每次请求成功后顺带补写失败队列;失败队列本身也有上限,不会无限涨。
176
+ recorder.flush();
177
+ } else {
178
+ log(`[dsh-dashcam] ingress:${requestEvent} 载荷里没有 messages 数组,已跳过`);
179
+ }
180
+ } catch (error) {
181
+ log(`[dsh-dashcam] ingress 请求事件异常(已忽略):${error.message}`);
182
+ }
183
+ return typeof next === 'function' ? next() : undefined;
184
+ };
185
+
186
+ // `session/event` 的签名是 (session, event);自定义事件仍按单参处理。
187
+ const onInbound = (first, second) => {
188
+ try {
189
+ const entries = second && typeof second === 'object' && typeof second.type === 'string'
190
+ ? [normalizeSessionEvent(first, second)].filter(Boolean)
191
+ : normalizeInboundPayload(first);
192
+ for (const entry of entries) recorder.recordInbound(entry);
193
+ } catch (error) {
194
+ log(`[dsh-dashcam] ingress 入站事件异常(已忽略):${error.message}`);
195
+ }
196
+ };
197
+
198
+ ctx.on(requestEvent, onRequest);
199
+ ctx.on(inboundEvent, onInbound);
200
+
201
+ return {
202
+ enabled: true,
203
+ recorder,
204
+ requestEvent,
205
+ inboundEvent,
206
+ dispose() {
207
+ try {
208
+ if (typeof ctx.off === 'function') {
209
+ ctx.off(requestEvent, onRequest);
210
+ ctx.off(inboundEvent, onInbound);
211
+ }
212
+ } catch {
213
+ // 卸载失败不影响 deactivate
214
+ }
215
+ recorder.dispose();
216
+ },
217
+ };
218
+ }