@okostream/mcp 0.12.2 → 0.12.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/README.md +38 -0
- package/dist/server.js +120498 -41
- package/package.json +8 -9
- package/dist/auth.js +0 -115
- package/dist/env.js +0 -71
- package/dist/firebase.js +0 -31
- package/dist/firebaseDefaults.js +0 -11
- package/dist/guides.js +0 -266
- package/dist/networkPolicy.js +0 -18
- package/dist/onboarding.js +0 -126
- package/dist/scopes.js +0 -22
- package/dist/sessionRepair.js +0 -43
- package/dist/telegramChannelSchema.js +0 -13
- package/dist/tools/account.js +0 -77
- package/dist/tools/audit.js +0 -44
- package/dist/tools/chat.js +0 -228
- package/dist/tools/context.js +0 -40
- package/dist/tools/eventAccess.js +0 -65
- package/dist/tools/eventCreate.js +0 -43
- package/dist/tools/events.js +0 -497
- package/dist/tools/formatting.js +0 -216
- package/dist/tools/imagePipeline.js +0 -597
- package/dist/tools/images.js +0 -243
- package/dist/tools/instrumentation.js +0 -40
- package/dist/tools/leads.js +0 -85
- package/dist/tools/lifecycle.js +0 -168
- package/dist/tools/live.js +0 -227
- package/dist/tools/notifications.js +0 -110
- package/dist/tools/schemas.js +0 -329
- package/dist/tools/sessions.js +0 -313
- package/dist/tools/setupValidation.js +0 -144
- package/dist/tools/telegram.js +0 -297
- package/dist/tools/workspaceAccess.js +0 -60
- package/dist/tools/workspaces.js +0 -345
- package/dist/tools.js +0 -38
- package/dist/utils.js +0 -76
- package/dist/version.js +0 -2
- package/dist/videoUrl.js +0 -166
package/dist/tools/formatting.js
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
// Пагинация, курсоры и приведение данных к виду, который читает модель.
|
|
2
|
-
//
|
|
3
|
-
// Курсор — непрозрачная для клиента строка: пара `timestampCursorFrom` и
|
|
4
|
-
// `parseTimestampCursor` обязана быть обратимой, иначе листание молча
|
|
5
|
-
// зацикливается или теряет страницу.
|
|
6
|
-
//
|
|
7
|
-
// Отчёт по сессии собирается в Markdown здесь, а не в обработчике: модели
|
|
8
|
-
// нужен текст, пригодный к пересказу, а не сырой документ.
|
|
9
|
-
import { Timestamp } from "firebase/firestore";
|
|
10
|
-
import { csvCell, safeString, serialize } from "../utils.js";
|
|
11
|
-
import { detectAndNormalize, extractIframeSrc } from "../videoUrl.js";
|
|
12
|
-
import { eventUrl, sessionReportUrl } from "./context.js";
|
|
13
|
-
export function cursorMeta(count, max, hasMore, nextCursor) {
|
|
14
|
-
return {
|
|
15
|
-
count,
|
|
16
|
-
limit: max,
|
|
17
|
-
hasMore,
|
|
18
|
-
nextCursor,
|
|
19
|
-
has_more: hasMore,
|
|
20
|
-
next_cursor: nextCursor,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
export function pickPage(items, max) {
|
|
24
|
-
return {
|
|
25
|
-
page: items.slice(0, max),
|
|
26
|
-
hasMore: items.length > max,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Значение `video.embedUrl` попадает прямо в `<iframe src>` на публичной
|
|
31
|
-
* странице события. Набор протоколов здесь строже, чем у кнопок
|
|
32
|
-
* (`normalizeCardButtonUrl`): `mailto:` и `tel:` в кадре бессмысленны, а
|
|
33
|
-
* `javascript:` — исполняемый код в контексте okostream.app у всех зрителей.
|
|
34
|
-
* Для распознанного провайдера адрес строим мы сами, но проверяем всё равно
|
|
35
|
-
* итоговое значение: так защита не зависит от того, какой ветвью оно пришло.
|
|
36
|
-
*/
|
|
37
|
-
function assertEmbeddableUrl(value) {
|
|
38
|
-
let url;
|
|
39
|
-
try {
|
|
40
|
-
url = new URL(value);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
throw new Error(`Invalid videoUrl "${value}". Provide an https:// link or a full <iframe> embed code.`);
|
|
44
|
-
}
|
|
45
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
46
|
-
throw new Error(`Unsupported videoUrl protocol "${url.protocol}". Only http(s) links can be embedded.`);
|
|
47
|
-
}
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
|
-
export function normalizeVideo(videoUrl) {
|
|
51
|
-
const raw = videoUrl?.trim();
|
|
52
|
-
if (!raw)
|
|
53
|
-
return { originalUrl: null, embedUrl: null };
|
|
54
|
-
const effective = extractIframeSrc(raw) ?? raw;
|
|
55
|
-
const detected = detectAndNormalize(effective);
|
|
56
|
-
if (detected.provider !== "unknown" && !detected.normalizedUrl) {
|
|
57
|
-
throw new Error(`Could not extract a valid ${detected.provider} video id from videoUrl.`);
|
|
58
|
-
}
|
|
59
|
-
return { originalUrl: raw, embedUrl: assertEmbeddableUrl(detected.normalizedUrl ?? effective) };
|
|
60
|
-
}
|
|
61
|
-
export function csvRows(headers, rows) {
|
|
62
|
-
return [headers, ...rows].map((row) => row.map(csvCell).join(",")).join("\n");
|
|
63
|
-
}
|
|
64
|
-
export function toMillis(value) {
|
|
65
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
66
|
-
return value;
|
|
67
|
-
if (value && typeof value === "object" && typeof value.toMillis === "function") {
|
|
68
|
-
return value.toMillis();
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
export function parseTimestampCursor(cursor) {
|
|
73
|
-
const millis = Number(cursor);
|
|
74
|
-
if (Number.isFinite(millis) && millis > 0)
|
|
75
|
-
return Timestamp.fromMillis(millis);
|
|
76
|
-
const parsed = Date.parse(cursor);
|
|
77
|
-
if (Number.isFinite(parsed))
|
|
78
|
-
return Timestamp.fromMillis(parsed);
|
|
79
|
-
throw new Error("Invalid cursor. Use the nextCursor value returned by the previous page.");
|
|
80
|
-
}
|
|
81
|
-
export function timestampCursorFrom(value) {
|
|
82
|
-
const millis = toMillis(value);
|
|
83
|
-
return millis == null ? null : String(millis);
|
|
84
|
-
}
|
|
85
|
-
export function parseIsoBound(value, label) {
|
|
86
|
-
if (value === undefined)
|
|
87
|
-
return null;
|
|
88
|
-
const parsed = Date.parse(value);
|
|
89
|
-
if (!Number.isFinite(parsed)) {
|
|
90
|
-
throw new Error(`Invalid ${label}: expected an ISO timestamp such as 2026-08-28T00:00:00+03:00.`);
|
|
91
|
-
}
|
|
92
|
-
return Timestamp.fromMillis(parsed);
|
|
93
|
-
}
|
|
94
|
-
// Компактная карточка события из полного (сериализованного) документа —
|
|
95
|
-
// той же формы, что grant-проекция в eventAccess.
|
|
96
|
-
export function compactEventFromFullDoc(event) {
|
|
97
|
-
const meta = (event.meta && typeof event.meta === "object" ? event.meta : {});
|
|
98
|
-
const telegram = (event.telegram && typeof event.telegram === "object" ? event.telegram : {});
|
|
99
|
-
return {
|
|
100
|
-
id: event.id,
|
|
101
|
-
ownerId: event.ownerId ?? null,
|
|
102
|
-
workspaceId: event.workspaceId ?? null,
|
|
103
|
-
title: event.title ?? null,
|
|
104
|
-
createdAt: event.createdAt ?? null,
|
|
105
|
-
eventDate: meta.eventDate ?? null,
|
|
106
|
-
speakerName: meta.speakerName ?? null,
|
|
107
|
-
pinned: event.pinned === true,
|
|
108
|
-
telegramEnabled: telegram.enabled === true,
|
|
109
|
-
trashed: Boolean(event.trashedAt),
|
|
110
|
-
trashedAt: event.trashedAt ?? null,
|
|
111
|
-
activeAnalyticsSessionId: event.activeAnalyticsSessionId ?? null,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
export function compactAnalyticsSession(raw, options) {
|
|
115
|
-
const session = serialize(raw);
|
|
116
|
-
const timeline = Array.isArray(session.timeline) ? session.timeline : null;
|
|
117
|
-
if (timeline) {
|
|
118
|
-
session.timelineBucketCount = timeline.length;
|
|
119
|
-
if (options.includeTimeline) {
|
|
120
|
-
const page = timeline.slice(-options.timelineLimit);
|
|
121
|
-
session.timeline = page;
|
|
122
|
-
session.timelineTruncated = timeline.length > page.length;
|
|
123
|
-
session.timelineReturned = page.length;
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
delete session.timeline;
|
|
127
|
-
session.timelineOmitted = true;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (!options.includeSnapshots) {
|
|
131
|
-
if (session.baselineSnapshot !== undefined) {
|
|
132
|
-
delete session.baselineSnapshot;
|
|
133
|
-
session.baselineSnapshotOmitted = true;
|
|
134
|
-
}
|
|
135
|
-
if (session.finalSnapshot !== undefined) {
|
|
136
|
-
delete session.finalSnapshot;
|
|
137
|
-
session.finalSnapshotOmitted = true;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
delete session[["n", "moEvaluation"].join("")];
|
|
141
|
-
return session;
|
|
142
|
-
}
|
|
143
|
-
export function numberMetric(value) {
|
|
144
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
145
|
-
}
|
|
146
|
-
export function formatSeconds(value) {
|
|
147
|
-
const seconds = numberMetric(value);
|
|
148
|
-
if (seconds == null)
|
|
149
|
-
return "n/a";
|
|
150
|
-
const total = Math.max(0, Math.round(seconds));
|
|
151
|
-
const h = Math.floor(total / 3600);
|
|
152
|
-
const m = Math.floor((total % 3600) / 60);
|
|
153
|
-
const s = total % 60;
|
|
154
|
-
if (h > 0)
|
|
155
|
-
return `${h}h ${String(m).padStart(2, "0")}m ${String(s).padStart(2, "0")}s`;
|
|
156
|
-
return `${m}m ${String(s).padStart(2, "0")}s`;
|
|
157
|
-
}
|
|
158
|
-
export function formatDateTime(value) {
|
|
159
|
-
const millis = toMillis(value);
|
|
160
|
-
if (millis == null)
|
|
161
|
-
return "n/a";
|
|
162
|
-
return new Date(millis).toISOString();
|
|
163
|
-
}
|
|
164
|
-
export function markdownMetric(label, value, suffix = "") {
|
|
165
|
-
const metric = value === null || value === undefined || value === "" ? "n/a" : String(value);
|
|
166
|
-
return `- ${label}: ${metric}${suffix}`;
|
|
167
|
-
}
|
|
168
|
-
export function sessionReportMarkdown({ eventId, event, sessionId, session, }) {
|
|
169
|
-
const lines = [
|
|
170
|
-
`# OKO session report`,
|
|
171
|
-
"",
|
|
172
|
-
`Event: ${safeString(event.title, eventId)}`,
|
|
173
|
-
`Event URL: ${eventUrl(eventId)}`,
|
|
174
|
-
`Report URL: ${sessionReportUrl(eventId, sessionId)}`,
|
|
175
|
-
`Session: ${sessionId}`,
|
|
176
|
-
"",
|
|
177
|
-
"## Timing",
|
|
178
|
-
markdownMetric("Started", formatDateTime(session.startedAt)),
|
|
179
|
-
markdownMetric("Ended", formatDateTime(session.endedAt)),
|
|
180
|
-
markdownMetric("Duration", formatSeconds(session.durationSec)),
|
|
181
|
-
"",
|
|
182
|
-
"## Audience",
|
|
183
|
-
markdownMetric("Peak viewers", session.peakViewers),
|
|
184
|
-
markdownMetric("Unique visitors", session.uniqueVisitors),
|
|
185
|
-
markdownMetric("Views", session.viewCount),
|
|
186
|
-
markdownMetric("Average watch time", formatSeconds(session.avgTimeSec)),
|
|
187
|
-
markdownMetric("Median watch time", formatSeconds(session.medianTimeSec)),
|
|
188
|
-
"",
|
|
189
|
-
"## Engagement",
|
|
190
|
-
markdownMetric("Chat messages", session.chatMessages),
|
|
191
|
-
markdownMetric("Banner impressions", session.bannerImpressions),
|
|
192
|
-
markdownMetric("Banner clicks", session.bannerClicks),
|
|
193
|
-
markdownMetric("Banner CTR", session.bannerCtr, session.bannerCtr == null ? "" : "%"),
|
|
194
|
-
markdownMetric("CTA clicks", session.ctaClicks),
|
|
195
|
-
markdownMetric("Cards clicks", session.cardsClicks),
|
|
196
|
-
];
|
|
197
|
-
return lines.join("\n");
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Поля `telegram`, которые копируются в дубликат события.
|
|
201
|
-
*
|
|
202
|
-
* 🔴 Инвариант: копия не наследует server-owned маркеры. Их ставит сервер по
|
|
203
|
-
* факту уже случившегося действия, и для нового события они ложны. Сейчас
|
|
204
|
-
* такой маркер один — `reminderSent` (CF17 ставит после отправки напоминания
|
|
205
|
-
* и навсегда пропускает события с ним), но список именно белый: новое
|
|
206
|
-
* серверное поле не должно попасть в копию по умолчанию.
|
|
207
|
-
*/
|
|
208
|
-
export const TELEGRAM_COPYABLE_KEYS = ["enabled", "channelIds", "autoReminder"];
|
|
209
|
-
export function copyableTelegram(source) {
|
|
210
|
-
const out = {};
|
|
211
|
-
for (const key of TELEGRAM_COPYABLE_KEYS) {
|
|
212
|
-
if (source[key] !== undefined)
|
|
213
|
-
out[key] = source[key];
|
|
214
|
-
}
|
|
215
|
-
return out;
|
|
216
|
-
}
|