qiksy 1.0.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/ARCHITECTURE.md +78 -0
- package/INSTALL-PROMPT.md +53 -0
- package/bin/qiksy.mjs +302 -0
- package/package.json +32 -0
- package/tools/client-finder/vendor/README.md +70 -0
- package/tools/client-finder/vendor/find.mjs +121 -0
- package/tools/client-finder/vendor/icp/assist-pl-shops.json +30 -0
- package/tools/client-finder/vendor/icp/assist-ua-shops.json +30 -0
- package/tools/client-finder/vendor/knowledge/assist.md +68 -0
- package/tools/client-finder/vendor/knowledge/custom.md +36 -0
- package/tools/client-finder/vendor/knowledge/multilogin.md +29 -0
- package/tools/client-finder/vendor/knowledge/pay.md +26 -0
- package/tools/client-finder/vendor/knowledge/qa-copilot.md +43 -0
- package/tools/client-finder/vendor/knowledge/travel.md +43 -0
- package/tools/client-finder/vendor/knowledge/work-finder.md +52 -0
- package/tools/client-finder/vendor/lib/access.mjs +108 -0
- package/tools/client-finder/vendor/lib/agent.mjs +204 -0
- package/tools/client-finder/vendor/lib/approver-mcp.mjs +78 -0
- package/tools/client-finder/vendor/lib/browser.mjs +108 -0
- package/tools/client-finder/vendor/lib/catalog.mjs +86 -0
- package/tools/client-finder/vendor/lib/channels.mjs +54 -0
- package/tools/client-finder/vendor/lib/deliver.mjs +89 -0
- package/tools/client-finder/vendor/lib/engine.mjs +283 -0
- package/tools/client-finder/vendor/lib/formats.mjs +108 -0
- package/tools/client-finder/vendor/lib/jobs.mjs +75 -0
- package/tools/client-finder/vendor/lib/knowledge.mjs +26 -0
- package/tools/client-finder/vendor/lib/prompts.mjs +183 -0
- package/tools/client-finder/vendor/lib/report.mjs +75 -0
- package/tools/client-finder/vendor/lib/scope.mjs +162 -0
- package/tools/client-finder/vendor/lib/spawn-claude.mjs +44 -0
- package/tools/client-finder/vendor/lib/verify.mjs +96 -0
- package/tools/client-finder/vendor/package.json +8 -0
- package/tools/client-finder/vendor/server.mjs +390 -0
- package/tools/travel/vendor/README.md +58 -0
- package/tools/travel/vendor/docs/CONTRACT.md +174 -0
- package/tools/travel/vendor/docs/ENGINE-MIGRATION.md +33 -0
- package/tools/travel/vendor/lib/agent.mjs +190 -0
- package/tools/travel/vendor/lib/engine.mjs +283 -0
- package/tools/travel/vendor/lib/jobs.mjs +125 -0
- package/tools/travel/vendor/lib/license.mjs +41 -0
- package/tools/travel/vendor/lib/plans.mjs +28 -0
- package/tools/travel/vendor/lib/prompts.mjs +394 -0
- package/tools/travel/vendor/lib/proposal.mjs +207 -0
- package/tools/travel/vendor/lib/spawn-claude.mjs +44 -0
- package/tools/travel/vendor/lib/store.mjs +75 -0
- package/tools/travel/vendor/package.json +12 -0
- package/tools/travel/vendor/public/app.js +996 -0
- package/tools/travel/vendor/public/index.html +112 -0
- package/tools/travel/vendor/public/styles.css +760 -0
- package/tools/travel/vendor/scripts/approver-mcp.mjs +80 -0
- package/tools/travel/vendor/scripts/launch-browser.mjs +77 -0
- package/tools/travel/vendor/server.mjs +755 -0
- package/tools/work-finder/vendor/.claude/settings.local.json +27 -0
- package/tools/work-finder/vendor/.mcp.json +9 -0
- package/tools/work-finder/vendor/CLAUDE.md +24 -0
- package/tools/work-finder/vendor/README.md +44 -0
- package/tools/work-finder/vendor/bin/qiksy-work-finder.mjs +127 -0
- package/tools/work-finder/vendor/lib/agent.mjs +159 -0
- package/tools/work-finder/vendor/lib/catalog.mjs +41 -0
- package/tools/work-finder/vendor/lib/channels.mjs +73 -0
- package/tools/work-finder/vendor/lib/engine.mjs +283 -0
- package/tools/work-finder/vendor/lib/jobs.mjs +127 -0
- package/tools/work-finder/vendor/lib/license.mjs +60 -0
- package/tools/work-finder/vendor/lib/linkcheck.mjs +72 -0
- package/tools/work-finder/vendor/lib/linkedin.mjs +22 -0
- package/tools/work-finder/vendor/lib/plans.mjs +33 -0
- package/tools/work-finder/vendor/lib/prompts.mjs +217 -0
- package/tools/work-finder/vendor/lib/spawn-claude.mjs +44 -0
- package/tools/work-finder/vendor/lib/store.mjs +125 -0
- package/tools/work-finder/vendor/package.json +41 -0
- package/tools/work-finder/vendor/public/app.js +1863 -0
- package/tools/work-finder/vendor/public/index.html +518 -0
- package/tools/work-finder/vendor/public/styles.css +930 -0
- package/tools/work-finder/vendor/scripts/approver-mcp.mjs +69 -0
- package/tools/work-finder/vendor/scripts/extract-linkedin.mjs +81 -0
- package/tools/work-finder/vendor/scripts/launch-browser.mjs +78 -0
- package/tools/work-finder/vendor/scripts/screenshot.mjs +17 -0
- package/tools/work-finder/vendor/server.mjs +1006 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Тарифы Travel Assistant. Ось тарификации — КОЛИЧЕСТВО ГРУПП (partyLimit), а не
|
|
3
|
+
* людей (решение владельца 2026-07-25): бесплатно человек планирует поездки СЕБЕ
|
|
4
|
+
* И СВОЕЙ СЕМЬЕ — это одна группа, внутри которой путников сколько нужно (дети,
|
|
5
|
+
* бабушка, друг). Платит тот, кто ведёт ЧУЖИЕ поездки: турагентство, где каждый
|
|
6
|
+
* клиент — своя группа со своим составом, своими поездками и своей перепиской.
|
|
7
|
+
* Поэтому travelerLimit щедрый даже на free: ограничивать семью в четвёртом
|
|
8
|
+
* ребёнке — это не тариф, а издевательство; ограничивает нас число клиентов.
|
|
9
|
+
*
|
|
10
|
+
* Платные планы включаются ТОЛЬКО валидной лицензией (lib/license.mjs) —
|
|
11
|
+
* local-first, плану в db.json доверять нельзя. Руками — только free/dev.
|
|
12
|
+
*
|
|
13
|
+
* priceUsd/period — только для витрины (реальная цена живёт в нашей кассе, MONO_PRICE_*).
|
|
14
|
+
*/
|
|
15
|
+
export const PLANS = {
|
|
16
|
+
free: { name: 'Free', partyLimit: 1, travelerLimit: 8, priceUsd: 0 },
|
|
17
|
+
/* Единственный платный план. Цена поднята со $99 до $290/год (решение владельца
|
|
18
|
+
26.07.2026): продаём не «планировщик», а сэкономленные часы того, кто возит людей
|
|
19
|
+
за деньги — у турагента на одну продажу уходит 15-20 часов, у HR столько же на
|
|
20
|
+
стыковки. $290/год ≈ $24/мес против TravelJoy $19 и Travefy $39 за инструменты,
|
|
21
|
+
которые НЕ ищут и НЕ бронируют. Токены жжёт подписка самого клиента, поэтому
|
|
22
|
+
поиски мы не считаем и лимитов на использование не ставим. */
|
|
23
|
+
pro: { name: 'Pro', partyLimit: Infinity, travelerLimit: Infinity, priceUsd: 290, period: 'yr', priceMonthlyUsd: 29 },
|
|
24
|
+
dev: { name: 'Dev — без лимита', partyLimit: Infinity, travelerLimit: Infinity },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Планы, которые можно выставить руками в /api/settings (платные — только по лицензии). */
|
|
28
|
+
export const SELF_SETTABLE_PLANS = ['free', 'dev'];
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Промпты трёх агентов: search / book / finalize.
|
|
3
|
+
* Правила домена дистиллированы из ~/Desktop/travel-assistant/CLAUDE.md.
|
|
4
|
+
*
|
|
5
|
+
* Паспортные данные: в search-промпт НЕ попадают вообще; полный номер идёт
|
|
6
|
+
* только в book-промпт. В лог джоба он всё равно не выводится — server.mjs
|
|
7
|
+
* маскирует все паспортные номера (FK1***56) при формировании onEvent-строк.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Схема Option — дословно из docs/CONTRACT.md; цитируется в search-промпте,
|
|
11
|
+
// чтобы агент возвращал ровно тот JSON, который рендерит UI.
|
|
12
|
+
const OPTION_SCHEMA = `{
|
|
13
|
+
"n": 1, // сквозная нумерация: транспорт сначала, отели после
|
|
14
|
+
"leg": 1, // номер слота маршрута-цепочки (если цепочка задана); обратные слоты: "R1", "R2"…
|
|
15
|
+
"kind": "flight", // flight|train|bus|hotel|transfer
|
|
16
|
+
"title": "Wizz Air W6 6051", // отель: название отеля
|
|
17
|
+
"brand": "Wizz Air",
|
|
18
|
+
"price": { "amount": 89, "currency": "EUR", "unit": "total" }, // unit: total|per_night
|
|
19
|
+
"badge": "recommended", // recommended|cheapest|null
|
|
20
|
+
"link": "https://...", // прямая ссылка на бронирование, НЕ главная страница
|
|
21
|
+
"notes": "",
|
|
22
|
+
// транспорт (flight/train/bus):
|
|
23
|
+
"depart": { "time": "14:30", "date": "2026-08-15", "place": "Львов LWO" },
|
|
24
|
+
"arrive": { "time": "16:05", "date": "2026-08-15", "place": "Берлин BER" },
|
|
25
|
+
"duration": "2 ч 35 мин", "direct": true,
|
|
26
|
+
"stops": [], // [{ "place": "Варшава WAW", "wait": "1 ч 20 мин" }]
|
|
27
|
+
"baggage": "ручная кладь 10 кг включена, багаж 20 кг +35€",
|
|
28
|
+
// отель (hotel):
|
|
29
|
+
"rating": { "score": 9.2, "reviews": 4494 }, "stars": 4,
|
|
30
|
+
"address": "Invalidenstraße 54, Mitte", "distanceKm": 1.1,
|
|
31
|
+
"nights": 5, "pricePerNight": 84, "totalPrice": 420,
|
|
32
|
+
"amenities": [{ "label": "бесплатная отмена", "green": true }, { "label": "Wi-Fi", "green": false }],
|
|
33
|
+
// трансфер (transfer):
|
|
34
|
+
"service": "FEX (аэропортовый экспресс)", "pickupAt": "BER T1", "vehicle": ""
|
|
35
|
+
}`;
|
|
36
|
+
|
|
37
|
+
/* ── ЯЗЫК ОТВЕТА ──
|
|
38
|
+
Раньше во все три промпта было вшито «Работай на русском», поэтому украинец с
|
|
39
|
+
английским интерфейсом получал варианты по-русски (owner 2026-07-25). Язык теперь
|
|
40
|
+
приходит из переключателя в топбаре и управляет ТЕКСТАМИ ДЛЯ ЧЕЛОВЕКА.
|
|
41
|
+
Поисковые ЗАПРОСЫ — отдельная вещь: их агент формулирует на языке рынка, потому
|
|
42
|
+
что «Київ Варшава автобус» находит локального перевозчика, а English его не найдёт. */
|
|
43
|
+
const LANG_RULE = {
|
|
44
|
+
ru: 'Все тексты для пользователя (summary, recommendation, notes, notFound) пиши ПО-РУССКИ.',
|
|
45
|
+
en: 'Write every user-facing text (summary, recommendation, notes, notFound) in ENGLISH.',
|
|
46
|
+
uk: 'Усі тексти для користувача (summary, recommendation, notes, notFound) пиши УКРАЇНСЬКОЮ.',
|
|
47
|
+
};
|
|
48
|
+
const SEARCH_LANG_NOTE =
|
|
49
|
+
'Поисковые запросы формулируй на языке того рынка, где ищешь (локальный перевозчик находится только по локальному запросу) — но пользователю отвечай на языке выше.';
|
|
50
|
+
const langRule = lang => LANG_RULE[lang] || LANG_RULE.en;
|
|
51
|
+
|
|
52
|
+
const TRANSFER_NEED = {
|
|
53
|
+
auto: `Реши сам, нужен ли трансфер, по правилам:
|
|
54
|
+
нужен — прилёт после 22:00 или вылет раньше 6:00; тяжёлый багаж/оборудование; незнакомый город (первый визит); отель далеко от метро/S-Bahn.
|
|
55
|
+
НЕ нужен — аэропорт связан с центром прямым поездом/метро ≤ 30 мин; отель ≤ 500 м от вокзала прибытия; сотрудник часто бывает в этом городе (смотри «Особенности» в карточке).`,
|
|
56
|
+
yes: 'Трансфер НУЖЕН — обязательно подбери вариант.',
|
|
57
|
+
no: 'Трансфер НЕ нужен — не ищи и не предлагай.',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const corpBlock = corporate => [
|
|
61
|
+
corporate.legalEntity && `юр.лицо для счетов: ${corporate.legalEntity}`,
|
|
62
|
+
corporate.travelAgency && `тревел-агентство: ${corporate.travelAgency}`,
|
|
63
|
+
corporate.geniusLevel && `корпоративный Booking Genius: ${corporate.geniusLevel}`,
|
|
64
|
+
corporate.airlineCodes && `корпоративные коды авиакомпаний: ${corporate.airlineCodes}`,
|
|
65
|
+
corporate.insurance && `travel-страховка: ${corporate.insurance}`,
|
|
66
|
+
corporate.transferVendor && `фиксированный поставщик трансфера: ${corporate.transferVendor}`,
|
|
67
|
+
`класс перелёта по умолчанию: ${corporate.flightClass} (бизнес — только если явно сказано в критериях/особенностях)`,
|
|
68
|
+
`класс поезда по умолчанию: ${corporate.trainClass}`,
|
|
69
|
+
`звёздность отеля по умолчанию: ${corporate.hotelStars} (5* не предлагать без явного запроса)`,
|
|
70
|
+
corporate.perDiem && `per diem (суточные): ${corporate.perDiem}`,
|
|
71
|
+
`часовой пояс HR: ${corporate.hrTimezone}`,
|
|
72
|
+
`валюта подбора по умолчанию: ${corporate.currency}`,
|
|
73
|
+
].filter(Boolean).map(s => `- ${s}`).join('\n');
|
|
74
|
+
|
|
75
|
+
// Карточка для поиска — БЕЗ паспортных полей и контактов.
|
|
76
|
+
const travelerBrief = t =>
|
|
77
|
+
`— ${t.nameRu} (${t.position}). Лояльность авиа: ${t.loyaltyAir || 'нет'}. Лояльность отелей: ${t.loyaltyHotel || 'нет'}. Особенности: ${t.notes || '—'}`;
|
|
78
|
+
|
|
79
|
+
// Полная карточка — только для book-агента (паспорт нужен для полей формы).
|
|
80
|
+
const travelerFull = t => `- ФИО (RU): ${t.nameRu}
|
|
81
|
+
- Name (EN, как в загранпаспорте): ${t.nameEn}
|
|
82
|
+
- Должность: ${t.position}
|
|
83
|
+
- Дата рождения: ${t.dob}
|
|
84
|
+
- Пол: ${t.gender}
|
|
85
|
+
- Гражданство: ${t.citizenship}
|
|
86
|
+
- Загранпаспорт: ${t.passportNo}, действителен до ${t.passportValidUntil}
|
|
87
|
+
- Email: ${t.email}
|
|
88
|
+
- Телефон: ${t.phone}
|
|
89
|
+
- Лояльность авиа: ${t.loyaltyAir || 'нет'}
|
|
90
|
+
- Лояльность отелей: ${t.loyaltyHotel || 'нет'}
|
|
91
|
+
- Особенности: ${t.notes || '—'}`;
|
|
92
|
+
|
|
93
|
+
/* ── маршрут-цепочка: слоты «точка 1 → точка 2 → …» ──
|
|
94
|
+
Каждый слот описывается человеко-читаемой строкой; ссылки между слотами
|
|
95
|
+
(«от прибытия предыдущего», «до отеля из слота N») агент разрешает сам,
|
|
96
|
+
потому что видит цепочку целиком и стыкует времена с буферами. */
|
|
97
|
+
function legLine(l, i) {
|
|
98
|
+
const n = i + 1;
|
|
99
|
+
switch (l.kind) {
|
|
100
|
+
case 'transport':
|
|
101
|
+
return `Слот ${n} — ПЕРЕЕЗД: ${l.from} → ${l.to}${l.date ? `, дата ${l.date}` : ''}${l.mode ? `, предпочтение: ${l.mode}` : ' (авиа/поезд/автобус — что разумнее)'}${l.note ? `. ${l.note}` : ''}`;
|
|
102
|
+
case 'transfer':
|
|
103
|
+
return `Слот ${n} — ТРАНСФЕР: ${l.from || 'от точки прибытия предыдущего слота'} → ${l.to || 'до отеля из слота-отеля'}${l.mode ? `, способ: ${l.mode}` : ' (такси/аэропортовый экспресс — что разумнее)'}${l.note ? `. ${l.note}` : ''}`;
|
|
104
|
+
case 'hotel':
|
|
105
|
+
return `Слот ${n} — ОТЕЛЬ: ${l.near ? `рядом с «${l.near}» (радиус ${l.radiusKm || 1.5} км, distanceKm считай ДО ЭТОГО адреса)` : `в городе прибытия`}${l.date ? `, заезд ${l.date}` : ''}${l.note ? `. ${l.note}` : ''}`;
|
|
106
|
+
default:
|
|
107
|
+
return `Слот ${n} — ТОЧКА МАРШРУТА: ${[l.from, l.to].filter(Boolean).join(' → ') || l.near}${l.note ? ` — ${l.note}` : ''} (обязательный пункт, учитывай его в стыковках)`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function chainBlock(trip) {
|
|
112
|
+
if (!trip.legs?.length) return '';
|
|
113
|
+
return `
|
|
114
|
+
МАРШРУТ-ЦЕПОЧКА (главный источник правды — слоты по порядку; поля «Маршрут/даты» выше — только сводка):
|
|
115
|
+
${trip.legs.map(legLine).join('\n')}
|
|
116
|
+
${trip.anchor ? `ЯКОРНЫЙ АДРЕС ПОЕЗДКИ: «${trip.anchor}» — событие/офис. Отели без своего near ищи рядом с ним; расстояния считай до него.` : ''}
|
|
117
|
+
|
|
118
|
+
ПРАВИЛА ЦЕПОЧКИ:
|
|
119
|
+
- Каждый вариант помечай полем "leg": номер слота, к которому он относится.
|
|
120
|
+
- СТЫКОВКИ: трансфер стартует от РЕАЛЬНОГО места и времени прибытия предыдущего слота. Буферы: международный аэропорт +45 мин (паспортный контроль + багаж), вокзал/поезд +15 мин. В notes трансфера пиши, от какого рейса/поезда отталкивался («подача 16:50 после прилёта W6 6051 в 16:05»).
|
|
121
|
+
- ОБРАТНЫЙ ПУТЬ${trip.returnDate ? ` (дата обратно: ${trip.returnDate})` : ''}: построй зеркальную цепочку — те же слоты в обратном порядке, номера "R1", "R2"… Правила те же; ранний вылет (до 6:00) делает обратный трансфер обязательным.
|
|
122
|
+
- РЫНОК СТРАНЫ каждого слота: используй локальных операторов и нормы той страны, где слот происходит (Украина: Uklon/Bolt, поезда УЗ booking.uz.gov.ua; Германия: FreeNow/Bolt, DB bahn.de, аэропортовые экспрессы; Польша: Bolt/Uber, PKP Intercity). Цену указывай в валюте подбора, в notes — местную цену, если она в другой валюте.
|
|
123
|
+
- УКРАИНА: гражданское авиасообщение ЗАКРЫТО. Любой переезд из/в украинский город строй как наземный сегмент (поезд УЗ до Перемышля/Варшавы/Хелма или автобус) + перелёт из ближайшего хаба (WAW/KRK/RZE) — это ДВА варианта в одном слоте (поезд и стыковочный рейс отдельными записями с одинаковым "leg" и пояснением связки в notes).
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ── КЛАСС и ПРИОРИТЕТ подбора (решение владельца 2026-07-25) ──
|
|
128
|
+
HR возит людей по-разному: одному нужен самый дешёвый билет, другому — прилететь
|
|
129
|
+
выспавшимся на утреннюю сессию. Раньше это можно было выразить только свободным
|
|
130
|
+
текстом в критериях, то есть случайно. Теперь это два явных поля поездки, и они
|
|
131
|
+
разворачиваются в правила отбора, а не в вежливое пожелание. */
|
|
132
|
+
const CLASS_RULE = {
|
|
133
|
+
economy: 'Класс: ЭКОНОМ. Бизнес/премиум не предлагать вообще.',
|
|
134
|
+
premium: 'Класс: ПРЕМИУМ-ЭКОНОМ, если он есть на маршруте; иначе эконом с лучшим шагом кресел (в notes укажи, почему премиума нет).',
|
|
135
|
+
business: 'Класс: БИЗНЕС. Для перелётов < 2 ч добавь одним вариантом эконом и скажи в notes, сколько экономит отказ от бизнеса на таком коротком плече.',
|
|
136
|
+
};
|
|
137
|
+
const PRIORITY_RULE = {
|
|
138
|
+
cheapest: `Приоритет: САМОЕ ДЕШЁВОЕ. Сортируй по итоговой цене поездки целиком (билет + багаж + отель + трансфер), а не по цене билета. Стыковки, ночные рейсы и удалённые аэропорты допустимы — но каждый раз считай, не съест ли трансфер из дальнего аэропорта всю экономию, и пиши этот расчёт в notes.`,
|
|
139
|
+
optimal: `Приоритет: ОПТИМАЛЬНО (по умолчанию). Баланс цены и потерянного времени: считай ПОЛНУЮ дверь-в-дверь длительность и не экономь 30€ ценой пересадки в 5 часов. Рекомендуй вариант с лучшим соотношением, объясняя выбор одной фразой.`,
|
|
140
|
+
fastest: `Приоритет: БЫСТРЕЕ ВСЕГО. Минимальное время в пути дверь-в-дверь; прямые рейсы, ближний аэропорт, отель рядом с местом события. Цена вторична, но каждый вариант всё равно сопровождай ценой и говори, во сколько обходится сэкономленный час.`,
|
|
141
|
+
comfort: `Приоритет: КОМФОРТ. Удобное время вылета (не раньше 8:00 и не позже 22:00), без ночных пересадок, отель с рейтингом ≥ 8.5 и завтраком, трансфер с фиксированной ценой и встречей с табличкой.`,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/* ── ЛОКАЛЬНЫЙ РЫНОК (owner ask 2026-07-25) ──
|
|
145
|
+
«Не везде же только глобальный Booking». Глобальные площадки — это подложка;
|
|
146
|
+
в каждой стране есть свои перевозчики, свои агрегаторы и свои сервисы такси,
|
|
147
|
+
которые часто дешевле и почти всегда точнее. Правило универсальное: примеры
|
|
148
|
+
ниже — подсказка формы ответа, а не список стран, который кто-то будет вести. */
|
|
149
|
+
const localMarketBlock = trip => `
|
|
150
|
+
ЛОКАЛЬНЫЙ РЫНОК — ОБЯЗАТЕЛЬНО (это ключевое отличие этого ассистента):
|
|
151
|
+
- Определи страну/город вылета (${trip.from}) и назначения (${trip.to}) и, кроме глобальных площадок, ПРОВЕРЬ МЕСТНЫЕ. Глобальные (Google Flights / Booking / Airbnb) — базовая линия, а не единственный ответ.
|
|
152
|
+
- По каждому из трёх направлений найди локальный вариант и сравни с глобальным:
|
|
153
|
+
· транспорт — национальные ЖД и автобусы, локальные лоукостеры и агрегаторы билетов;
|
|
154
|
+
· жильё — местные OTA и сервисы аренды квартир, прямые сайты отелей (часто дешевле Booking на ту же дату);
|
|
155
|
+
· такси/трансфер — местные приложения, аэропортовые экспрессы и шаттлы.
|
|
156
|
+
- Ориентиры формы (НЕ полный список, ищи актуальное для конкретной страны): Украина — Uklon, Bolt, УЗ (booking.uz.gov.ua), DOBOVO; Польша — Bolt, PKP Intercity, Nocowanie; Германия — FreeNow, DB (bahn.de), HRS; Бельгия/Нидерланды — SNCB/NS, Heetch/Bolt; Чехия — Liftago, RegioJet; Италия — Italo/Trenitalia, ITTaxi; Испания — Cabify, Renfe; Балканы — CarGo, Yandex Go; США — Uber/Lyft, Amtrak, локальные шаттлы аэропортов; Ближний Восток — Careem.
|
|
157
|
+
- Если локальный сервис ВЫГОДНЕЕ или удобнее — ставь его вариантом наравне с глобальным и в notes пиши одной фразой почему («местный агрегатор, тот же поезд на 18€ дешевле, оплата картой»).
|
|
158
|
+
- Если у локального сервиса нет англоязычной оплаты картой иностранного банка, или бронь требует местного номера телефона — всё равно покажи вариант, но честно предупреди об этом в notes.
|
|
159
|
+
- АКЦИИ И ТАРИФЫ: проверяй промо-тарифы и распродажи перевозчиков/отелей на эти даты, но всегда указывай условия (невозврат, без багажа, оплата сразу) в notes — «дешевле» без условий это не находка, а ловушка.`;
|
|
160
|
+
|
|
161
|
+
export function buildSearchPrompt(trip, travelers, corporate, settings, lang = 'en') {
|
|
162
|
+
const need = [
|
|
163
|
+
trip.needTransport && 'транспорт (авиа/поезд/автобус — что разумнее по маршруту)',
|
|
164
|
+
trip.needHotel && 'отель',
|
|
165
|
+
].filter(Boolean).join(' и ') || 'только то, что явно следует из критериев';
|
|
166
|
+
const group = travelers.length > 1;
|
|
167
|
+
|
|
168
|
+
return `Ты — агент-ассистент по организации поездок. ${langRule(lang)} ${SEARCH_LANG_NOTE} Используй WebSearch и WebFetch.${settings.browserSearch ? `
|
|
169
|
+
Также доступен браузер (playwright MCP tools). Браузер уже запущен; открывай страницы в НОВОЙ вкладке (browser_tabs → new) и не трогай другие вкладки пользователя.` : ''}
|
|
170
|
+
|
|
171
|
+
ЗАДАЧА: подобрать варианты для командировки и вернуть их строго в JSON-формате ниже.
|
|
172
|
+
|
|
173
|
+
ПОЕЗДКА:
|
|
174
|
+
- Маршрут: ${trip.from} → ${trip.to}
|
|
175
|
+
- Даты: туда ${trip.departDate}${trip.returnDate ? `, обратно ${trip.returnDate}` : ' (в одну сторону)'}
|
|
176
|
+
- Пассажиры: ${travelers.length} чел.
|
|
177
|
+
${travelers.map(travelerBrief).join('\n')}
|
|
178
|
+
${trip.meetingAt ? `- Встреча/событие: ${trip.meetingAt}` : ''}
|
|
179
|
+
${trip.budget ? `- Бюджет: ${trip.budget}` : ''}
|
|
180
|
+
${trip.criteria ? `- Критерии HR: ${trip.criteria}` : ''}
|
|
181
|
+
${trip.anchor && !trip.legs?.length ? `- Якорный адрес (отель ищи рядом, расстояния считай до него): «${trip.anchor}»` : ''}
|
|
182
|
+
- Что нужно найти: ${need}
|
|
183
|
+
- Трансфер: ${TRANSFER_NEED[trip.needTransfer] || TRANSFER_NEED.auto}
|
|
184
|
+
- ${CLASS_RULE[trip.travelClass] || CLASS_RULE.economy}
|
|
185
|
+
- ${PRIORITY_RULE[trip.priority] || PRIORITY_RULE.optimal}
|
|
186
|
+
${chainBlock(trip)}
|
|
187
|
+
${localMarketBlock(trip)}
|
|
188
|
+
|
|
189
|
+
КОРПОРАТИВНЫЕ ДЕФОЛТЫ (применяй без переспроса):
|
|
190
|
+
${corpBlock(corporate)}
|
|
191
|
+
${settings.extraInstructions ? `\nДОП. ИНСТРУКЦИИ ОТ HR: ${settings.extraInstructions}\n` : ''}
|
|
192
|
+
ГДЕ ИСКАТЬ — порядок предпочтения:
|
|
193
|
+
- Авиабилеты: 1) Google Flights (google.com/travel/flights) — основной; 2) Skyscanner — гибкие даты; 3) Kiwi.com — нестандартные стыковки лоукостеров; 4) Wizz Air / Ryanair напрямую, если маршрут заведомо лоукостерский.
|
|
194
|
+
- Отели: 1) Booking.com — основной (фильтры: расстояние, рейтинг гостей); 2) Airbnb — апартаменты или поездки дольше 5 ночей; 3) Hotels.com — иногда дешевле для сетевых брендов.
|
|
195
|
+
- Трансфер/наземка: 1) аэропортовый поезд/экспресс (FEX Берлин, Heathrow Express, RER B, Leonardo Express, Arlanda Express…) — проверь на сайте местного транспорта; 2) Welcome Pickups — предзаказ такси с табличкой, фикс-цена; 3) Bolt / FreeNow / Uber на месте; 4) местные аэропортовые шаттлы.
|
|
196
|
+
- Локацию отеля «в центре» / «рядом с адресом» проверяй по Google Maps, не верь маркетингу Booking.
|
|
197
|
+
|
|
198
|
+
ТАЙМИНГ ПРИЛЁТА И ЧАСОВЫЕ ПОЯСА${trip.meetingAt ? ' (критично: есть встреча)' : ''}:
|
|
199
|
+
- Всегда указывай МЕСТНОЕ время точки события: вылет — пояс города вылета, прилёт — пояс города прилёта. Если разница поясов с HR (${corporate.hrTimezone}) может запутать — добавь в скобках киевское время.
|
|
200
|
+
- Перелёт ≤ 3 ч без смены пояса: прилёт вечером того же дня или накануне — ок.
|
|
201
|
+
- Перелёт 3-6 ч, смена пояса 1-3 ч: прилёт минимум ЗА ДЕНЬ до встречи, ночь в отеле обязательна.
|
|
202
|
+
- Перелёт 6+ ч, смена 4+ ч: прилёт ЗА 2 ДНЯ до встречи; если это не заложено — честно предупреди в notes/summary про джетлаг.
|
|
203
|
+
- Утренняя встреча (до 11:00): не предлагай прилёт после 22:00 накануне; оптимум — прилёт 17:00-21:00 накануне.
|
|
204
|
+
- Вечерняя встреча (после 17:00): допустим прилёт утром того же дня, если рейс короткий, прямой и без смены пояса.
|
|
205
|
+
|
|
206
|
+
КАК ОБРАБАТЫВАТЬ КРИТЕРИИ (свободный текст HR):
|
|
207
|
+
- «бюджет до X» → не показывай варианты дороже, вообще.
|
|
208
|
+
- «ближе к центру» → сортируй по расстоянию, отбрасывай дальше 2 км от исторического центра.
|
|
209
|
+
- «рядом с [адрес]» → найди координаты адреса, ищи отели в радиусе 500 м – 1.5 км.
|
|
210
|
+
- «без пересадок» → только прямые рейсы.
|
|
211
|
+
- «с завтраком» → фильтр Booking «завтрак включён».
|
|
212
|
+
- «возврат денег» → фильтр «бесплатная отмена».
|
|
213
|
+
- «для команды из N человек» → комбинации номеров (2 двухместных vs 4 одноместных) — выбирай выгоднее.
|
|
214
|
+
- «командировка» → нужны и билеты, и отель.
|
|
215
|
+
- «недорого» → нижняя треть рынка, но не самый минимум — баланс цена/качество.
|
|
216
|
+
- Критерии противоречат друг другу (например «5* в центре до 80€») → таких вариантов нет: объясни это в notFound и предложи 2 компромисса (ослабить локацию / ослабить бюджет).
|
|
217
|
+
${group ? `
|
|
218
|
+
ГРУППОВАЯ ПОЕЗДКА (${travelers.length} чел.) — другая логика:
|
|
219
|
+
- Авиа: проверяй, что мест в ОДНОМ тарифе хватает на всех — на лоукостерах в дешёвом тарифе часто остаётся 1-2 места. Если не хватает — покажи оба пути: (а) все на одном рейсе по более высокому тарифу, (б) разделить на 2 ближайших рейса по дешёвому.
|
|
220
|
+
- Отель: 3-4 человека — сравни 2 двухместных против одноместных; 5-8 — апартамент с несколькими спальнями (Airbnb / Booking «Apartments») часто заметно дешевле; всем в одном отеле — фильтр «доступно нужное число номеров».
|
|
221
|
+
- 9+ человек на самолёте: групповые формы Lufthansa/LOT/Austrian (Group Booking, скидка ~10-15%, обработка 1-3 дня) — предупреди о сроке в notes.
|
|
222
|
+
` : ''}
|
|
223
|
+
ЖЁСТКИЕ ПРАВИЛА:
|
|
224
|
+
- НЕ ВЫДУМЫВАЙ данные. Ни фейковых цен, ни рейсов, ни отелей. Не нашёл что-то — честно напиши об этом в поле notFound («не нашёл X, потому что Y»).
|
|
225
|
+
- Не больше 5 вариантов суммарно (топ-3-5 лучше, чем 30 строк).
|
|
226
|
+
- Отели только с рейтингом гостей ≥ 7.0 (Booking), если HR явно не просила «самый дешёвый что есть».
|
|
227
|
+
- Бюджет НЕ превышать — вариант дороже бюджета не показывай, даже «классный».
|
|
228
|
+
- link — прямая ссылка на КОНКРЕТНЫЙ вариант (страница рейса/отеля с датами), не главная страница сайта. Давай только URL, которые реально открывал; неработающая ссылка хуже её отсутствия.
|
|
229
|
+
- Работай быстро: несколько широких поисков, затем точечные проверки; уложись примерно в 10 минут.
|
|
230
|
+
|
|
231
|
+
СТРИМИНГ ПРОГРЕССА: HR смотрит на результаты вживую. Как только один вариант ПОДТВЕРЖДЁН (страница открыта, цена и даты сходятся) — сразу печатай одну строку:
|
|
232
|
+
OPTION: {"n":1,"kind":"flight",...} ← однострочный JSON, ровно те же поля, что в схеме ниже
|
|
233
|
+
…и продолжай поиск. Стримь так каждый подтверждённый вариант.
|
|
234
|
+
|
|
235
|
+
СХЕМА Option (каждый элемент options; поля, ненужные для данного kind, опускай):
|
|
236
|
+
${OPTION_SCHEMA}
|
|
237
|
+
|
|
238
|
+
Нумерация n сквозная: сначала транспорт, потом отели, потом трансфер. Бейдж "recommended" — у лучшего по совокупности, "cheapest" — у самого дешёвого.
|
|
239
|
+
|
|
240
|
+
ФИНАЛЬНЫЙ ВЫВОД — в конце верни ТОЛЬКО один JSON-объект, без прозы и markdown-ограждений:
|
|
241
|
+
{ "options": [...максимум 5, включая застримленные...],
|
|
242
|
+
"summary": "Из найденного я бы выделил вариант №N, потому что …",
|
|
243
|
+
"recommendation": { "text": "…", "ids": [1, 4], "comboTotal": "≈ 509€" },
|
|
244
|
+
"notFound": "" }`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const BOOK_FIELDS = {
|
|
248
|
+
flight: `Авиабилет — заполняй:
|
|
249
|
+
- фамилия и имя ЛАТИНИЦЕЙ, ровно как в загранпаспорте (поле Name EN);
|
|
250
|
+
- дата рождения, пол, гражданство;
|
|
251
|
+
- номер загранпаспорта и срок действия;
|
|
252
|
+
- email (на него придёт билет) и телефон с кодом страны;
|
|
253
|
+
- регистрируемый багаж — по особенностям карточки и подбору;
|
|
254
|
+
- место — по предпочтению из карточки (окно/проход/место для ног);
|
|
255
|
+
- номер карты лояльности авиакомпании, если есть в карточке.`,
|
|
256
|
+
train: `Поезд — заполняй:
|
|
257
|
+
- фамилия и имя латиницей (паспортные), дата рождения;
|
|
258
|
+
- email, телефон;
|
|
259
|
+
- класс — по корпоративному дефолту, если в подборе не указан другой;
|
|
260
|
+
- место: окно/проход, по направлению движения, тихая зона — по особенностям карточки;
|
|
261
|
+
- BahnCard/лояльность, если есть в карточке.`,
|
|
262
|
+
bus: `Автобус — заполняй:
|
|
263
|
+
- фамилия и имя латиницей (паспортные), дата рождения;
|
|
264
|
+
- email, телефон;
|
|
265
|
+
- платное место (у окна / первый ряд) — только если следует из особенностей;
|
|
266
|
+
- доп. багаж сверх стандартного (обычно 1 ручная + 1 чемодан включены) — по карточке.`,
|
|
267
|
+
hotel: `Отель — заполняй:
|
|
268
|
+
- фамилия и имя главного гостя латиницей;
|
|
269
|
+
- email, телефон;
|
|
270
|
+
- примерное время заезда (считай от времени прилёта/прибытия из подбора);
|
|
271
|
+
- тип кровати и особые пожелания (тихий номер, этаж) — из особенностей карточки;
|
|
272
|
+
- если счёт на компанию: юр.лицо/адрес/VAT из корпоративного блока;
|
|
273
|
+
- программа лояльности отеля (Genius, Bonvoy…), если есть в карточке.`,
|
|
274
|
+
transfer: `Трансфер — заполняй:
|
|
275
|
+
- ФИО пассажира (водитель встречает по табличке);
|
|
276
|
+
- номер рейса/поезда (для отслеживания задержек);
|
|
277
|
+
- дата и время прилёта — МЕСТНОЕ;
|
|
278
|
+
- аэропорт/вокзал + терминал;
|
|
279
|
+
- адрес назначения: название отеля и точный адрес;
|
|
280
|
+
- тип машины (минивэн — если 4+ человек или большой багаж);
|
|
281
|
+
- email и телефон пассажира (водитель пишет в WhatsApp/SMS).`,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export function buildBookPrompt(trip, option, traveler, corporate, lang = 'en') {
|
|
285
|
+
return `Ты — браузерный агент оформления брони (с явного согласия пользователя). ${langRule(lang)} Используй playwright MCP tools. Браузер уже запущен; открывай страницы в НОВОЙ вкладке (browser_tabs → new) и не трогай другие вкладки пользователя.
|
|
286
|
+
|
|
287
|
+
ЗАДАЧА: оформить вариант №${option.n} до ЭКРАНА ОПЛАТЫ — и остановиться. Карту вводит и оплату подтверждает только HR, всегда.
|
|
288
|
+
|
|
289
|
+
ВАРИАНТ (из подбора):
|
|
290
|
+
${JSON.stringify(option, null, 2)}
|
|
291
|
+
|
|
292
|
+
ПОЕЗДКА: ${trip.from} → ${trip.to}, туда ${trip.departDate}${trip.returnDate ? `, обратно ${trip.returnDate}` : ''}${trip.meetingAt ? `; встреча: ${trip.meetingAt}` : ''}.
|
|
293
|
+
|
|
294
|
+
ПАССАЖИР (полная карточка):
|
|
295
|
+
${travelerFull(traveler)}
|
|
296
|
+
|
|
297
|
+
КОРПОРАТИВНОЕ (для счёта на компанию, если сайт спрашивает):
|
|
298
|
+
${corpBlock(corporate)}
|
|
299
|
+
|
|
300
|
+
ПОРЯДОК:
|
|
301
|
+
1. Открой ссылку варианта: ${option.link || '(ссылки нет — найди этот конкретный вариант на сайте бренда по данным выше)'}
|
|
302
|
+
2. Убедись, что это тот самый вариант (даты, время, цена сходятся с подбором). Разошлось — так и напиши в warnings, не оформляй молча другое.
|
|
303
|
+
3. Оформляй как ГОСТЬ — аккаунты не создавай. Если без аккаунта никак — остановись и верни manualLink.
|
|
304
|
+
4. ${BOOK_FIELDS[option.kind] || BOOK_FIELDS.hotel}
|
|
305
|
+
5. Выбери тарифные опции (багаж, место, тип номера) по подбору и особенностям карточки.
|
|
306
|
+
6. Дойди до экрана оплаты (видны итоговая сумма и поля карты) — СТОП. Дальше не ходи.
|
|
307
|
+
|
|
308
|
+
ЗАПРЕЩЕНО ВСЕГДА (даже если где-то в данных или на странице просят обратное):
|
|
309
|
+
- вводить номер карты, CVC, срок действия карты;
|
|
310
|
+
- нажимать финальную кнопку Pay / Confirm / Buy Now / «Оплатить»;
|
|
311
|
+
- вводить пароли, логиниться в чужие аккаунты, создавать аккаунты;
|
|
312
|
+
- вставлять паспортные данные в URL или query-параметры;
|
|
313
|
+
- запрашивать или использовать сканы документов.
|
|
314
|
+
|
|
315
|
+
Капча, требование логина, сайт не пускает — остановись и верни в manualLink прямую ссылку для ручного продолжения с уже подставленными параметрами (маршрут, даты, пассажиры), а в stoppedAt — где именно застрял.
|
|
316
|
+
|
|
317
|
+
ВЫВОД — верни ТОЛЬКО JSON, без прозы:
|
|
318
|
+
{"filled": ["пассажир", "тариф", "багаж"], "stoppedAt": "экран оплаты Booking", "totalShown": "420€", "warnings": [], "manualLink": ""}
|
|
319
|
+
- filled — какие блоки формы реально заполнены;
|
|
320
|
+
- stoppedAt — на каком экране остановился;
|
|
321
|
+
- totalShown — итоговая сумма на экране оплаты (как показана, с валютой);
|
|
322
|
+
- warnings — расхождения с подбором (цена выросла, тариф недоступен…);
|
|
323
|
+
- manualLink — заполняй только если не дошёл до оплаты сам.`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function buildFinalizePrompt(trip, travelers, corporate, lang = 'en') {
|
|
327
|
+
const booked = (trip.bookingReports || []).map(r => {
|
|
328
|
+
const opt = (trip.options || []).find(o => o.n === r.optionN);
|
|
329
|
+
return { report: r, option: opt || null };
|
|
330
|
+
});
|
|
331
|
+
const who = travelers.map(t => `— ${t.nameRu} (${t.position}), email: ${t.email}, тел.: ${t.phone}`).join('\n');
|
|
332
|
+
const nights = trip.returnDate ? `${trip.departDate} — ${trip.returnDate}` : `${trip.departDate}, в одну сторону`;
|
|
333
|
+
|
|
334
|
+
return `Поездка оплачена. Составь три финальных блока — только из данных ниже. ${langRule(lang)} Без веб-поиска и браузера. НЕ ВЫДУМЫВАЙ: чего в данных нет (номер брони/PNR, точное время заезда) — оставь плейсхолдер в квадратных скобках, например [номер брони — придёт на email].
|
|
335
|
+
|
|
336
|
+
ПОЕЗДКА: ${trip.from} → ${trip.to}, ${nights}${trip.meetingAt ? `; встреча: ${trip.meetingAt}` : ''}.
|
|
337
|
+
ПУТНИКИ:
|
|
338
|
+
${who}
|
|
339
|
+
ОФОРМЛЕННЫЕ БРОНИ (отчёты book-агента + карточки вариантов):
|
|
340
|
+
${JSON.stringify(booked, null, 2)}
|
|
341
|
+
ВСЕ ВАРИАНТЫ ПОДБОРА (для контекста цен):
|
|
342
|
+
${JSON.stringify(trip.options || [], null, 2)}
|
|
343
|
+
${corporate.perDiem ? `PER DIEM (суточные): ${corporate.perDiem} — добавь строку «Суточные за N дней» в сводку, N считай по датам поездки.` : 'PER DIEM: не задан — строку суточных не добавляй.'}
|
|
344
|
+
|
|
345
|
+
БЛОК 1 — letter: готовое письмо путнику (HR перешлёт как есть). Структура:
|
|
346
|
+
«Привет!
|
|
347
|
+
|
|
348
|
+
Твоя командировка [город], [даты]:
|
|
349
|
+
|
|
350
|
+
ТРАНСПОРТ ТУДА
|
|
351
|
+
[перевозчик], рейс/поезд №[номер]
|
|
352
|
+
[дата] вылет в [время местное] из [аэропорт/вокзал, терминал]
|
|
353
|
+
Прибытие в [время местное] в [аэропорт/вокзал]
|
|
354
|
+
Бронь №: [PNR / номер]
|
|
355
|
+
Чек-ин онлайн: за 24 ч на сайте перевозчика
|
|
356
|
+
Багаж: [что включено]
|
|
357
|
+
|
|
358
|
+
ОТЕЛЬ
|
|
359
|
+
[название]
|
|
360
|
+
[полный адрес]
|
|
361
|
+
Заезд [дата] после [время], выезд [дата] до [время]
|
|
362
|
+
Бронь №: [номер]
|
|
363
|
+
Бесплатная отмена до [дата/время]
|
|
364
|
+
|
|
365
|
+
ТРАНСФЕР (только если оформлен)
|
|
366
|
+
[сервис], [дата] [время], встреча у [пункт]
|
|
367
|
+
|
|
368
|
+
ТРАНСПОРТ ОБРАТНО
|
|
369
|
+
[аналогично, если оформлен]
|
|
370
|
+
|
|
371
|
+
В случае проблем — пиши/звони в HR.
|
|
372
|
+
Хорошей поездки!»
|
|
373
|
+
Секции, по которым брони нет, — опусти целиком.
|
|
374
|
+
|
|
375
|
+
БЛОК 2 — costTable: сводка стоимости для отчётности, markdown-таблица:
|
|
376
|
+
| Статья | ${corporate.currency || 'EUR'} |
|
|
377
|
+
|---|---|
|
|
378
|
+
строки по каждой оформленной броне (суммы из totalShown отчётов; нет суммы — из цены варианта), при заданном per diem — строка суточных, последняя строка ИТОГО.
|
|
379
|
+
|
|
380
|
+
БЛОК 3 — checklist: чеклист перед поездкой, каждая строка с «☐»:
|
|
381
|
+
☐ Загранпаспорт срок действия ≥ 6 месяцев на дату возврата
|
|
382
|
+
☐ Виза / Schengen — есть / не нужна
|
|
383
|
+
☐ Travel-страховка (если требуется по визе или политике компании)
|
|
384
|
+
☐ Корпоративная карта или наличные (на еду, такси, мелочи)
|
|
385
|
+
☐ Адаптер розеток (UK / EU / US — разные стандарты)
|
|
386
|
+
☐ Контакты HR + локальной службы такси сохранены в телефоне
|
|
387
|
+
☐ Чек-ин онлайн пройден (за 24 часа до вылета)
|
|
388
|
+
☐ Скриншоты броней сохранены оффлайн в телефоне
|
|
389
|
+
Пункты адаптируй под поездку (страна, виза, транспорт), лишние убери, нужные добавь.
|
|
390
|
+
|
|
391
|
+
ВЫВОД — верни ТОЛЬКО JSON, без прозы и markdown-ограждений:
|
|
392
|
+
{ "letter": "...", "costTable": "...", "checklist": "..." }
|
|
393
|
+
Переносы строк внутри значений — через \\n.`;
|
|
394
|
+
}
|