@goodandready/dsh-session-control 0.1.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.
package/lib/index.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Серверная половина dsh-session-control.
3
+ *
4
+ * Здесь живут пространство настроек (закрепления и скрытия) и один маршрут —
5
+ * расшифровка заархивированной сессии.
6
+ *
7
+ * Почему маршрут вообще понадобился. Заархивированную сессию нельзя открыть в
8
+ * браузере: окно истории у клиента поднимается только для сессии «на сцене»,
9
+ * то есть текущей, а текущей архивная быть не может — политика ядра снимает
10
+ * такой выбор. Потоковый `session/follow` по обычному запросу не открывается,
11
+ * ему нужен потоковый переносчик, а `session/page` отвечает «failed to observe
12
+ * session», пока за сессией не следят.
13
+ *
14
+ * Зато на хосте есть штатная дверь: `sessionPersistence.readRaw(id)` отдаёт
15
+ * сохранённый лог, не поднимая живую сессию. Через неё мы и читаем.
16
+ */
17
+ import z from '@deepseek-ai/schemastery'
18
+ import { transcriptFromEvents } from './transcript.js'
19
+
20
+ export const name = 'dsh-session-control'
21
+
22
+ /** Пространство настроек. Совпадает с ключом карточки settings.plugin.item. */
23
+ export const NS = 'dsh-session-control'
24
+
25
+ /** Сколько сообщений отдаём за один запрос: расшифровка читается, а не листается бесконечно. */
26
+ const TRANSCRIPT_LIMIT = 400
27
+
28
+ export const Config = z.object({
29
+ /**
30
+ * Идентификаторы закреплённых сессий. Порядок массива — порядок в панели.
31
+ * Закрепление общее на всю панель, а не внутри папки.
32
+ */
33
+ pinned: z.array(z.string()).default([]),
34
+ /**
35
+ * Идентификаторы скрытых сессий. Наше скрытие обратимо, в отличие от
36
+ * ядрового архива: WorkspaceRegistry.archiveSession только добавляет, метода
37
+ * возврата в API нет.
38
+ */
39
+ hidden: z.array(z.string()).default([]),
40
+ /**
41
+ * Прятать сессии без единого сообщения. Их непрерывно плодят расписания,
42
+ * мессенджер и доска, и в списке они мешают настоящим разговорам.
43
+ * Текущая сессия не прячется никогда, даже пустая: иначе пропадёт строка,
44
+ * в которую человек как раз собирается писать.
45
+ */
46
+ hideBlank: z.boolean().default(true),
47
+ })
48
+
49
+
50
+
51
+ export function apply(ctx, config) {
52
+ ctx.inject(['settings'], (sctx) => {
53
+ sctx.settings.register(NS, Config, { base: config })
54
+ })
55
+
56
+ ctx.inject(['webServer', 'sessionPersistence'], (rctx) => {
57
+ rctx.effect(() => rctx.webServer.register({
58
+ kind: 'exact',
59
+ path: '/dsh-session-control/transcript',
60
+ handler: async (req, res) => {
61
+ const reply = (code, body) => {
62
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' })
63
+ res.end(JSON.stringify(body))
64
+ }
65
+ try {
66
+ const url = new URL(req.url, 'http://localhost')
67
+ const sessionId = url.searchParams.get('session')
68
+ if (sessionId === null || sessionId === '') {
69
+ reply(400, { ok: false, error: 'session parameter is required' })
70
+ return
71
+ }
72
+ // Сервис берём через get, а не как свойство контекста: так делает и
73
+ // ядровый экспорт логов.
74
+ const store = rctx.get('sessionPersistence')
75
+ // Поверхность бэкенда различается между выпусками ядра: где-то есть
76
+ // readRaw, где-то пара resolveLog + readStoredLog. Спрашиваем, что
77
+ // умеет этот, вместо того чтобы полагаться на версию.
78
+ if (store === undefined
79
+ || typeof store.resolveLog !== 'function'
80
+ || typeof store.readStoredLog !== 'function') {
81
+ reply(501, { ok: false, error: 'this session backend cannot read stored logs' })
82
+ return
83
+ }
84
+ // Старые логи формата v0 ядро отказывается мигрировать при чтении
85
+ // и возвращает пустой лог, не сообщая об отказе. Отличаем это от
86
+ // действительно пустой сессии: врать «сообщений нет» нельзя.
87
+ let messages = []
88
+ let unreadable = false
89
+ let missing = false
90
+ try {
91
+ // resolveLog сам запускает миграцию формата и бросает на старых
92
+ // логах, поэтому под перехватом должен быть и он, а не только
93
+ // чтение.
94
+ const path = await store.resolveLog(sessionId)
95
+ if (path === undefined) {
96
+ missing = true
97
+ } else {
98
+ const log = await store.readStoredLog(path, sessionId)
99
+ const events = (log && log.events) || []
100
+ messages = transcriptFromEvents(events)
101
+ unreadable = events.length === 0
102
+ }
103
+ } catch (refused) {
104
+ unreadable = true
105
+ }
106
+ if (missing) {
107
+ reply(404, { ok: false, error: 'session not found' })
108
+ return
109
+ }
110
+ if (unreadable) {
111
+ reply(200, { ok: true, sessionId, unreadable: true, total: 0, truncated: false, messages: [] })
112
+ return
113
+ }
114
+ reply(200, {
115
+ ok: true,
116
+ sessionId,
117
+ total: messages.length,
118
+ truncated: messages.length > TRANSCRIPT_LIMIT,
119
+ messages: messages.slice(-TRANSCRIPT_LIMIT),
120
+ })
121
+ } catch (failure) {
122
+ reply(500, { ok: false, error: (failure && failure.message) || String(failure) })
123
+ }
124
+ },
125
+ }), 'dsh-session-control: маршрут расшифровки')
126
+ })
127
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Превращение лога сессии в расшифровку.
3
+ *
4
+ * Модуль намеренно без единой внешней зависимости: это чистая логика, и она
5
+ * должна проверяться тестами без харнесса и без установки профиля.
6
+ */
7
+
8
+ /**
9
+ * Отобрать из событий то, что человек считает разговором.
10
+ *
11
+ * Берём его собственные сообщения, ответы агента и факт вызова инструмента.
12
+ * Рассуждения модели пропускаем — это её черновик, а не содержание беседы.
13
+ *
14
+ * @param events - события лога в порядке записи.
15
+ * @returns сообщения в том же порядке.
16
+ */
17
+ export function transcriptFromEvents(events) {
18
+ const out = []
19
+ for (const event of events || []) {
20
+ const kind = event && event.type
21
+ if (kind === 'user/message' || kind === 'assistant/message') {
22
+ // Две формы в одном логе: у сообщений пользователя содержимое лежит
23
+ // прямо в data.content, у ответов агента — в data.message.content.
24
+ // Читаем обе, иначе половина разговора пропадает молча.
25
+ const data = event.data || {}
26
+ const message = data.message || {}
27
+ const parts = Array.isArray(data.content)
28
+ ? data.content
29
+ : (Array.isArray(message.content) ? message.content : [])
30
+ const text = parts
31
+ .filter((part) => part && part.type === 'text' && typeof part.text === 'string')
32
+ .map((part) => part.text)
33
+ .join('\n')
34
+ if (text.trim() === '') continue
35
+ out.push({ role: kind === 'user/message' ? 'user' : 'assistant', text, time: event.time })
36
+ } else if (kind === 'tool/call') {
37
+ const data = event.data || {}
38
+ const toolName = data.name || (data.call && data.call.name)
39
+ if (typeof toolName === 'string' && toolName !== '') {
40
+ out.push({ role: 'tool', text: toolName, time: event.time })
41
+ }
42
+ }
43
+ }
44
+ return out
45
+ }
46
+
47
+ /**
48
+ * То же самое, но из сырого текста лога — одна JSON-запись на строку.
49
+ *
50
+ * Битые строки пропускаем: одна повреждённая запись не должна лишать человека
51
+ * всей остальной расшифровки.
52
+ *
53
+ * @param raw - содержимое файла лога.
54
+ * @returns сообщения в порядке лога.
55
+ */
56
+ export function parseTranscript(raw) {
57
+ const events = []
58
+ for (const line of String(raw).split('\n')) {
59
+ if (line.trim() === '') continue
60
+ try {
61
+ events.push(JSON.parse(line))
62
+ } catch (notJson) {
63
+ continue
64
+ }
65
+ }
66
+ return transcriptFromEvents(events)
67
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@goodandready/dsh-session-control",
3
+ "version": "0.1.0",
4
+ "description": "Session management for the DeepSeek Harness sidebar: pin conversations, search their contents, read archived transcripts and hide the noise",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "lib/",
15
+ "cordis.patch.yml",
16
+ "README.md",
17
+ "CHANGELOG.md",
18
+ "LICENSE"
19
+ ],
20
+ "dsh": {
21
+ "bundle": {
22
+ "patch": "./cordis.patch.yml"
23
+ },
24
+ "client": {
25
+ "platform": "web",
26
+ "inject": [
27
+ "@deepseek-ai/dsh-api-remotes",
28
+ "@deepseek-ai/dsh-api-session-controller",
29
+ "@deepseek-ai/dsh-api-workspace-controller",
30
+ "@deepseek-ai/dsh-client-connection",
31
+ "@deepseek-ai/dsh-client-locale",
32
+ "@deepseek-ai/dsh-client-ui-conversation",
33
+ "@deepseek-ai/dsh-client-ui-renderer",
34
+ "@deepseek-ai/dsh-client-ui-session",
35
+ "@deepseek-ai/dsh-client-ui-settings",
36
+ "@deepseek-ai/dsh-client-ui-sidebar"
37
+ ]
38
+ }
39
+ },
40
+ "peerDependencies": {
41
+ "@deepseek-ai/cordis": "^4.0.2"
42
+ },
43
+ "scripts": {
44
+ "test": "node --test test/*.test.mjs"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/GooDAnDReaDY/dsh-session-control.git"
49
+ },
50
+ "homepage": "https://github.com/GooDAnDReaDY/dsh-session-control#readme",
51
+ "bugs": {
52
+ "url": "https://github.com/GooDAnDReaDY/dsh-session-control/issues"
53
+ }
54
+ }