dsh-manage-session 0.5.7 → 2.0.4
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.ja.md +33 -229
- package/CHANGELOG.ko.md +33 -229
- package/CHANGELOG.md +33 -363
- package/INSTALL.ja.md +164 -164
- package/INSTALL.ko.md +164 -164
- package/INSTALL.md +164 -164
- package/INSTALL.zh.md +164 -164
- package/LICENSE +21 -21
- package/README.en.md +41 -46
- package/README.ja.md +33 -57
- package/README.ko.md +32 -55
- package/README.md +64 -64
- package/cordis.patch.yml +19 -16
- package/dsh.plugin.json +4 -4
- package/lib/client.js +661 -3082
- package/lib/index.d.ts +79 -970
- package/lib/index.js +249 -2669
- package/locale/de.json +2 -2
- package/locale/en.json +2 -2
- package/locale/es.json +2 -2
- package/locale/fr.json +2 -2
- package/locale/it.json +2 -2
- package/locale/ja.json +2 -2
- package/locale/ko.json +2 -2
- package/locale/ru.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +15 -25
- package/screenshots.json +3 -3
- package/src/client/bridge-scope.ts +202 -213
- package/src/client/card.tsx +154 -154
- package/src/client/health-panel.tsx +49 -49
- package/src/client/host-api.ts +72 -6
- package/src/client/index.ts +594 -503
- package/src/client/locales.ts +893 -893
- package/src/client/manage-locales.ts +178 -0
- package/src/client/{search/manage.tsx → manage.tsx} +430 -404
- package/src/client/panel.tsx +131 -129
- package/src/client/session-menu-legacy.ts +2 -3
- package/src/client/session-menu.tsx +5 -5
- package/src/config.ts +43 -99
- package/src/host/health/attribution.ts +118 -118
- package/src/host/health/lossless.ts +128 -128
- package/src/host/health/repair.ts +2 -2
- package/src/host/health/source-kind.ts +350 -350
- package/src/host/history/archive-source.ts +250 -250
- package/src/host/history/archive.ts +249 -249
- package/src/host/history/favorites.ts +71 -71
- package/src/host/history/purge.ts +370 -370
- package/src/host/history/tombstones.ts +3 -3
- package/src/host/registry/archive-registry.ts +317 -317
- package/src/host/settings-bridge.ts +222 -264
- package/src/index.ts +66 -694
- package/src/client/search/card.tsx +0 -563
- package/src/client/search/host-api.ts +0 -213
- package/src/client/search/locales.ts +0 -861
- package/src/client/search/locate.ts +0 -330
- package/src/client/search/panel.tsx +0 -940
- package/src/client/search/platform.ts +0 -136
- package/src/host/index/archive-source.ts +0 -90
- package/src/host/index/drs-source.ts +0 -211
- package/src/host/index/engine.ts +0 -699
- package/src/host/index/extract.ts +0 -272
- package/src/host/index/peers.ts +0 -62
- package/src/host/index/rebuild.ts +0 -432
- package/src/host/index/schema.ts +0 -136
- package/src/host/index/snapshot.ts +0 -135
- package/src/host/index/sync.ts +0 -413
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Platform detection and the keyboard-label vocabulary, for the client half.
|
|
3
|
-
*
|
|
4
|
-
* Why this module exists
|
|
5
|
-
* ----------------------
|
|
6
|
-
* A shortcut hint is the one place where a single hard-coded string is wrong
|
|
7
|
-
* for most users: `⌘K` is meaningless on Windows, `Ctrl K` is wrong on macOS.
|
|
8
|
-
* The obvious implementation — one `navigator.platform` test — is also the
|
|
9
|
-
* fragile one: that API is deprecated, and it returns an empty string in some
|
|
10
|
-
* privacy configurations, which silently degrades every Mac user to the
|
|
11
|
-
* Windows vocabulary without any error to notice.
|
|
12
|
-
*
|
|
13
|
-
* This module is the single owner of both the platform answer and every label
|
|
14
|
-
* derived from it. The hint rendered in the sidebar, the hint rendered in the
|
|
15
|
-
* panel footer, and the chord the keydown handler actually matches all read
|
|
16
|
-
* from here — so a platform fix cannot land in one of them and miss another,
|
|
17
|
-
* and the advertised key cannot drift away from the bound key.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/** The navigator fields the detection reads (all optional, all untrusted). */
|
|
21
|
-
export interface NavigatorLike {
|
|
22
|
-
userAgentData?: { platform?: unknown }
|
|
23
|
-
platform?: unknown
|
|
24
|
-
userAgent?: unknown
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Resolve the running platform string, three tiers deep.
|
|
29
|
-
*
|
|
30
|
-
* 1. UA-CH (`navigator.userAgentData.platform`) — modern and precise.
|
|
31
|
-
* 2. `navigator.platform` — broadly available, deprecated, and empty in some
|
|
32
|
-
* privacy modes.
|
|
33
|
-
* 3. The UA string — last resort, still non-empty in practice.
|
|
34
|
-
*
|
|
35
|
-
* @param nav - the navigator to read; `undefined` is allowed (non-DOM host).
|
|
36
|
-
* @returns a lower-cased platform string, or `''` when nothing could be read.
|
|
37
|
-
*/
|
|
38
|
-
export function detectPlatform(nav: NavigatorLike | undefined): string {
|
|
39
|
-
const chPlatform = nav?.userAgentData?.platform
|
|
40
|
-
if (typeof chPlatform === 'string' && chPlatform !== '') return chPlatform.toLowerCase()
|
|
41
|
-
const legacy = nav?.platform
|
|
42
|
-
if (typeof legacy === 'string' && legacy !== '') return legacy.toLowerCase()
|
|
43
|
-
const agent = nav?.userAgent
|
|
44
|
-
if (typeof agent === 'string' && agent !== '') return agent.toLowerCase()
|
|
45
|
-
return ''
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** The keyboard vocabulary for one platform. */
|
|
49
|
-
export interface PlatformLabels {
|
|
50
|
-
/** The lower-cased platform string these labels were derived from. */
|
|
51
|
-
platform: string
|
|
52
|
-
isMac: boolean
|
|
53
|
-
isWindows: boolean
|
|
54
|
-
/** Invoke-key modifier: `⌘` on macOS, `Ctrl` elsewhere. */
|
|
55
|
-
modLabel: string
|
|
56
|
-
/** Secondary modifier: `⌥` on macOS, `Alt` elsewhere. */
|
|
57
|
-
altLabel: string
|
|
58
|
-
/** Shift: `⇧` on macOS, `Shift` elsewhere. */
|
|
59
|
-
shiftLabel: string
|
|
60
|
-
/** Confirm: the arrow glyph is legible on every platform. */
|
|
61
|
-
enterLabel: string
|
|
62
|
-
/** Escape: `esc` is the macOS spelling, `Esc` the one everywhere else. */
|
|
63
|
-
escLabel: string
|
|
64
|
-
/**
|
|
65
|
-
* The whole invoke chord as one face, ready for a key chip: `⌘K` on macOS,
|
|
66
|
-
* `Ctrl K` on Windows and Linux. `Ctrl K` keeps the space because `CtrlK`
|
|
67
|
-
* reads as a word rather than as two keys.
|
|
68
|
-
*/
|
|
69
|
-
invokeLabel: string
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Derive the label vocabulary from a platform string.
|
|
74
|
-
*
|
|
75
|
-
* An unrecognised platform — an empty string, or a UA that names neither
|
|
76
|
-
* system — takes the non-Mac vocabulary on purpose: `Ctrl` is legible to
|
|
77
|
-
* everyone, while `⌘` is opaque to anyone who has never used a Mac.
|
|
78
|
-
*
|
|
79
|
-
* @param platform - the string produced by {@link detectPlatform}.
|
|
80
|
-
* @returns the label vocabulary for that platform.
|
|
81
|
-
*/
|
|
82
|
-
export function platformLabels(platform: string): PlatformLabels {
|
|
83
|
-
const isMac = /mac|iphone|ipad|ipod/u.test(platform)
|
|
84
|
-
const isWindows = /win/u.test(platform)
|
|
85
|
-
const modLabel = isMac ? '⌘' : 'Ctrl'
|
|
86
|
-
return {
|
|
87
|
-
platform,
|
|
88
|
-
isMac,
|
|
89
|
-
isWindows,
|
|
90
|
-
modLabel,
|
|
91
|
-
altLabel: isMac ? '⌥' : 'Alt',
|
|
92
|
-
shiftLabel: isMac ? '⇧' : 'Shift',
|
|
93
|
-
enterLabel: '↵',
|
|
94
|
-
escLabel: isMac ? 'esc' : 'Esc',
|
|
95
|
-
invokeLabel: isMac ? `${modLabel}K` : `${modLabel} K`,
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Read `globalThis.navigator` without assuming a DOM (or a DOM lib). */
|
|
100
|
-
function currentNavigator(): NavigatorLike | undefined {
|
|
101
|
-
const candidate = (globalThis as { navigator?: unknown }).navigator
|
|
102
|
-
if (candidate === null || typeof candidate !== 'object') return undefined
|
|
103
|
-
return candidate as NavigatorLike
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** The labels for the platform this bundle is running on. */
|
|
107
|
-
export const LABELS: PlatformLabels = platformLabels(detectPlatform(currentNavigator()))
|
|
108
|
-
|
|
109
|
-
/** The minimal keydown face the chord test reads. */
|
|
110
|
-
export interface KeyChordEvent {
|
|
111
|
-
key: string
|
|
112
|
-
ctrlKey: boolean
|
|
113
|
-
metaKey: boolean
|
|
114
|
-
altKey: boolean
|
|
115
|
-
shiftKey: boolean
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Whether a keydown event is the invoke chord advertised by
|
|
120
|
-
* {@link PlatformLabels.invokeLabel}.
|
|
121
|
-
*
|
|
122
|
-
* The hint and the binding have to agree: a footer promising `⌘K` while the
|
|
123
|
-
* handler matches `Ctrl+K` is worse than showing no hint at all. Both sides
|
|
124
|
-
* call this, so neither can be changed alone. Requiring the other modifier to
|
|
125
|
-
* be *absent* keeps the two chords distinct instead of letting either one
|
|
126
|
-
* satisfy both platforms.
|
|
127
|
-
*
|
|
128
|
-
* @param event - the keydown payload.
|
|
129
|
-
* @param isMac - the running platform's macOS-ness, from {@link LABELS}.
|
|
130
|
-
* @returns `true` when the event is the invoke chord.
|
|
131
|
-
*/
|
|
132
|
-
export function isInvokeChord(event: KeyChordEvent, isMac: boolean): boolean {
|
|
133
|
-
if (event.key !== 'k' && event.key !== 'K') return false
|
|
134
|
-
if (event.altKey || event.shiftKey) return false
|
|
135
|
-
return isMac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey
|
|
136
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Official archive-set source resolution.
|
|
3
|
-
*
|
|
4
|
-
* Primary: the in-process `workspaceRegistry` service (the same fact the UI
|
|
5
|
-
* filters by). Fallback: read the canonical storage hub file directly — the
|
|
6
|
-
* workspace domain persists `archivedSessionIds` under the `global` segment
|
|
7
|
-
* (storage-json: `~/.dsh/storages/workspace.json`; storage-sqlite variant
|
|
8
|
-
* exists but the JSON fallback file is what stock web profiles ship).
|
|
9
|
-
* Resolution order is decided per read; failures degrade to "no archive set"
|
|
10
|
-
* and are reported through the diagnostics face.
|
|
11
|
-
*
|
|
12
|
-
* READ-ONLY by contract. The archive set is WRITTEN by dsh-manage-session
|
|
13
|
-
* (会话管家 → 病案室); this package only consumes it to keep archived sessions
|
|
14
|
-
* out of the index. Format contract: `dsh-归档文件格式契约-20260914.md`.
|
|
15
|
-
*/
|
|
16
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
17
|
-
import { homedir } from 'node:os'
|
|
18
|
-
import { join } from 'node:path'
|
|
19
|
-
|
|
20
|
-
/** One resolved archive read: ids plus which source served them. */
|
|
21
|
-
export interface SwitchArchiveRead {
|
|
22
|
-
ids: readonly string[]
|
|
23
|
-
source: 'registry' | 'storage-file' | 'none'
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** The workspaceRegistry mirror face (getter only). */
|
|
27
|
-
export interface SwitchRegistryFace {
|
|
28
|
-
readonly archivedSessionIds: readonly string[]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Diagnostics describing how the archive set is being resolved. */
|
|
32
|
-
export interface SwitchArchiveDiagnostics {
|
|
33
|
-
source: 'registry' | 'storage-file' | 'none'
|
|
34
|
-
ids: number
|
|
35
|
-
error?: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** DSH storage hub candidates for the workspace domain (json backend). */
|
|
39
|
-
function storageFileCandidates(): string[] {
|
|
40
|
-
return [
|
|
41
|
-
join(homedir(), '.dsh', 'storages', 'workspace.json'),
|
|
42
|
-
]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Parse the storage hub file's global.archivedSessionIds; throw on malformed content. */
|
|
46
|
-
function readStorageFile(path: string): readonly string[] {
|
|
47
|
-
const parsed = JSON.parse(readFileSync(path, 'utf8')) as {
|
|
48
|
-
global?: { archivedSessionIds?: unknown }
|
|
49
|
-
}
|
|
50
|
-
const ids = parsed.global?.archivedSessionIds
|
|
51
|
-
if (!Array.isArray(ids)) throw new Error(`storage hub "${path}" holds no global.archivedSessionIds array`)
|
|
52
|
-
return ids.filter((id): id is string => typeof id === 'string')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Resolve the official archive set once.
|
|
57
|
-
* @param registry - lazy workspaceRegistry face (may be absent or throw).
|
|
58
|
-
* @returns the archive ids plus which source served them.
|
|
59
|
-
*/
|
|
60
|
-
export function readArchiveSet(registry?: SwitchRegistryFace): SwitchArchiveRead {
|
|
61
|
-
if (registry !== undefined) {
|
|
62
|
-
try {
|
|
63
|
-
const ids = registry.archivedSessionIds
|
|
64
|
-
if (Array.isArray(ids)) return { ids, source: 'registry' }
|
|
65
|
-
} catch { /* registry not started yet — fall through to the file */ }
|
|
66
|
-
}
|
|
67
|
-
for (const path of storageFileCandidates()) {
|
|
68
|
-
if (!existsSync(path)) continue
|
|
69
|
-
try {
|
|
70
|
-
return { ids: readStorageFile(path), source: 'storage-file' }
|
|
71
|
-
} catch { /* malformed file — try the next candidate */ }
|
|
72
|
-
}
|
|
73
|
-
return { ids: [], source: 'none' }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Build the lazy source face the syncer expects, with diagnostics capture. */
|
|
77
|
-
export function createArchiveSource(getRegistry: () => SwitchRegistryFace | undefined): {
|
|
78
|
-
read: () => SwitchArchiveRead
|
|
79
|
-
diagnostics: () => SwitchArchiveDiagnostics
|
|
80
|
-
} {
|
|
81
|
-
let last: SwitchArchiveDiagnostics = { source: 'none', ids: 0 }
|
|
82
|
-
return {
|
|
83
|
-
read: () => {
|
|
84
|
-
const read = readArchiveSet(getRegistry())
|
|
85
|
-
last = { source: read.source, ids: read.ids.length }
|
|
86
|
-
return read
|
|
87
|
-
},
|
|
88
|
-
diagnostics: () => ({ ...last }),
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DRS 语料源:dsh-reference-session-collector 投影库的 HTTP 只读客户端(T7.1)。
|
|
3
|
-
*
|
|
4
|
-
* 语料契约 = DRS 单条 fenced 路由 `GET /api/dsh-reference-session-collector`:
|
|
5
|
-
* - `op=sessions&offset=&limit=` → `{ok, total, rows:[sessions 行]}`(分页);
|
|
6
|
-
* - `op=session&session=<id>&fields=search` → `{ok, session, rows:[检索臂]}`。
|
|
7
|
-
*
|
|
8
|
-
* 字段对照(T7.0,DRS wire snake → steward 引擎列):
|
|
9
|
-
* sessions.version ← artifact_mtime_ms(水位代理:日志重写必 bump mtime);
|
|
10
|
-
* sessions.updated_at ← last_time_ms;title/cwd/archived ← 同名列;
|
|
11
|
-
* docs.type ← kind;docs.surface ← on_surface(1='current'/0='shadowed');
|
|
12
|
-
* docs.time ← time;docs.text ← search_text;index_text 由本引擎
|
|
13
|
-
* segmentForIndex 重算(自有切分面不动)。
|
|
14
|
-
*
|
|
15
|
-
* 自调用信任面:DRS 的 Host 门是精确匹配(trustedHosts 全小写比对),本客户
|
|
16
|
-
* 端可带 hostHeader 显式回显首个 trustedHosts 条目,规避「条目省略端口而
|
|
17
|
-
* URL 带端口」的精确匹配失配;loopback + 无 trustedHosts 的老宿主天然放行。
|
|
18
|
-
*/
|
|
19
|
-
/** DRS API 路径(与 dsh-reference-session-collector 的 DRS_API_PATH 字面一致;不跨包 import)。 */
|
|
20
|
-
export const DRS_API_PATH = '/api/dsh-reference-session-collector'
|
|
21
|
-
|
|
22
|
-
/** 语料列表分页大小(百会话级一轮 1-2 页;DRS 侧 offset/limit 透传)。 */
|
|
23
|
-
export const DRS_CORPUS_PAGE = 500
|
|
24
|
-
|
|
25
|
-
/** 一个 DRS 会话头(watermark 代理 + 检索排序字段)。 */
|
|
26
|
-
export interface DrsCorpusHead {
|
|
27
|
-
sessionId: string
|
|
28
|
-
version: number
|
|
29
|
-
title: string
|
|
30
|
-
cwd: string
|
|
31
|
-
updatedAt: number
|
|
32
|
-
archived: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 一个 DRS 检索臂文档(surface 已折算成引擎列的字符串域)。 */
|
|
36
|
-
export interface DrsCorpusDoc {
|
|
37
|
-
seq: number
|
|
38
|
-
type: string
|
|
39
|
-
surface: 'current' | 'shadowed'
|
|
40
|
-
time: number
|
|
41
|
-
text: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 一个 DRS 会话的完整语料(头 + docs)。 */
|
|
45
|
-
export interface DrsCorpusSession {
|
|
46
|
-
head: DrsCorpusHead
|
|
47
|
-
docs: DrsCorpusDoc[]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface DrsCorpusSourceOptions {
|
|
51
|
-
/** DRS API 基址(http://host[:port];解析序见 resolveDrsBaseUrl)。 */
|
|
52
|
-
baseUrl: string
|
|
53
|
-
/** 注入面(单测);缺省全局 fetch。 */
|
|
54
|
-
fetchImpl?: typeof fetch
|
|
55
|
-
/** 显式 Host 头(trustedHosts 首条回显;缺省不覆盖)。 */
|
|
56
|
-
hostHeader?: string
|
|
57
|
-
log?: (message: string) => void
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 基址解析:显式配置 > 首个 trustedHosts 条目(补 http:// 前缀)>
|
|
62
|
-
* `http://127.0.0.1:8080`(宿主默认口;trustedHosts 缺席的老线 DRS 门本就放行)。
|
|
63
|
-
*/
|
|
64
|
-
export function resolveDrsBaseUrl(explicit: string | undefined, trustedHosts?: readonly string[]): string {
|
|
65
|
-
const trimmed = explicit?.trim() ?? ''
|
|
66
|
-
const pick = trimmed !== '' ? trimmed : trustedHosts?.find(entry => entry.trim() !== '')?.trim()
|
|
67
|
-
if (pick === undefined || pick === '') return 'http://127.0.0.1:8080'
|
|
68
|
-
return /^https?:\/\//u.test(pick) ? pick : `http://${pick}`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class DrsCorpusSource {
|
|
72
|
-
private readonly fetchImpl: typeof fetch
|
|
73
|
-
|
|
74
|
-
constructor(private readonly options: DrsCorpusSourceOptions) {
|
|
75
|
-
this.fetchImpl = options.fetchImpl ?? fetch
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** 一个 GET 的 JSON 包络(ok!==true 或非 2xx 一律抛,由调用方回退 native)。 */
|
|
79
|
-
private async getJson(query: string, atHost?: string): Promise<Record<string, unknown>> {
|
|
80
|
-
// atHost=同源转发臂(调用方请求的 Host 头):宿主端口动态分配时
|
|
81
|
-
// 配置缺省口(8080)猜不中——同源直连以请求 Host 优先。
|
|
82
|
-
const source = atHost !== undefined && atHost !== '' ? `http://${atHost}` : this.options.baseUrl
|
|
83
|
-
const base = source.replace(/\/+$/u, '')
|
|
84
|
-
const url = `${base}${DRS_API_PATH}${query}`
|
|
85
|
-
let response: Response
|
|
86
|
-
try {
|
|
87
|
-
response = await this.fetchImpl(url, {
|
|
88
|
-
headers: {
|
|
89
|
-
accept: 'application/json',
|
|
90
|
-
...(this.options.hostHeader === undefined ? {} : { host: this.options.hostHeader }),
|
|
91
|
-
},
|
|
92
|
-
})
|
|
93
|
-
} catch (err) {
|
|
94
|
-
throw new Error(`DRS ${query}: ${String(err instanceof Error ? err.message : err)}`)
|
|
95
|
-
}
|
|
96
|
-
if (!response.ok) throw new Error(`DRS ${query} → HTTP ${String(response.status)}`)
|
|
97
|
-
const body = await response.json() as unknown
|
|
98
|
-
if (typeof body !== 'object' || body === null) throw new Error(`DRS ${query}: 响应非 JSON 对象`)
|
|
99
|
-
const record = body as Record<string, unknown>
|
|
100
|
-
if (record['ok'] !== true) {
|
|
101
|
-
throw new Error(`DRS ${query}: ok=false(${String(record['error'] ?? 'unknown')})`)
|
|
102
|
-
}
|
|
103
|
-
return record
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private static headOf(row: Record<string, unknown>, fallbackId: string): DrsCorpusHead {
|
|
107
|
-
return {
|
|
108
|
-
sessionId: String(row['session_id'] ?? fallbackId),
|
|
109
|
-
version: Number(row['artifact_mtime_ms'] ?? 0),
|
|
110
|
-
title: String(row['title'] ?? ''),
|
|
111
|
-
cwd: String(row['cwd'] ?? ''),
|
|
112
|
-
updatedAt: Number(row['last_time_ms'] ?? 0),
|
|
113
|
-
archived: Number(row['archived'] ?? 0) === 1,
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 全量语料头(内部翻页;rows 空或凑满 total 即止)。 */
|
|
118
|
-
async listSessions(): Promise<DrsCorpusHead[]> {
|
|
119
|
-
const heads: DrsCorpusHead[] = []
|
|
120
|
-
let offset = 0
|
|
121
|
-
for (;;) {
|
|
122
|
-
const page = await this.getJson(`?op=sessions&offset=${String(offset)}&limit=${String(DRS_CORPUS_PAGE)}`)
|
|
123
|
-
const rows = Array.isArray(page['rows']) ? page['rows'] as Record<string, unknown>[] : []
|
|
124
|
-
for (const row of rows) {
|
|
125
|
-
const head = DrsCorpusSource.headOf(row, '')
|
|
126
|
-
if (head.sessionId !== '') heads.push(head)
|
|
127
|
-
}
|
|
128
|
-
offset += rows.length
|
|
129
|
-
const total = Number(page['total'] ?? heads.length)
|
|
130
|
-
if (rows.length === 0 || offset >= total) break
|
|
131
|
-
}
|
|
132
|
-
return heads
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** 一个会话的检索臂 docs;DRS 无此会话(session:null)返回 undefined。 */
|
|
136
|
-
async readSession(sessionId: string): Promise<DrsCorpusSession | undefined> {
|
|
137
|
-
const body = await this.getJson(`?op=session&session=${encodeURIComponent(sessionId)}&fields=search`)
|
|
138
|
-
if (body['session'] === null || body['session'] === undefined) return undefined
|
|
139
|
-
const head = DrsCorpusSource.headOf(body['session'] as Record<string, unknown>, sessionId)
|
|
140
|
-
const docs = (Array.isArray(body['rows']) ? body['rows'] as Record<string, unknown>[] : []).map(row => ({
|
|
141
|
-
seq: Number(row['seq'] ?? 0),
|
|
142
|
-
type: String(row['kind'] ?? ''),
|
|
143
|
-
surface: Number(row['on_surface'] ?? 1) === 1 ? 'current' as const : 'shadowed' as const,
|
|
144
|
-
time: Number(row['time'] ?? 0),
|
|
145
|
-
text: String(row['search_text'] ?? ''),
|
|
146
|
-
}))
|
|
147
|
-
return { head, docs }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* 分组内容搜索直连臂(op=search&group=session,DRS surface.searchGrouped 同构):
|
|
152
|
-
* 会话分组/加权/过滤/锚定节选全部在 DRS 侧完成,本方法只搬 wire →
|
|
153
|
-
* 引擎命中形状(SwitchSearchHit 字段同名)。失败(网络/非 2xx/包络)直抛,
|
|
154
|
-
* 调用侧按硬依赖语义显式报错——**不回退本地索引**。
|
|
155
|
-
*/
|
|
156
|
-
async searchGrouped(request: {
|
|
157
|
-
query: string
|
|
158
|
-
/** 原事件 type 列表(调用方展开粗类);空=DRS 缺省 user+assistant。 */
|
|
159
|
-
kinds?: readonly string[]
|
|
160
|
-
archived?: 'active' | 'archived' | 'all'
|
|
161
|
-
from?: number
|
|
162
|
-
to?: number
|
|
163
|
-
sortBy?: 'relevance' | 'time'
|
|
164
|
-
limit?: number
|
|
165
|
-
/** 命中展开(wire `expand=1`):同会话多命中各自成行;缺省聚合。 */
|
|
166
|
-
expand?: boolean
|
|
167
|
-
/** 同源 Host 覆盖(转发调用方请求头;优先于装配 baseUrl)。 */
|
|
168
|
-
atHost?: string
|
|
169
|
-
}): Promise<{
|
|
170
|
-
sessionId: string
|
|
171
|
-
title: string
|
|
172
|
-
seq: number
|
|
173
|
-
type: string
|
|
174
|
-
time: number
|
|
175
|
-
updatedAt: number
|
|
176
|
-
snippet: string
|
|
177
|
-
}[]> {
|
|
178
|
-
const params = new URLSearchParams()
|
|
179
|
-
params.set('op', 'search')
|
|
180
|
-
params.set('group', 'session')
|
|
181
|
-
params.set('q', request.query)
|
|
182
|
-
if (request.kinds !== undefined && request.kinds.length > 0) params.set('kinds', request.kinds.join(','))
|
|
183
|
-
if (request.archived !== undefined) params.set('archived', request.archived)
|
|
184
|
-
if (request.from !== undefined) params.set('from', String(request.from))
|
|
185
|
-
if (request.to !== undefined) params.set('to', String(request.to))
|
|
186
|
-
if (request.sortBy === 'time') params.set('sort', 'time')
|
|
187
|
-
if (request.limit !== undefined) params.set('limit', String(request.limit))
|
|
188
|
-
if (request.expand === true) params.set('expand', '1')
|
|
189
|
-
const body = await this.getJson(`?${params.toString()}`, request.atHost)
|
|
190
|
-
const hits = Array.isArray(body['hits']) ? body['hits'] as Record<string, unknown>[] : []
|
|
191
|
-
return hits.map(row => ({
|
|
192
|
-
sessionId: String(row['session_id'] ?? ''),
|
|
193
|
-
title: String(row['title'] ?? ''),
|
|
194
|
-
seq: Number(row['seq'] ?? 0),
|
|
195
|
-
type: String(row['type'] ?? ''),
|
|
196
|
-
time: Number(row['time'] ?? 0),
|
|
197
|
-
updatedAt: Number(row['updated_at'] ?? 0),
|
|
198
|
-
snippet: String(row['snippet'] ?? ''),
|
|
199
|
-
}))
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** 可性探针(op=status):硬依赖预检/降级提示用;失败返回 false。 */
|
|
203
|
-
async probe(atHost?: string): Promise<boolean> {
|
|
204
|
-
try {
|
|
205
|
-
await this.getJson('?op=status', atHost)
|
|
206
|
-
return true
|
|
207
|
-
} catch {
|
|
208
|
-
return false
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|