@iola_adm/iola-cli 0.2.21 → 0.2.23
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 +5 -3
- package/package.json +1 -1
- package/skills/yandex-services/SKILL.md +41 -0
- package/src/cli.js +773 -7
- package/wiki/Skills-/320/270-toolsets.md +15 -1
- package/wiki/Yandex-Connector.md +29 -16
- package/wiki//320/241/320/272/320/270/320/273/320/273/321/213-/320/264/320/273/321/217-/320/266/320/270/321/202/320/265/320/273/320/265/320/271.md +49 -1
package/README.md
CHANGED
|
@@ -199,7 +199,9 @@ iola yandex status
|
|
|
199
199
|
|
|
200
200
|
Yandex Connector использует две встроенные OAuth-группы: `IOLA CLI A` для Yandex ID, Диска, Почты и документов через Диск; `IOLA CLI B` для Календаря, Контактов и Телемоста через календарь. Такси, Маркет и Доставка записаны в backlog только как сценарии подготовки ссылки/маршрута/списка без заказа и оплаты.
|
|
201
201
|
|
|
202
|
-
В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. Мастер считает коннектор готовым только после токенов обеих групп.
|
|
202
|
+
В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. Мастер считает коннектор готовым только после токенов обеих групп.
|
|
203
|
+
|
|
204
|
+
Первый набор Yandex tools уже доступен: профиль Yandex ID, список/поиск/папки/ссылки Яндекс Диска, статус/список/поиск/чтение/отправка Яндекс Почты, события Календаря, Контакты и подготовка встречи через календарь. Отправка письма, удаление файлов, публикация ссылок и создание событий требуют явного подтверждения.
|
|
203
205
|
|
|
204
206
|
Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
|
|
205
207
|
|
|
@@ -252,8 +254,8 @@ iola version --check
|
|
|
252
254
|
- поиск и выгрузка открытых данных;
|
|
253
255
|
- локальная SQLite-БД, история, сессии и FTS-поиск;
|
|
254
256
|
- AI-профили для IOLA local, Ollama, YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
|
|
255
|
-
- Yandex Connector: единая точка подключения пользовательских сервисов Яндекса с локальным хранением OAuth
|
|
256
|
-
- локальный tool-agent для модели IOLA с tools
|
|
257
|
+
- Yandex Connector: единая точка подключения пользовательских сервисов Яндекса с локальным хранением OAuth-токенов;
|
|
258
|
+
- локальный tool-agent для модели IOLA с tools открытых данных, файлов, браузера и сервисов Яндекса;
|
|
257
259
|
- ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
|
|
258
260
|
- личные облачные диски: Яндекс Диск и Облако Mail.ru для сохранения отчетов, backup и документов;
|
|
259
261
|
- subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yandex-services
|
|
3
|
+
description: Сервисы Яндекса через Yandex Connector: ID, Диск, Почта, Календарь, Контакты и Телемост.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Используй этот skill, когда пользователь явно просит работать с сервисами Яндекса: Яндекс Диск, почта, письма, календарь, встречи, контакты, Телемост или профиль Яндекса.
|
|
7
|
+
|
|
8
|
+
Не смешивай Яндекс Диск с локальными файлами на компьютере. Если пользователь говорит просто "файл" или "папка" без слов "Яндекс", "Диск", "облако", уточни: "Где выполнить действие: на компьютере или на Яндекс Диске?"
|
|
9
|
+
|
|
10
|
+
Доступные tools:
|
|
11
|
+
|
|
12
|
+
- `yandex_identity_me` - проверить подключенный аккаунт Яндекса.
|
|
13
|
+
- `yandex_disk_info` - посмотреть место на Яндекс Диске.
|
|
14
|
+
- `yandex_disk_ls` - показать папку на Яндекс Диске.
|
|
15
|
+
- `yandex_disk_mkdir` - создать папку на Яндекс Диске.
|
|
16
|
+
- `yandex_disk_find` - найти файл или папку на Яндекс Диске.
|
|
17
|
+
- `yandex_disk_save_text` - сохранить текстовый результат в файл на Яндекс Диске.
|
|
18
|
+
- `yandex_disk_upload` - загрузить локальный файл на Яндекс Диск.
|
|
19
|
+
- `yandex_disk_download` - скачать файл с Яндекс Диска.
|
|
20
|
+
- `yandex_disk_share` - сделать публичную ссылку.
|
|
21
|
+
- `yandex_disk_unshare` - снять публичную ссылку.
|
|
22
|
+
- `yandex_disk_delete` - удалить файл или папку.
|
|
23
|
+
- `yandex_mail_status` - проверить доступ к Яндекс Почте.
|
|
24
|
+
- `yandex_mail_list` - показать последние письма.
|
|
25
|
+
- `yandex_mail_search` - найти письма.
|
|
26
|
+
- `yandex_mail_read` - прочитать письмо по UID.
|
|
27
|
+
- `yandex_mail_send` - отправить письмо.
|
|
28
|
+
- `yandex_calendar_status` - проверить доступ к Яндекс Календарю.
|
|
29
|
+
- `yandex_calendar_list` - показать события.
|
|
30
|
+
- `yandex_calendar_create_event` - создать событие.
|
|
31
|
+
- `yandex_contacts_status` - проверить доступ к Яндекс Контактам.
|
|
32
|
+
- `yandex_contacts_list` - показать контакты.
|
|
33
|
+
- `yandex_contacts_search` - найти контакт.
|
|
34
|
+
- `yandex_telemost_create_event` - создать календарное событие для встречи.
|
|
35
|
+
|
|
36
|
+
Безопасность:
|
|
37
|
+
|
|
38
|
+
- Для отправки письма, удаления файлов, публикации ссылки и создания событий нужен явный запрос пользователя и `confirm=true`.
|
|
39
|
+
- Не отправляй письма, не удаляй файлы и не публикуй ссылки по косвенному намерению.
|
|
40
|
+
- Не выводи OAuth-токены, API-ключи, пароли и секреты.
|
|
41
|
+
- Если сервис не подключен, скажи запустить `iola yandex setup` или открыть `/master`.
|
package/src/cli.js
CHANGED
|
@@ -151,7 +151,32 @@ const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
|
151
151
|
const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "get_card", "export_report", "file_read", "browser_open", "get_current_date"];
|
|
152
152
|
const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
|
|
153
153
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
154
|
-
const
|
|
154
|
+
const YANDEX_TOOLS = [
|
|
155
|
+
"yandex_identity_me",
|
|
156
|
+
"yandex_disk_info",
|
|
157
|
+
"yandex_disk_ls",
|
|
158
|
+
"yandex_disk_mkdir",
|
|
159
|
+
"yandex_disk_find",
|
|
160
|
+
"yandex_disk_save_text",
|
|
161
|
+
"yandex_disk_upload",
|
|
162
|
+
"yandex_disk_download",
|
|
163
|
+
"yandex_disk_share",
|
|
164
|
+
"yandex_disk_unshare",
|
|
165
|
+
"yandex_disk_delete",
|
|
166
|
+
"yandex_mail_status",
|
|
167
|
+
"yandex_mail_list",
|
|
168
|
+
"yandex_mail_search",
|
|
169
|
+
"yandex_mail_read",
|
|
170
|
+
"yandex_mail_send",
|
|
171
|
+
"yandex_calendar_status",
|
|
172
|
+
"yandex_calendar_create_event",
|
|
173
|
+
"yandex_calendar_list",
|
|
174
|
+
"yandex_contacts_status",
|
|
175
|
+
"yandex_contacts_list",
|
|
176
|
+
"yandex_contacts_search",
|
|
177
|
+
"yandex_telemost_create_event",
|
|
178
|
+
];
|
|
179
|
+
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS];
|
|
155
180
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
156
181
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
157
182
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
@@ -176,6 +201,13 @@ const TOOLSETS = {
|
|
|
176
201
|
description: "Внешние AI-провайдеры и Codex CLI.",
|
|
177
202
|
permissions: { externalAi: true, codex: true },
|
|
178
203
|
},
|
|
204
|
+
yandex: {
|
|
205
|
+
description: "Сервисы Яндекса через Yandex Connector: ID, Диск, Почта, Календарь, Контакты.",
|
|
206
|
+
permissions: {
|
|
207
|
+
externalApi: true,
|
|
208
|
+
localTools: Object.fromEntries(YANDEX_TOOLS.map((tool) => [tool, true])),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
179
211
|
"local-files-read": {
|
|
180
212
|
description: "Чтение файлов, дерево папок и поиск внутри workspace.",
|
|
181
213
|
permissions: { readFiles: true, localTools: { files_tree: true, files_read: true, files_search: true } },
|
|
@@ -306,6 +338,29 @@ const DEFAULT_AI_CONFIG = {
|
|
|
306
338
|
export_report: true,
|
|
307
339
|
file_read: false,
|
|
308
340
|
browser_open: true,
|
|
341
|
+
yandex_identity_me: true,
|
|
342
|
+
yandex_disk_info: true,
|
|
343
|
+
yandex_disk_ls: true,
|
|
344
|
+
yandex_disk_mkdir: true,
|
|
345
|
+
yandex_disk_find: true,
|
|
346
|
+
yandex_disk_save_text: true,
|
|
347
|
+
yandex_disk_upload: false,
|
|
348
|
+
yandex_disk_download: false,
|
|
349
|
+
yandex_disk_share: false,
|
|
350
|
+
yandex_disk_unshare: false,
|
|
351
|
+
yandex_disk_delete: false,
|
|
352
|
+
yandex_mail_status: true,
|
|
353
|
+
yandex_mail_list: true,
|
|
354
|
+
yandex_mail_search: true,
|
|
355
|
+
yandex_mail_read: true,
|
|
356
|
+
yandex_mail_send: false,
|
|
357
|
+
yandex_calendar_status: true,
|
|
358
|
+
yandex_calendar_create_event: false,
|
|
359
|
+
yandex_calendar_list: true,
|
|
360
|
+
yandex_contacts_status: true,
|
|
361
|
+
yandex_contacts_list: true,
|
|
362
|
+
yandex_contacts_search: true,
|
|
363
|
+
yandex_telemost_create_event: false,
|
|
309
364
|
files_tree: false,
|
|
310
365
|
files_read: false,
|
|
311
366
|
files_search: false,
|
|
@@ -322,7 +377,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
322
377
|
codex: true,
|
|
323
378
|
},
|
|
324
379
|
toolsets: {
|
|
325
|
-
enabled: ["data-read", "reports", "sync", "ai"],
|
|
380
|
+
enabled: ["data-read", "reports", "sync", "ai", "yandex"],
|
|
326
381
|
},
|
|
327
382
|
files: {
|
|
328
383
|
mode: "locked",
|
|
@@ -336,7 +391,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
336
391
|
suggestions: true,
|
|
337
392
|
},
|
|
338
393
|
skills: {
|
|
339
|
-
enabled: ["education", "open-data", "geo", "personal-docs", "reports", "local-model", "local-files", "browser-agent"],
|
|
394
|
+
enabled: ["education", "open-data", "geo", "personal-docs", "reports", "local-model", "local-files", "browser-agent", "yandex-services"],
|
|
340
395
|
},
|
|
341
396
|
cloud: {
|
|
342
397
|
activeProvider: "",
|
|
@@ -3795,6 +3850,563 @@ async function yandexUserInfo(token) {
|
|
|
3795
3850
|
return response.json();
|
|
3796
3851
|
}
|
|
3797
3852
|
|
|
3853
|
+
async function getYandexOAuthToken(appId = "core") {
|
|
3854
|
+
if (process.env.YANDEX_OAUTH_TOKEN) return process.env.YANDEX_OAUTH_TOKEN;
|
|
3855
|
+
const secrets = await loadSecrets();
|
|
3856
|
+
return secrets.yandex?.oauthApps?.[appId]?.token
|
|
3857
|
+
|| (appId === "core" ? secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token : "")
|
|
3858
|
+
|| "";
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
async function requireYandexOAuthToken(appId = "core", service = "Yandex Connector") {
|
|
3862
|
+
const token = await getYandexOAuthToken(appId);
|
|
3863
|
+
if (!token) throw new Error(`${service} не подключен. Запустите: iola yandex setup`);
|
|
3864
|
+
return token;
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
async function getYandexIdentityProfile() {
|
|
3868
|
+
const token = await requireYandexOAuthToken("core", "Yandex ID");
|
|
3869
|
+
const profile = await yandexUserInfo(token);
|
|
3870
|
+
return {
|
|
3871
|
+
login: profile.login || "",
|
|
3872
|
+
displayName: profile.display_name || profile.real_name || "",
|
|
3873
|
+
defaultEmail: profile.default_email || profile.default_email_alias || "",
|
|
3874
|
+
emails: profile.emails || [],
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
async function yandexDiskInfo() {
|
|
3879
|
+
const payload = await yandexDiskRequest("GET", "/", { query: { fields: "total_space,used_space,trash_size,system_folders" } });
|
|
3880
|
+
return {
|
|
3881
|
+
provider: "yandex-disk",
|
|
3882
|
+
totalSpace: payload.total_space || 0,
|
|
3883
|
+
usedSpace: payload.used_space || 0,
|
|
3884
|
+
trashSize: payload.trash_size || 0,
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
async function yandexDiskSaveText(text, remotePath) {
|
|
3889
|
+
if (!text) throw new Error("Текст для сохранения пустой.");
|
|
3890
|
+
const target = remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/notes/iola-${timestampForFile()}.txt`;
|
|
3891
|
+
const tempPath = path.join(CONFIG_DIR, `yandex-save-${Date.now()}.txt`);
|
|
3892
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
3893
|
+
await writeFile(tempPath, text, "utf8");
|
|
3894
|
+
try {
|
|
3895
|
+
return await yandexDiskUpload(tempPath, target, { overwrite: true });
|
|
3896
|
+
} finally {
|
|
3897
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
async function yandexDiskUnshare(remotePath) {
|
|
3902
|
+
await yandexDiskRequest("PUT", "/resources/unpublish", { query: { path: normalizeYandexDiskPath(remotePath) } });
|
|
3903
|
+
return { provider: "yandex-disk", remote: remotePath, status: "unpublished" };
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
async function yandexDiskDelete(remotePath, options = {}) {
|
|
3907
|
+
if (!options.confirm) throw new Error("Для удаления с Яндекс Диска нужен аргумент confirm=true.");
|
|
3908
|
+
await yandexDiskRequest("DELETE", "/resources", { query: { path: normalizeYandexDiskPath(remotePath), permanently: Boolean(options.permanently) } });
|
|
3909
|
+
return { provider: "yandex-disk", remote: remotePath, status: options.permanently ? "deleted-permanently" : "moved-to-trash" };
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
async function executeYandexTool(tool, args = {}) {
|
|
3913
|
+
if (tool === "yandex_identity_me") return getYandexIdentityProfile();
|
|
3914
|
+
if (tool === "yandex_disk_info") return yandexDiskInfo();
|
|
3915
|
+
if (tool === "yandex_disk_ls") return yandexDiskList(args.path || args.remotePath || CLOUD_DEFAULT_REMOTE_DIR, { allowMissingRoot: true });
|
|
3916
|
+
if (tool === "yandex_disk_mkdir") return cloudCreateFolder("yandex-disk", args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/Новая папка`);
|
|
3917
|
+
if (tool === "yandex_disk_find") return yandexDiskFind(args.query || args.name || "", { path: args.path || CLOUD_DEFAULT_REMOTE_DIR, depth: args.depth || 4, limit: args.limit || 20 });
|
|
3918
|
+
if (tool === "yandex_disk_save_text") return yandexDiskSaveText(args.text || args.content || "", args.path || args.remotePath);
|
|
3919
|
+
if (tool === "yandex_disk_upload") return yandexDiskUpload(args.localPath || args.file || args.path, args.remotePath || args.remote || `${CLOUD_DEFAULT_REMOTE_DIR}/${path.basename(args.localPath || args.file || "file.txt")}`, { overwrite: args.overwrite !== false });
|
|
3920
|
+
if (tool === "yandex_disk_download") return yandexDiskDownload(args.remotePath || args.path, args.outputPath || args.output || path.basename(args.remotePath || args.path || "download"));
|
|
3921
|
+
if (tool === "yandex_disk_share") {
|
|
3922
|
+
if (!args.confirm) throw new Error("Для публикации ссылки нужен аргумент confirm=true.");
|
|
3923
|
+
return yandexDiskShare(args.remotePath || args.path);
|
|
3924
|
+
}
|
|
3925
|
+
if (tool === "yandex_disk_unshare") return yandexDiskUnshare(args.remotePath || args.path);
|
|
3926
|
+
if (tool === "yandex_disk_delete") return yandexDiskDelete(args.remotePath || args.path, args);
|
|
3927
|
+
if (tool === "yandex_mail_status") return yandexMailStatus();
|
|
3928
|
+
if (tool === "yandex_mail_list") return yandexMailList({ mailbox: args.mailbox || "INBOX", limit: args.limit || 10, unread: Boolean(args.unread) });
|
|
3929
|
+
if (tool === "yandex_mail_search") return yandexMailSearch(args.query || "", { mailbox: args.mailbox || "INBOX", limit: args.limit || 20 });
|
|
3930
|
+
if (tool === "yandex_mail_read") return yandexMailRead(args.uid || args.id, { mailbox: args.mailbox || "INBOX" });
|
|
3931
|
+
if (tool === "yandex_mail_send") return yandexMailSend(args);
|
|
3932
|
+
if (tool === "yandex_calendar_status") return yandexCalendarStatus();
|
|
3933
|
+
if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
|
|
3934
|
+
if (tool === "yandex_calendar_list") return yandexCalendarList(args);
|
|
3935
|
+
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
3936
|
+
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
3937
|
+
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
3938
|
+
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
3939
|
+
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
async function yandexMailCredentials() {
|
|
3943
|
+
const [token, profile] = await Promise.all([
|
|
3944
|
+
requireYandexOAuthToken("core", "Яндекс Почта"),
|
|
3945
|
+
getYandexIdentityProfile(),
|
|
3946
|
+
]);
|
|
3947
|
+
const email = profile.defaultEmail || profile.emails?.[0] || (profile.login ? `${profile.login}@yandex.ru` : "");
|
|
3948
|
+
if (!email) throw new Error("Не удалось определить email Яндекс Почты.");
|
|
3949
|
+
return { token, email };
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
async function yandexMailStatus() {
|
|
3953
|
+
const { email } = await yandexMailCredentials();
|
|
3954
|
+
const session = await imapConnect();
|
|
3955
|
+
try {
|
|
3956
|
+
await imapAuthenticate(session, email, await requireYandexOAuthToken("core", "Яндекс Почта"));
|
|
3957
|
+
const inbox = await imapCommand(session, "SELECT INBOX");
|
|
3958
|
+
return { email, status: "ok", inbox: parseImapSelect(inbox) };
|
|
3959
|
+
} finally {
|
|
3960
|
+
await imapClose(session);
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
async function yandexMailList(options = {}) {
|
|
3965
|
+
const { token, email } = await yandexMailCredentials();
|
|
3966
|
+
const session = await imapConnect();
|
|
3967
|
+
try {
|
|
3968
|
+
await imapAuthenticate(session, email, token);
|
|
3969
|
+
await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
|
|
3970
|
+
const criterion = options.unread ? "UNSEEN" : "ALL";
|
|
3971
|
+
const search = await imapCommand(session, `UID SEARCH ${criterion}`);
|
|
3972
|
+
const uids = parseImapSearchUids(search).slice(-Number(options.limit || 10));
|
|
3973
|
+
if (!uids.length) return [];
|
|
3974
|
+
const fetch = await imapCommand(session, `UID FETCH ${uids.join(",")} (UID FLAGS ENVELOPE RFC822.SIZE BODY.PEEK[TEXT]<0.800>)`, { timeout: 45000 });
|
|
3975
|
+
return parseImapFetchSummaries(fetch);
|
|
3976
|
+
} finally {
|
|
3977
|
+
await imapClose(session);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
async function yandexMailSearch(query, options = {}) {
|
|
3982
|
+
const normalized = normalizeGeoText(query);
|
|
3983
|
+
if (!normalized) return yandexMailList(options);
|
|
3984
|
+
const rows = await yandexMailList({ ...options, limit: Math.max(50, Number(options.limit || 20) * 3) });
|
|
3985
|
+
return rows.filter((row) => normalizeGeoText(`${row.from} ${row.subject} ${row.snippet}`).includes(normalized)).slice(0, Number(options.limit || 20));
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
async function yandexMailRead(uid, options = {}) {
|
|
3989
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
3990
|
+
const { token, email } = await yandexMailCredentials();
|
|
3991
|
+
const session = await imapConnect();
|
|
3992
|
+
try {
|
|
3993
|
+
await imapAuthenticate(session, email, token);
|
|
3994
|
+
await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
|
|
3995
|
+
const fetch = await imapCommand(session, `UID FETCH ${Number(uid)} (UID FLAGS ENVELOPE RFC822.SIZE BODY.PEEK[])`, { timeout: 60000 });
|
|
3996
|
+
return parseImapFetchSummaries(fetch, { full: true })[0] || { uid, status: "not-found" };
|
|
3997
|
+
} finally {
|
|
3998
|
+
await imapClose(session);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
async function yandexMailSend(args = {}) {
|
|
4003
|
+
if (!args.confirm) throw new Error("Для отправки письма нужен аргумент confirm=true.");
|
|
4004
|
+
const to = Array.isArray(args.to) ? args.to : String(args.to || "").split(/[;,]/).map((item) => item.trim()).filter(Boolean);
|
|
4005
|
+
if (!to.length) throw new Error("Получатель письма не указан.");
|
|
4006
|
+
const subject = args.subject || "Сообщение от IOLA CLI";
|
|
4007
|
+
const text = args.text || args.body || "";
|
|
4008
|
+
if (!text.trim()) throw new Error("Текст письма пустой.");
|
|
4009
|
+
const { token, email } = await yandexMailCredentials();
|
|
4010
|
+
const session = await smtpConnect();
|
|
4011
|
+
try {
|
|
4012
|
+
await smtpCommand(session, `EHLO ${os.hostname() || "iola.local"}`);
|
|
4013
|
+
await smtpCommand(session, `AUTH XOAUTH2 ${buildXoauth2(email, token)}`);
|
|
4014
|
+
await smtpCommand(session, `MAIL FROM:<${email}>`);
|
|
4015
|
+
for (const recipient of to) await smtpCommand(session, `RCPT TO:<${recipient}>`);
|
|
4016
|
+
await smtpCommand(session, "DATA", { expect: /^354/u });
|
|
4017
|
+
await smtpCommand(session, `${buildMimeMessage({ from: email, to, subject, text })}\r\n.`);
|
|
4018
|
+
await smtpCommand(session, "QUIT").catch(() => {});
|
|
4019
|
+
return { from: email, to, subject, status: "sent" };
|
|
4020
|
+
} finally {
|
|
4021
|
+
session.socket.destroy();
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
function buildXoauth2(email, token) {
|
|
4026
|
+
return Buffer.from(`user=${email}\x01auth=Bearer ${token}\x01\x01`).toString("base64");
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
function buildMimeMessage({ from, to, subject, text }) {
|
|
4030
|
+
const encodedSubject = Buffer.from(subject, "utf8").toString("base64");
|
|
4031
|
+
return [
|
|
4032
|
+
`From: ${from}`,
|
|
4033
|
+
`To: ${to.join(", ")}`,
|
|
4034
|
+
`Subject: =?UTF-8?B?${encodedSubject}?=`,
|
|
4035
|
+
"MIME-Version: 1.0",
|
|
4036
|
+
"Content-Type: text/plain; charset=utf-8",
|
|
4037
|
+
"Content-Transfer-Encoding: base64",
|
|
4038
|
+
"",
|
|
4039
|
+
Buffer.from(text.replace(/\r?\n/g, "\r\n"), "utf8").toString("base64").replace(/.{1,76}/g, "$&\r\n").trim(),
|
|
4040
|
+
].join("\r\n");
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
function imapConnect() {
|
|
4044
|
+
return tlsLineSession("imap.yandex.ru", 993, "* OK");
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
function smtpConnect() {
|
|
4048
|
+
return tlsLineSession("smtp.yandex.ru", 465, /^220/u);
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
function tlsLineSession(host, port, greetingPattern) {
|
|
4052
|
+
return new Promise((resolve, reject) => {
|
|
4053
|
+
const socket = tls.connect({ host, port, servername: host, timeout: 30000 }, () => {});
|
|
4054
|
+
const session = { socket, buffer: "", lines: [], tag: 0 };
|
|
4055
|
+
const timer = setTimeout(() => {
|
|
4056
|
+
socket.destroy();
|
|
4057
|
+
reject(new Error(`${host}:${port} connection timeout`));
|
|
4058
|
+
}, 30000);
|
|
4059
|
+
socket.setEncoding("utf8");
|
|
4060
|
+
socket.on("data", (chunk) => {
|
|
4061
|
+
session.buffer += chunk;
|
|
4062
|
+
const parts = session.buffer.split(/\r?\n/);
|
|
4063
|
+
session.buffer = parts.pop() || "";
|
|
4064
|
+
session.lines.push(...parts.filter(Boolean));
|
|
4065
|
+
const text = session.lines.join("\n");
|
|
4066
|
+
const ok = typeof greetingPattern === "string" ? text.includes(greetingPattern) : greetingPattern.test(text);
|
|
4067
|
+
if (ok) {
|
|
4068
|
+
clearTimeout(timer);
|
|
4069
|
+
resolve(session);
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
4072
|
+
socket.on("error", (error) => {
|
|
4073
|
+
clearTimeout(timer);
|
|
4074
|
+
reject(error);
|
|
4075
|
+
});
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
async function imapAuthenticate(session, email, token) {
|
|
4080
|
+
await imapCommand(session, `AUTHENTICATE XOAUTH2 ${buildXoauth2(email, token)}`, { sensitive: token });
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
function imapCommand(session, command, options = {}) {
|
|
4084
|
+
const tag = `A${++session.tag}`;
|
|
4085
|
+
session.lines = [];
|
|
4086
|
+
return sendTaggedCommand(session, `${tag} ${command}`, new RegExp(`^${tag} (OK|NO|BAD)`, "imu"), {
|
|
4087
|
+
timeout: options.timeout || 30000,
|
|
4088
|
+
sensitive: options.sensitive,
|
|
4089
|
+
ok: new RegExp(`^${tag} OK`, "imu"),
|
|
4090
|
+
});
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
function smtpCommand(session, command, options = {}) {
|
|
4094
|
+
session.lines = [];
|
|
4095
|
+
return sendTaggedCommand(session, command, options.expect || /^[235]|\n[235]/u, {
|
|
4096
|
+
timeout: options.timeout || 30000,
|
|
4097
|
+
ok: options.expect || /^[235]|\n[235]/u,
|
|
4098
|
+
});
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
function sendTaggedCommand(session, command, donePattern, options = {}) {
|
|
4102
|
+
return new Promise((resolve, reject) => {
|
|
4103
|
+
const timer = setTimeout(() => reject(new Error("Yandex mail command timeout")), options.timeout || 30000);
|
|
4104
|
+
const onData = () => {
|
|
4105
|
+
const text = session.lines.join("\n");
|
|
4106
|
+
if (!donePattern.test(text)) return;
|
|
4107
|
+
clearTimeout(timer);
|
|
4108
|
+
session.socket.off("data", onData);
|
|
4109
|
+
if (options.ok && !options.ok.test(text)) {
|
|
4110
|
+
reject(new Error(sanitizeSecretFromText(`Yandex mail command failed: ${text}`, options.sensitive)));
|
|
4111
|
+
} else {
|
|
4112
|
+
resolve(text);
|
|
4113
|
+
}
|
|
4114
|
+
};
|
|
4115
|
+
session.socket.on("data", onData);
|
|
4116
|
+
session.socket.write(`${command}\r\n`);
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
async function imapClose(session) {
|
|
4121
|
+
await imapCommand(session, "LOGOUT").catch(() => {});
|
|
4122
|
+
session.socket.destroy();
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
function quoteImapMailbox(value) {
|
|
4126
|
+
return `"${String(value || "INBOX").replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
function parseImapSelect(text) {
|
|
4130
|
+
return {
|
|
4131
|
+
exists: Number(text.match(/\* (\d+) EXISTS/iu)?.[1] || 0),
|
|
4132
|
+
recent: Number(text.match(/\* (\d+) RECENT/iu)?.[1] || 0),
|
|
4133
|
+
unseen: Number(text.match(/UNSEEN (\d+)/iu)?.[1] || 0),
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
function parseImapSearchUids(text) {
|
|
4138
|
+
return [...String(text).matchAll(/\* SEARCH ([\d\s]+)/giu)]
|
|
4139
|
+
.flatMap((match) => match[1].trim().split(/\s+/).map(Number).filter(Boolean));
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
function parseImapFetchSummaries(text, options = {}) {
|
|
4143
|
+
const rows = [];
|
|
4144
|
+
const chunks = String(text || "").split(/\n(?=\* \d+ FETCH)/u);
|
|
4145
|
+
for (const chunk of chunks) {
|
|
4146
|
+
const uid = Number(chunk.match(/UID (\d+)/iu)?.[1] || 0);
|
|
4147
|
+
if (!uid) continue;
|
|
4148
|
+
const subject = decodeMimeHeader(chunk.match(/ENVELOPE \([^\n]*?"([^"]*)"/iu)?.[1] || chunk.match(/Subject:\s*([^\r\n]+)/iu)?.[1] || "");
|
|
4149
|
+
const from = decodeMimeHeader(chunk.match(/From:\s*([^\r\n]+)/iu)?.[1] || chunk.match(/NIL NIL "([^"]*)" "([^"]*)"/iu)?.slice(1, 3).filter(Boolean).join("@") || "");
|
|
4150
|
+
const date = chunk.match(/INTERNALDATE "([^"]+)"/iu)?.[1] || chunk.match(/Date:\s*([^\r\n]+)/iu)?.[1] || "";
|
|
4151
|
+
const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0, 800);
|
|
4152
|
+
rows.push({ uid, date, from, subject, snippet: body.replace(/\s+/g, " ").trim().slice(0, options.full ? 12000 : 500) });
|
|
4153
|
+
}
|
|
4154
|
+
return rows;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
function decodeMimeHeader(value) {
|
|
4158
|
+
return String(value || "").replace(/=\?UTF-8\?B\?([^?]+)\?=/giu, (_, data) => Buffer.from(data, "base64").toString("utf8"));
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
function stripMailBody(value) {
|
|
4162
|
+
return String(value || "")
|
|
4163
|
+
.replace(/\r/g, "")
|
|
4164
|
+
.split(/\n\)/u)[0]
|
|
4165
|
+
.replace(/^[\s\S]*?\n\n/u, "")
|
|
4166
|
+
.replace(/[^\S\n]+/g, " ")
|
|
4167
|
+
.trim();
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
async function yandexDavRequest(url, token, options = {}) {
|
|
4171
|
+
const response = await fetch(url, {
|
|
4172
|
+
method: options.method || "GET",
|
|
4173
|
+
headers: {
|
|
4174
|
+
Authorization: `OAuth ${token}`,
|
|
4175
|
+
...(options.xml ? { "content-type": "application/xml; charset=utf-8" } : {}),
|
|
4176
|
+
...(options.ics ? { "content-type": "text/calendar; charset=utf-8" } : {}),
|
|
4177
|
+
...(options.headers || {}),
|
|
4178
|
+
},
|
|
4179
|
+
body: options.body,
|
|
4180
|
+
signal: AbortSignal.timeout(Number(options.timeout || 30000)),
|
|
4181
|
+
});
|
|
4182
|
+
const text = await response.text().catch(() => "");
|
|
4183
|
+
if (!response.ok && response.status !== 207) {
|
|
4184
|
+
throw new Error(`Yandex DAV request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text.slice(0, 1000), token)}`);
|
|
4185
|
+
}
|
|
4186
|
+
return text;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
async function yandexCalendarStatus() {
|
|
4190
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4191
|
+
const url = await yandexCalendarBaseUrl(token);
|
|
4192
|
+
const text = await yandexDavRequest(url, token, { method: "PROPFIND", headers: { Depth: "0" }, xml: true, body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/></d:prop></d:propfind>" });
|
|
4193
|
+
return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || "calendar" };
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
async function yandexCalendarCreateEvent(args = {}) {
|
|
4197
|
+
if (!args.confirm) throw new Error("Для создания события в календаре нужен аргумент confirm=true.");
|
|
4198
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4199
|
+
const baseUrl = await yandexCalendarBaseUrl(token);
|
|
4200
|
+
const uid = `${randomUUID()}@iola-cli`;
|
|
4201
|
+
const start = toIcsDate(args.start || args.date || new Date(Date.now() + 3600000).toISOString());
|
|
4202
|
+
const end = toIcsDate(args.end || new Date(Date.now() + 7200000).toISOString());
|
|
4203
|
+
const summary = args.title || args.summary || "Событие IOLA";
|
|
4204
|
+
const description = args.description || "";
|
|
4205
|
+
const ics = buildIcsEvent({ uid, start, end, summary, description, location: args.location || "" });
|
|
4206
|
+
const url = `${baseUrl}${encodeURIComponent(uid)}.ics`;
|
|
4207
|
+
await yandexDavRequest(url, token, { method: "PUT", ics: true, body: ics, timeout: 45000 });
|
|
4208
|
+
return { status: "created", uid, title: summary, start: args.start || args.date || "", url };
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
async function yandexCalendarList(args = {}) {
|
|
4212
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4213
|
+
const baseUrl = await yandexCalendarBaseUrl(token);
|
|
4214
|
+
const start = toIcsDate(args.start || new Date().toISOString());
|
|
4215
|
+
const end = toIcsDate(args.end || new Date(Date.now() + 14 * 86400000).toISOString());
|
|
4216
|
+
const body = `<?xml version="1.0"?>
|
|
4217
|
+
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
4218
|
+
<d:prop><d:getetag/><c:calendar-data/></d:prop>
|
|
4219
|
+
<c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VEVENT"><c:time-range start="${start}" end="${end}"/></c:comp-filter></c:comp-filter></c:filter>
|
|
4220
|
+
</c:calendar-query>`;
|
|
4221
|
+
const text = await yandexDavRequest(baseUrl, token, { method: "REPORT", headers: { Depth: "1" }, xml: true, body, timeout: 45000 });
|
|
4222
|
+
return parseIcsEvents(text).slice(0, Number(args.limit || 20));
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
async function yandexCalendarBaseUrl(token) {
|
|
4226
|
+
const root = "https://caldav.yandex.ru/";
|
|
4227
|
+
const principalXml = await yandexDavRequest(root, token, {
|
|
4228
|
+
method: "PROPFIND",
|
|
4229
|
+
headers: { Depth: "0" },
|
|
4230
|
+
xml: true,
|
|
4231
|
+
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:current-user-principal/></d:prop></d:propfind>",
|
|
4232
|
+
});
|
|
4233
|
+
const principalHref = extractDavHref(principalXml, "current-user-principal");
|
|
4234
|
+
if (!principalHref) throw new Error("Яндекс Календарь не вернул current-user-principal.");
|
|
4235
|
+
const homeXml = await yandexDavRequest(new URL(principalHref, root).toString(), token, {
|
|
4236
|
+
method: "PROPFIND",
|
|
4237
|
+
headers: { Depth: "0" },
|
|
4238
|
+
xml: true,
|
|
4239
|
+
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\"><d:prop><c:calendar-home-set/></d:prop></d:propfind>",
|
|
4240
|
+
});
|
|
4241
|
+
const homeHref = extractDavHref(homeXml, "calendar-home-set");
|
|
4242
|
+
if (!homeHref) throw new Error("Яндекс Календарь не вернул calendar-home-set.");
|
|
4243
|
+
const listXml = await yandexDavRequest(new URL(homeHref, root).toString(), token, {
|
|
4244
|
+
method: "PROPFIND",
|
|
4245
|
+
headers: { Depth: "1" },
|
|
4246
|
+
xml: true,
|
|
4247
|
+
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>",
|
|
4248
|
+
});
|
|
4249
|
+
const calendarHref = extractCalendarCollectionHref(listXml);
|
|
4250
|
+
if (!calendarHref) throw new Error("В Яндекс Календаре не найдена календарная коллекция.");
|
|
4251
|
+
return new URL(calendarHref, root).toString();
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
async function yandexContactsStatus() {
|
|
4255
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4256
|
+
const url = await yandexContactsBaseUrl(token);
|
|
4257
|
+
const text = await yandexDavRequest(url, token, { method: "PROPFIND", headers: { Depth: "0" }, xml: true, body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/></d:prop></d:propfind>" });
|
|
4258
|
+
return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || "contacts" };
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
async function yandexContactsList(args = {}) {
|
|
4262
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4263
|
+
const url = await yandexContactsBaseUrl(token);
|
|
4264
|
+
const body = "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:getetag/><d:getcontenttype/></d:prop></d:propfind>";
|
|
4265
|
+
const text = await yandexDavRequest(url, token, { method: "PROPFIND", headers: { Depth: "1" }, xml: true, body, timeout: 45000 });
|
|
4266
|
+
const hrefs = extractVcardHrefs(text).slice(0, Number(args.limit || 50));
|
|
4267
|
+
const rows = [];
|
|
4268
|
+
for (const href of hrefs) {
|
|
4269
|
+
const card = await yandexDavRequest(new URL(href, "https://carddav.yandex.ru/").toString(), token, { method: "GET", timeout: 30000 }).catch(() => "");
|
|
4270
|
+
rows.push(...parseVCards(card));
|
|
4271
|
+
}
|
|
4272
|
+
return rows.slice(0, Number(args.limit || 50));
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
async function yandexContactsSearch(query, args = {}) {
|
|
4276
|
+
const normalized = normalizeGeoText(query);
|
|
4277
|
+
const rows = await yandexContactsList({ limit: Math.max(100, Number(args.limit || 20) * 4) });
|
|
4278
|
+
if (!normalized) return rows.slice(0, Number(args.limit || 20));
|
|
4279
|
+
return rows.filter((row) => normalizeGeoText(`${row.name} ${row.email} ${row.phone}`).includes(normalized)).slice(0, Number(args.limit || 20));
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
async function yandexContactsBaseUrl(token) {
|
|
4283
|
+
const root = "https://carddav.yandex.ru/";
|
|
4284
|
+
const principalXml = await yandexDavRequest(root, token, {
|
|
4285
|
+
method: "PROPFIND",
|
|
4286
|
+
headers: { Depth: "0" },
|
|
4287
|
+
xml: true,
|
|
4288
|
+
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:current-user-principal/></d:prop></d:propfind>",
|
|
4289
|
+
});
|
|
4290
|
+
const principalHref = extractDavHref(principalXml, "current-user-principal");
|
|
4291
|
+
if (!principalHref) throw new Error("Яндекс Контакты не вернули current-user-principal.");
|
|
4292
|
+
const homeXml = await yandexDavRequest(new URL(principalHref, root).toString(), token, {
|
|
4293
|
+
method: "PROPFIND",
|
|
4294
|
+
headers: { Depth: "0" },
|
|
4295
|
+
xml: true,
|
|
4296
|
+
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\"><d:prop><card:addressbook-home-set/></d:prop></d:propfind>",
|
|
4297
|
+
});
|
|
4298
|
+
const homeHref = extractDavHref(homeXml, "addressbook-home-set");
|
|
4299
|
+
if (!homeHref) throw new Error("Яндекс Контакты не вернули addressbook-home-set.");
|
|
4300
|
+
const listXml = await yandexDavRequest(new URL(homeHref, root).toString(), token, {
|
|
4301
|
+
method: "PROPFIND",
|
|
4302
|
+
headers: { Depth: "1" },
|
|
4303
|
+
xml: true,
|
|
4304
|
+
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>",
|
|
4305
|
+
});
|
|
4306
|
+
const addressBookHref = extractAddressBookCollectionHref(listXml);
|
|
4307
|
+
if (!addressBookHref) throw new Error("В Яндекс Контактах не найдена адресная книга.");
|
|
4308
|
+
return new URL(addressBookHref, root).toString();
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
async function yandexTelemostCreateEvent(args = {}) {
|
|
4312
|
+
const description = [
|
|
4313
|
+
args.description || "",
|
|
4314
|
+
"",
|
|
4315
|
+
"Телемост: создайте ссылку в Яндекс Календаре, если интерфейс календаря предложит видеовстречу.",
|
|
4316
|
+
].join("\n").trim();
|
|
4317
|
+
return yandexCalendarCreateEvent({ ...args, description });
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
function buildIcsEvent({ uid, start, end, summary, description, location }) {
|
|
4321
|
+
const escape = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
|
|
4322
|
+
return [
|
|
4323
|
+
"BEGIN:VCALENDAR",
|
|
4324
|
+
"VERSION:2.0",
|
|
4325
|
+
"PRODID:-//IOLA CLI//Yandex Calendar//RU",
|
|
4326
|
+
"BEGIN:VEVENT",
|
|
4327
|
+
`UID:${uid}`,
|
|
4328
|
+
`DTSTAMP:${toIcsDate(new Date().toISOString())}`,
|
|
4329
|
+
`DTSTART:${start}`,
|
|
4330
|
+
`DTEND:${end}`,
|
|
4331
|
+
`SUMMARY:${escape(summary)}`,
|
|
4332
|
+
description ? `DESCRIPTION:${escape(description)}` : "",
|
|
4333
|
+
location ? `LOCATION:${escape(location)}` : "",
|
|
4334
|
+
"END:VEVENT",
|
|
4335
|
+
"END:VCALENDAR",
|
|
4336
|
+
"",
|
|
4337
|
+
].filter(Boolean).join("\r\n");
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
function toIcsDate(value) {
|
|
4341
|
+
const date = new Date(value);
|
|
4342
|
+
if (Number.isNaN(date.getTime())) throw new Error(`Некорректная дата: ${value}`);
|
|
4343
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/u, "Z");
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
function parseIcsEvents(xmlOrIcs) {
|
|
4347
|
+
const decoded = decodeXml(stripXmlTags(xmlOrIcs));
|
|
4348
|
+
return decoded.split("BEGIN:VEVENT").slice(1).map((chunk) => ({
|
|
4349
|
+
uid: chunk.match(/\nUID:([^\n\r]+)/u)?.[1] || "",
|
|
4350
|
+
title: chunk.match(/\nSUMMARY:([^\n\r]+)/u)?.[1] || "",
|
|
4351
|
+
start: chunk.match(/\nDTSTART[^:]*:([^\n\r]+)/u)?.[1] || "",
|
|
4352
|
+
end: chunk.match(/\nDTEND[^:]*:([^\n\r]+)/u)?.[1] || "",
|
|
4353
|
+
location: chunk.match(/\nLOCATION:([^\n\r]+)/u)?.[1] || "",
|
|
4354
|
+
})).filter((item) => item.uid || item.title);
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4357
|
+
function extractDavHref(xml, propertyName) {
|
|
4358
|
+
const pattern = new RegExp(`<[^:>]*:?${propertyName}[^>]*>[\\s\\S]*?<[^:>]*:?href[^>]*>([\\s\\S]*?)<\\/[^:>]*:?href>`, "iu");
|
|
4359
|
+
const value = String(xml || "").match(pattern)?.[1] || "";
|
|
4360
|
+
return decodeXml(stripXmlTags(value)).trim();
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
function extractCalendarCollectionHref(xml) {
|
|
4364
|
+
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
4365
|
+
for (const response of responses) {
|
|
4366
|
+
if (!/<[^:>]*:?calendar(?:\s|>|\/)/iu.test(response)) continue;
|
|
4367
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
4368
|
+
if (href && !/\/(?:inbox|outbox)\//iu.test(href)) return href;
|
|
4369
|
+
}
|
|
4370
|
+
return "";
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
function extractAddressBookCollectionHref(xml) {
|
|
4374
|
+
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
4375
|
+
for (const response of responses) {
|
|
4376
|
+
if (!/<[^:>]*:?addressbook(?:\s|>|\/)/iu.test(response)) continue;
|
|
4377
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
4378
|
+
if (href) return href;
|
|
4379
|
+
}
|
|
4380
|
+
return "";
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
function extractVcardHrefs(xml) {
|
|
4384
|
+
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
4385
|
+
const rows = [];
|
|
4386
|
+
for (const response of responses) {
|
|
4387
|
+
if (!/text\/x-vcard|text\/vcard/iu.test(response)) continue;
|
|
4388
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
4389
|
+
if (href) rows.push(href);
|
|
4390
|
+
}
|
|
4391
|
+
return rows;
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
function parseVCards(xmlOrCards) {
|
|
4395
|
+
const decoded = decodeXml(stripXmlTags(xmlOrCards)).replace(/\r/g, "");
|
|
4396
|
+
return decoded.split("BEGIN:VCARD").slice(1).map((chunk) => {
|
|
4397
|
+
const email = chunk.match(/^EMAIL[^:]*:([^\n]+)/mu)?.[1] || "";
|
|
4398
|
+
const phone = chunk.match(/^TEL[^:]*:([^\n]+)/mu)?.[1] || "";
|
|
4399
|
+
const name = cleanVcardName(chunk.match(/^FN:([^\n]+)/mu)?.[1] || chunk.match(/^N:([^\n]+)/mu)?.[1] || "", email);
|
|
4400
|
+
return { name, email, phone };
|
|
4401
|
+
}).filter((item) => item.name || item.email || item.phone);
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
function cleanVcardName(value, fallback = "") {
|
|
4405
|
+
const text = String(value || "").replace(/;/g, " ").replace(/\s+/g, " ").trim();
|
|
4406
|
+
if (!text || /^[\s;]+$/u.test(String(value || ""))) return fallback || "";
|
|
4407
|
+
return text;
|
|
4408
|
+
}
|
|
4409
|
+
|
|
3798
4410
|
function normalizeYandexServiceList(values) {
|
|
3799
4411
|
const aliases = {
|
|
3800
4412
|
id: "identity",
|
|
@@ -4872,6 +5484,11 @@ async function handlePermissions(args) {
|
|
|
4872
5484
|
value: permissions.localTools?.[tool] === true ? "allow" : "deny",
|
|
4873
5485
|
scope: "file-tool",
|
|
4874
5486
|
})),
|
|
5487
|
+
...YANDEX_TOOLS.map((tool) => ({
|
|
5488
|
+
permission: `localTools.${tool}`,
|
|
5489
|
+
value: permissions.localTools?.[tool] === true ? "allow" : "deny",
|
|
5490
|
+
scope: "yandex-tool",
|
|
5491
|
+
})),
|
|
4875
5492
|
{ permission: "readFiles", value: permissions.readFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
4876
5493
|
{ permission: "editFiles", value: permissions.editFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
4877
5494
|
{ permission: "deleteFiles", value: permissions.deleteFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
@@ -5817,12 +6434,33 @@ async function listAiModels(provider) {
|
|
|
5817
6434
|
];
|
|
5818
6435
|
}
|
|
5819
6436
|
|
|
6437
|
+
return listCodexModels();
|
|
6438
|
+
}
|
|
6439
|
+
|
|
6440
|
+
async function listCodexModels() {
|
|
5820
6441
|
const version = await getCommandVersion("codex", ["--version"]);
|
|
6442
|
+
const cacheFile = path.join(os.homedir(), ".codex", "models_cache.json");
|
|
6443
|
+
try {
|
|
6444
|
+
const cache = JSON.parse(await readFile(cacheFile, "utf8"));
|
|
6445
|
+
const models = (cache.models || [])
|
|
6446
|
+
.filter((model) => model?.slug && (model.visibility === "list" || model.visibility === undefined))
|
|
6447
|
+
.sort((left, right) => Number(right.priority || 0) - Number(left.priority || 0))
|
|
6448
|
+
.map((model) => ({
|
|
6449
|
+
id: model.slug,
|
|
6450
|
+
provider: "codex",
|
|
6451
|
+
note: `${model.display_name || model.slug} - ${version}`,
|
|
6452
|
+
priority: Number(model.priority || 0),
|
|
6453
|
+
contextWindow: model.context_window || model.max_context_window || null,
|
|
6454
|
+
}));
|
|
6455
|
+
if (models.length > 0) return models;
|
|
6456
|
+
} catch {
|
|
6457
|
+
// Fallback below covers fresh installs before Codex creates models_cache.json.
|
|
6458
|
+
}
|
|
5821
6459
|
return [
|
|
5822
6460
|
{ id: "gpt-5.5", provider: "codex", note: version },
|
|
5823
|
-
{ id: "gpt-5", provider: "codex", note: version },
|
|
5824
|
-
{ id: "gpt-5-
|
|
5825
|
-
{ id: "gpt-5-
|
|
6461
|
+
{ id: "gpt-5.4", provider: "codex", note: version },
|
|
6462
|
+
{ id: "gpt-5.4-mini", provider: "codex", note: version },
|
|
6463
|
+
{ id: "gpt-5.3-codex-spark", provider: "codex", note: version },
|
|
5826
6464
|
];
|
|
5827
6465
|
}
|
|
5828
6466
|
|
|
@@ -8717,6 +9355,20 @@ async function aiAsk(args, context = {}) {
|
|
|
8717
9355
|
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
8718
9356
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
8719
9357
|
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
9358
|
+
const yandexAnswer = await buildYandexDirectAnswer(question);
|
|
9359
|
+
if (yandexAnswer) {
|
|
9360
|
+
if (historyEnabled) {
|
|
9361
|
+
recordAskHistory({ question, answer: yandexAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
9362
|
+
appendSessionExchange(sessionId, question, yandexAnswer, dataContext, "");
|
|
9363
|
+
}
|
|
9364
|
+
emitEvent(options, "answer", { length: yandexAnswer.length, sessionId, direct: true, yandex: true });
|
|
9365
|
+
if (options.output) {
|
|
9366
|
+
await assertPermission("writeFiles");
|
|
9367
|
+
await writeFile(options.output, yandexAnswer, "utf8");
|
|
9368
|
+
}
|
|
9369
|
+
if (!options.quiet) console.log(yandexAnswer);
|
|
9370
|
+
return yandexAnswer;
|
|
9371
|
+
}
|
|
8720
9372
|
const cloudAnswer = await buildCloudDirectAnswer(question);
|
|
8721
9373
|
if (cloudAnswer) {
|
|
8722
9374
|
if (historyEnabled) {
|
|
@@ -8831,6 +9483,66 @@ async function buildDirectDataAnswer(question, dataContext) {
|
|
|
8831
9483
|
].join("\n");
|
|
8832
9484
|
}
|
|
8833
9485
|
|
|
9486
|
+
async function buildYandexDirectAnswer(question) {
|
|
9487
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
9488
|
+
if (!/(яндекс|yandex|почт|письм|календар|контакт|телемост)/iu.test(normalized)) return "";
|
|
9489
|
+
try {
|
|
9490
|
+
if (/(аккаунт|профил|логин|кто подключен)/iu.test(normalized) && /(яндекс|yandex)/iu.test(normalized)) {
|
|
9491
|
+
const profile = await getYandexIdentityProfile();
|
|
9492
|
+
return [
|
|
9493
|
+
"Подключен Yandex ID:",
|
|
9494
|
+
`Логин: ${profile.login || "-"}`,
|
|
9495
|
+
`Имя: ${profile.displayName || "-"}`,
|
|
9496
|
+
`Email: ${profile.defaultEmail || "-"}`,
|
|
9497
|
+
].join("\n");
|
|
9498
|
+
}
|
|
9499
|
+
|
|
9500
|
+
if (/(почт|письм|email|e-mail)/iu.test(normalized)) {
|
|
9501
|
+
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
9502
|
+
const result = await yandexMailStatus();
|
|
9503
|
+
return `Яндекс Почта подключена: ${result.email}. Входящие: ${result.inbox?.exists ?? "-"}.`;
|
|
9504
|
+
}
|
|
9505
|
+
const rows = /(найди|поиск)/iu.test(normalized)
|
|
9506
|
+
? await yandexMailSearch(cleanupYandexQuery(question), { limit: 10 })
|
|
9507
|
+
: await yandexMailList({ limit: 10, unread: /непрочитан/iu.test(normalized) });
|
|
9508
|
+
if (!rows.length) return "Писем по запросу не найдено.";
|
|
9509
|
+
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. #${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}`)].join("\n");
|
|
9510
|
+
}
|
|
9511
|
+
|
|
9512
|
+
if (/(календар|событи)/iu.test(normalized) && !/(создай|добавь|запланируй)/iu.test(normalized)) {
|
|
9513
|
+
const rows = await yandexCalendarList({ limit: 10 });
|
|
9514
|
+
if (!rows.length) return "В ближайшие дни событий в Яндекс Календаре не найдено.";
|
|
9515
|
+
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.start || "-"}`)].join("\n");
|
|
9516
|
+
}
|
|
9517
|
+
|
|
9518
|
+
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
9519
|
+
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
9520
|
+
const result = await yandexContactsStatus();
|
|
9521
|
+
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
9522
|
+
}
|
|
9523
|
+
const query = cleanupYandexQuery(question);
|
|
9524
|
+
const rows = /(найди|поиск)/iu.test(normalized) && query
|
|
9525
|
+
? await yandexContactsSearch(query, { limit: 10 })
|
|
9526
|
+
: await yandexContactsList({ limit: 20 });
|
|
9527
|
+
if (!rows.length) return "Контакты по запросу не найдены.";
|
|
9528
|
+
return ["Яндекс Контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
|
|
9529
|
+
}
|
|
9530
|
+
} catch (error) {
|
|
9531
|
+
return `Не смог выполнить запрос к сервисам Яндекса: ${error instanceof Error ? error.message : String(error)}`;
|
|
9532
|
+
}
|
|
9533
|
+
return "";
|
|
9534
|
+
}
|
|
9535
|
+
|
|
9536
|
+
function cleanupYandexQuery(question) {
|
|
9537
|
+
const stop = /^(?:в|на|у|из|для|по|яндекс|yandex|найди|поиск|покажи|посмотри|проверь|почт\p{L}*|письм\p{L}*|календар\p{L}*|контакт\p{L}*)$/iu;
|
|
9538
|
+
return String(question || "")
|
|
9539
|
+
.replace(/[?.!]+$/u, "")
|
|
9540
|
+
.split(/[^\p{L}\p{N}@._+-]+/gu)
|
|
9541
|
+
.filter((token) => token && !stop.test(token))
|
|
9542
|
+
.join(" ")
|
|
9543
|
+
.trim();
|
|
9544
|
+
}
|
|
9545
|
+
|
|
8834
9546
|
async function buildCloudDirectAnswer(question) {
|
|
8835
9547
|
if (!isCloudQuestion(question)) return "";
|
|
8836
9548
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
@@ -9227,6 +9939,11 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
9227
9939
|
if (!options.quiet) console.log(casualAnswer);
|
|
9228
9940
|
return casualAnswer;
|
|
9229
9941
|
}
|
|
9942
|
+
const yandexAnswer = await buildYandexDirectAnswer(question);
|
|
9943
|
+
if (yandexAnswer) {
|
|
9944
|
+
if (!options.quiet) console.log(yandexAnswer);
|
|
9945
|
+
return yandexAnswer;
|
|
9946
|
+
}
|
|
9230
9947
|
const cloudAnswer = await buildCloudDirectAnswer(question);
|
|
9231
9948
|
if (cloudAnswer) {
|
|
9232
9949
|
if (!options.quiet) console.log(cloudAnswer);
|
|
@@ -9435,6 +10152,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
9435
10152
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
9436
10153
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
9437
10154
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
10155
|
+
"Yandex tools: yandex_identity_me {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_save_text {path,text}, yandex_mail_list {limit,unread}, yandex_mail_search {query}, yandex_mail_read {uid}, yandex_calendar_list {start,end}, yandex_contacts_search {query}.",
|
|
10156
|
+
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_delete, yandex_mail_send, yandex_calendar_create_event, yandex_telemost_create_event.",
|
|
9438
10157
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
9439
10158
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
9440
10159
|
`Вопрос: ${question}`,
|
|
@@ -9492,6 +10211,27 @@ function parseJsonObject(text) {
|
|
|
9492
10211
|
|
|
9493
10212
|
function inferToolPlan(question, options = {}) {
|
|
9494
10213
|
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
10214
|
+
if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
|
|
10215
|
+
return { steps: [{ tool: "yandex_identity_me", args: {} }] };
|
|
10216
|
+
}
|
|
10217
|
+
if (/(яндекс|диск|облак)/iu.test(normalized)) {
|
|
10218
|
+
if (/(создай|сделай).{0,30}папк/iu.test(normalized)) {
|
|
10219
|
+
const folder = question.match(/папк[ауи]?\s+["«]?([^"»\n]+)["»]?/iu)?.[1]?.trim() || "Новая папка";
|
|
10220
|
+
return { steps: [{ tool: "yandex_disk_mkdir", args: { path: `${CLOUD_DEFAULT_REMOTE_DIR}/${folder}` } }] };
|
|
10221
|
+
}
|
|
10222
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_find", args: { query: question, path: CLOUD_DEFAULT_REMOTE_DIR, limit: 20 } }] };
|
|
10223
|
+
return { steps: [{ tool: "yandex_disk_ls", args: { path: CLOUD_DEFAULT_REMOTE_DIR } }] };
|
|
10224
|
+
}
|
|
10225
|
+
if (/(почт|письм|email|e-mail)/iu.test(normalized)) {
|
|
10226
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { query: question, limit: 20 } }] };
|
|
10227
|
+
return { steps: [{ tool: "yandex_mail_list", args: { limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
|
|
10228
|
+
}
|
|
10229
|
+
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
10230
|
+
return { steps: [{ tool: normalized.includes("телемост") ? "yandex_telemost_create_event" : "yandex_calendar_list", args: { limit: 20 } }] };
|
|
10231
|
+
}
|
|
10232
|
+
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
10233
|
+
return { steps: [{ tool: "yandex_contacts_search", args: { query: question, limit: 20 } }] };
|
|
10234
|
+
}
|
|
9495
10235
|
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
9496
10236
|
const steps = [];
|
|
9497
10237
|
if (normalized.includes("без телефона")) {
|
|
@@ -9845,7 +10585,10 @@ function formatToolExecutionError(error, plan) {
|
|
|
9845
10585
|
}
|
|
9846
10586
|
|
|
9847
10587
|
function availableToolNames(options = {}) {
|
|
9848
|
-
const names = new Set(LOCAL_TOOLS);
|
|
10588
|
+
const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS]);
|
|
10589
|
+
if (options.files) {
|
|
10590
|
+
for (const tool of FILE_TOOLS) names.add(tool);
|
|
10591
|
+
}
|
|
9849
10592
|
for (const tool of getLocalMcpToolNames()) names.add(tool);
|
|
9850
10593
|
return [...names];
|
|
9851
10594
|
}
|
|
@@ -9906,6 +10649,11 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
9906
10649
|
} else if (step.tool === "get_current_date") {
|
|
9907
10650
|
current = [getCurrentDateInfo()];
|
|
9908
10651
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
10652
|
+
} else if (YANDEX_TOOLS.includes(step.tool)) {
|
|
10653
|
+
await assertPermission("externalApi");
|
|
10654
|
+
const result = await executeYandexTool(step.tool, step.args || {});
|
|
10655
|
+
current = Array.isArray(result) ? result : [result];
|
|
10656
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
9909
10657
|
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
9910
10658
|
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
9911
10659
|
current = Array.isArray(result) ? result : [result];
|
|
@@ -10037,10 +10785,21 @@ function formatToolResult(result, options) {
|
|
|
10037
10785
|
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
10038
10786
|
}
|
|
10039
10787
|
if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
|
|
10788
|
+
if (row.provider === "yandex-disk" && row.publicUrl) return `Публичная ссылка: ${row.publicUrl}`;
|
|
10789
|
+
if (row.provider === "yandex-disk" && row.remote) return `Яндекс Диск: ${row.status || "ok"} ${row.remote}`;
|
|
10790
|
+
if (row.uid && (row.subject || row.from)) return `Письмо #${row.uid}: ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}`;
|
|
10791
|
+
if (row.login || row.defaultEmail) return `Yandex ID: ${row.displayName || row.login || "-"}${row.defaultEmail ? `, ${row.defaultEmail}` : ""}`;
|
|
10792
|
+
if (row.title && (row.start || row.end)) return `${row.title}: ${row.start || "-"}${row.end ? ` - ${row.end}` : ""}`;
|
|
10793
|
+
if (row.email || row.phone) return formatYandexContact(row);
|
|
10040
10794
|
return `${row.name || row.check || row.inn || "строка"}: ${row.address || row.phone || row.email || row.website || row.count || ""}`;
|
|
10041
10795
|
}).join("\n");
|
|
10042
10796
|
}
|
|
10043
10797
|
|
|
10798
|
+
function formatYandexContact(row) {
|
|
10799
|
+
const name = row.name && row.name !== row.email ? row.name : "";
|
|
10800
|
+
return [name || row.email || "Контакт", name && row.email ? row.email : "", row.phone || ""].filter(Boolean).join(", ");
|
|
10801
|
+
}
|
|
10802
|
+
|
|
10044
10803
|
function applyRuntimeConfig(target, value) {
|
|
10045
10804
|
if (!value) {
|
|
10046
10805
|
return;
|
|
@@ -11794,6 +12553,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
|
11794
12553
|
if (enabled.has("geo") && isGeoQuestion(normalized)) selected.add("geo");
|
|
11795
12554
|
const cloudQuestion = isCloudQuestion(normalized);
|
|
11796
12555
|
if (enabled.has("personal-docs") && cloudQuestion) selected.add("personal-docs");
|
|
12556
|
+
if (enabled.has("yandex-services") && /(яндекс|диск|почт|письм|календар|контакт|телемост|облако)/iu.test(normalized)) selected.add("yandex-services");
|
|
11797
12557
|
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
11798
12558
|
if (enabled.has("local-files") && !cloudQuestion && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
11799
12559
|
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|
|
@@ -13523,6 +14283,12 @@ function sanitizeConfig(config) {
|
|
|
13523
14283
|
if (Array.isArray(next.yandex?.authorizedServices)) {
|
|
13524
14284
|
next.yandex.authorizedServices = next.yandex.authorizedServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
|
|
13525
14285
|
}
|
|
14286
|
+
if (Array.isArray(next.yandex?.enabledServices) && next.yandex.enabledServices.length > 0) {
|
|
14287
|
+
next.toolsets = next.toolsets || {};
|
|
14288
|
+
next.toolsets.enabled = [...new Set([...(next.toolsets.enabled || []), "yandex"])];
|
|
14289
|
+
next.skills = next.skills || {};
|
|
14290
|
+
next.skills.enabled = [...new Set([...(next.skills.enabled || []), "yandex-services"])];
|
|
14291
|
+
}
|
|
13526
14292
|
const localProfile = next.ai?.profiles?.local;
|
|
13527
14293
|
if (localProfile?.provider === "iola") {
|
|
13528
14294
|
if (!localProfile.runtime || localProfile.model === "iola-router-1b") {
|
|
@@ -8,7 +8,8 @@ Skills не подмешиваются в каждый запрос целико
|
|
|
8
8
|
- `reports` - когда нужен отчет, выгрузка, CSV/XLSX или проверка качества;
|
|
9
9
|
- `local-files` - когда пользователь просит работать с локальными файлами, папками, архивами или документами;
|
|
10
10
|
- `browser-agent` - когда запрос связан с сайтом, URL, страницей, скриншотом или браузером;
|
|
11
|
-
- `local-model` - инструкции для локальных компактных моделей и tool
|
|
11
|
+
- `local-model` - инструкции для локальных компактных моделей и tool-планирования;
|
|
12
|
+
- `yandex-services` - когда запрос связан с Yandex Connector: Яндекс Диск, Почта, Календарь, Контакты, Телемост или Yandex ID.
|
|
12
13
|
|
|
13
14
|
Обычный диалог вроде `привет` не получает инструкции про слои, отчеты, файлы и браузер.
|
|
14
15
|
|
|
@@ -38,3 +39,16 @@ iola tools profile full
|
|
|
38
39
|
|
|
39
40
|
Режим `safe` подходит для чтения и анализа без записи файлов и без запуска sync.
|
|
40
41
|
Режим `full` предназначен для доверенного локального пользователя.
|
|
42
|
+
|
|
43
|
+
## Yandex toolset
|
|
44
|
+
|
|
45
|
+
Toolset `yandex` включает локальные tools для пользовательских сервисов Яндекса:
|
|
46
|
+
|
|
47
|
+
- Yandex ID: профиль, логин, email;
|
|
48
|
+
- Яндекс Диск: список, создание папок, поиск, сохранение текста, загрузка, скачивание, публичные ссылки;
|
|
49
|
+
- Яндекс Почта: статус, список писем, поиск, чтение, отправка;
|
|
50
|
+
- Яндекс Календарь: статус, список событий, создание события;
|
|
51
|
+
- Яндекс Контакты: статус, список, поиск;
|
|
52
|
+
- Телемост: подготовка встречи через календарное событие.
|
|
53
|
+
|
|
54
|
+
Опасные действия ограничены: отправка письма, удаление файлов, публикация ссылок и создание событий требуют явного подтверждения в tool-вызове. Токены хранятся локально в `~/.iola/secrets.json`.
|
package/wiki/Yandex-Connector.md
CHANGED
|
@@ -35,15 +35,12 @@ iola yandex token delete
|
|
|
35
35
|
Готово к первому контуру:
|
|
36
36
|
|
|
37
37
|
- `identity` - Yandex ID, профиль пользователя, логин и email;
|
|
38
|
-
- `disk` - Яндекс Диск, папка `/IOLA`, файлы, папки, загрузка, скачивание и публичные ссылки.
|
|
39
|
-
|
|
40
|
-
Исследуется:
|
|
41
|
-
|
|
42
38
|
- `mail` - Яндекс Почта, чтение и поиск писем, отправка только после явного подтверждения;
|
|
43
39
|
- `calendar` - Яндекс Календарь;
|
|
44
40
|
- `contacts` - Яндекс Контакты;
|
|
41
|
+
- `disk` - Яндекс Диск, папка `/IOLA`, файлы, папки, загрузка, скачивание и публичные ссылки;
|
|
45
42
|
- `docs` - Яндекс Документы / 360 через Диск;
|
|
46
|
-
- `telemost` - Яндекс Телемост через
|
|
43
|
+
- `telemost` - Яндекс Телемост через календарное событие.
|
|
47
44
|
|
|
48
45
|
Отдельные ключи, не обычный OAuth бытового Яндекса:
|
|
49
46
|
|
|
@@ -112,17 +109,33 @@ iola cloud doctor
|
|
|
112
109
|
|
|
113
110
|
## Что значит включить сервис
|
|
114
111
|
|
|
115
|
-
OAuth-права дают CLI разрешение обращаться к сервису Яндекса.
|
|
116
|
-
|
|
117
|
-
- `
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
120
|
-
- `
|
|
121
|
-
- `
|
|
122
|
-
- `
|
|
123
|
-
- `
|
|
124
|
-
|
|
125
|
-
|
|
112
|
+
OAuth-права дают CLI разрешение обращаться к сервису Яндекса. Практическая работа идет через локальные tools:
|
|
113
|
+
|
|
114
|
+
- `yandex_identity_me` - проверить пользователя и email;
|
|
115
|
+
- `yandex_disk_info` - место на Яндекс Диске;
|
|
116
|
+
- `yandex_disk_ls` - список файлов и папок;
|
|
117
|
+
- `yandex_disk_mkdir` - создать папку;
|
|
118
|
+
- `yandex_disk_find` - найти файл или папку;
|
|
119
|
+
- `yandex_disk_save_text` - сохранить текстовый результат на Диск;
|
|
120
|
+
- `yandex_disk_upload` - загрузить локальный файл на Диск;
|
|
121
|
+
- `yandex_disk_download` - скачать файл с Диска;
|
|
122
|
+
- `yandex_disk_share` - создать публичную ссылку;
|
|
123
|
+
- `yandex_disk_unshare` - снять публичную ссылку;
|
|
124
|
+
- `yandex_disk_delete` - удалить файл или папку;
|
|
125
|
+
- `yandex_mail_status` - проверить доступ к Почте;
|
|
126
|
+
- `yandex_mail_list` - показать последние письма;
|
|
127
|
+
- `yandex_mail_search` - найти письма;
|
|
128
|
+
- `yandex_mail_read` - прочитать письмо по UID;
|
|
129
|
+
- `yandex_mail_send` - отправить письмо;
|
|
130
|
+
- `yandex_calendar_status` - проверить доступ к Календарю;
|
|
131
|
+
- `yandex_calendar_list` - показать события;
|
|
132
|
+
- `yandex_calendar_create_event` - создать событие;
|
|
133
|
+
- `yandex_contacts_status` - проверить доступ к Контактам;
|
|
134
|
+
- `yandex_contacts_list` - показать контакты;
|
|
135
|
+
- `yandex_contacts_search` - найти контакт;
|
|
136
|
+
- `yandex_telemost_create_event` - создать календарное событие для встречи.
|
|
137
|
+
|
|
138
|
+
Включение сервиса в `/yandex` разрешает CLI использовать соответствующую категорию. Отправка письма, удаление файлов, публикация ссылок и создание событий требуют явного намерения пользователя и подтверждения в tool-вызове.
|
|
126
139
|
|
|
127
140
|
## Иконка приложения
|
|
128
141
|
|
|
@@ -147,9 +147,57 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
147
147
|
|
|
148
148
|
Помочь разобрать личные документы: найти старые отчеты, крупные файлы, дубликаты и предложить структуру папок.
|
|
149
149
|
|
|
150
|
+
## Yandex services skills
|
|
151
|
+
|
|
152
|
+
Эти skills работают через Yandex Connector. Пользователь входит в свой Яндекс-аккаунт, а токены хранятся локально на компьютере.
|
|
153
|
+
|
|
154
|
+
### yandex-identity
|
|
155
|
+
|
|
156
|
+
Проверить, какой Yandex ID подключен: логин, имя и email.
|
|
157
|
+
|
|
158
|
+
### yandex-disk
|
|
159
|
+
|
|
160
|
+
Работать с Яндекс Диском:
|
|
161
|
+
|
|
162
|
+
- показать файлы и папки в `/IOLA`;
|
|
163
|
+
- создать папку;
|
|
164
|
+
- найти файл или папку;
|
|
165
|
+
- сохранить текстовый результат;
|
|
166
|
+
- создать или снять публичную ссылку;
|
|
167
|
+
- загрузить или скачать файл.
|
|
168
|
+
|
|
169
|
+
### yandex-mail
|
|
170
|
+
|
|
171
|
+
Работать с Яндекс Почтой:
|
|
172
|
+
|
|
173
|
+
- проверить доступ к почте;
|
|
174
|
+
- показать последние или непрочитанные письма;
|
|
175
|
+
- найти письмо;
|
|
176
|
+
- прочитать письмо по UID;
|
|
177
|
+
- отправить письмо только после явного подтверждения.
|
|
178
|
+
|
|
179
|
+
### yandex-calendar
|
|
180
|
+
|
|
181
|
+
Работать с Яндекс Календарем:
|
|
182
|
+
|
|
183
|
+
- показать ближайшие события;
|
|
184
|
+
- создать событие только после явного подтверждения.
|
|
185
|
+
|
|
186
|
+
### yandex-contacts
|
|
187
|
+
|
|
188
|
+
Работать с Яндекс Контактами:
|
|
189
|
+
|
|
190
|
+
- проверить доступ к адресной книге;
|
|
191
|
+
- показать контакты;
|
|
192
|
+
- найти контакт по имени, email или телефону.
|
|
193
|
+
|
|
194
|
+
### yandex-telemost
|
|
195
|
+
|
|
196
|
+
Подготовить встречу через календарное событие. CLI не должен сам нажимать финальные кнопки в интерфейсе Яндекса без явного действия пользователя.
|
|
197
|
+
|
|
150
198
|
## Yandex Connector backlog
|
|
151
199
|
|
|
152
|
-
Эти сценарии зафиксированы для следующего
|
|
200
|
+
Эти сценарии зафиксированы для следующего этапа. Они не должны оформлять заказ, списывать деньги или нажимать финальную кнопку вместо пользователя.
|
|
153
201
|
|
|
154
202
|
### taxi-prepare-ride
|
|
155
203
|
|