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.
@@ -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
- }