dsh-plugin-mobile-link 0.6.0 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/cordis.patch.yml +12 -0
- package/lib/client.js +101 -41
- package/lib/diagnostics.js +172 -0
- package/lib/dlp.js +10 -1
- package/lib/hello.js +35 -3
- package/lib/history/filter-scan.js +145 -0
- package/lib/history/records.js +28 -0
- package/lib/history/session-snapshot.js +119 -0
- package/lib/history.js +135 -24
- package/lib/index.js +16 -0
- package/lib/journal.js +279 -0
- package/lib/link.js +113 -8
- package/lib/pairing.js +60 -9
- package/lib/router.js +215 -2
- package/lib/routes.js +34 -34
- package/lib/session-digest.js +117 -0
- package/lib/status.js +32 -3
- package/package.json +1 -1
- package/lib/history/probe.js +0 -195
package/lib/history/probe.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { HOST_CALL_TIMEOUT_MS, withTimeout, messageOf } from './shared.js'
|
|
2
|
-
import { bytesOf, summarizeEvents } from './records.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 只读测量(spec 007 T008/T047):`GET /mobile-link/history-probe` 用,锁定映射之后删。
|
|
6
|
-
*
|
|
7
|
-
* 它只读、不改任何状态;`getQuery` 是个取值函数而不是服务对象本身,因为宿主注入
|
|
8
|
-
* (`ctx.inject(['sessionQuery'])`)可能在服务建好之后才发生。
|
|
9
|
-
*/
|
|
10
|
-
export function createProbeRunner({ getQuery, logger }) {
|
|
11
|
-
return async function probe(args = {}) {
|
|
12
|
-
const sessionId = args.sessionId
|
|
13
|
-
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
14
|
-
return { ok: false, error: 'sessionId is required' }
|
|
15
|
-
}
|
|
16
|
-
if (getQuery() === undefined) {
|
|
17
|
-
return { ok: false, error: 'ctx.getQuery() is not mounted in this Host' }
|
|
18
|
-
}
|
|
19
|
-
const limit = Number.isFinite(Number(args.n)) && Number(args.n) > 0 ? Number(args.n) : 60
|
|
20
|
-
const out = { ok: true, sessionId, limit }
|
|
21
|
-
|
|
22
|
-
// 1. Whole log: how many events, and is log position == seq?
|
|
23
|
-
const t0 = Date.now()
|
|
24
|
-
try {
|
|
25
|
-
const records = await withTimeout(getQuery().listEvents(sessionId), HOST_CALL_TIMEOUT_MS, 'listEvents')
|
|
26
|
-
const mismatches = []
|
|
27
|
-
records.forEach((record, index) => {
|
|
28
|
-
if (record.seq !== index && mismatches.length < 5) mismatches.push({ index, seq: record.seq })
|
|
29
|
-
})
|
|
30
|
-
out.listEvents = {
|
|
31
|
-
...summarizeEvents(records),
|
|
32
|
-
ms: Date.now() - t0,
|
|
33
|
-
offsetEqualsSeq: mismatches.length === 0,
|
|
34
|
-
firstMismatches: mismatches,
|
|
35
|
-
}
|
|
36
|
-
} catch (error) {
|
|
37
|
-
out.listEvents = { ms: Date.now() - t0, error: messageOf(error) }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 2. Exact window read around a seq (`readEvent`): raw events, contiguous.
|
|
41
|
-
const targetSeq = Number.isFinite(Number(args.seq)) ? Number(args.seq) : out.listEvents?.lastSeq
|
|
42
|
-
if (Number.isInteger(targetSeq) && targetSeq >= 0) {
|
|
43
|
-
const t1 = Date.now()
|
|
44
|
-
try {
|
|
45
|
-
const window = await withTimeout(
|
|
46
|
-
getQuery().readEvent({ sessionId, seq: targetSeq, before: limit, after: 0 }),
|
|
47
|
-
HOST_CALL_TIMEOUT_MS,
|
|
48
|
-
'readEvent',
|
|
49
|
-
)
|
|
50
|
-
out.readEvent = {
|
|
51
|
-
requested: { seq: targetSeq, before: limit, after: 0 },
|
|
52
|
-
...summarizeEvents(window.events),
|
|
53
|
-
startSeq: window.startSeq ?? null,
|
|
54
|
-
endSeq: window.endSeq ?? null,
|
|
55
|
-
rawEventCount: window.events?.length ?? 0,
|
|
56
|
-
bytes: bytesOf(window.events),
|
|
57
|
-
ms: Date.now() - t1,
|
|
58
|
-
}
|
|
59
|
-
} catch (error) {
|
|
60
|
-
out.readEvent = { requested: { seq: targetSeq, before: limit }, ms: Date.now() - t1, error: messageOf(error) }
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 3. Seq-range filter (`filterEvents`): message-aligned semantic documents.
|
|
65
|
-
if (Number.isInteger(targetSeq) && targetSeq >= 0) {
|
|
66
|
-
const from = Math.max(0, targetSeq - limit + 1)
|
|
67
|
-
const t2 = Date.now()
|
|
68
|
-
try {
|
|
69
|
-
const docs = await withTimeout(
|
|
70
|
-
getQuery().filterEvents(sessionId, [{ kind: 'seq', from, to: targetSeq }]),
|
|
71
|
-
HOST_CALL_TIMEOUT_MS,
|
|
72
|
-
'filterEvents(seq)',
|
|
73
|
-
)
|
|
74
|
-
out.filterEventsSeq = {
|
|
75
|
-
requested: { from, to: targetSeq },
|
|
76
|
-
...summarizeEvents(docs),
|
|
77
|
-
bytes: bytesOf(docs),
|
|
78
|
-
sample: docs.slice(0, 2).map((doc) => ({ seq: doc.seq, type: doc.type, textLen: doc.text?.length ?? 0 })),
|
|
79
|
-
ms: Date.now() - t2,
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
out.filterEventsSeq = { requested: { from, to: targetSeq }, ms: Date.now() - t2, error: messageOf(error) }
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 4. Literal text scan (`filterEvents(text)`): the fallback for search when
|
|
87
|
-
// the full-text index is closed (base bundle ships `openAt: never`).
|
|
88
|
-
if (typeof args.text === 'string' && args.text.length > 0) {
|
|
89
|
-
const t3 = Date.now()
|
|
90
|
-
try {
|
|
91
|
-
const docs = await withTimeout(
|
|
92
|
-
getQuery().filterEvents(sessionId, [{ kind: 'text', text: args.text }]),
|
|
93
|
-
HOST_CALL_TIMEOUT_MS,
|
|
94
|
-
'filterEvents(text)',
|
|
95
|
-
)
|
|
96
|
-
out.filterEventsText = {
|
|
97
|
-
query: args.text,
|
|
98
|
-
...summarizeEvents(docs),
|
|
99
|
-
sample: docs.slice(0, 3).map((doc) => ({
|
|
100
|
-
seq: doc.seq,
|
|
101
|
-
type: doc.type,
|
|
102
|
-
textLen: doc.text?.length ?? 0,
|
|
103
|
-
head: (doc.text ?? '').slice(0, 60),
|
|
104
|
-
})),
|
|
105
|
-
ms: Date.now() - t3,
|
|
106
|
-
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
out.filterEventsText = { query: args.text, ms: Date.now() - t3, error: messageOf(error) }
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 5. Indexed search (`searchEvents`): expected to fail while the SQLite
|
|
113
|
-
// index is closed, which is why step 4 must be able to carry search.
|
|
114
|
-
if (typeof args.text === 'string' && args.text.length > 0) {
|
|
115
|
-
const t4 = Date.now()
|
|
116
|
-
try {
|
|
117
|
-
const page = await withTimeout(
|
|
118
|
-
getQuery().searchEvents({ sessionId, query: args.text, limit: 20 }),
|
|
119
|
-
HOST_CALL_TIMEOUT_MS,
|
|
120
|
-
'searchEvents',
|
|
121
|
-
)
|
|
122
|
-
out.searchEvents = {
|
|
123
|
-
query: args.text,
|
|
124
|
-
count: page.items?.length ?? 0,
|
|
125
|
-
hasMore: page.hasMore ?? null,
|
|
126
|
-
first: page.items?.[0] ? { seq: page.items[0].seq, type: page.items[0].type } : null,
|
|
127
|
-
ms: Date.now() - t4,
|
|
128
|
-
}
|
|
129
|
-
} catch (error) {
|
|
130
|
-
out.searchEvents = { query: args.text, ms: Date.now() - t4, error: messageOf(error) }
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 7. Chunked literal scan: `filterEvents(text)` returns every match with its
|
|
135
|
-
// full text, so the connector must bound each call by a seq range and
|
|
136
|
-
// stop at the requested hit count. This measures that shape.
|
|
137
|
-
if (typeof args.text === 'string' && args.text.length > 0 && Number.isInteger(out.listEvents?.lastSeq ?? null)) {
|
|
138
|
-
const chunk = Number.isFinite(Number(args.chunk)) && Number(args.chunk) > 0 ? Number(args.chunk) : 2000
|
|
139
|
-
const wanted = Number.isFinite(Number(args.hits)) && Number(args.hits) > 0 ? Number(args.hits) : 50
|
|
140
|
-
const lastSeq = out.listEvents.lastSeq
|
|
141
|
-
const t6 = Date.now()
|
|
142
|
-
const hits = []
|
|
143
|
-
let calls = 0
|
|
144
|
-
let to = lastSeq
|
|
145
|
-
try {
|
|
146
|
-
while (to >= 0 && hits.length < wanted && calls < 50) {
|
|
147
|
-
const from = Math.max(0, to - chunk + 1)
|
|
148
|
-
const docs = await withTimeout(
|
|
149
|
-
getQuery().filterEvents(sessionId, [
|
|
150
|
-
{ kind: 'seq', from, to },
|
|
151
|
-
{ kind: 'text', text: args.text },
|
|
152
|
-
]),
|
|
153
|
-
HOST_CALL_TIMEOUT_MS,
|
|
154
|
-
`filterEvents(chunk ${from}-${to})`,
|
|
155
|
-
)
|
|
156
|
-
calls++
|
|
157
|
-
for (const doc of docs) {
|
|
158
|
-
if (hits.length >= wanted) break
|
|
159
|
-
hits.push({ seq: doc.seq, type: doc.type, textLen: doc.text?.length ?? 0 })
|
|
160
|
-
}
|
|
161
|
-
to = from - 1
|
|
162
|
-
}
|
|
163
|
-
out.chunkedText = {
|
|
164
|
-
query: args.text,
|
|
165
|
-
chunk,
|
|
166
|
-
calls,
|
|
167
|
-
hits: hits.length,
|
|
168
|
-
firstSeq: hits[0]?.seq ?? null,
|
|
169
|
-
lastSeq: hits[hits.length - 1]?.seq ?? null,
|
|
170
|
-
scannedFrom: to + 1,
|
|
171
|
-
ms: Date.now() - t6,
|
|
172
|
-
}
|
|
173
|
-
} catch (error) {
|
|
174
|
-
out.chunkedText = { query: args.text, calls, hits: hits.length, ms: Date.now() - t6, error: messageOf(error) }
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 6. Cold read: no Agent activation. `readSession` returns the log snapshot.
|
|
179
|
-
const t5 = Date.now()
|
|
180
|
-
try {
|
|
181
|
-
const snapshot = await withTimeout(getQuery().readSession(sessionId), HOST_CALL_TIMEOUT_MS, 'readSession')
|
|
182
|
-
out.readSession = {
|
|
183
|
-
events: snapshot.events?.length ?? null,
|
|
184
|
-
startSeq: snapshot.startSeq ?? null,
|
|
185
|
-
endSeq: snapshot.endSeq ?? null,
|
|
186
|
-
headerKeys: snapshot.session ? Object.keys(snapshot.session).slice(0, 8) : [],
|
|
187
|
-
ms: Date.now() - t5,
|
|
188
|
-
}
|
|
189
|
-
} catch (error) {
|
|
190
|
-
out.readSession = { ms: Date.now() - t5, error: messageOf(error) }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return out
|
|
194
|
-
}
|
|
195
|
-
}
|