@iola_adm/iola-cli 0.2.33 → 0.2.34
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 +2 -1
- package/package.json +4 -1
- package/skills/user-skills/SKILL.md +40 -0
- package/skills/yandex-services/SKILL.md +55 -4
- package/src/cli.js +1996 -90
- package/wiki/Skills-/320/270-toolsets.md +23 -2
- package/wiki/Yandex-Connector.md +46 -1
- 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 +79 -2
package/src/cli.js
CHANGED
|
@@ -151,18 +151,31 @@ 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 USER_SKILL_TOOLS = ["user_skill_create", "user_skill_enable", "user_skill_disable", "user_skill_delete", "user_skill_list"];
|
|
154
155
|
const YANDEX_TOOLS = [
|
|
155
156
|
"yandex_identity_me",
|
|
156
157
|
"yandex_disk_info",
|
|
157
158
|
"yandex_disk_ls",
|
|
158
159
|
"yandex_disk_mkdir",
|
|
159
160
|
"yandex_disk_find",
|
|
161
|
+
"yandex_disk_stat",
|
|
162
|
+
"yandex_disk_exists",
|
|
163
|
+
"yandex_disk_read_text",
|
|
160
164
|
"yandex_disk_save_text",
|
|
161
165
|
"yandex_disk_upload",
|
|
162
166
|
"yandex_disk_download",
|
|
167
|
+
"yandex_disk_move",
|
|
168
|
+
"yandex_disk_copy",
|
|
169
|
+
"yandex_disk_rename",
|
|
163
170
|
"yandex_disk_share",
|
|
171
|
+
"yandex_disk_share_qr",
|
|
172
|
+
"yandex_disk_share_email",
|
|
173
|
+
"yandex_disk_package_share_email",
|
|
164
174
|
"yandex_disk_unshare",
|
|
165
175
|
"yandex_disk_delete",
|
|
176
|
+
"yandex_disk_trash_list",
|
|
177
|
+
"yandex_disk_restore",
|
|
178
|
+
"yandex_disk_empty_trash",
|
|
166
179
|
"yandex_mail_status",
|
|
167
180
|
"yandex_mail_folders",
|
|
168
181
|
"yandex_mail_list",
|
|
@@ -185,11 +198,35 @@ const YANDEX_TOOLS = [
|
|
|
185
198
|
"yandex_contacts_status",
|
|
186
199
|
"yandex_contacts_list",
|
|
187
200
|
"yandex_contacts_search",
|
|
201
|
+
"yandex_contacts_get",
|
|
188
202
|
"yandex_contacts_create",
|
|
203
|
+
"yandex_contacts_update",
|
|
204
|
+
"yandex_contacts_delete",
|
|
189
205
|
"yandex_contacts_add_email",
|
|
206
|
+
"yandex_contacts_add_phone",
|
|
207
|
+
"yandex_contacts_add_address",
|
|
208
|
+
"yandex_contacts_add_note",
|
|
209
|
+
"yandex_contacts_add_birthday",
|
|
210
|
+
"yandex_contacts_add_org",
|
|
211
|
+
"yandex_contacts_remove_email",
|
|
212
|
+
"yandex_contacts_remove_phone",
|
|
213
|
+
"yandex_contacts_export_vcard",
|
|
214
|
+
"yandex_contacts_export_csv",
|
|
215
|
+
"yandex_contacts_import_vcard",
|
|
216
|
+
"yandex_contacts_import_csv",
|
|
217
|
+
"yandex_contacts_find_incomplete",
|
|
218
|
+
"yandex_contacts_find_duplicates",
|
|
219
|
+
"yandex_contacts_backup_to_disk",
|
|
220
|
+
"yandex_contacts_birthdays_to_calendar",
|
|
221
|
+
"yandex_contact_send_mail",
|
|
222
|
+
"yandex_contact_send_disk_link_qr",
|
|
223
|
+
"yandex_contact_create_disk_folder",
|
|
224
|
+
"yandex_contact_create_calendar_event",
|
|
225
|
+
"yandex_contact_create_telemost_event",
|
|
226
|
+
"yandex_contact_from_public_entity",
|
|
190
227
|
"yandex_telemost_create_event",
|
|
191
228
|
];
|
|
192
|
-
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS];
|
|
229
|
+
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
|
|
193
230
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
194
231
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
195
232
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
@@ -247,6 +284,13 @@ const TOOLSETS = {
|
|
|
247
284
|
localTools: Object.fromEntries(ALL_LOCAL_TOOLS.map((tool) => [tool, true])),
|
|
248
285
|
},
|
|
249
286
|
},
|
|
287
|
+
"user-skills": {
|
|
288
|
+
description: "Создание и управление пользовательскими skills на базе встроенных tools.",
|
|
289
|
+
permissions: {
|
|
290
|
+
writeFiles: true,
|
|
291
|
+
localTools: Object.fromEntries(USER_SKILL_TOOLS.map((tool) => [tool, true])),
|
|
292
|
+
},
|
|
293
|
+
},
|
|
250
294
|
};
|
|
251
295
|
const FEATURES = {
|
|
252
296
|
"sqlite-history": { stage: "stable", defaultEnabled: true, description: "Запись истории AI-запросов в SQLite." },
|
|
@@ -390,7 +434,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
390
434
|
codex: true,
|
|
391
435
|
},
|
|
392
436
|
toolsets: {
|
|
393
|
-
enabled: ["data-read", "reports", "sync", "ai", "yandex"],
|
|
437
|
+
enabled: ["data-read", "reports", "sync", "ai", "yandex", "user-skills"],
|
|
394
438
|
},
|
|
395
439
|
files: {
|
|
396
440
|
mode: "locked",
|
|
@@ -404,7 +448,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
404
448
|
suggestions: true,
|
|
405
449
|
},
|
|
406
450
|
skills: {
|
|
407
|
-
enabled: ["education", "open-data", "geo", "personal-docs", "reports", "local-model", "local-files", "browser-agent", "yandex-services"],
|
|
451
|
+
enabled: ["education", "open-data", "geo", "personal-docs", "reports", "local-model", "local-files", "browser-agent", "yandex-services", "user-skills"],
|
|
408
452
|
},
|
|
409
453
|
cloud: {
|
|
410
454
|
activeProvider: "",
|
|
@@ -768,7 +812,7 @@ Usage:
|
|
|
768
812
|
iola settings list|get|validate|doctor|init
|
|
769
813
|
iola wiki [open|links]
|
|
770
814
|
iola context list|show|init
|
|
771
|
-
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
815
|
+
iola skills list|show|paths|create|enable|disable|delete|bundles|bundle|doctor
|
|
772
816
|
iola tools list|toolsets|enable|disable|profile
|
|
773
817
|
iola files status|mode|approvals|tree|read|search|write|patch
|
|
774
818
|
iola cloud setup|status|ls|find|upload|download|share|save|backup
|
|
@@ -2949,20 +2993,7 @@ async function handleSkills(args) {
|
|
|
2949
2993
|
const config = await loadConfig();
|
|
2950
2994
|
|
|
2951
2995
|
if (action === "list" || action === "ls") {
|
|
2952
|
-
|
|
2953
|
-
enabled: isSkillEnabled(config, skill.name) ? "yes" : "no",
|
|
2954
|
-
name: skill.name,
|
|
2955
|
-
source: skill.source,
|
|
2956
|
-
description: skill.description,
|
|
2957
|
-
file: skill.file,
|
|
2958
|
-
}));
|
|
2959
|
-
printTable(rows, [
|
|
2960
|
-
["enabled", "Вкл"],
|
|
2961
|
-
["name", "Skill"],
|
|
2962
|
-
["source", "Источник"],
|
|
2963
|
-
["description", "Описание"],
|
|
2964
|
-
["file", "Файл"],
|
|
2965
|
-
]);
|
|
2996
|
+
printSkillsList(listSkills(config), config);
|
|
2966
2997
|
return;
|
|
2967
2998
|
}
|
|
2968
2999
|
|
|
@@ -3016,6 +3047,30 @@ async function handleSkills(args) {
|
|
|
3016
3047
|
return;
|
|
3017
3048
|
}
|
|
3018
3049
|
|
|
3050
|
+
if (action === "create" || action === "new") {
|
|
3051
|
+
const options = parseOptions(args.slice(2));
|
|
3052
|
+
const result = await userSkillCreate({
|
|
3053
|
+
name,
|
|
3054
|
+
description: options.description || "",
|
|
3055
|
+
instructions: options.instructions || options.text || options.prompt || options._.join(" "),
|
|
3056
|
+
tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
|
|
3057
|
+
enable: Boolean(options.enable),
|
|
3058
|
+
overwrite: Boolean(options.force),
|
|
3059
|
+
confirm: true,
|
|
3060
|
+
});
|
|
3061
|
+
console.log(`Skill создан: ${result.name}`);
|
|
3062
|
+
console.log(`Файл: ${result.file}`);
|
|
3063
|
+
if (result.enabled) console.log("Skill включен.");
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
if (action === "delete" || action === "remove" || action === "rm") {
|
|
3068
|
+
const options = parseOptions(args.slice(2));
|
|
3069
|
+
const result = await userSkillDelete(name, { confirm: Boolean(options.yes || options.force) });
|
|
3070
|
+
console.log(`Skill удален: ${result.name}`);
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3019
3074
|
if (action === "enable" || action === "disable") {
|
|
3020
3075
|
if (!name) throw new Error("Имя skill обязательно.");
|
|
3021
3076
|
const enabled = new Set(config.skills?.enabled || []);
|
|
@@ -3026,7 +3081,7 @@ async function handleSkills(args) {
|
|
|
3026
3081
|
return;
|
|
3027
3082
|
}
|
|
3028
3083
|
|
|
3029
|
-
throw new Error("Команды skills: list, paths, show NAME, enable NAME, disable NAME, bundles, bundle enable NAME, doctor.");
|
|
3084
|
+
throw new Error("Команды skills: list, paths, show NAME, create NAME --description TEXT --instructions TEXT [--enable], enable NAME, disable NAME, delete NAME --yes, bundles, bundle enable NAME, doctor.");
|
|
3030
3085
|
}
|
|
3031
3086
|
|
|
3032
3087
|
async function handleTools(args) {
|
|
@@ -3322,6 +3377,11 @@ async function handleYandex(args) {
|
|
|
3322
3377
|
return;
|
|
3323
3378
|
}
|
|
3324
3379
|
|
|
3380
|
+
if (action === "contacts-maintenance" || action === "contacts-watch" || action === "contacts-doctor") {
|
|
3381
|
+
await handleYandexContactsMaintenance([target, ...rest].filter(Boolean));
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3325
3385
|
if (action === "enable" || action === "disable") {
|
|
3326
3386
|
const services = [target, ...rest].filter((item) => item && !String(item).startsWith("--"));
|
|
3327
3387
|
if (services.length === 0) throw new Error("Укажите сервисы. Пример: iola yandex enable disk mail calendar");
|
|
@@ -3358,6 +3418,7 @@ async function handleYandex(args) {
|
|
|
3358
3418
|
iola yandex status|doctor
|
|
3359
3419
|
iola yandex services
|
|
3360
3420
|
iola yandex mail-watch on|off|status|tick [--minutes 5]
|
|
3421
|
+
iola yandex contacts-maintenance on|off|status|tick [--days 7] [--backup]
|
|
3361
3422
|
iola yandex enable disk mail calendar
|
|
3362
3423
|
iola yandex disable mail
|
|
3363
3424
|
iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
|
|
@@ -3428,6 +3489,55 @@ async function handleYandexMailWatch(args = []) {
|
|
|
3428
3489
|
throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
|
|
3429
3490
|
}
|
|
3430
3491
|
|
|
3492
|
+
async function handleYandexContactsMaintenance(args = []) {
|
|
3493
|
+
const [action = "status", ...rest] = args;
|
|
3494
|
+
const options = parseOptions(rest);
|
|
3495
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3496
|
+
const days = Math.max(1, Number(options.days || options.interval || rest.find((item) => /^\d+$/u.test(String(item))) || 7));
|
|
3497
|
+
const result = await yandexContactsMaintenanceEnable(days, { backup: Boolean(options.backup) });
|
|
3498
|
+
console.log(`Проверка контактов включена: каждые ${days} дней.`);
|
|
3499
|
+
console.log(`Backup на Диск: ${result.backup ? "yes" : "no"}.`);
|
|
3500
|
+
console.log("Для работы по расписанию должен запускаться cron tick: вручную, через daemon или Windows Task Scheduler.");
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3504
|
+
await yandexContactsMaintenanceDisable();
|
|
3505
|
+
console.log("Проверка контактов выключена.");
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3509
|
+
const result = await yandexContactsMaintenanceTick({ force: true, backup: options.backup });
|
|
3510
|
+
if (!result.enabled) {
|
|
3511
|
+
console.log("Проверка контактов выключена.");
|
|
3512
|
+
} else {
|
|
3513
|
+
printKeyValue({
|
|
3514
|
+
status: "ok",
|
|
3515
|
+
contacts: result.total,
|
|
3516
|
+
incomplete: result.incomplete,
|
|
3517
|
+
duplicateGroups: result.duplicateGroups,
|
|
3518
|
+
backup: result.backupRemote || "-",
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
if (action === "status" || action === "doctor") {
|
|
3524
|
+
const config = await loadConfig();
|
|
3525
|
+
const maintenance = config.yandex?.contactsMaintenance || {};
|
|
3526
|
+
printKeyValue({
|
|
3527
|
+
enabled: maintenance.enabled ? "yes" : "no",
|
|
3528
|
+
days: maintenance.days || "-",
|
|
3529
|
+
backup: maintenance.backup ? "yes" : "no",
|
|
3530
|
+
lastRunAt: maintenance.lastRunAt || "-",
|
|
3531
|
+
lastTotal: maintenance.lastTotal || "-",
|
|
3532
|
+
lastIncomplete: maintenance.lastIncomplete || "-",
|
|
3533
|
+
lastDuplicateGroups: maintenance.lastDuplicateGroups || "-",
|
|
3534
|
+
cron: listCronJobs().some((job) => job.command === "yandex contacts-maintenance tick") ? "yes" : "no",
|
|
3535
|
+
});
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
throw new Error("Команды: iola yandex contacts-maintenance on --days 7 [--backup] | off | status | tick");
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3431
3541
|
async function setupYandexConnector(args = []) {
|
|
3432
3542
|
const options = parseOptions(args);
|
|
3433
3543
|
const config = await loadConfig();
|
|
@@ -3999,15 +4109,27 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
3999
4109
|
if (tool === "yandex_disk_ls") return yandexDiskList(args.path || args.remotePath || CLOUD_DEFAULT_REMOTE_DIR, { allowMissingRoot: true });
|
|
4000
4110
|
if (tool === "yandex_disk_mkdir") return cloudCreateFolder("yandex-disk", args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/Новая папка`);
|
|
4001
4111
|
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 });
|
|
4112
|
+
if (tool === "yandex_disk_stat") return yandexDiskStat(args.path || args.remotePath || CLOUD_DEFAULT_REMOTE_DIR);
|
|
4113
|
+
if (tool === "yandex_disk_exists") return yandexDiskExists(args.path || args.remotePath || CLOUD_DEFAULT_REMOTE_DIR);
|
|
4114
|
+
if (tool === "yandex_disk_read_text") return yandexDiskReadText(args.path || args.remotePath, { maxBytes: args.maxBytes || args["max-bytes"] });
|
|
4002
4115
|
if (tool === "yandex_disk_save_text") return yandexDiskSaveText(args.text || args.content || "", args.path || args.remotePath);
|
|
4003
4116
|
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 });
|
|
4004
4117
|
if (tool === "yandex_disk_download") return yandexDiskDownload(args.remotePath || args.path, args.outputPath || args.output || path.basename(args.remotePath || args.path || "download"));
|
|
4118
|
+
if (tool === "yandex_disk_move") return yandexDiskMove(args.from || args.source || args.path, args.to || args.target || args.remotePath, args);
|
|
4119
|
+
if (tool === "yandex_disk_copy") return yandexDiskCopy(args.from || args.source || args.path, args.to || args.target || args.remotePath, args);
|
|
4120
|
+
if (tool === "yandex_disk_rename") return yandexDiskRename(args.path || args.remotePath, args.name || args.newName || args.to, args);
|
|
4005
4121
|
if (tool === "yandex_disk_share") {
|
|
4006
4122
|
if (!args.confirm) throw new Error("Для публикации ссылки нужен аргумент confirm=true.");
|
|
4007
4123
|
return yandexDiskShare(args.remotePath || args.path);
|
|
4008
4124
|
}
|
|
4125
|
+
if (tool === "yandex_disk_share_qr") return yandexDiskShareWithQr(args.remotePath || args.path, args);
|
|
4126
|
+
if (tool === "yandex_disk_share_email") return yandexDiskShareEmail(args);
|
|
4127
|
+
if (tool === "yandex_disk_package_share_email") return yandexDiskPackageShareEmail(args);
|
|
4009
4128
|
if (tool === "yandex_disk_unshare") return yandexDiskUnshare(args.remotePath || args.path);
|
|
4010
4129
|
if (tool === "yandex_disk_delete") return yandexDiskDelete(args.remotePath || args.path, args);
|
|
4130
|
+
if (tool === "yandex_disk_trash_list") return yandexDiskTrashList(args.path || args.remotePath || "", args);
|
|
4131
|
+
if (tool === "yandex_disk_restore") return yandexDiskRestore(args.path || args.remotePath, args);
|
|
4132
|
+
if (tool === "yandex_disk_empty_trash") return yandexDiskEmptyTrash(args);
|
|
4011
4133
|
if (tool === "yandex_mail_status") return yandexMailStatus();
|
|
4012
4134
|
if (tool === "yandex_mail_folders") return yandexMailFolders();
|
|
4013
4135
|
if (tool === "yandex_mail_list") return yandexMailList({ mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), limit: args.limit || 10, unread: Boolean(args.unread) });
|
|
@@ -4030,8 +4152,32 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4030
4152
|
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
4031
4153
|
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
4032
4154
|
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
4155
|
+
if (tool === "yandex_contacts_get") return yandexContactsGet(args.query || args.name || args.email || args.phone || "", args);
|
|
4033
4156
|
if (tool === "yandex_contacts_create") return yandexContactsCreate(args);
|
|
4157
|
+
if (tool === "yandex_contacts_update") return yandexContactsUpdate(args.query || args.name || args.email || "", args);
|
|
4158
|
+
if (tool === "yandex_contacts_delete") return yandexContactsDelete(args.query || args.name || args.email || "", args);
|
|
4034
4159
|
if (tool === "yandex_contacts_add_email") return yandexContactsAddEmail(args.query || args.name || "", args.email, args);
|
|
4160
|
+
if (tool === "yandex_contacts_add_phone") return yandexContactsUpdate(args.query || args.name || "", { ...args, phone: args.phone, mode: "add-phone" });
|
|
4161
|
+
if (tool === "yandex_contacts_add_address") return yandexContactsUpdate(args.query || args.name || "", { ...args, address: args.address, mode: "add-address" });
|
|
4162
|
+
if (tool === "yandex_contacts_add_note") return yandexContactsUpdate(args.query || args.name || "", { ...args, note: args.note || args.text, mode: "add-note" });
|
|
4163
|
+
if (tool === "yandex_contacts_add_birthday") return yandexContactsUpdate(args.query || args.name || "", { ...args, birthday: args.birthday || args.date, mode: "add-birthday" });
|
|
4164
|
+
if (tool === "yandex_contacts_add_org") return yandexContactsUpdate(args.query || args.name || "", { ...args, org: args.org || args.organization, title: args.title || args.position, mode: "add-org" });
|
|
4165
|
+
if (tool === "yandex_contacts_remove_email") return yandexContactsUpdate(args.query || args.name || "", { ...args, removeEmail: args.email || true, mode: "remove-email" });
|
|
4166
|
+
if (tool === "yandex_contacts_remove_phone") return yandexContactsUpdate(args.query || args.name || "", { ...args, removePhone: args.phone || true, mode: "remove-phone" });
|
|
4167
|
+
if (tool === "yandex_contacts_export_vcard") return yandexContactsExport("vcard", args);
|
|
4168
|
+
if (tool === "yandex_contacts_export_csv") return yandexContactsExport("csv", args);
|
|
4169
|
+
if (tool === "yandex_contacts_import_vcard") return yandexContactsImport("vcard", args);
|
|
4170
|
+
if (tool === "yandex_contacts_import_csv") return yandexContactsImport("csv", args);
|
|
4171
|
+
if (tool === "yandex_contacts_find_incomplete") return yandexContactsFindIncomplete(args);
|
|
4172
|
+
if (tool === "yandex_contacts_find_duplicates") return yandexContactsFindDuplicates(args);
|
|
4173
|
+
if (tool === "yandex_contacts_backup_to_disk") return yandexContactsBackupToDisk(args);
|
|
4174
|
+
if (tool === "yandex_contacts_birthdays_to_calendar") return yandexContactsBirthdaysToCalendar(args);
|
|
4175
|
+
if (tool === "yandex_contact_send_mail") return yandexContactSendMail(args);
|
|
4176
|
+
if (tool === "yandex_contact_send_disk_link_qr") return yandexContactSendDiskLinkQr(args);
|
|
4177
|
+
if (tool === "yandex_contact_create_disk_folder") return yandexContactCreateDiskFolder(args);
|
|
4178
|
+
if (tool === "yandex_contact_create_calendar_event") return yandexContactCreateCalendarEvent(args);
|
|
4179
|
+
if (tool === "yandex_contact_create_telemost_event") return yandexContactCreateTelemostEvent(args);
|
|
4180
|
+
if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
|
|
4035
4181
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
4036
4182
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
4037
4183
|
}
|
|
@@ -4203,6 +4349,78 @@ async function yandexMailWatchDisable() {
|
|
|
4203
4349
|
return { enabled: false };
|
|
4204
4350
|
}
|
|
4205
4351
|
|
|
4352
|
+
async function yandexContactsMaintenanceEnable(days = 7, options = {}) {
|
|
4353
|
+
const config = await loadConfig();
|
|
4354
|
+
const safeDays = Math.max(1, Number(days || 7));
|
|
4355
|
+
await saveConfig({
|
|
4356
|
+
yandex: {
|
|
4357
|
+
...(config.yandex || {}),
|
|
4358
|
+
contactsMaintenance: {
|
|
4359
|
+
...(config.yandex?.contactsMaintenance || {}),
|
|
4360
|
+
enabled: true,
|
|
4361
|
+
days: safeDays,
|
|
4362
|
+
backup: Boolean(options.backup),
|
|
4363
|
+
updatedAt: new Date().toISOString(),
|
|
4364
|
+
},
|
|
4365
|
+
},
|
|
4366
|
+
});
|
|
4367
|
+
await upsertCronJob(`каждые ${safeDays} дней`, "yandex contacts-maintenance tick", { replaceCommand: true });
|
|
4368
|
+
return { enabled: true, days: safeDays, backup: Boolean(options.backup) };
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4371
|
+
async function yandexContactsMaintenanceDisable() {
|
|
4372
|
+
const config = await loadConfig();
|
|
4373
|
+
await saveConfig({
|
|
4374
|
+
yandex: {
|
|
4375
|
+
...(config.yandex || {}),
|
|
4376
|
+
contactsMaintenance: {
|
|
4377
|
+
...(config.yandex?.contactsMaintenance || {}),
|
|
4378
|
+
enabled: false,
|
|
4379
|
+
updatedAt: new Date().toISOString(),
|
|
4380
|
+
},
|
|
4381
|
+
},
|
|
4382
|
+
});
|
|
4383
|
+
deleteCronJobsByCommand("yandex contacts-maintenance tick");
|
|
4384
|
+
return { enabled: false };
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
async function yandexContactsMaintenanceTick(options = {}) {
|
|
4388
|
+
const config = await loadConfig();
|
|
4389
|
+
const maintenance = config.yandex?.contactsMaintenance || {};
|
|
4390
|
+
if (!maintenance.enabled && !options.force) return { enabled: false };
|
|
4391
|
+
const contacts = await yandexContactsList({ limit: Number(options.limit || 1000) });
|
|
4392
|
+
const incomplete = await yandexContactsFindIncomplete({ limit: Number(options.limit || 1000) });
|
|
4393
|
+
const duplicates = await yandexContactsFindDuplicates({ limit: Number(options.limit || 1000) });
|
|
4394
|
+
let backupRemote = "";
|
|
4395
|
+
if (options.backup || maintenance.backup) {
|
|
4396
|
+
const backup = await yandexContactsBackupToDisk({ format: "csv", confirm: true, limit: Number(options.limit || 1000) });
|
|
4397
|
+
backupRemote = backup.remote || "";
|
|
4398
|
+
}
|
|
4399
|
+
await saveConfig({
|
|
4400
|
+
yandex: {
|
|
4401
|
+
...(config.yandex || {}),
|
|
4402
|
+
contactsMaintenance: {
|
|
4403
|
+
...maintenance,
|
|
4404
|
+
enabled: maintenance.enabled !== false,
|
|
4405
|
+
lastRunAt: new Date().toISOString(),
|
|
4406
|
+
lastTotal: contacts.length,
|
|
4407
|
+
lastIncomplete: incomplete.length,
|
|
4408
|
+
lastDuplicateGroups: duplicates.length,
|
|
4409
|
+
lastBackupRemote: backupRemote || maintenance.lastBackupRemote || "",
|
|
4410
|
+
},
|
|
4411
|
+
},
|
|
4412
|
+
});
|
|
4413
|
+
return {
|
|
4414
|
+
enabled: true,
|
|
4415
|
+
total: contacts.length,
|
|
4416
|
+
incomplete: incomplete.length,
|
|
4417
|
+
duplicateGroups: duplicates.length,
|
|
4418
|
+
backupRemote,
|
|
4419
|
+
incompletePreview: incomplete.slice(0, 10),
|
|
4420
|
+
duplicatePreview: duplicates.slice(0, 10),
|
|
4421
|
+
};
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4206
4424
|
async function yandexMailRead(uid, options = {}) {
|
|
4207
4425
|
if (!uid) throw new Error("UID письма обязателен.");
|
|
4208
4426
|
const { token, email } = await yandexMailCredentials();
|
|
@@ -4776,7 +4994,7 @@ async function yandexCalendarCreateEvent(args = {}) {
|
|
|
4776
4994
|
const end = toIcsDate(args.end || new Date(Date.now() + 7200000).toISOString());
|
|
4777
4995
|
const summary = args.title || args.summary || "Событие IOLA";
|
|
4778
4996
|
const description = args.description || "";
|
|
4779
|
-
const ics = buildIcsEvent({ uid, start, end, summary, description, location: args.location || "" });
|
|
4997
|
+
const ics = buildIcsEvent({ uid, start, end, summary, description, location: args.location || "", attendees: args.attendees || args.to || [] });
|
|
4780
4998
|
const url = `${baseUrl}${encodeURIComponent(uid)}.ics`;
|
|
4781
4999
|
await yandexDavRequest(url, token, { method: "PUT", ics: true, body: ics, timeout: 45000 });
|
|
4782
5000
|
return { status: "created", uid, title: summary, start: args.start || args.date || "", url };
|
|
@@ -4848,9 +5066,9 @@ async function yandexContactsList(args = {}) {
|
|
|
4848
5066
|
|
|
4849
5067
|
async function yandexContactsSearch(query, args = {}) {
|
|
4850
5068
|
const normalized = normalizeGeoText(query);
|
|
4851
|
-
const rows = await yandexContactsList({ limit: Math.max(
|
|
5069
|
+
const rows = await yandexContactsList({ limit: Math.max(1000, Number(args.limit || 20) * 10) });
|
|
4852
5070
|
if (!normalized) return rows.slice(0, Number(args.limit || 20));
|
|
4853
|
-
return rows.filter((row) =>
|
|
5071
|
+
return rows.filter((row) => contactMatchesQuery(row, normalized)).slice(0, Number(args.limit || 20));
|
|
4854
5072
|
}
|
|
4855
5073
|
|
|
4856
5074
|
async function resolveYandexMailRecipientFromContacts(query) {
|
|
@@ -4871,58 +5089,397 @@ async function yandexContactsAddEmail(query, email, args = {}) {
|
|
|
4871
5089
|
if (!args.confirm) throw new Error("Для изменения контакта нужен аргумент confirm=true.");
|
|
4872
5090
|
if (!query) throw new Error("Укажите имя или часть имени контакта.");
|
|
4873
5091
|
if (!email || !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(String(email))) throw new Error("Укажите корректный email.");
|
|
4874
|
-
const
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
const
|
|
5092
|
+
const result = await yandexContactsUpdate(query, { ...args, email, mode: "add-email" });
|
|
5093
|
+
if (result.status === "updated") return { status: "updated", name: result.name, email };
|
|
5094
|
+
return result;
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
async function yandexContactsGet(query, args = {}) {
|
|
5098
|
+
const resolved = await resolveYandexContact(query, args);
|
|
5099
|
+
if (resolved.status !== "ok") return resolved;
|
|
5100
|
+
const { card: _card, ...contact } = resolved.contact;
|
|
5101
|
+
return { status: "ok", ...contact };
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
async function yandexContactsUpdate(query, args = {}) {
|
|
5105
|
+
if (!args.confirm) throw new Error("Для изменения контакта нужен аргумент confirm=true.");
|
|
5106
|
+
if (!query) throw new Error("Укажите имя, email или телефон контакта.");
|
|
5107
|
+
const resolved = await resolveYandexContact(query, args);
|
|
5108
|
+
if (resolved.status !== "ok") return resolved;
|
|
5109
|
+
const contact = resolved.contact;
|
|
5110
|
+
let card = contact.card;
|
|
5111
|
+
if (args.name || args.fullName) {
|
|
5112
|
+
card = upsertVcardProperty(card, "FN", args.name || args.fullName, { replace: true });
|
|
5113
|
+
card = upsertVcardProperty(card, "N", `${escapeVcardValue(args.name || args.fullName)};;;;`, { replace: true, raw: true });
|
|
5114
|
+
}
|
|
5115
|
+
if (args.email) card = upsertVcardProperty(card, "EMAIL;TYPE=INTERNET", String(args.email).trim(), { replace: args.overwriteEmail || args.overwrite || args.mode === "set-email" });
|
|
5116
|
+
if (args.phone) card = upsertVcardProperty(card, "TEL;TYPE=CELL", String(args.phone).trim(), { replace: args.overwritePhone || args.overwrite || args.mode === "set-phone" });
|
|
5117
|
+
if (args.address) card = upsertVcardProperty(card, "ADR;TYPE=HOME", `;;;;${escapeVcardValue(args.address)};;`, { replace: args.overwriteAddress || args.overwrite || args.mode === "set-address", raw: true });
|
|
5118
|
+
if (args.note) card = upsertVcardProperty(card, "NOTE", args.note, { replace: args.overwriteNote || args.overwrite || args.mode === "set-note" });
|
|
5119
|
+
if (args.birthday) card = upsertVcardProperty(card, "BDAY", normalizeVcardBirthday(args.birthday), { replace: true });
|
|
5120
|
+
if (args.org) card = upsertVcardProperty(card, "ORG", args.org, { replace: args.overwriteOrg || args.overwrite || args.mode === "set-org" });
|
|
5121
|
+
if (args.title) card = upsertVcardProperty(card, "TITLE", args.title, { replace: true });
|
|
5122
|
+
if (args.categories || args.group) card = upsertVcardProperty(card, "CATEGORIES", Array.isArray(args.categories) ? args.categories.join(",") : (args.categories || args.group), { replace: Boolean(args.overwriteCategories) });
|
|
5123
|
+
if (args.removeEmail) card = removeVcardProperty(card, "EMAIL", args.removeEmail === true ? "" : args.removeEmail);
|
|
5124
|
+
if (args.removePhone) card = removeVcardProperty(card, "TEL", args.removePhone === true ? "" : args.removePhone);
|
|
5125
|
+
if (card === contact.card) return { status: "unchanged", name: contact.name, email: contact.email, phone: contact.phone };
|
|
5126
|
+
await saveYandexContactCard(contact.href, card);
|
|
5127
|
+
const parsed = parseVCards(card)[0] || {};
|
|
5128
|
+
return { status: "updated", ...parsed, href: contact.href };
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
async function yandexContactsDelete(query, args = {}) {
|
|
5132
|
+
if (!args.confirm) throw new Error("Для удаления контакта нужен аргумент confirm=true.");
|
|
5133
|
+
const resolved = await resolveYandexContact(query, args);
|
|
5134
|
+
if (resolved.status !== "ok") return resolved;
|
|
4881
5135
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4882
|
-
await yandexDavRequest(new URL(contact.href, "https://carddav.yandex.ru/").toString(), token, {
|
|
4883
|
-
|
|
4884
|
-
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4885
|
-
body: updatedCard,
|
|
4886
|
-
timeout: 45000,
|
|
4887
|
-
});
|
|
4888
|
-
return { status: "updated", name: contact.name, email };
|
|
5136
|
+
await yandexDavRequest(new URL(resolved.contact.href, "https://carddav.yandex.ru/").toString(), token, { method: "DELETE", timeout: 45000 });
|
|
5137
|
+
return { status: "deleted", name: resolved.contact.name, email: resolved.contact.email, phone: resolved.contact.phone };
|
|
4889
5138
|
}
|
|
4890
5139
|
|
|
4891
5140
|
async function yandexContactsCreate(args = {}) {
|
|
4892
5141
|
if (!args.confirm) throw new Error("Для создания контакта нужен аргумент confirm=true.");
|
|
4893
5142
|
const name = String(args.name || args.email || "").trim();
|
|
4894
5143
|
const email = String(args.email || "").trim();
|
|
5144
|
+
const phone = String(args.phone || "").trim();
|
|
4895
5145
|
if (!name) throw new Error("Имя контакта обязательно.");
|
|
4896
|
-
if (
|
|
5146
|
+
if (email && !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email)) throw new Error("Укажите корректный email.");
|
|
5147
|
+
if (!email && !phone) throw new Error("Для контакта нужен хотя бы email или телефон.");
|
|
4897
5148
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4898
5149
|
const baseUrl = await yandexContactsBaseUrl(token);
|
|
4899
5150
|
const uid = `${randomUUID()}@iola-cli`;
|
|
4900
|
-
const card =
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
5151
|
+
const card = buildVcard({
|
|
5152
|
+
uid,
|
|
5153
|
+
name,
|
|
5154
|
+
email,
|
|
5155
|
+
phone,
|
|
5156
|
+
address: args.address,
|
|
5157
|
+
note: args.note,
|
|
5158
|
+
birthday: args.birthday,
|
|
5159
|
+
org: args.org || args.organization,
|
|
5160
|
+
title: args.title || args.position,
|
|
5161
|
+
categories: args.categories || args.group,
|
|
5162
|
+
});
|
|
4909
5163
|
await yandexDavRequest(new URL(`${encodeURIComponent(uid)}.vcf`, baseUrl).toString(), token, {
|
|
4910
5164
|
method: "PUT",
|
|
4911
5165
|
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4912
5166
|
body: card,
|
|
4913
5167
|
timeout: 45000,
|
|
4914
5168
|
});
|
|
4915
|
-
return { status: "created", name, email, uid };
|
|
5169
|
+
return { status: "created", name, email, phone, uid };
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
async function yandexContactsExport(format, args = {}) {
|
|
5173
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 1000), full: true });
|
|
5174
|
+
const safeFormat = format === "csv" ? "csv" : "vcard";
|
|
5175
|
+
const output = args.output || path.join(CONFIG_DIR, `yandex-contacts-${timestampForFile()}.${safeFormat === "csv" ? "csv" : "vcf"}`);
|
|
5176
|
+
const text = safeFormat === "csv"
|
|
5177
|
+
? contactsToCsv(rows)
|
|
5178
|
+
: rows.map((row) => row.card).filter(Boolean).join("\r\n");
|
|
5179
|
+
await mkdir(path.dirname(path.resolve(output)), { recursive: true });
|
|
5180
|
+
await writeFile(output, text, "utf8");
|
|
5181
|
+
saveArtifact("yandex-contacts-export", output, output, { rows: rows.length, format: safeFormat });
|
|
5182
|
+
return { status: "exported", output: path.resolve(output), rows: rows.length, format: safeFormat };
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
async function yandexContactsFindIncomplete(args = {}) {
|
|
5186
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 500) });
|
|
5187
|
+
return rows.filter((row) => {
|
|
5188
|
+
if (args.field === "email") return !row.email;
|
|
5189
|
+
if (args.field === "phone") return !row.phone;
|
|
5190
|
+
if (args.field === "address") return !row.address;
|
|
5191
|
+
return !row.email || !row.phone || !row.name;
|
|
5192
|
+
}).slice(0, Number(args.limit || 50));
|
|
5193
|
+
}
|
|
5194
|
+
|
|
5195
|
+
async function yandexContactsFindDuplicates(args = {}) {
|
|
5196
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 1000) });
|
|
5197
|
+
const groups = new Map();
|
|
5198
|
+
for (const row of rows) {
|
|
5199
|
+
const keys = [
|
|
5200
|
+
row.email ? `email:${row.email.toLocaleLowerCase("en-US")}` : "",
|
|
5201
|
+
row.phone ? `phone:${normalizePhone(row.phone)}` : "",
|
|
5202
|
+
row.name ? `name:${normalizeContactLookupText(row.name)}` : "",
|
|
5203
|
+
].filter((key) => key && !key.endsWith(":"));
|
|
5204
|
+
for (const key of keys) {
|
|
5205
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
5206
|
+
groups.get(key).push(row);
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
const seen = new Set();
|
|
5210
|
+
const duplicates = [];
|
|
5211
|
+
for (const [key, group] of groups.entries()) {
|
|
5212
|
+
if (group.length < 2) continue;
|
|
5213
|
+
const signature = group.map((row) => row.href || `${row.name}:${row.email}:${row.phone}`).sort().join("|");
|
|
5214
|
+
if (seen.has(signature)) continue;
|
|
5215
|
+
seen.add(signature);
|
|
5216
|
+
duplicates.push({ status: "duplicate-group", key, count: group.length, contacts: group.slice(0, 10) });
|
|
5217
|
+
}
|
|
5218
|
+
return duplicates.slice(0, Number(args.limit || 20));
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
async function yandexContactsBackupToDisk(args = {}) {
|
|
5222
|
+
if (!args.confirm) throw new Error("Для резервной копии контактов на Диск нужен аргумент confirm=true.");
|
|
5223
|
+
const format = args.format === "csv" ? "csv" : "vcard";
|
|
5224
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 1000), full: true });
|
|
5225
|
+
const remotePath = args.path || `${CLOUD_DEFAULT_REMOTE_DIR}/contacts/yandex-contacts-${timestampForFile()}.${format === "csv" ? "csv" : "vcf"}`;
|
|
5226
|
+
const text = format === "csv" ? contactsToCsv(rows) : rows.map((row) => row.card).filter(Boolean).join("\r\n");
|
|
5227
|
+
const saved = await yandexDiskSaveText(text, remotePath);
|
|
5228
|
+
return { provider: "yandex-disk", status: "contacts-backup", remote: saved.remote, rows: rows.length, format };
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
async function yandexContactsImport(format, args = {}) {
|
|
5232
|
+
if (!args.confirm) throw new Error("Для импорта контактов нужен аргумент confirm=true.");
|
|
5233
|
+
const inputPath = args.path || args.file || args.input;
|
|
5234
|
+
if (!inputPath) throw new Error("Укажите файл для импорта.");
|
|
5235
|
+
const text = await readFile(path.resolve(inputPath), "utf8");
|
|
5236
|
+
const rows = format === "csv" ? parseContactsCsv(text) : parseVCards(text);
|
|
5237
|
+
const existing = await yandexContactsList({ limit: 1000 });
|
|
5238
|
+
const existingEmails = new Set(existing.flatMap((row) => row.emails || []).map((email) => email.toLocaleLowerCase("en-US")));
|
|
5239
|
+
const existingPhones = new Set(existing.flatMap((row) => row.phones || []).map(normalizePhone).filter(Boolean));
|
|
5240
|
+
const created = [];
|
|
5241
|
+
const skipped = [];
|
|
5242
|
+
for (const row of rows.slice(0, Number(args.limit || 200))) {
|
|
5243
|
+
const email = row.email || row.emails?.[0] || "";
|
|
5244
|
+
const phone = row.phone || row.phones?.[0] || "";
|
|
5245
|
+
const emailKey = email.toLocaleLowerCase("en-US");
|
|
5246
|
+
const phoneKey = normalizePhone(phone);
|
|
5247
|
+
if ((!email && !phone) || (emailKey && existingEmails.has(emailKey)) || (phoneKey && existingPhones.has(phoneKey))) {
|
|
5248
|
+
skipped.push(row);
|
|
5249
|
+
continue;
|
|
5250
|
+
}
|
|
5251
|
+
const createdRow = await yandexContactsCreate({
|
|
5252
|
+
name: row.name || email || phone,
|
|
5253
|
+
email,
|
|
5254
|
+
phone,
|
|
5255
|
+
address: row.address,
|
|
5256
|
+
note: row.note,
|
|
5257
|
+
birthday: row.birthday,
|
|
5258
|
+
org: row.org,
|
|
5259
|
+
title: row.title,
|
|
5260
|
+
categories: row.categories,
|
|
5261
|
+
confirm: true,
|
|
5262
|
+
});
|
|
5263
|
+
created.push(createdRow);
|
|
5264
|
+
if (emailKey) existingEmails.add(emailKey);
|
|
5265
|
+
if (phoneKey) existingPhones.add(phoneKey);
|
|
5266
|
+
}
|
|
5267
|
+
return { status: "contacts-imported", format, input: path.resolve(inputPath), created: created.length, skipped: skipped.length };
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
async function yandexContactsBirthdaysToCalendar(args = {}) {
|
|
5271
|
+
if (!args.confirm) throw new Error("Для создания событий дней рождения нужен аргумент confirm=true.");
|
|
5272
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 1000) });
|
|
5273
|
+
const withBirthday = rows.filter((row) => row.birthday);
|
|
5274
|
+
const created = [];
|
|
5275
|
+
for (const contact of withBirthday.slice(0, Number(args.maxCreate || 50))) {
|
|
5276
|
+
const next = nextBirthdayDate(contact.birthday);
|
|
5277
|
+
if (!next) continue;
|
|
5278
|
+
const result = await yandexCalendarCreateEvent({
|
|
5279
|
+
title: `День рождения: ${contact.name || contact.email || contact.phone}`,
|
|
5280
|
+
start: next.start,
|
|
5281
|
+
end: next.end,
|
|
5282
|
+
description: `Контакт: ${formatYandexContact(contact)}`,
|
|
5283
|
+
confirm: true,
|
|
5284
|
+
});
|
|
5285
|
+
created.push(result);
|
|
5286
|
+
}
|
|
5287
|
+
return { status: "birthday-events-created", created: created.length, totalWithBirthday: withBirthday.length };
|
|
5288
|
+
}
|
|
5289
|
+
|
|
5290
|
+
async function yandexContactSendMail(args = {}) {
|
|
5291
|
+
if (!args.confirm) throw new Error("Для отправки письма контакту нужен аргумент confirm=true.");
|
|
5292
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5293
|
+
if (resolved.status !== "ok") return resolved;
|
|
5294
|
+
const contact = resolved.contact;
|
|
5295
|
+
if (!contact.email) return { status: "no-email", contact: stripYandexContactPrivateFields(contact) };
|
|
5296
|
+
const sent = await yandexMailSend({
|
|
5297
|
+
to: [contact.email],
|
|
5298
|
+
subject: args.subject || "Сообщение от IOLA CLI",
|
|
5299
|
+
text: args.text || args.message || "",
|
|
5300
|
+
confirm: true,
|
|
5301
|
+
});
|
|
5302
|
+
return { status: "contact-mail-sent", contact: contact.name || contact.email, to: sent.to, subject: sent.subject };
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
async function yandexContactSendDiskLinkQr(args = {}) {
|
|
5306
|
+
if (!args.confirm) throw new Error("Для отправки ссылки контакту нужен аргумент confirm=true.");
|
|
5307
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5308
|
+
if (resolved.status !== "ok") return resolved;
|
|
5309
|
+
const contact = resolved.contact;
|
|
5310
|
+
if (!contact.email) return { status: "no-email", contact: stripYandexContactPrivateFields(contact) };
|
|
5311
|
+
const result = await yandexDiskShareEmail({
|
|
5312
|
+
remotePath: args.path || args.remotePath || args.target,
|
|
5313
|
+
to: [contact.email],
|
|
5314
|
+
subject: args.subject,
|
|
5315
|
+
text: args.text || args.message,
|
|
5316
|
+
confirm: true,
|
|
5317
|
+
});
|
|
5318
|
+
return { ...result, status: "contact-disk-link-sent", contact: contact.name || contact.email };
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
async function yandexContactCreateDiskFolder(args = {}) {
|
|
5322
|
+
if (!args.confirm) throw new Error("Для создания папки контакта нужен аргумент confirm=true.");
|
|
5323
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5324
|
+
if (resolved.status !== "ok") return resolved;
|
|
5325
|
+
const contact = resolved.contact;
|
|
5326
|
+
const folder = args.path || `${CLOUD_DEFAULT_REMOTE_DIR}/contacts/${slugYandexDiskName(contact.name || contact.email || contact.phone || "contact")}`;
|
|
5327
|
+
await cloudCreateFolder("yandex-disk", folder);
|
|
5328
|
+
const cardPath = path.posix.join(folder, "contact.vcf");
|
|
5329
|
+
await yandexDiskSaveText(contact.card, cardPath);
|
|
5330
|
+
const note = [
|
|
5331
|
+
`Контакт: ${contact.name || "-"}`,
|
|
5332
|
+
`Email: ${(contact.emails || []).join(", ") || "-"}`,
|
|
5333
|
+
`Телефон: ${(contact.phones || []).join(", ") || "-"}`,
|
|
5334
|
+
contact.address ? `Адрес: ${contact.address}` : "",
|
|
5335
|
+
contact.org ? `Организация: ${contact.org}` : "",
|
|
5336
|
+
contact.title ? `Должность: ${contact.title}` : "",
|
|
5337
|
+
contact.note ? `Заметка: ${contact.note}` : "",
|
|
5338
|
+
].filter(Boolean).join("\n");
|
|
5339
|
+
await yandexDiskSaveText(note, path.posix.join(folder, "README.txt"));
|
|
5340
|
+
return { provider: "yandex-disk", status: "contact-folder-created", contact: contact.name || contact.email, remote: folder, cardPath };
|
|
5341
|
+
}
|
|
5342
|
+
|
|
5343
|
+
async function yandexContactCreateCalendarEvent(args = {}) {
|
|
5344
|
+
if (!args.confirm) throw new Error("Для создания встречи с контактом нужен аргумент confirm=true.");
|
|
5345
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5346
|
+
if (resolved.status !== "ok") return resolved;
|
|
5347
|
+
const contact = resolved.contact;
|
|
5348
|
+
if (!contact.email) return { status: "no-email", contact: stripYandexContactPrivateFields(contact) };
|
|
5349
|
+
const result = await yandexCalendarCreateEvent({
|
|
5350
|
+
...args,
|
|
5351
|
+
title: args.title || `Встреча: ${contact.name || contact.email}`,
|
|
5352
|
+
description: [args.description || "", `Контакт: ${contact.name || "-"}`, `Email: ${contact.email}`].filter(Boolean).join("\n"),
|
|
5353
|
+
attendees: [contact.email],
|
|
5354
|
+
confirm: true,
|
|
5355
|
+
});
|
|
5356
|
+
return { ...result, status: "contact-calendar-event-created", contact: contact.name || contact.email, attendee: contact.email };
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
async function yandexContactCreateTelemostEvent(args = {}) {
|
|
5360
|
+
const result = await yandexContactCreateCalendarEvent({
|
|
5361
|
+
...args,
|
|
5362
|
+
title: args.title || `Телемост: ${args.contact || args.name || args.query || "контакт"}`,
|
|
5363
|
+
description: [args.description || "", "Телемост: создайте ссылку в Яндекс Календаре, если интерфейс календаря предложит видеовстречу."].filter(Boolean).join("\n"),
|
|
5364
|
+
confirm: true,
|
|
5365
|
+
});
|
|
5366
|
+
return { ...result, status: "contact-telemost-event-created" };
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5369
|
+
async function yandexContactFromPublicEntity(args = {}) {
|
|
5370
|
+
if (!args.confirm) throw new Error("Для создания контакта из городского слоя нужен аргумент confirm=true.");
|
|
5371
|
+
const layer = normalizeEntityLayer(args.layer || (/(сад|детсад)/iu.test(args.query || "") ? "kindergartens" : "schools"));
|
|
5372
|
+
const sourceQuery = args.query || args.name || args.inn || "";
|
|
5373
|
+
const number = String(sourceQuery).match(/№?\s*(\d{1,4})/u)?.[1] || "";
|
|
5374
|
+
const query = number
|
|
5375
|
+
? (layer === "kindergartens" ? `детский сад ${number}` : `школа ${number}`)
|
|
5376
|
+
: sourceQuery;
|
|
5377
|
+
const rows = await searchPublicEntities({ layer, query, limit: 5 });
|
|
5378
|
+
if (!rows.length) return { status: "not-found", query: args.query || args.name || args.inn || "" };
|
|
5379
|
+
const entity = rows[0];
|
|
5380
|
+
const name = args.contactName || entity.name || entity.fns_short_name || args.query;
|
|
5381
|
+
const email = entity.email || "";
|
|
5382
|
+
const phone = entity.phone || "";
|
|
5383
|
+
if (!email && !phone) return { status: "no-contact-fields", entity };
|
|
5384
|
+
return yandexContactsCreate({
|
|
5385
|
+
name,
|
|
5386
|
+
email,
|
|
5387
|
+
phone,
|
|
5388
|
+
address: entity.address,
|
|
5389
|
+
org: entity.name,
|
|
5390
|
+
note: `Создано из открытого слоя ${layer}. ИНН: ${entity.inn || "-"}.`,
|
|
5391
|
+
confirm: true,
|
|
5392
|
+
});
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
async function resolveYandexContact(query, args = {}) {
|
|
5396
|
+
const rows = await yandexContactsList({ limit: Math.max(500, Number(args.limit || 100)), full: true });
|
|
5397
|
+
const normalized = normalizeContactLookupText(query || args.query || args.name || args.email || args.phone || "");
|
|
5398
|
+
const matches = rows.filter((row) => contactMatchesQuery(row, normalized)).slice(0, 20);
|
|
5399
|
+
if (!matches.length) return { status: "not-found", query };
|
|
5400
|
+
const exact = pickExactYandexContactMatch(matches, query, args);
|
|
5401
|
+
if (exact) return { status: "ok", contact: exact };
|
|
5402
|
+
if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", query, contacts: matches.map(stripYandexContactPrivateFields) };
|
|
5403
|
+
return { status: "ok", contact: matches[0] };
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
function pickExactYandexContactMatch(matches, query, args = {}) {
|
|
5407
|
+
const text = String(query || args.email || args.phone || args.name || "").trim();
|
|
5408
|
+
const email = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0]?.toLocaleLowerCase("en-US");
|
|
5409
|
+
if (email) return matches.find((row) => row.emails?.some((item) => item.toLocaleLowerCase("en-US") === email));
|
|
5410
|
+
const phone = normalizePhone(args.phone || text);
|
|
5411
|
+
if (phone.length >= 7) return matches.find((row) => row.phones?.some((item) => normalizePhone(item).endsWith(phone) || phone.endsWith(normalizePhone(item))));
|
|
5412
|
+
const normalized = normalizeContactLookupText(text);
|
|
5413
|
+
return matches.find((row) => normalizeContactLookupText(row.name) === normalized) || null;
|
|
5414
|
+
}
|
|
5415
|
+
|
|
5416
|
+
function stripYandexContactPrivateFields(row = {}) {
|
|
5417
|
+
const { card: _card, ...rest } = row;
|
|
5418
|
+
return rest;
|
|
5419
|
+
}
|
|
5420
|
+
|
|
5421
|
+
async function saveYandexContactCard(href, card) {
|
|
5422
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
5423
|
+
await yandexDavRequest(new URL(href, "https://carddav.yandex.ru/").toString(), token, {
|
|
5424
|
+
method: "PUT",
|
|
5425
|
+
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
5426
|
+
body: ensureVcardCrlf(card),
|
|
5427
|
+
timeout: 45000,
|
|
5428
|
+
});
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5431
|
+
function buildVcard(args = {}) {
|
|
5432
|
+
const uid = args.uid || `${randomUUID()}@iola-cli`;
|
|
5433
|
+
const lines = [
|
|
5434
|
+
"BEGIN:VCARD",
|
|
5435
|
+
"VERSION:3.0",
|
|
5436
|
+
`UID:${uid}`,
|
|
5437
|
+
`FN:${escapeVcardValue(args.name || args.email || args.phone || "Контакт")}`,
|
|
5438
|
+
`N:${escapeVcardValue(args.name || args.email || args.phone || "Контакт")};;;;`,
|
|
5439
|
+
args.email ? `EMAIL;TYPE=INTERNET:${String(args.email).trim()}` : "",
|
|
5440
|
+
args.phone ? `TEL;TYPE=CELL:${escapeVcardValue(args.phone)}` : "",
|
|
5441
|
+
args.address ? `ADR;TYPE=HOME:;;;;${escapeVcardValue(args.address)};;` : "",
|
|
5442
|
+
args.org ? `ORG:${escapeVcardValue(args.org)}` : "",
|
|
5443
|
+
args.title ? `TITLE:${escapeVcardValue(args.title)}` : "",
|
|
5444
|
+
args.birthday ? `BDAY:${normalizeVcardBirthday(args.birthday)}` : "",
|
|
5445
|
+
args.note ? `NOTE:${escapeVcardValue(args.note)}` : "",
|
|
5446
|
+
args.categories ? `CATEGORIES:${escapeVcardValue(Array.isArray(args.categories) ? args.categories.join(",") : args.categories)}` : "",
|
|
5447
|
+
"END:VCARD",
|
|
5448
|
+
"",
|
|
5449
|
+
].filter((line) => line !== "");
|
|
5450
|
+
return lines.join("\r\n");
|
|
4916
5451
|
}
|
|
4917
5452
|
|
|
4918
5453
|
function upsertVcardEmail(card, email, options = {}) {
|
|
5454
|
+
return upsertVcardProperty(card, "EMAIL;TYPE=INTERNET", email, { replace: Boolean(options.overwrite) });
|
|
5455
|
+
}
|
|
5456
|
+
|
|
5457
|
+
function upsertVcardProperty(card, property, value, options = {}) {
|
|
4919
5458
|
const text = String(card || "").replace(/\r/g, "").trim();
|
|
4920
5459
|
if (!text.includes("BEGIN:VCARD")) throw new Error("Контакт не похож на vCard.");
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
5460
|
+
const propName = String(property || "").split(";")[0].toLocaleUpperCase("en-US");
|
|
5461
|
+
const rawValue = options.raw ? String(value || "") : escapeVcardValue(value);
|
|
5462
|
+
if (!rawValue) return text;
|
|
5463
|
+
const line = `${property}:${rawValue}`;
|
|
5464
|
+
const pattern = new RegExp(`^${escapeRegExp(propName)}[^:]*:[^\\n]*`, "imu");
|
|
5465
|
+
if (pattern.test(text)) {
|
|
5466
|
+
if (!options.replace) return text;
|
|
5467
|
+
return text.replace(pattern, line);
|
|
4924
5468
|
}
|
|
4925
|
-
return text.replace(/\nEND:VCARD/iu, `\
|
|
5469
|
+
return text.replace(/\nEND:VCARD/iu, `\n${line}\nEND:VCARD`);
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
function removeVcardProperty(card, property, value = "") {
|
|
5473
|
+
const text = String(card || "").replace(/\r/g, "").trim();
|
|
5474
|
+
const propName = String(property || "").split(";")[0].toLocaleUpperCase("en-US");
|
|
5475
|
+
const lines = text.split(/\n/u);
|
|
5476
|
+
const needle = String(value || "").toLocaleLowerCase("ru-RU");
|
|
5477
|
+
const next = lines.filter((line) => {
|
|
5478
|
+
if (!new RegExp(`^${escapeRegExp(propName)}(?:[;:]|$)`, "iu").test(line)) return true;
|
|
5479
|
+
if (!needle) return false;
|
|
5480
|
+
return !line.toLocaleLowerCase("ru-RU").includes(needle);
|
|
5481
|
+
});
|
|
5482
|
+
return next.join("\n");
|
|
4926
5483
|
}
|
|
4927
5484
|
|
|
4928
5485
|
function escapeVcardValue(value) {
|
|
@@ -4930,10 +5487,28 @@ function escapeVcardValue(value) {
|
|
|
4930
5487
|
}
|
|
4931
5488
|
|
|
4932
5489
|
function contactMatchesQuery(contact, normalizedQuery) {
|
|
4933
|
-
const
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
5490
|
+
const query = normalizeContactLookupText(normalizedQuery);
|
|
5491
|
+
const contactText = normalizeContactLookupText([
|
|
5492
|
+
contact.name,
|
|
5493
|
+
contact.email,
|
|
5494
|
+
...(contact.emails || []),
|
|
5495
|
+
contact.phone,
|
|
5496
|
+
...(contact.phones || []),
|
|
5497
|
+
contact.address,
|
|
5498
|
+
contact.org,
|
|
5499
|
+
contact.title,
|
|
5500
|
+
contact.note,
|
|
5501
|
+
contact.categories,
|
|
5502
|
+
].filter(Boolean).join(" "));
|
|
5503
|
+
const queryPhone = normalizePhone(normalizedQuery);
|
|
5504
|
+
if (queryPhone.length >= 7 && [...(contact.phones || []), contact.phone].some((phone) => {
|
|
5505
|
+
const contactPhone = normalizePhone(phone);
|
|
5506
|
+
return contactPhone.length >= 7 && (contactPhone.includes(queryPhone) || queryPhone.includes(contactPhone));
|
|
5507
|
+
})) return true;
|
|
5508
|
+
const normalizedQueryText = query;
|
|
5509
|
+
if (!contactText || !normalizedQueryText) return false;
|
|
5510
|
+
if (contactText.includes(normalizedQueryText)) return true;
|
|
5511
|
+
const queryTokens = normalizedQueryText.split(/\s+/u).filter(Boolean);
|
|
4937
5512
|
const contactTokens = contactText.split(/\s+/u).filter(Boolean);
|
|
4938
5513
|
return queryTokens.every((queryToken) => contactTokens.some((contactToken) => contactToken.startsWith(queryToken.slice(0, Math.max(4, Math.min(queryToken.length, 6)))) || queryToken.startsWith(contactToken.slice(0, Math.max(4, Math.min(contactToken.length, 6))))));
|
|
4939
5514
|
}
|
|
@@ -4942,6 +5517,99 @@ function normalizeContactLookupText(value) {
|
|
|
4942
5517
|
return normalizeGeoText(String(value || "").replace(/\b(?:кому|контакт|письмо|сообщение)\b/giu, " ")).trim();
|
|
4943
5518
|
}
|
|
4944
5519
|
|
|
5520
|
+
function normalizePhone(value) {
|
|
5521
|
+
return String(value || "").replace(/[^\d]+/g, "");
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
function normalizeVcardBirthday(value) {
|
|
5525
|
+
const text = String(value || "").trim();
|
|
5526
|
+
const iso = text.match(/^(\d{4})-(\d{2})-(\d{2})$/u);
|
|
5527
|
+
if (iso) return `${iso[1]}-${iso[2]}-${iso[3]}`;
|
|
5528
|
+
const ru = text.match(/^(\d{1,2})[.\-/](\d{1,2})(?:[.\-/](\d{2,4}))?$/u);
|
|
5529
|
+
if (ru) {
|
|
5530
|
+
const year = ru[3] ? (ru[3].length === 2 ? `20${ru[3]}` : ru[3]) : "1900";
|
|
5531
|
+
return `${year}-${String(ru[2]).padStart(2, "0")}-${String(ru[1]).padStart(2, "0")}`;
|
|
5532
|
+
}
|
|
5533
|
+
return text;
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
function contactsToCsv(rows) {
|
|
5537
|
+
const headers = ["name", "email", "emails", "phone", "phones", "address", "org", "title", "birthday", "note", "categories"];
|
|
5538
|
+
return [
|
|
5539
|
+
headers.join(","),
|
|
5540
|
+
...rows.map((row) => headers.map((key) => contactsCsvCell(Array.isArray(row[key]) ? row[key].join("; ") : row[key])).join(",")),
|
|
5541
|
+
].join("\n");
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
function contactsCsvCell(value) {
|
|
5545
|
+
return `"${String(value || "").replace(/"/g, '""')}"`;
|
|
5546
|
+
}
|
|
5547
|
+
|
|
5548
|
+
function parseContactsCsv(text) {
|
|
5549
|
+
const rows = parseSimpleCsv(text);
|
|
5550
|
+
return rows.map((row) => ({
|
|
5551
|
+
name: row.name || row.Name || row["Имя"] || row["ФИО"] || "",
|
|
5552
|
+
email: row.email || row.Email || row["Почта"] || row["Email"] || "",
|
|
5553
|
+
phone: row.phone || row.Phone || row["Телефон"] || "",
|
|
5554
|
+
address: row.address || row.Address || row["Адрес"] || "",
|
|
5555
|
+
org: row.org || row.organization || row["Организация"] || "",
|
|
5556
|
+
title: row.title || row.position || row["Должность"] || "",
|
|
5557
|
+
birthday: row.birthday || row.Birthday || row["День рождения"] || "",
|
|
5558
|
+
note: row.note || row.Note || row["Заметка"] || "",
|
|
5559
|
+
categories: row.categories || row.group || row["Группа"] || "",
|
|
5560
|
+
})).filter((row) => row.name || row.email || row.phone);
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
function parseSimpleCsv(text) {
|
|
5564
|
+
const lines = String(text || "").replace(/\r/g, "").split("\n").filter((line) => line.trim());
|
|
5565
|
+
if (!lines.length) return [];
|
|
5566
|
+
const headers = splitContactsCsvLine(lines[0]).map((header) => header.trim());
|
|
5567
|
+
return lines.slice(1).map((line) => {
|
|
5568
|
+
const values = splitContactsCsvLine(line);
|
|
5569
|
+
return Object.fromEntries(headers.map((header, index) => [header, values[index] || ""]));
|
|
5570
|
+
});
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
function splitContactsCsvLine(line) {
|
|
5574
|
+
const cells = [];
|
|
5575
|
+
let current = "";
|
|
5576
|
+
let quoted = false;
|
|
5577
|
+
for (let index = 0; index < String(line || "").length; index += 1) {
|
|
5578
|
+
const char = line[index];
|
|
5579
|
+
const next = line[index + 1];
|
|
5580
|
+
if (char === "\"" && quoted && next === "\"") {
|
|
5581
|
+
current += "\"";
|
|
5582
|
+
index += 1;
|
|
5583
|
+
} else if (char === "\"") {
|
|
5584
|
+
quoted = !quoted;
|
|
5585
|
+
} else if (char === "," && !quoted) {
|
|
5586
|
+
cells.push(current);
|
|
5587
|
+
current = "";
|
|
5588
|
+
} else {
|
|
5589
|
+
current += char;
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
cells.push(current);
|
|
5593
|
+
return cells.map((cell) => cell.trim());
|
|
5594
|
+
}
|
|
5595
|
+
|
|
5596
|
+
function nextBirthdayDate(value) {
|
|
5597
|
+
const normalized = normalizeVcardBirthday(value);
|
|
5598
|
+
const match = normalized.match(/^\d{4}-(\d{2})-(\d{2})$/u)
|
|
5599
|
+
|| normalized.match(/^\d{4}(\d{2})(\d{2})$/u)
|
|
5600
|
+
|| normalized.match(/^(\d{2})-(\d{2})$/u)
|
|
5601
|
+
|| normalized.match(/^(\d{2})(\d{2})$/u);
|
|
5602
|
+
if (!match) return null;
|
|
5603
|
+
const now = new Date();
|
|
5604
|
+
let date = new Date(now.getFullYear(), Number(match[1]) - 1, Number(match[2]), 9, 0, 0);
|
|
5605
|
+
if (date < now) date = new Date(now.getFullYear() + 1, Number(match[1]) - 1, Number(match[2]), 9, 0, 0);
|
|
5606
|
+
return { start: date.toISOString(), end: new Date(date.getTime() + 3600000).toISOString() };
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5609
|
+
function escapeRegExp(value) {
|
|
5610
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5611
|
+
}
|
|
5612
|
+
|
|
4945
5613
|
async function yandexContactsBaseUrl(token) {
|
|
4946
5614
|
const root = "https://carddav.yandex.ru/";
|
|
4947
5615
|
const principalXml = await yandexDavRequest(root, token, {
|
|
@@ -4980,8 +5648,12 @@ async function yandexTelemostCreateEvent(args = {}) {
|
|
|
4980
5648
|
return yandexCalendarCreateEvent({ ...args, description });
|
|
4981
5649
|
}
|
|
4982
5650
|
|
|
4983
|
-
function buildIcsEvent({ uid, start, end, summary, description, location }) {
|
|
5651
|
+
function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [] }) {
|
|
4984
5652
|
const escape = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
|
|
5653
|
+
const attendeeLines = (Array.isArray(attendees) ? attendees : [attendees])
|
|
5654
|
+
.map((email) => String(email || "").trim())
|
|
5655
|
+
.filter((email) => /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email))
|
|
5656
|
+
.map((email) => `ATTENDEE;CN=${escape(email)};ROLE=REQ-PARTICIPANT:mailto:${email}`);
|
|
4985
5657
|
return [
|
|
4986
5658
|
"BEGIN:VCALENDAR",
|
|
4987
5659
|
"VERSION:2.0",
|
|
@@ -4994,6 +5666,7 @@ function buildIcsEvent({ uid, start, end, summary, description, location }) {
|
|
|
4994
5666
|
`SUMMARY:${escape(summary)}`,
|
|
4995
5667
|
description ? `DESCRIPTION:${escape(description)}` : "",
|
|
4996
5668
|
location ? `LOCATION:${escape(location)}` : "",
|
|
5669
|
+
...attendeeLines,
|
|
4997
5670
|
"END:VEVENT",
|
|
4998
5671
|
"END:VCALENDAR",
|
|
4999
5672
|
"",
|
|
@@ -5057,19 +5730,75 @@ function extractVcardHrefs(xml) {
|
|
|
5057
5730
|
function parseVCards(xmlOrCards) {
|
|
5058
5731
|
const decoded = decodeXml(stripXmlTags(xmlOrCards)).replace(/\r/g, "");
|
|
5059
5732
|
return decoded.split("BEGIN:VCARD").slice(1).map((chunk) => {
|
|
5060
|
-
const
|
|
5061
|
-
const
|
|
5062
|
-
const
|
|
5063
|
-
|
|
5733
|
+
const card = `BEGIN:VCARD\n${chunk}`.replace(/\n+/g, "\n").trim();
|
|
5734
|
+
const lines = unfoldVcardLines(chunk);
|
|
5735
|
+
const emails = vcardValues(lines, "EMAIL");
|
|
5736
|
+
const phones = vcardValues(lines, "TEL");
|
|
5737
|
+
const name = cleanVcardName(vcardValue(lines, "FN") || vcardValue(lines, "N"), emails[0] || phones[0] || "");
|
|
5738
|
+
const address = cleanVcardAddress(vcardValue(lines, "ADR"));
|
|
5739
|
+
const row = {
|
|
5740
|
+
uid: vcardValue(lines, "UID"),
|
|
5741
|
+
name,
|
|
5742
|
+
email: emails[0] || "",
|
|
5743
|
+
emails,
|
|
5744
|
+
phone: phones[0] || "",
|
|
5745
|
+
phones,
|
|
5746
|
+
address,
|
|
5747
|
+
org: cleanVcardName(vcardValue(lines, "ORG")),
|
|
5748
|
+
title: cleanVcardName(vcardValue(lines, "TITLE")),
|
|
5749
|
+
note: unescapeVcardValue(vcardValue(lines, "NOTE")),
|
|
5750
|
+
birthday: vcardValue(lines, "BDAY"),
|
|
5751
|
+
categories: vcardValue(lines, "CATEGORIES"),
|
|
5752
|
+
card: ensureVcardCrlf(card),
|
|
5753
|
+
};
|
|
5754
|
+
return row;
|
|
5064
5755
|
}).filter((item) => item.name || item.email || item.phone);
|
|
5065
5756
|
}
|
|
5066
5757
|
|
|
5067
5758
|
function cleanVcardName(value, fallback = "") {
|
|
5068
|
-
const text =
|
|
5759
|
+
const text = unescapeVcardValue(value).replace(/;/g, " ").replace(/\s+/g, " ").trim();
|
|
5069
5760
|
if (!text || /^[\s;]+$/u.test(String(value || ""))) return fallback || "";
|
|
5070
5761
|
return text;
|
|
5071
5762
|
}
|
|
5072
5763
|
|
|
5764
|
+
function unfoldVcardLines(value) {
|
|
5765
|
+
return String(value || "")
|
|
5766
|
+
.replace(/\r/g, "")
|
|
5767
|
+
.replace(/\n[ \t]/g, "")
|
|
5768
|
+
.split(/\n/u)
|
|
5769
|
+
.map((line) => line.trim())
|
|
5770
|
+
.filter(Boolean);
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
function vcardValues(lines, property) {
|
|
5774
|
+
const prop = String(property || "").toLocaleUpperCase("en-US");
|
|
5775
|
+
return lines
|
|
5776
|
+
.filter((line) => line.toLocaleUpperCase("en-US").startsWith(`${prop};`) || line.toLocaleUpperCase("en-US").startsWith(`${prop}:`))
|
|
5777
|
+
.map((line) => unescapeVcardValue(line.slice(line.indexOf(":") + 1).trim()))
|
|
5778
|
+
.filter(Boolean);
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
function vcardValue(lines, property) {
|
|
5782
|
+
return vcardValues(lines, property)[0] || "";
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
function cleanVcardAddress(value) {
|
|
5786
|
+
return unescapeVcardValue(value).split(";").map((part) => part.trim()).filter(Boolean).join(", ");
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
function unescapeVcardValue(value) {
|
|
5790
|
+
return String(value || "")
|
|
5791
|
+
.replace(/\\n/giu, "\n")
|
|
5792
|
+
.replace(/\\,/gu, ",")
|
|
5793
|
+
.replace(/\\;/gu, ";")
|
|
5794
|
+
.replace(/\\\\/gu, "\\")
|
|
5795
|
+
.trim();
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5798
|
+
function ensureVcardCrlf(value) {
|
|
5799
|
+
return String(value || "").replace(/\r/g, "").replace(/\n/g, "\r\n").trim() + "\r\n";
|
|
5800
|
+
}
|
|
5801
|
+
|
|
5073
5802
|
function normalizeYandexServiceList(values) {
|
|
5074
5803
|
const aliases = {
|
|
5075
5804
|
id: "identity",
|
|
@@ -5278,6 +6007,121 @@ async function yandexDiskFind(query, options = {}) {
|
|
|
5278
6007
|
.slice(0, Number(options.limit || 50));
|
|
5279
6008
|
}
|
|
5280
6009
|
|
|
6010
|
+
async function yandexDiskStat(remotePath) {
|
|
6011
|
+
if (!remotePath) throw new Error("Путь на Яндекс Диске обязателен.");
|
|
6012
|
+
const payload = await yandexDiskRequest("GET", "/resources", {
|
|
6013
|
+
query: {
|
|
6014
|
+
path: normalizeYandexDiskPath(remotePath),
|
|
6015
|
+
fields: "name,path,type,size,created,modified,mime_type,public_url,preview,md5,sha256,embedded",
|
|
6016
|
+
},
|
|
6017
|
+
});
|
|
6018
|
+
return formatYandexDiskResource(payload);
|
|
6019
|
+
}
|
|
6020
|
+
|
|
6021
|
+
async function yandexDiskExists(remotePath) {
|
|
6022
|
+
try {
|
|
6023
|
+
const stat = await yandexDiskStat(remotePath);
|
|
6024
|
+
return { provider: "yandex-disk", path: stat.path, exists: true, type: stat.type, name: stat.name };
|
|
6025
|
+
} catch (error) {
|
|
6026
|
+
if (/404|DiskNotFoundError|Path not found/iu.test(String(error?.message || ""))) {
|
|
6027
|
+
return { provider: "yandex-disk", path: remotePath, exists: false };
|
|
6028
|
+
}
|
|
6029
|
+
throw error;
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
async function yandexDiskReadText(remotePath, options = {}) {
|
|
6034
|
+
if (!remotePath) throw new Error("Путь к текстовому файлу на Яндекс Диске обязателен.");
|
|
6035
|
+
const maxBytes = Number(options.maxBytes || 200000);
|
|
6036
|
+
const download = await yandexDiskRequest("GET", "/resources/download", { query: { path: normalizeYandexDiskPath(remotePath) } });
|
|
6037
|
+
const response = await fetch(download.href, { signal: AbortSignal.timeout(120000) });
|
|
6038
|
+
if (!response.ok) throw new Error(`Yandex Disk read failed: ${response.status} ${response.statusText}`);
|
|
6039
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
6040
|
+
if (buffer.length > maxBytes) throw new Error(`Файл слишком большой для чтения: ${buffer.length} байт. Лимит: ${maxBytes}.`);
|
|
6041
|
+
return { provider: "yandex-disk", remote: remotePath, text: buffer.toString("utf8"), size: buffer.length };
|
|
6042
|
+
}
|
|
6043
|
+
|
|
6044
|
+
async function yandexDiskMove(from, to, options = {}) {
|
|
6045
|
+
if (!options.confirm) throw new Error("Для перемещения на Яндекс Диске нужен аргумент confirm=true.");
|
|
6046
|
+
if (!from || !to) throw new Error("Для перемещения нужны from и to.");
|
|
6047
|
+
await ensureYandexDiskDir(path.dirname(to), { allowExisting: true });
|
|
6048
|
+
await yandexDiskRequest("POST", "/resources/move", {
|
|
6049
|
+
query: { from: normalizeYandexDiskPath(from), path: normalizeYandexDiskPath(to), overwrite: Boolean(options.overwrite) },
|
|
6050
|
+
timeout: 60000,
|
|
6051
|
+
});
|
|
6052
|
+
return { provider: "yandex-disk", status: "moved", from, remote: to };
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
async function yandexDiskCopy(from, to, options = {}) {
|
|
6056
|
+
if (!options.confirm) throw new Error("Для копирования на Яндекс Диске нужен аргумент confirm=true.");
|
|
6057
|
+
if (!from || !to) throw new Error("Для копирования нужны from и to.");
|
|
6058
|
+
await ensureYandexDiskDir(path.dirname(to), { allowExisting: true });
|
|
6059
|
+
await yandexDiskRequest("POST", "/resources/copy", {
|
|
6060
|
+
query: { from: normalizeYandexDiskPath(from), path: normalizeYandexDiskPath(to), overwrite: Boolean(options.overwrite) },
|
|
6061
|
+
timeout: 60000,
|
|
6062
|
+
});
|
|
6063
|
+
return { provider: "yandex-disk", status: "copied", from, remote: to };
|
|
6064
|
+
}
|
|
6065
|
+
|
|
6066
|
+
async function yandexDiskRename(remotePath, newName, options = {}) {
|
|
6067
|
+
if (!remotePath || !newName) throw new Error("Для переименования нужны путь и новое имя.");
|
|
6068
|
+
const current = denormalizeYandexDiskPath(normalizeYandexDiskPath(remotePath));
|
|
6069
|
+
const target = path.posix.join(path.posix.dirname(current), slugYandexDiskName(newName));
|
|
6070
|
+
return yandexDiskMove(remotePath, target, { ...options, confirm: true });
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
async function yandexDiskTrashList(remotePath = "", options = {}) {
|
|
6074
|
+
const query = { limit: Number(options.limit || 100) };
|
|
6075
|
+
if (remotePath) query.path = normalizeYandexDiskPath(remotePath);
|
|
6076
|
+
const payload = await yandexDiskRequest("GET", "/trash/resources", { query });
|
|
6077
|
+
const items = payload._embedded?.items || [];
|
|
6078
|
+
return items.map(formatYandexDiskResource);
|
|
6079
|
+
}
|
|
6080
|
+
|
|
6081
|
+
async function yandexDiskRestore(remotePath, options = {}) {
|
|
6082
|
+
if (!options.confirm) throw new Error("Для восстановления из корзины нужен аргумент confirm=true.");
|
|
6083
|
+
if (!remotePath) throw new Error("Путь в корзине обязателен.");
|
|
6084
|
+
await yandexDiskRequest("PUT", "/trash/resources/restore", {
|
|
6085
|
+
query: { path: normalizeYandexDiskPath(remotePath), name: options.name || "", overwrite: Boolean(options.overwrite) },
|
|
6086
|
+
timeout: 60000,
|
|
6087
|
+
});
|
|
6088
|
+
return { provider: "yandex-disk", status: "restored", remote: remotePath };
|
|
6089
|
+
}
|
|
6090
|
+
|
|
6091
|
+
async function yandexDiskEmptyTrash(options = {}) {
|
|
6092
|
+
if (!options.confirm) throw new Error("Для очистки корзины нужен аргумент confirm=true.");
|
|
6093
|
+
await yandexDiskRequest("DELETE", "/trash/resources", {
|
|
6094
|
+
query: { path: options.path ? normalizeYandexDiskPath(options.path) : "" },
|
|
6095
|
+
timeout: 60000,
|
|
6096
|
+
});
|
|
6097
|
+
return { provider: "yandex-disk", status: "trash-empty-requested", remote: options.path || "trash" };
|
|
6098
|
+
}
|
|
6099
|
+
|
|
6100
|
+
function formatYandexDiskResource(item = {}) {
|
|
6101
|
+
return {
|
|
6102
|
+
provider: "yandex-disk",
|
|
6103
|
+
type: item.type === "dir" ? "dir" : "file",
|
|
6104
|
+
name: item.name || path.basename(item.path || ""),
|
|
6105
|
+
path: denormalizeYandexDiskPath(item.path || ""),
|
|
6106
|
+
remote: denormalizeYandexDiskPath(item.path || ""),
|
|
6107
|
+
size: item.size || 0,
|
|
6108
|
+
mimeType: item.mime_type || "",
|
|
6109
|
+
created: item.created || "",
|
|
6110
|
+
modified: item.modified || "",
|
|
6111
|
+
publicUrl: item.public_url || "",
|
|
6112
|
+
md5: item.md5 || "",
|
|
6113
|
+
sha256: item.sha256 || "",
|
|
6114
|
+
};
|
|
6115
|
+
}
|
|
6116
|
+
|
|
6117
|
+
function slugYandexDiskName(value) {
|
|
6118
|
+
return String(value || "")
|
|
6119
|
+
.replace(/[\\/:*?"<>|]+/gu, "-")
|
|
6120
|
+
.replace(/\s+/gu, " ")
|
|
6121
|
+
.trim()
|
|
6122
|
+
|| `item-${timestampForFile()}`;
|
|
6123
|
+
}
|
|
6124
|
+
|
|
5281
6125
|
async function yandexDiskListRecursive(remotePath, options = {}) {
|
|
5282
6126
|
const depth = Number(options.depth || 4);
|
|
5283
6127
|
const limit = Number(options.limit || 200);
|
|
@@ -5325,6 +6169,119 @@ async function yandexDiskShare(remotePath) {
|
|
|
5325
6169
|
return { provider: "yandex-disk", remote: remotePath, publicUrl: payload.public_url || "-" };
|
|
5326
6170
|
}
|
|
5327
6171
|
|
|
6172
|
+
async function yandexDiskShareWithQr(remotePath, options = {}) {
|
|
6173
|
+
if (!options.confirm) throw new Error("Для публикации ссылки и QR нужен аргумент confirm=true.");
|
|
6174
|
+
if (!remotePath) throw new Error("Путь на Яндекс Диске обязателен.");
|
|
6175
|
+
const shared = await yandexDiskShare(remotePath);
|
|
6176
|
+
const qrRemotePath = options.qrPath || buildYandexDiskQrPath(remotePath);
|
|
6177
|
+
const tempPath = path.join(CONFIG_DIR, `qr-${Date.now()}.png`);
|
|
6178
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
6179
|
+
try {
|
|
6180
|
+
await createQrPng(shared.publicUrl, tempPath);
|
|
6181
|
+
await yandexDiskUpload(tempPath, qrRemotePath, { overwrite: true });
|
|
6182
|
+
} finally {
|
|
6183
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
6184
|
+
}
|
|
6185
|
+
const qrShared = await yandexDiskShare(qrRemotePath);
|
|
6186
|
+
return {
|
|
6187
|
+
provider: "yandex-disk",
|
|
6188
|
+
status: "shared-with-qr",
|
|
6189
|
+
remote: remotePath,
|
|
6190
|
+
publicUrl: shared.publicUrl,
|
|
6191
|
+
qrRemote: qrRemotePath,
|
|
6192
|
+
qrPublicUrl: qrShared.publicUrl,
|
|
6193
|
+
};
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
async function yandexDiskShareEmail(args = {}) {
|
|
6197
|
+
if (!args.confirm) throw new Error("Для отправки ссылки по почте нужен аргумент confirm=true.");
|
|
6198
|
+
const remotePath = args.remotePath || args.path || args.targetFolder || args.target;
|
|
6199
|
+
if (!remotePath) throw new Error("Путь на Яндекс Диске обязателен.");
|
|
6200
|
+
const to = await resolveYandexShareRecipients(args);
|
|
6201
|
+
const shared = await yandexDiskShareWithQr(remotePath, { ...args, confirm: true });
|
|
6202
|
+
const subject = args.subject || `Ссылка на Яндекс Диск: ${path.posix.basename(remotePath) || "материалы"}`;
|
|
6203
|
+
const text = [
|
|
6204
|
+
args.text || args.message || "Здравствуйте. Направляю ссылку на материалы.",
|
|
6205
|
+
"",
|
|
6206
|
+
`Ссылка: ${shared.publicUrl}`,
|
|
6207
|
+
`QR-код: ${shared.qrPublicUrl}`,
|
|
6208
|
+
"",
|
|
6209
|
+
"QR-код сохранен на Яндекс Диске:",
|
|
6210
|
+
shared.qrRemote,
|
|
6211
|
+
].join("\n");
|
|
6212
|
+
const sent = await yandexMailSend({ to, subject, text, confirm: true });
|
|
6213
|
+
return { ...shared, status: "shared-and-sent", to: sent.to, subject };
|
|
6214
|
+
}
|
|
6215
|
+
|
|
6216
|
+
async function yandexDiskPackageShareEmail(args = {}) {
|
|
6217
|
+
if (!args.confirm) throw new Error("Для пакетной отправки папки нужен аргумент confirm=true.");
|
|
6218
|
+
const sourcePath = args.sourcePath || args.source || args.from;
|
|
6219
|
+
const targetFolder = args.targetFolder || args.target || args.to;
|
|
6220
|
+
if (!sourcePath || !targetFolder) throw new Error("Нужны sourcePath и targetFolder.");
|
|
6221
|
+
await cloudCreateFolder("yandex-disk", targetFolder);
|
|
6222
|
+
const items = await yandexDiskList(sourcePath);
|
|
6223
|
+
const mode = /move|перен/iu.test(args.mode || "") ? "move" : "copy";
|
|
6224
|
+
const transferred = [];
|
|
6225
|
+
for (const item of items.slice(0, Number(args.limit || 100))) {
|
|
6226
|
+
const destination = path.posix.join(targetFolder, item.name);
|
|
6227
|
+
if (mode === "move") {
|
|
6228
|
+
await yandexDiskMove(item.path, destination, { confirm: true, overwrite: args.overwrite !== false });
|
|
6229
|
+
} else {
|
|
6230
|
+
await yandexDiskCopy(item.path, destination, { confirm: true, overwrite: args.overwrite !== false });
|
|
6231
|
+
}
|
|
6232
|
+
transferred.push({ from: item.path, to: destination, type: item.type });
|
|
6233
|
+
}
|
|
6234
|
+
const shared = await yandexDiskShareEmail({
|
|
6235
|
+
remotePath: targetFolder,
|
|
6236
|
+
to: args.to,
|
|
6237
|
+
email: args.email,
|
|
6238
|
+
contact: args.contact || args.contactQuery,
|
|
6239
|
+
subject: args.subject || `Материалы на Яндекс Диске: ${path.posix.basename(targetFolder)}`,
|
|
6240
|
+
text: args.text || args.message || `Собрал материалы в папку ${targetFolder}.`,
|
|
6241
|
+
confirm: true,
|
|
6242
|
+
});
|
|
6243
|
+
return {
|
|
6244
|
+
...shared,
|
|
6245
|
+
status: "package-shared-and-sent",
|
|
6246
|
+
sourcePath,
|
|
6247
|
+
targetFolder,
|
|
6248
|
+
mode,
|
|
6249
|
+
transferred: transferred.length,
|
|
6250
|
+
};
|
|
6251
|
+
}
|
|
6252
|
+
|
|
6253
|
+
async function resolveYandexShareRecipients(args = {}) {
|
|
6254
|
+
const emails = Array.isArray(args.to) ? args.to : String(args.to || args.email || "").split(/[;,]/u).map((item) => item.trim()).filter(Boolean);
|
|
6255
|
+
if (emails.length) return emails;
|
|
6256
|
+
const contactQuery = args.contact || args.contactQuery || args.name || "";
|
|
6257
|
+
if (!contactQuery) throw new Error("Укажите email или контакт получателя.");
|
|
6258
|
+
const result = await resolveYandexMailRecipientFromContacts(contactQuery);
|
|
6259
|
+
if (result.status === "not-found") throw new Error(`В Яндекс Контактах не нашел: ${contactQuery}. Укажите email вручную.`);
|
|
6260
|
+
if (result.status === "no-email") throw new Error(`Контакт найден, но email не указан: ${result.contact.name}. Укажите email вручную.`);
|
|
6261
|
+
if (result.status === "ambiguous") {
|
|
6262
|
+
const rows = result.contacts.map((contact, index) => `${index + 1}. ${contact.name || contact.email || "Контакт"}${contact.email ? `, ${contact.email}` : ", email не указан"}`).join("\n");
|
|
6263
|
+
throw new Error(`Нашел несколько контактов для "${contactQuery}". Уточните получателя:\n${rows}`);
|
|
6264
|
+
}
|
|
6265
|
+
return [result.contact.email];
|
|
6266
|
+
}
|
|
6267
|
+
|
|
6268
|
+
async function createQrPng(text, outputPath) {
|
|
6269
|
+
const QRCode = await import("qrcode");
|
|
6270
|
+
await QRCode.toFile(outputPath, String(text || ""), {
|
|
6271
|
+
type: "png",
|
|
6272
|
+
errorCorrectionLevel: "M",
|
|
6273
|
+
margin: 2,
|
|
6274
|
+
width: 512,
|
|
6275
|
+
});
|
|
6276
|
+
}
|
|
6277
|
+
|
|
6278
|
+
function buildYandexDiskQrPath(remotePath) {
|
|
6279
|
+
const current = denormalizeYandexDiskPath(normalizeYandexDiskPath(remotePath));
|
|
6280
|
+
const dir = path.posix.dirname(current);
|
|
6281
|
+
const base = path.posix.basename(current).replace(/\.[^.]+$/u, "");
|
|
6282
|
+
return path.posix.join(dir, `${slugYandexDiskName(base || "link")}-qr.png`);
|
|
6283
|
+
}
|
|
6284
|
+
|
|
5328
6285
|
async function ensureYandexDiskDir(remotePath, options = {}) {
|
|
5329
6286
|
const normalized = normalizeYandexDiskPath(remotePath || CLOUD_DEFAULT_REMOTE_DIR);
|
|
5330
6287
|
const plain = denormalizeYandexDiskPath(normalized);
|
|
@@ -5345,7 +6302,7 @@ async function ensureYandexDiskDir(remotePath, options = {}) {
|
|
|
5345
6302
|
|
|
5346
6303
|
function normalizeYandexDiskPath(remotePath) {
|
|
5347
6304
|
const text = String(remotePath || CLOUD_DEFAULT_REMOTE_DIR).trim().replace(/\\/g, "/");
|
|
5348
|
-
if (text.startsWith("disk:") || text.startsWith("app:")) return text;
|
|
6305
|
+
if (text.startsWith("disk:") || text.startsWith("app:") || text.startsWith("trash:")) return text;
|
|
5349
6306
|
return text.startsWith("/") ? text : `/${text}`;
|
|
5350
6307
|
}
|
|
5351
6308
|
|
|
@@ -9692,6 +10649,10 @@ function isCronDue(job) {
|
|
|
9692
10649
|
if (everyMinutes) {
|
|
9693
10650
|
return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyMinutes[1]) * 60 * 1000;
|
|
9694
10651
|
}
|
|
10652
|
+
const everyDays = normalized.match(/кажд(?:ые|ую)\s+(\d+)\s*(?:дн|день|дня|дней)/u) || normalized.match(/every\s+(\d+)\s*(?:d|day|days)/u);
|
|
10653
|
+
if (everyDays) {
|
|
10654
|
+
return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyDays[1]) * 24 * 60 * 60 * 1000;
|
|
10655
|
+
}
|
|
9695
10656
|
if (normalized.includes("каждый день") || normalized.includes("daily")) {
|
|
9696
10657
|
return !lastRun || now.toISOString().slice(0, 10) !== lastRun.toISOString().slice(0, 10);
|
|
9697
10658
|
}
|
|
@@ -10061,19 +11022,35 @@ async function aiAsk(args, context = {}) {
|
|
|
10061
11022
|
if (!options.quiet) console.log(casualAnswer);
|
|
10062
11023
|
return casualAnswer;
|
|
10063
11024
|
}
|
|
10064
|
-
const
|
|
10065
|
-
if (
|
|
11025
|
+
const userSkillAnswer = await buildUserSkillDirectAnswer(question);
|
|
11026
|
+
if (userSkillAnswer) {
|
|
10066
11027
|
if (historyEnabled) {
|
|
10067
|
-
recordAskHistory({ question, answer:
|
|
10068
|
-
appendSessionExchange(sessionId, question,
|
|
11028
|
+
recordAskHistory({ question, answer: userSkillAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
11029
|
+
appendSessionExchange(sessionId, question, userSkillAnswer, dataContext, "");
|
|
10069
11030
|
}
|
|
10070
|
-
emitEvent(options, "answer", { length:
|
|
11031
|
+
emitEvent(options, "answer", { length: userSkillAnswer.length, sessionId, direct: true, skill: true });
|
|
10071
11032
|
if (options.output) {
|
|
10072
11033
|
await assertPermission("writeFiles");
|
|
10073
|
-
await writeFile(options.output,
|
|
11034
|
+
await writeFile(options.output, userSkillAnswer, "utf8");
|
|
11035
|
+
}
|
|
11036
|
+
if (!options.quiet) console.log(userSkillAnswer);
|
|
11037
|
+
return userSkillAnswer;
|
|
11038
|
+
}
|
|
11039
|
+
if (/(контакт|адресн)/iu.test(question) && !isExplicitYandexDiskPathDelete(question)) {
|
|
11040
|
+
const yandexContactAnswer = await buildYandexDirectAnswer(question, context.history || history);
|
|
11041
|
+
if (yandexContactAnswer) {
|
|
11042
|
+
if (historyEnabled) {
|
|
11043
|
+
recordAskHistory({ question, answer: yandexContactAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
11044
|
+
appendSessionExchange(sessionId, question, yandexContactAnswer, dataContext, "");
|
|
11045
|
+
}
|
|
11046
|
+
emitEvent(options, "answer", { length: yandexContactAnswer.length, sessionId, direct: true, yandex: true });
|
|
11047
|
+
if (options.output) {
|
|
11048
|
+
await assertPermission("writeFiles");
|
|
11049
|
+
await writeFile(options.output, yandexContactAnswer, "utf8");
|
|
11050
|
+
}
|
|
11051
|
+
if (!options.quiet) console.log(yandexContactAnswer);
|
|
11052
|
+
return yandexContactAnswer;
|
|
10074
11053
|
}
|
|
10075
|
-
if (!options.quiet) console.log(yandexAnswer);
|
|
10076
|
-
return yandexAnswer;
|
|
10077
11054
|
}
|
|
10078
11055
|
const cloudAnswer = await buildCloudDirectAnswer(question);
|
|
10079
11056
|
if (cloudAnswer) {
|
|
@@ -10089,6 +11066,20 @@ async function aiAsk(args, context = {}) {
|
|
|
10089
11066
|
if (!options.quiet) console.log(cloudAnswer);
|
|
10090
11067
|
return cloudAnswer;
|
|
10091
11068
|
}
|
|
11069
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, context.history || history);
|
|
11070
|
+
if (yandexAnswer) {
|
|
11071
|
+
if (historyEnabled) {
|
|
11072
|
+
recordAskHistory({ question, answer: yandexAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
11073
|
+
appendSessionExchange(sessionId, question, yandexAnswer, dataContext, "");
|
|
11074
|
+
}
|
|
11075
|
+
emitEvent(options, "answer", { length: yandexAnswer.length, sessionId, direct: true, yandex: true });
|
|
11076
|
+
if (options.output) {
|
|
11077
|
+
await assertPermission("writeFiles");
|
|
11078
|
+
await writeFile(options.output, yandexAnswer, "utf8");
|
|
11079
|
+
}
|
|
11080
|
+
if (!options.quiet) console.log(yandexAnswer);
|
|
11081
|
+
return yandexAnswer;
|
|
11082
|
+
}
|
|
10092
11083
|
const geoAnswer = await buildGeoDirectAnswer(question);
|
|
10093
11084
|
if (geoAnswer) {
|
|
10094
11085
|
if (historyEnabled) {
|
|
@@ -10214,6 +11205,10 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10214
11205
|
].join("\n");
|
|
10215
11206
|
}
|
|
10216
11207
|
|
|
11208
|
+
if (/(контакт|адресн)/iu.test(normalized) && !mailFollowup && !isExplicitYandexDiskPathDelete(question)) {
|
|
11209
|
+
return await buildYandexContactsDirectAnswer(question, normalized);
|
|
11210
|
+
}
|
|
11211
|
+
|
|
10217
11212
|
if (mailFollowup || /(почт|письм|email|e-mail|спам|чернов|отправлен|исходящ|корзин)/iu.test(normalized)) {
|
|
10218
11213
|
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
10219
11214
|
const minutes = Number(String(question || "").match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 5);
|
|
@@ -10368,6 +11363,38 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10368
11363
|
}
|
|
10369
11364
|
|
|
10370
11365
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
11366
|
+
if (/(дубликат|повтор)/iu.test(normalized)) {
|
|
11367
|
+
const rows = await yandexContactsFindDuplicates({ limit: 20 });
|
|
11368
|
+
if (!rows.length) return "Дубликаты контактов не найдены.";
|
|
11369
|
+
return rows.map((row) => formatToolResult({ rows: [row], outputs: [] }, {})).join("\n");
|
|
11370
|
+
}
|
|
11371
|
+
if (/(неполн|без\s+email|без\s+почт|без\s+телефон|без\s+адрес)/iu.test(normalized)) {
|
|
11372
|
+
const field = /без\s+(?:email|почт)/iu.test(normalized) ? "email" : /без\s+телефон/iu.test(normalized) ? "phone" : /без\s+адрес/iu.test(normalized) ? "address" : "";
|
|
11373
|
+
const rows = await yandexContactsFindIncomplete({ field, limit: 30 });
|
|
11374
|
+
if (!rows.length) return "Неполные контакты по этому признаку не найдены.";
|
|
11375
|
+
return ["Неполные контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
|
|
11376
|
+
}
|
|
11377
|
+
if (/(экспорт|выгруз|сохрани|резерв|backup|бэкап)/iu.test(normalized) && /(диск|яндекс.?диск|облак)/iu.test(normalized)) {
|
|
11378
|
+
const result = await yandexContactsBackupToDisk({ format: /csv/iu.test(normalized) ? "csv" : "vcard", confirm: true });
|
|
11379
|
+
return `Контакты сохранены на Яндекс Диск: ${result.remote}. Записей: ${result.rows}.`;
|
|
11380
|
+
}
|
|
11381
|
+
if (/(экспорт|выгруз)/iu.test(normalized)) {
|
|
11382
|
+
const result = await yandexContactsExport(/csv/iu.test(normalized) ? "csv" : "vcard", {});
|
|
11383
|
+
return `Контакты экспортированы: ${result.output}. Записей: ${result.rows}.`;
|
|
11384
|
+
}
|
|
11385
|
+
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(normalized)) {
|
|
11386
|
+
const draft = parseYandexContactCreateRequest(question);
|
|
11387
|
+
if (!draft.name || (!draft.email && !draft.phone)) return "Для создания контакта укажите имя и email или телефон.";
|
|
11388
|
+
const result = await yandexContactsCreate({ ...draft, confirm: true });
|
|
11389
|
+
return `Контакт создан: ${formatYandexContact(result)}.`;
|
|
11390
|
+
}
|
|
11391
|
+
if (/(удали|удалить).{0,40}контакт/iu.test(normalized)) {
|
|
11392
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
11393
|
+
const result = await yandexContactsDelete(query, { confirm: true });
|
|
11394
|
+
if (result.status === "ambiguous") return [`Нашел несколько контактов. Уточните:`, ...result.contacts.map((contact, index) => `${index + 1}. ${formatYandexContact(contact)}`)].join("\n");
|
|
11395
|
+
if (result.status === "not-found") return `Контакт не найден: ${query}.`;
|
|
11396
|
+
return `Контакт удален: ${formatYandexContact(result)}.`;
|
|
11397
|
+
}
|
|
10371
11398
|
if (/(добав|запиши|сохрани).{0,40}(email|e-mail|почт)/iu.test(normalized)) {
|
|
10372
11399
|
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
10373
11400
|
const query = cleanupYandexContactEmailTarget(question, email);
|
|
@@ -10383,6 +11410,54 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10383
11410
|
}
|
|
10384
11411
|
return `Email добавлен в контакт: ${result.name || query}, ${result.email}.`;
|
|
10385
11412
|
}
|
|
11413
|
+
if (/(добав|запиши|сохрани).{0,40}(телефон|номер)/iu.test(normalized)) {
|
|
11414
|
+
const phone = String(question || "").match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || "";
|
|
11415
|
+
const query = cleanupYandexContactActionQuery(question.replace(phone, " "));
|
|
11416
|
+
if (!phone || !query) return "Укажите контакт и телефон.";
|
|
11417
|
+
const result = await yandexContactsUpdate(query, { phone, mode: "add-phone", confirm: true });
|
|
11418
|
+
if (result.status !== "updated") return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11419
|
+
return `Телефон добавлен: ${formatYandexContact(result)}.`;
|
|
11420
|
+
}
|
|
11421
|
+
if (/(добав|запиши|сохрани).{0,40}(адрес)/iu.test(normalized)) {
|
|
11422
|
+
const address = question.match(/(?:адрес|по адресу)\s*:?\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
11423
|
+
const query = cleanupYandexContactActionQuery(question.replace(address, " "));
|
|
11424
|
+
if (!address || !query) return "Укажите контакт и адрес.";
|
|
11425
|
+
const result = await yandexContactsUpdate(query, { address, mode: "add-address", confirm: true });
|
|
11426
|
+
if (result.status !== "updated") return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11427
|
+
return `Адрес добавлен: ${formatYandexContact(result)}.`;
|
|
11428
|
+
}
|
|
11429
|
+
if (/(добав|запиши|сохрани).{0,40}(заметк|коммент|примеч)/iu.test(normalized)) {
|
|
11430
|
+
const note = question.match(/(?:заметк\p{L}*|коммент\p{L}*|примеч\p{L}*)\s*:?\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
11431
|
+
const query = cleanupYandexContactActionQuery(question.replace(note, " "));
|
|
11432
|
+
if (!note || !query) return "Укажите контакт и текст заметки.";
|
|
11433
|
+
const result = await yandexContactsUpdate(query, { note, mode: "add-note", confirm: true });
|
|
11434
|
+
if (result.status !== "updated") return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11435
|
+
return `Заметка добавлена: ${formatYandexContact(result)}.`;
|
|
11436
|
+
}
|
|
11437
|
+
if (/(создай|сделай).{0,40}(папк).{0,40}(контакт)/iu.test(normalized)) {
|
|
11438
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
11439
|
+
const result = await yandexContactCreateDiskFolder({ query, confirm: true });
|
|
11440
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11441
|
+
}
|
|
11442
|
+
if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(normalized)) {
|
|
11443
|
+
const remotePath = extractCloudPath(question);
|
|
11444
|
+
const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
|
|
11445
|
+
const result = await yandexContactSendDiskLinkQr({ contact, path: remotePath, confirm: true });
|
|
11446
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11447
|
+
}
|
|
11448
|
+
if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
|
|
11449
|
+
const draft = parseYandexMailSendRequest(question);
|
|
11450
|
+
const result = await yandexContactSendMail({ contact: draft.contactQuery || cleanupYandexContactActionQuery(question), subject: draft.subject, text: draft.text, confirm: true });
|
|
11451
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11452
|
+
}
|
|
11453
|
+
if (/(создай|добавь|запланируй).{0,40}(встреч|событи|календар|телемост)/iu.test(normalized)) {
|
|
11454
|
+
const dateTime = extractDateTimeFromText(question);
|
|
11455
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
11456
|
+
const result = /телемост/iu.test(normalized)
|
|
11457
|
+
? await yandexContactCreateTelemostEvent({ query, ...dateTime, confirm: true })
|
|
11458
|
+
: await yandexContactCreateCalendarEvent({ query, ...dateTime, confirm: true });
|
|
11459
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11460
|
+
}
|
|
10386
11461
|
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
10387
11462
|
const result = await yandexContactsStatus();
|
|
10388
11463
|
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
@@ -10400,6 +11475,212 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10400
11475
|
return "";
|
|
10401
11476
|
}
|
|
10402
11477
|
|
|
11478
|
+
async function buildYandexContactsDirectAnswer(question, normalized = "") {
|
|
11479
|
+
const text = normalized || String(question || "").toLocaleLowerCase("ru-RU");
|
|
11480
|
+
if (/(авто|автомат|регуляр|кажд|период|обслужив|проверяй|проверку)/iu.test(text) && /(контакт)/iu.test(text) && /(включ|запусти|начни|поставь|создай)/iu.test(text)) {
|
|
11481
|
+
const days = Number(String(question || "").match(/(\d+)\s*(?:дн|день|дня|дней)/iu)?.[1] || 7);
|
|
11482
|
+
const result = await yandexContactsMaintenanceEnable(days, { backup: /(backup|бэкап|резерв|диск)/iu.test(text) });
|
|
11483
|
+
return `Регулярная проверка контактов включена: каждые ${result.days} дней. Backup на Диск: ${result.backup ? "yes" : "no"}.`;
|
|
11484
|
+
}
|
|
11485
|
+
if (/(авто|автомат|регуляр|кажд|период|обслужив|проверяй|проверку)/iu.test(text) && /(контакт)/iu.test(text) && /(выключ|отключ|останов|убери)/iu.test(text)) {
|
|
11486
|
+
await yandexContactsMaintenanceDisable();
|
|
11487
|
+
return "Регулярная проверка контактов выключена.";
|
|
11488
|
+
}
|
|
11489
|
+
if (/(обслужив|провер|диагност|doctor)/iu.test(text) && /(контакт)/iu.test(text) && /(сейчас|запусти|сделай|проверь)/iu.test(text)) {
|
|
11490
|
+
const result = await yandexContactsMaintenanceTick({ force: true, backup: /(backup|бэкап|резерв)/iu.test(text) });
|
|
11491
|
+
return [
|
|
11492
|
+
"Проверка контактов выполнена:",
|
|
11493
|
+
`Всего: ${result.total}`,
|
|
11494
|
+
`Неполных: ${result.incomplete}`,
|
|
11495
|
+
`Групп дубликатов: ${result.duplicateGroups}`,
|
|
11496
|
+
result.backupRemote ? `Backup: ${result.backupRemote}` : "",
|
|
11497
|
+
].filter(Boolean).join("\n");
|
|
11498
|
+
}
|
|
11499
|
+
if (/(дубликат|повтор)/iu.test(text)) {
|
|
11500
|
+
const rows = await yandexContactsFindDuplicates({ limit: 20 });
|
|
11501
|
+
if (!rows.length) return "Дубликаты контактов не найдены.";
|
|
11502
|
+
return rows.map((row) => formatToolResult({ rows: [row], outputs: [] }, {})).join("\n");
|
|
11503
|
+
}
|
|
11504
|
+
if (/(неполн|без\s+email|без\s+почт|без\s+телефон|без\s+адрес)/iu.test(text)) {
|
|
11505
|
+
const field = /без\s+(?:email|почт)/iu.test(text) ? "email" : /без\s+телефон/iu.test(text) ? "phone" : /без\s+адрес/iu.test(text) ? "address" : "";
|
|
11506
|
+
const rows = await yandexContactsFindIncomplete({ field, limit: 30 });
|
|
11507
|
+
if (!rows.length) return "Неполные контакты по этому признаку не найдены.";
|
|
11508
|
+
return ["Неполные контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
|
|
11509
|
+
}
|
|
11510
|
+
if (/(экспорт|выгруз|сохрани|резерв|backup|бэкап)/iu.test(text) && /(диск|яндекс.?диск|облак)/iu.test(text)) {
|
|
11511
|
+
const result = await yandexContactsBackupToDisk({ format: /csv/iu.test(text) ? "csv" : "vcard", confirm: true });
|
|
11512
|
+
return `Контакты сохранены на Яндекс Диск: ${result.remote}. Записей: ${result.rows}.`;
|
|
11513
|
+
}
|
|
11514
|
+
if (/(импорт|импортируй|загрузи).{0,40}контакт/iu.test(text)) {
|
|
11515
|
+
const inputPath = extractLocalInputPath(question) || question.match(/(?:из|файл)\s+([^\s]+(?:\.csv|\.vcf|\.vcard))/iu)?.[1] || "";
|
|
11516
|
+
if (!inputPath) return "Укажите файл CSV или vCard для импорта контактов.";
|
|
11517
|
+
const result = await yandexContactsImport(/\.csv$/iu.test(inputPath) ? "csv" : "vcard", { path: inputPath, confirm: true });
|
|
11518
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11519
|
+
}
|
|
11520
|
+
if (/(дн[еиь]\s+рожден|день\s+рождени|дней\s+рождени|birthday)/iu.test(text) && /(календар|событи|напомин)/iu.test(text)) {
|
|
11521
|
+
const result = await yandexContactsBirthdaysToCalendar({ confirm: true });
|
|
11522
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11523
|
+
}
|
|
11524
|
+
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(text) && /(школ|детск\w*\s+сад|детсад|садик|инн)/iu.test(text)) {
|
|
11525
|
+
const result = await yandexContactFromPublicEntity({ query: cleanupYandexContactActionQuery(question), confirm: true });
|
|
11526
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11527
|
+
}
|
|
11528
|
+
if (/(экспорт|выгруз)/iu.test(text)) {
|
|
11529
|
+
const result = await yandexContactsExport(/csv/iu.test(text) ? "csv" : "vcard", {});
|
|
11530
|
+
return `Контакты экспортированы: ${result.output}. Записей: ${result.rows}.`;
|
|
11531
|
+
}
|
|
11532
|
+
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(text)) {
|
|
11533
|
+
const draft = parseYandexContactCreateRequest(question);
|
|
11534
|
+
if (!draft.name || (!draft.email && !draft.phone)) return "Для создания контакта укажите имя и email или телефон.";
|
|
11535
|
+
const result = await yandexContactsCreate({ ...draft, confirm: true });
|
|
11536
|
+
return `Контакт создан: ${formatYandexContact(result)}.`;
|
|
11537
|
+
}
|
|
11538
|
+
if (/(удали|удалить).{0,40}контакт/iu.test(text)) {
|
|
11539
|
+
const query = extractEmailAddress(question) || String(question || "").match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || cleanupYandexContactActionQuery(question);
|
|
11540
|
+
const result = await yandexContactsDelete(query, { confirm: true });
|
|
11541
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11542
|
+
}
|
|
11543
|
+
if (/(добав|запиши|сохрани).{0,40}(email|e-mail|почт)/iu.test(text)) {
|
|
11544
|
+
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
11545
|
+
const query = cleanupYandexContactEmailTarget(question, email);
|
|
11546
|
+
if (!email || !query) return "Укажите контакт и email. Пример: добавь email petrov@example.com к контакту Петров.";
|
|
11547
|
+
const result = await yandexContactsAddEmail(query, email, { confirm: true });
|
|
11548
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11549
|
+
}
|
|
11550
|
+
if (/(удали|удалить|убери).{0,40}(email|e-mail|почт)/iu.test(text)) {
|
|
11551
|
+
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
11552
|
+
const query = cleanupYandexContactEmailTarget(question, email) || cleanupYandexContactActionQuery(question);
|
|
11553
|
+
if (!query) return "Укажите контакт, у которого удалить email.";
|
|
11554
|
+
const result = await yandexContactsUpdate(query, { removeEmail: email || true, mode: "remove-email", confirm: true });
|
|
11555
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11556
|
+
}
|
|
11557
|
+
if (/(добав|запиши|сохрани).{0,40}(телефон|номер)/iu.test(text)) {
|
|
11558
|
+
const phone = String(question || "").match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || "";
|
|
11559
|
+
const query = cleanupYandexContactActionQuery(question.replace(phone, " "));
|
|
11560
|
+
if (!phone || !query) return "Укажите контакт и телефон.";
|
|
11561
|
+
const result = await yandexContactsUpdate(query, { phone, mode: "add-phone", confirm: true });
|
|
11562
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11563
|
+
}
|
|
11564
|
+
if (/(удали|удалить|убери).{0,40}(телефон|номер)/iu.test(text)) {
|
|
11565
|
+
const phone = String(question || "").match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || "";
|
|
11566
|
+
const query = cleanupYandexContactActionQuery(question.replace(phone, " "));
|
|
11567
|
+
if (!query) return "Укажите контакт, у которого удалить телефон.";
|
|
11568
|
+
const result = await yandexContactsUpdate(query, { removePhone: phone || true, mode: "remove-phone", confirm: true });
|
|
11569
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11570
|
+
}
|
|
11571
|
+
if (/(переимен|измени\s+имя|смени\s+имя)/iu.test(text) && /(контакт)/iu.test(text)) {
|
|
11572
|
+
const newName = question.match(/(?:в|на|как)\s+["«]?([^"».,!?]+)["»]?\s*$/iu)?.[1]?.trim() || "";
|
|
11573
|
+
const query = cleanupYandexContactActionQuery(question.replace(newName, " "));
|
|
11574
|
+
if (!query || !newName) return "Укажите контакт и новое имя.";
|
|
11575
|
+
const result = await yandexContactsUpdate(query, { name: newName, confirm: true });
|
|
11576
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11577
|
+
}
|
|
11578
|
+
if (/(добав|запиши|сохрани).{0,40}(день\s+рожд|дат[ау]\s+рожд|birthday)/iu.test(text)) {
|
|
11579
|
+
const birthday = question.match(/(\d{1,2}[.\-/]\d{1,2}(?:[.\-/]\d{2,4})?|\d{4}-\d{2}-\d{2})/u)?.[1] || "";
|
|
11580
|
+
const query = cleanupYandexContactActionQuery(question.replace(birthday, " "));
|
|
11581
|
+
if (!birthday || !query) return "Укажите контакт и дату рождения.";
|
|
11582
|
+
const result = await yandexContactsUpdate(query, { birthday, mode: "add-birthday", confirm: true });
|
|
11583
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11584
|
+
}
|
|
11585
|
+
if (/(добав|запиши|сохрани).{0,40}(организац|должност)/iu.test(text)) {
|
|
11586
|
+
const org = question.match(/(?:организац\p{L}*|орг)\s*:?\s*(.*?)(?=\s+должност\p{L}*\s*:|$)/iu)?.[1]?.trim() || "";
|
|
11587
|
+
const title = question.match(/должност\p{L}*\s*:?\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
11588
|
+
const query = cleanupYandexContactActionQuery(question.replace(org, " ").replace(title, " "));
|
|
11589
|
+
if (!query || (!org && !title)) return "Укажите контакт и организацию или должность.";
|
|
11590
|
+
const result = await yandexContactsUpdate(query, { org, title, mode: "add-org", confirm: true });
|
|
11591
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11592
|
+
}
|
|
11593
|
+
if (/(добав|запиши|сохрани).{0,40}(адрес)/iu.test(text)) {
|
|
11594
|
+
const address = question.match(/(?:адрес|по адресу)\s*:?\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
11595
|
+
const query = cleanupYandexContactActionQuery(question.replace(address, " "));
|
|
11596
|
+
if (!address || !query) return "Укажите контакт и адрес.";
|
|
11597
|
+
const result = await yandexContactsUpdate(query, { address, mode: "add-address", confirm: true });
|
|
11598
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11599
|
+
}
|
|
11600
|
+
if (/(добав|запиши|сохрани).{0,40}(заметк|коммент|примеч)/iu.test(text)) {
|
|
11601
|
+
const note = question.match(/(?:заметка|комментарий|примечание)\s*:\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
11602
|
+
const query = cleanupYandexContactActionQuery(question.replace(note, " "));
|
|
11603
|
+
if (!note || !query) return "Укажите контакт и текст заметки.";
|
|
11604
|
+
const result = await yandexContactsUpdate(query, { note, mode: "add-note", confirm: true });
|
|
11605
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11606
|
+
}
|
|
11607
|
+
if (/(создай|сделай).{0,40}(папк).{0,40}(контакт)/iu.test(text)) {
|
|
11608
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
11609
|
+
const result = await yandexContactCreateDiskFolder({ query, confirm: true });
|
|
11610
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11611
|
+
}
|
|
11612
|
+
if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(text)) {
|
|
11613
|
+
const remotePath = extractCloudPath(question);
|
|
11614
|
+
const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
|
|
11615
|
+
const result = await yandexContactSendDiskLinkQr({ contact, path: remotePath, confirm: true });
|
|
11616
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11617
|
+
}
|
|
11618
|
+
if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(text)) {
|
|
11619
|
+
const draft = parseYandexMailSendRequest(question);
|
|
11620
|
+
const result = await yandexContactSendMail({ contact: draft.contactQuery || cleanupYandexContactActionQuery(question), subject: draft.subject, text: draft.text, confirm: true });
|
|
11621
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11622
|
+
}
|
|
11623
|
+
if (/(создай|добавь|запланируй).{0,40}(встреч|событи|календар|телемост)/iu.test(text)) {
|
|
11624
|
+
const dateTime = extractDateTimeFromText(question);
|
|
11625
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
11626
|
+
const result = /телемост/iu.test(text)
|
|
11627
|
+
? await yandexContactCreateTelemostEvent({ query, ...dateTime, confirm: true })
|
|
11628
|
+
: await yandexContactCreateCalendarEvent({ query, ...dateTime, confirm: true });
|
|
11629
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11630
|
+
}
|
|
11631
|
+
if (/(статус|проверь|работает|доступ)/iu.test(text)) {
|
|
11632
|
+
const result = await yandexContactsStatus();
|
|
11633
|
+
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
11634
|
+
}
|
|
11635
|
+
const query = cleanupYandexQuery(question);
|
|
11636
|
+
const rows = /(найди|поиск|покажи|посмотри)/iu.test(text) && query
|
|
11637
|
+
? await yandexContactsSearch(query, { limit: 10 })
|
|
11638
|
+
: await yandexContactsList({ limit: 20 });
|
|
11639
|
+
if (!rows.length) return "Контакты по запросу не найдены.";
|
|
11640
|
+
return ["Яндекс Контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
|
|
11641
|
+
}
|
|
11642
|
+
|
|
11643
|
+
async function buildUserSkillDirectAnswer(question) {
|
|
11644
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
11645
|
+
if (!/(skill|скилл|скил|навык)/iu.test(normalized)) return "";
|
|
11646
|
+
if (/(создай|добавь|сделай|create|new)/iu.test(normalized)) {
|
|
11647
|
+
const name = extractUserSkillNameFromQuestion(question) || "user-skill";
|
|
11648
|
+
const result = await userSkillCreate({
|
|
11649
|
+
name,
|
|
11650
|
+
description: extractUserSkillDescription(question, name),
|
|
11651
|
+
instructions: question,
|
|
11652
|
+
tools: inferUserSkillTools(question),
|
|
11653
|
+
enable: true,
|
|
11654
|
+
confirm: true,
|
|
11655
|
+
});
|
|
11656
|
+
return `Skill создан: ${result.name}\nФайл: ${result.file}\nSkill включен.`;
|
|
11657
|
+
}
|
|
11658
|
+
if (/(выключ|отключ|disable)/iu.test(normalized)) {
|
|
11659
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
11660
|
+
if (!name) return "Какой skill выключить? Укажите имя.";
|
|
11661
|
+
const result = await userSkillSetEnabled(name, false);
|
|
11662
|
+
return `Skill ${result.name}: disabled`;
|
|
11663
|
+
}
|
|
11664
|
+
if (/(включ|enable)/iu.test(normalized)) {
|
|
11665
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
11666
|
+
if (!name) return "Какой skill включить? Укажите имя.";
|
|
11667
|
+
const result = await userSkillSetEnabled(name, true);
|
|
11668
|
+
return `Skill ${result.name}: enabled`;
|
|
11669
|
+
}
|
|
11670
|
+
if (/(удали|удалить|remove|delete)/iu.test(normalized)) {
|
|
11671
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
11672
|
+
if (!name) return "Какой skill удалить? Укажите имя.";
|
|
11673
|
+
const result = await userSkillDelete(name, { confirm: true });
|
|
11674
|
+
return `Skill удален: ${result.name}`;
|
|
11675
|
+
}
|
|
11676
|
+
if (/(список|покажи|какие|list)/iu.test(normalized)) {
|
|
11677
|
+
const skills = listSkills(await loadConfig()).filter((skill) => skill.source === "user");
|
|
11678
|
+
if (!skills.length) return "Пользовательских skills пока нет.";
|
|
11679
|
+
return ["Пользовательские skills:", ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)].join("\n");
|
|
11680
|
+
}
|
|
11681
|
+
return "";
|
|
11682
|
+
}
|
|
11683
|
+
|
|
10403
11684
|
function isYandexServiceQuestion(normalized) {
|
|
10404
11685
|
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
|
|
10405
11686
|
}
|
|
@@ -10485,6 +11766,13 @@ function isYandexMailReadRequest(normalizedQuestion) {
|
|
|
10485
11766
|
|| /(покажи\s+содерж|о чем|о чём|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(normalizedQuestion);
|
|
10486
11767
|
}
|
|
10487
11768
|
|
|
11769
|
+
function isExplicitYandexDiskPathDelete(question) {
|
|
11770
|
+
const text = String(question || "");
|
|
11771
|
+
return /(удали|удалить|перемести\s+в\s+корзин)/iu.test(text)
|
|
11772
|
+
&& /(яндекс.?диск|диск|облак|\/IOLA\/)/iu.test(text)
|
|
11773
|
+
&& Boolean(extractCloudPath(text));
|
|
11774
|
+
}
|
|
11775
|
+
|
|
10488
11776
|
function parseYandexMailReplyRequest(question, previousAssistantText = "") {
|
|
10489
11777
|
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
10490
11778
|
const uid = resolveYandexMailUidFromQuestion(text, previousAssistantText);
|
|
@@ -10542,6 +11830,49 @@ function cleanupYandexContactEmailTarget(question, email) {
|
|
|
10542
11830
|
.trim();
|
|
10543
11831
|
}
|
|
10544
11832
|
|
|
11833
|
+
function parseYandexContactCreateRequest(question) {
|
|
11834
|
+
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
11835
|
+
const email = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
11836
|
+
const phone = text.match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || "";
|
|
11837
|
+
const nameMatch = text.match(/(?:контакт|контакта)\s+["«]?([^"»:,]+)["»]?/iu)
|
|
11838
|
+
|| text.match(/(?:создай|добавь|запиши|сохрани)\s+["«]?([^"»:,]+?)["»]?\s+(?:контакт|email|телефон|номер|почт)/iu);
|
|
11839
|
+
const address = text.match(/(?:адрес|по адресу)\s*:?\s*(.*?)(?=\s+(?:заметка|телефон|email|почта|организация|должность)\s*:|$)/iu)?.[1]?.trim() || "";
|
|
11840
|
+
const note = text.match(/(?:заметка|примечание)\s*:?\s*(.*)$/iu)?.[1]?.trim() || "";
|
|
11841
|
+
const org = text.match(/(?:организация|орг)\s*:?\s*(.*?)(?=\s+(?:заметка|телефон|email|почта|должность)\s*:|$)/iu)?.[1]?.trim() || "";
|
|
11842
|
+
const title = text.match(/(?:должность)\s*:?\s*(.*?)(?=\s+(?:заметка|телефон|email|почта|организация)\s*:|$)/iu)?.[1]?.trim() || "";
|
|
11843
|
+
const rawName = (nameMatch?.[1] || text.replace(email, " ").replace(phone, " "))
|
|
11844
|
+
.replace(/\b(?:email|e-mail|почта|телефон|номер|адрес|заметка|организация|должность)\b[\s\S]*$/iu, "")
|
|
11845
|
+
.trim();
|
|
11846
|
+
return {
|
|
11847
|
+
name: cleanupYandexContactQuery(rawName),
|
|
11848
|
+
email,
|
|
11849
|
+
phone,
|
|
11850
|
+
address,
|
|
11851
|
+
note,
|
|
11852
|
+
org,
|
|
11853
|
+
title,
|
|
11854
|
+
};
|
|
11855
|
+
}
|
|
11856
|
+
|
|
11857
|
+
function cleanupYandexContactActionQuery(question) {
|
|
11858
|
+
const stopWords = new Set([
|
|
11859
|
+
"и", "к", "ко", "с", "со", "у", "для", "из", "на", "в", "во", "сегодня", "завтра", "послезавтра", "час", "часа", "часов", "день", "дня", "рождения", "рождение", "дату", "дата", "диске", "облаке", "контакт", "контакта", "контакту", "контактом", "адресная", "книга", "создай", "сделай",
|
|
11860
|
+
"добавь", "добавить", "запиши", "сохрани", "удали", "удалить", "папку", "папка", "отправь", "пошли",
|
|
11861
|
+
"напиши", "письмо", "сообщение", "ссылку", "ссылка", "qr", "qr-код", "диск", "яндекс", "телемост",
|
|
11862
|
+
"встречу", "встреча", "событие", "календарь", "телефон", "номер", "адрес", "заметка", "заметку", "комментарий",
|
|
11863
|
+
"примечание", "тема", "текст",
|
|
11864
|
+
]);
|
|
11865
|
+
return String(question || "")
|
|
11866
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, " ")
|
|
11867
|
+
.replace(/(?:\+?\d[\d\s()\-]{6,}\d)/gu, " ")
|
|
11868
|
+
.replace(/\/[^\s"»]+/gu, " ")
|
|
11869
|
+
.replace(/[,:;.!?«»"()]+/gu, " ")
|
|
11870
|
+
.split(/\s+/u)
|
|
11871
|
+
.filter((token) => token && !stopWords.has(token.toLocaleLowerCase("ru-RU")))
|
|
11872
|
+
.join(" ")
|
|
11873
|
+
.trim();
|
|
11874
|
+
}
|
|
11875
|
+
|
|
10545
11876
|
function formatYandexMailSummary(row) {
|
|
10546
11877
|
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
10547
11878
|
}
|
|
@@ -10636,6 +11967,64 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10636
11967
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
10637
11968
|
try {
|
|
10638
11969
|
const provider = await getCloudProvider();
|
|
11970
|
+
if (provider === "yandex-disk" && /(мест[оа]|сколько.*занято|сколько.*свобод|инфо|статус)/iu.test(normalized)) {
|
|
11971
|
+
const info = await yandexDiskInfo();
|
|
11972
|
+
return [
|
|
11973
|
+
"Яндекс Диск:",
|
|
11974
|
+
`Занято: ${formatBytes(info.usedSpace)} из ${formatBytes(info.totalSpace)}.`,
|
|
11975
|
+
`Корзина: ${formatBytes(info.trashSize)}.`,
|
|
11976
|
+
].join("\n");
|
|
11977
|
+
}
|
|
11978
|
+
if (provider === "yandex-disk" && /(перенеси|перемести|скопируй|копир).{0,80}(из|с).{0,30}папк/iu.test(normalized) && /(ссылк|qr|qr-код|почт|email|контакт|@)/iu.test(normalized)) {
|
|
11979
|
+
const packageRequest = parseYandexDiskPackageRequest(question);
|
|
11980
|
+
if (!packageRequest.sourcePath || !packageRequest.targetFolder) return "Укажите исходную и целевую папку на Яндекс Диске.";
|
|
11981
|
+
if (!packageRequest.email && !packageRequest.contact) return "Укажите email или контакт, кому отправить ссылку.";
|
|
11982
|
+
const result = await yandexDiskPackageShareEmail({
|
|
11983
|
+
...packageRequest,
|
|
11984
|
+
confirm: true,
|
|
11985
|
+
});
|
|
11986
|
+
return [
|
|
11987
|
+
`${result.mode === "move" ? "Перенес" : "Скопировал"} объектов: ${result.transferred}.`,
|
|
11988
|
+
`Папка: ${result.targetFolder}`,
|
|
11989
|
+
`Отправил: ${result.to.join(", ")}.`,
|
|
11990
|
+
`Ссылка: ${result.publicUrl}`,
|
|
11991
|
+
`QR-код: ${result.qrPublicUrl}`,
|
|
11992
|
+
].join("\n");
|
|
11993
|
+
}
|
|
11994
|
+
if (provider === "yandex-disk" && /(корзин|удаленн|удалённ)/iu.test(normalized) && /(покажи|список|что|есть)/iu.test(normalized)) {
|
|
11995
|
+
const rows = await yandexDiskTrashList("", { limit: 20 });
|
|
11996
|
+
if (!rows.length) return "Корзина Яндекс Диска пуста.";
|
|
11997
|
+
return ["Корзина Яндекс Диска:", ...rows.map((row, index) => `${index + 1}. ${row.type === "dir" ? "папка" : "файл"} ${row.name} — ${row.path}`)].join("\n");
|
|
11998
|
+
}
|
|
11999
|
+
if (provider === "yandex-disk" && /(есть\s+ли|существует|проверь).{0,40}(файл|папк|\/)/iu.test(normalized)) {
|
|
12000
|
+
const remotePath = extractCloudPath(question);
|
|
12001
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12002
|
+
const result = await yandexDiskExists(normalizeCloudUserPath(remotePath, provider));
|
|
12003
|
+
return result.exists ? `На Яндекс Диске найдено: ${result.path} (${result.type}).` : `На Яндекс Диске не найдено: ${result.path}.`;
|
|
12004
|
+
}
|
|
12005
|
+
if (provider === "yandex-disk" && /(свойств|карточк|метадан|размер|информац).{0,40}(файл|папк|\/)/iu.test(normalized)) {
|
|
12006
|
+
const remotePath = extractCloudPath(question);
|
|
12007
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12008
|
+
const result = await yandexDiskStat(normalizeCloudUserPath(remotePath, provider));
|
|
12009
|
+
return [
|
|
12010
|
+
`${result.type === "dir" ? "Папка" : "Файл"}: ${result.name}`,
|
|
12011
|
+
`Путь: ${result.path}`,
|
|
12012
|
+
`Размер: ${formatBytes(result.size)}`,
|
|
12013
|
+
result.modified ? `Изменен: ${result.modified}` : "",
|
|
12014
|
+
result.mimeType ? `Тип: ${result.mimeType}` : "",
|
|
12015
|
+
result.publicUrl ? `Публичная ссылка: ${result.publicUrl}` : "",
|
|
12016
|
+
].filter(Boolean).join("\n");
|
|
12017
|
+
}
|
|
12018
|
+
if (provider === "yandex-disk" && /(очисти|очистить).{0,30}корзин/iu.test(normalized)) {
|
|
12019
|
+
const result = await yandexDiskEmptyTrash({ confirm: true });
|
|
12020
|
+
return `Очистка корзины запрошена: ${result.remote}.`;
|
|
12021
|
+
}
|
|
12022
|
+
if (provider === "yandex-disk" && /(восстанов|верни).{0,40}(корзин|диск|файл|папк)/iu.test(normalized)) {
|
|
12023
|
+
const remotePath = extractCloudPath(question);
|
|
12024
|
+
if (!remotePath) return "Укажите путь объекта в корзине Яндекс Диска.";
|
|
12025
|
+
const result = await yandexDiskRestore(normalizeCloudUserPath(remotePath, provider), { confirm: true });
|
|
12026
|
+
return `Восстановил из корзины: ${result.remote}.`;
|
|
12027
|
+
}
|
|
10639
12028
|
if (/(созда|сдела|добав).{0,30}(папк|директор)/iu.test(normalized) || /(папк|директор).{0,30}(созда|сдела|добав)/iu.test(normalized)) {
|
|
10640
12029
|
const folderName = extractCloudFolderName(question) || "Новая папка";
|
|
10641
12030
|
const remotePath = normalizeCloudUserPath(folderName, provider);
|
|
@@ -10663,17 +12052,101 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10663
12052
|
].join("\n");
|
|
10664
12053
|
}
|
|
10665
12054
|
|
|
12055
|
+
if (provider === "yandex-disk" && /(сними|убери|закрой).{0,30}(ссылк|публик)/iu.test(normalized)) {
|
|
12056
|
+
const remotePath = extractCloudPath(question);
|
|
12057
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12058
|
+
const result = await yandexDiskUnshare(normalizeCloudUserPath(remotePath, provider));
|
|
12059
|
+
return `Публичная ссылка снята: ${result.remote}.`;
|
|
12060
|
+
}
|
|
12061
|
+
|
|
12062
|
+
if (provider === "yandex-disk" && /(отправь|отправить|пошли|перешли).{0,80}(ссылк|qr|qr-код|код)/iu.test(normalized) && /(почт|email|e-mail|контакт|@)/iu.test(normalized)) {
|
|
12063
|
+
const remotePath = extractCloudPath(question);
|
|
12064
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12065
|
+
const recipient = extractShareRecipient(question);
|
|
12066
|
+
if (!recipient.email && !recipient.contact) return "Укажите email или имя контакта, кому отправить ссылку.";
|
|
12067
|
+
const result = await yandexDiskShareEmail({
|
|
12068
|
+
remotePath: normalizeCloudUserPath(remotePath, provider),
|
|
12069
|
+
to: recipient.email,
|
|
12070
|
+
contact: recipient.contact,
|
|
12071
|
+
subject: extractMailSubject(question) || "",
|
|
12072
|
+
text: extractShareMessage(question) || "",
|
|
12073
|
+
confirm: true,
|
|
12074
|
+
});
|
|
12075
|
+
return [
|
|
12076
|
+
`Отправил ссылку на Яндекс Диск: ${result.to.join(", ")}.`,
|
|
12077
|
+
`Ссылка: ${result.publicUrl}`,
|
|
12078
|
+
`QR-код: ${result.qrPublicUrl}`,
|
|
12079
|
+
].join("\n");
|
|
12080
|
+
}
|
|
12081
|
+
|
|
10666
12082
|
if (/(ссылк|поделись|опубликуй)/iu.test(normalized)) {
|
|
10667
12083
|
const remotePath = extractCloudPath(question);
|
|
10668
12084
|
if (!remotePath) return "Укажите путь к файлу на облачном диске, например: /IOLA/reports/report.md";
|
|
10669
|
-
const
|
|
10670
|
-
|
|
12085
|
+
const withQr = provider === "yandex-disk" && /(qr|qr-код|код)/iu.test(normalized);
|
|
12086
|
+
const result = withQr
|
|
12087
|
+
? await yandexDiskShareWithQr(normalizeCloudUserPath(remotePath, provider), { confirm: true })
|
|
12088
|
+
: await cloudShare(provider, normalizeCloudUserPath(remotePath, provider));
|
|
12089
|
+
return withQr
|
|
12090
|
+
? `Публичная ссылка: ${result.publicUrl}\nQR-код: ${result.qrPublicUrl}`
|
|
12091
|
+
: `Публичная ссылка: ${result.publicUrl}`;
|
|
12092
|
+
}
|
|
12093
|
+
|
|
12094
|
+
if (provider === "yandex-disk" && /(прочитай|открой|покажи содержим|текст)/iu.test(normalized) && /(файл|\.txt|\.md|\.json|\.csv)/iu.test(normalized) && !/(сохрани|запиши)/iu.test(normalized)) {
|
|
12095
|
+
const remotePath = extractCloudPath(question);
|
|
12096
|
+
if (!remotePath) return "Укажите путь к текстовому файлу на Яндекс Диске.";
|
|
12097
|
+
const result = await yandexDiskReadText(normalizeCloudUserPath(remotePath, provider));
|
|
12098
|
+
return [`Файл ${result.remote}:`, result.text.slice(0, 4000)].join("\n");
|
|
12099
|
+
}
|
|
12100
|
+
|
|
12101
|
+
if (provider === "yandex-disk" && /(скачай|загрузи\s+с\s+диска|сохрани\s+на\s+комп)/iu.test(normalized)) {
|
|
12102
|
+
const remotePath = extractCloudPath(question);
|
|
12103
|
+
if (!remotePath) return "Укажите путь к файлу на Яндекс Диске.";
|
|
12104
|
+
const outputPath = extractLocalOutputPath(question) || path.basename(remotePath);
|
|
12105
|
+
const result = await yandexDiskDownload(normalizeCloudUserPath(remotePath, provider), outputPath);
|
|
12106
|
+
return `Скачал файл с Яндекс Диска: ${result.local}`;
|
|
12107
|
+
}
|
|
12108
|
+
|
|
12109
|
+
if (provider === "yandex-disk" && /(загрузи|отправь|положи).{0,40}(на яндекс.?диск|на диск|в облак)/iu.test(normalized)) {
|
|
12110
|
+
const localPath = extractLocalInputPath(question);
|
|
12111
|
+
if (!localPath) return "Укажите локальный путь к файлу для загрузки на Яндекс Диск.";
|
|
12112
|
+
const remotePath = extractCloudPath(question) || `${cloudRootForProvider(provider)}/${path.basename(localPath)}`;
|
|
12113
|
+
const result = await yandexDiskUpload(localPath, normalizeCloudUserPath(remotePath, provider), { overwrite: true });
|
|
12114
|
+
return `Загрузил файл на Яндекс Диск: ${result.remote}`;
|
|
12115
|
+
}
|
|
12116
|
+
|
|
12117
|
+
if (provider === "yandex-disk" && /(?:^|\s)(?:переименуй|переименовать|переименовать|rename)(?:\s|$)/iu.test(normalized)) {
|
|
12118
|
+
const remotePath = extractCloudPath(question);
|
|
12119
|
+
const newName = extractCloudNewName(question);
|
|
12120
|
+
if (!remotePath || !newName) return "Укажите путь и новое имя. Пример: переименуй /IOLA/a.txt в b.txt на Яндекс Диске.";
|
|
12121
|
+
const result = await yandexDiskRename(normalizeCloudUserPath(remotePath, provider), newName, { confirm: true, overwrite: true });
|
|
12122
|
+
return `Переименовал на Яндекс Диске: ${result.from} -> ${result.remote}`;
|
|
12123
|
+
}
|
|
12124
|
+
|
|
12125
|
+
if (provider === "yandex-disk" && /(перемести|move)/iu.test(normalized)) {
|
|
12126
|
+
const { from, to } = extractCloudTwoPaths(question);
|
|
12127
|
+
if (!from || !to) return "Укажите откуда и куда переместить на Яндекс Диске.";
|
|
12128
|
+
const result = await yandexDiskMove(normalizeCloudUserPath(from, provider), normalizeCloudUserPath(to, provider), { confirm: true, overwrite: true });
|
|
12129
|
+
return `Переместил на Яндекс Диске: ${result.from} -> ${result.remote}`;
|
|
12130
|
+
}
|
|
12131
|
+
|
|
12132
|
+
if (provider === "yandex-disk" && /(скопируй|копир|copy)/iu.test(normalized)) {
|
|
12133
|
+
const { from, to } = extractCloudTwoPaths(question);
|
|
12134
|
+
if (!from || !to) return "Укажите откуда и куда скопировать на Яндекс Диске.";
|
|
12135
|
+
const result = await yandexDiskCopy(normalizeCloudUserPath(from, provider), normalizeCloudUserPath(to, provider), { confirm: true, overwrite: true });
|
|
12136
|
+
return `Скопировал на Яндекс Диске: ${result.from} -> ${result.remote}`;
|
|
12137
|
+
}
|
|
12138
|
+
|
|
12139
|
+
if (provider === "yandex-disk" && /(удали|удалить|перемести.*корзин)/iu.test(normalized)) {
|
|
12140
|
+
const remotePath = extractCloudPath(question);
|
|
12141
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12142
|
+
const result = await yandexDiskDelete(normalizeCloudUserPath(remotePath, provider), { confirm: true, permanently: /навсегда|окончательно|безвозвратно/iu.test(normalized) });
|
|
12143
|
+
return `Удалил на Яндекс Диске: ${result.remote} (${result.status}).`;
|
|
10671
12144
|
}
|
|
10672
12145
|
|
|
10673
12146
|
if (/(сохрани|запиши).{0,40}(на яндекс диске|в облак|на диск)/iu.test(normalized)) {
|
|
10674
12147
|
const text = cleanupCloudSaveText(question);
|
|
10675
12148
|
if (!text) return "Что сохранить на облачный диск?";
|
|
10676
|
-
const remotePath = `${cloudRootForProvider(provider)}/notes/iola-${timestampForFile()}.txt`;
|
|
12149
|
+
const remotePath = extractCloudPath(question) || `${cloudRootForProvider(provider)}/notes/iola-${timestampForFile()}.txt`;
|
|
10677
12150
|
const tempPath = path.join(CONFIG_DIR, `cloud-save-${Date.now()}.txt`);
|
|
10678
12151
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
10679
12152
|
await writeFile(tempPath, text, "utf8");
|
|
@@ -10691,7 +12164,7 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10691
12164
|
}
|
|
10692
12165
|
|
|
10693
12166
|
function isCloudQuestion(question) {
|
|
10694
|
-
return /(
|
|
12167
|
+
return /(\/IOLA\/|яндекс.?диск|yandex.?disk|облак|облачн|на диск|с диска|в диск|cloud|mail\.?ru|публичн.*ссылк|поделиться.*файл|qr-код|qr\s+код)/iu.test(String(question || ""));
|
|
10695
12168
|
}
|
|
10696
12169
|
|
|
10697
12170
|
function normalizeCloudUserPath(value, provider = "yandex-disk") {
|
|
@@ -10714,16 +12187,112 @@ function extractCloudFolderName(question) {
|
|
|
10714
12187
|
|
|
10715
12188
|
function extractCloudPath(question) {
|
|
10716
12189
|
const text = String(question || "").trim();
|
|
10717
|
-
const pathMatch = text.match(/(?:^|\s)(\/IOLA\/[^\s]+|\/[^\s]+)/iu);
|
|
10718
|
-
if (pathMatch?.[1]) return pathMatch[1];
|
|
10719
12190
|
const quoted = text.match(/["«]([^"»]+)["»]/u);
|
|
10720
|
-
if (quoted?.[1]) return quoted[1];
|
|
12191
|
+
if (quoted?.[1]) return cleanupCloudPathCandidate(quoted[1]);
|
|
12192
|
+
const iolaPath = text.match(/(?:^|\s)(\/IOLA\/.+)$/iu)?.[1];
|
|
12193
|
+
if (iolaPath) return cleanupCloudPathCandidate(iolaPath);
|
|
12194
|
+
const pathMatch = text.match(/(?:^|\s)(\/IOLA\/[^\s]+|\/[^\s]+)/iu);
|
|
12195
|
+
if (pathMatch?.[1]) return cleanupCloudPathCandidate(pathMatch[1]);
|
|
10721
12196
|
const afterFolder = text.match(/(?:папк[аеуы]?|файл[ае]?)\s+([^,.!?]+)/iu);
|
|
10722
12197
|
return afterFolder?.[1] ? cleanupCloudObjectName(afterFolder[1]) : "";
|
|
10723
12198
|
}
|
|
10724
12199
|
|
|
12200
|
+
function cleanupCloudPathCandidate(value) {
|
|
12201
|
+
return String(value || "")
|
|
12202
|
+
.replace(/\s+(?:по\s+почт[еуы]|на\s+почт[уые]|контакту|получател[юя]|кому|с\s+темой|тема\s*:|текст\s*:).*$/iu, "")
|
|
12203
|
+
.replace(/[.!?]+$/u, "")
|
|
12204
|
+
.trim();
|
|
12205
|
+
}
|
|
12206
|
+
|
|
12207
|
+
function extractCloudTwoPaths(question) {
|
|
12208
|
+
const text = String(question || "").trim();
|
|
12209
|
+
const quoted = [...text.matchAll(/["«]([^"»]+)["»]/gu)].map((match) => match[1]).filter(Boolean);
|
|
12210
|
+
if (quoted.length >= 2) return { from: quoted[0], to: quoted[1] };
|
|
12211
|
+
const paths = [...text.matchAll(/(?:^|\s)(\/[^\s,;]+)/gu)].map((match) => match[1]).filter(Boolean);
|
|
12212
|
+
if (paths.length >= 2) return { from: paths[0], to: paths[1] };
|
|
12213
|
+
const match = text.match(/(?:из|с|откуда)\s+(.+?)\s+(?:в|на|куда)\s+(.+?)(?:\s+на\s+яндекс|\s+на\s+диск|\s+в\s+облак|$)/iu)
|
|
12214
|
+
|| text.match(/(?:перемести|скопируй|копируй|copy|move)\s+(.+?)\s+(?:в|на|куда)\s+(.+?)(?:\s+на\s+яндекс|\s+на\s+диск|\s+в\s+облак|$)/iu);
|
|
12215
|
+
return match ? { from: cleanupCloudObjectName(match[1]), to: cleanupCloudObjectName(match[2]) } : { from: "", to: "" };
|
|
12216
|
+
}
|
|
12217
|
+
|
|
12218
|
+
function extractCloudNewName(question) {
|
|
12219
|
+
const text = String(question || "").trim();
|
|
12220
|
+
return text.match(/(?:в|на|как)\s+["«]?([^"».,!?/\\]+(?:\.[a-z0-9а-яё]+)?)["»]?\s*(?:на\s+яндекс|на\s+диск|в\s+облак|$)/iu)?.[1]?.trim()
|
|
12221
|
+
|| text.match(/(?:нов(?:ое|ый|ым)?\s+им(?:я|енем)|названи(?:е|ем))\s+["«]?([^"».,!?/\\]+)["»]?/iu)?.[1]?.trim()
|
|
12222
|
+
|| "";
|
|
12223
|
+
}
|
|
12224
|
+
|
|
12225
|
+
function extractLocalInputPath(question) {
|
|
12226
|
+
const text = String(question || "").trim();
|
|
12227
|
+
const quoted = [...text.matchAll(/["«]([^"»]+)["»]/gu)].map((match) => match[1]).find((item) => /^[a-z]:[\\/]|\.{0,2}[\\/]/iu.test(item));
|
|
12228
|
+
if (quoted) return quoted;
|
|
12229
|
+
return text.match(/([a-z]:[\\/][^"»\s]+|\.\.?[\\/][^"»\s]+)/iu)?.[1] || "";
|
|
12230
|
+
}
|
|
12231
|
+
|
|
12232
|
+
function extractLocalOutputPath(question) {
|
|
12233
|
+
const text = String(question || "").trim();
|
|
12234
|
+
return text.match(/(?:в|на|как|куда)\s+([a-z]:[\\/][^"»\s]+|\.\.?[\\/][^"»\s]+)/iu)?.[1] || "";
|
|
12235
|
+
}
|
|
12236
|
+
|
|
12237
|
+
function extractShareRecipient(question) {
|
|
12238
|
+
const text = String(question || "");
|
|
12239
|
+
const email = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
12240
|
+
if (email) return { email, contact: "" };
|
|
12241
|
+
const contact = text.match(/(?:контакт[ау]?|получател[юя]|кому|для|почт[уе])\s+["«]?([^"».,;!?@]+)["»]?/iu)?.[1]
|
|
12242
|
+
|| text.match(/(?:отправь|отправить|пошли|перешли)\s+([^"».,;!?@]+?)\s+(?:ссылк|qr|qr-код|код)/iu)?.[1]
|
|
12243
|
+
|| "";
|
|
12244
|
+
return { email: "", contact: cleanupYandexContactQuery(contact) };
|
|
12245
|
+
}
|
|
12246
|
+
|
|
12247
|
+
function extractMailSubject(question) {
|
|
12248
|
+
return String(question || "").match(/(?:тема|subject)\s*:\s*(.*?)(?=\s+(?:текст|сообщение|body)\s*:|$)/iu)?.[1]?.trim() || "";
|
|
12249
|
+
}
|
|
12250
|
+
|
|
12251
|
+
function extractShareMessage(question) {
|
|
12252
|
+
return String(question || "").match(/(?:текст|сообщение|body)\s*:\s*(.*)$/iu)?.[1]?.trim() || "";
|
|
12253
|
+
}
|
|
12254
|
+
|
|
12255
|
+
function parseYandexDiskPackageRequest(question) {
|
|
12256
|
+
const text = String(question || "").trim();
|
|
12257
|
+
const quoted = [...text.matchAll(/["«]([^"»]+)["»]/gu)].map((match) => match[1]);
|
|
12258
|
+
const paths = [...text.matchAll(/(?:^|\s)(\/[^\s,;]+)/gu)].map((match) => match[1]);
|
|
12259
|
+
const sourcePath = text.match(/(?:из|с)\s+папк[иы]?\s+([^,;]+?)(?=\s+(?:создай|сделай|и|отправь|перешли|на\s+яндекс)|[,;]|$)/iu)?.[1]?.trim()
|
|
12260
|
+
|| quoted[1]
|
|
12261
|
+
|| paths[1]
|
|
12262
|
+
|| "";
|
|
12263
|
+
const targetFolder = text.match(/(?:создай|сделай)\s+папк[уи]?\s+([^,;]+?)(?=\s+(?:на\s+яндекс|и|,|$)|[,;]|$)/iu)?.[1]?.trim()
|
|
12264
|
+
|| text.match(/(?:в|куда|целев\w*)\s+папк[уи]?\s+([^,;]+?)(?=\s+(?:на\s+яндекс|и|,|$))/iu)?.[1]?.trim()
|
|
12265
|
+
|| quoted[0]
|
|
12266
|
+
|| paths[0]
|
|
12267
|
+
|| "";
|
|
12268
|
+
const recipient = extractShareRecipient(question);
|
|
12269
|
+
return {
|
|
12270
|
+
sourcePath: normalizeCloudUserPath(cleanupCloudObjectName(sourcePath), "yandex-disk"),
|
|
12271
|
+
targetFolder: normalizeCloudUserPath(cleanupCloudObjectName(targetFolder), "yandex-disk"),
|
|
12272
|
+
mode: /(перенеси|перемести|move)/iu.test(text) ? "move" : "copy",
|
|
12273
|
+
email: recipient.email,
|
|
12274
|
+
contact: recipient.contact,
|
|
12275
|
+
subject: extractMailSubject(question),
|
|
12276
|
+
text: extractShareMessage(question),
|
|
12277
|
+
};
|
|
12278
|
+
}
|
|
12279
|
+
|
|
12280
|
+
function formatBytes(value) {
|
|
12281
|
+
const bytes = Number(value || 0);
|
|
12282
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 Б";
|
|
12283
|
+
const units = ["Б", "КБ", "МБ", "ГБ", "ТБ"];
|
|
12284
|
+
let size = bytes;
|
|
12285
|
+
let index = 0;
|
|
12286
|
+
while (size >= 1024 && index < units.length - 1) {
|
|
12287
|
+
size /= 1024;
|
|
12288
|
+
index += 1;
|
|
12289
|
+
}
|
|
12290
|
+
return `${size.toFixed(size >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
|
|
12291
|
+
}
|
|
12292
|
+
|
|
10725
12293
|
function cleanupCloudObjectName(value) {
|
|
10726
12294
|
return String(value || "")
|
|
12295
|
+
.replace(/\s+(?:на\s+яндекс.?диск(?:е)?|на\s+диск(?:е)?|в\s+облак(?:е|о)?).*/iu, " ")
|
|
10727
12296
|
.replace(/\b(?:на|в|у меня|яндекс.?диск(?:е)?|диск(?:е)?|облак(?:е|о)?|создай|сделай|добавь|покажи)\b/giu, " ")
|
|
10728
12297
|
.replace(/\s+/g, " ")
|
|
10729
12298
|
.trim();
|
|
@@ -10731,7 +12300,8 @@ function cleanupCloudObjectName(value) {
|
|
|
10731
12300
|
|
|
10732
12301
|
function cleanupCloudQuery(question) {
|
|
10733
12302
|
return String(question || "")
|
|
10734
|
-
.replace(
|
|
12303
|
+
.replace(/яндекс\s+диск(?:е)?/giu, " ")
|
|
12304
|
+
.replace(/(?:найди|поиск|где лежит|на|в|яндекс|облак(?:е|о)?|диск(?:е)?|файл|документ)/giu, " ")
|
|
10735
12305
|
.replace(/[?.!]+$/u, "")
|
|
10736
12306
|
.replace(/\s+/g, " ")
|
|
10737
12307
|
.trim();
|
|
@@ -10740,6 +12310,7 @@ function cleanupCloudQuery(question) {
|
|
|
10740
12310
|
function cleanupCloudSaveText(question) {
|
|
10741
12311
|
return String(question || "")
|
|
10742
12312
|
.replace(/^.*?(?:сохрани|запиши)\s+/iu, "")
|
|
12313
|
+
.replace(/\s+(?:в|на|как)\s+\/[^\s]+/giu, " ")
|
|
10743
12314
|
.replace(/\s+(?:на яндекс диске|в облак[ео]|на диск).*$/iu, "")
|
|
10744
12315
|
.trim();
|
|
10745
12316
|
}
|
|
@@ -11027,16 +12598,28 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
11027
12598
|
if (!options.quiet) console.log(casualAnswer);
|
|
11028
12599
|
return casualAnswer;
|
|
11029
12600
|
}
|
|
11030
|
-
const
|
|
11031
|
-
if (
|
|
11032
|
-
if (!options.quiet) console.log(
|
|
11033
|
-
return
|
|
12601
|
+
const userSkillAnswer = await buildUserSkillDirectAnswer(question);
|
|
12602
|
+
if (userSkillAnswer) {
|
|
12603
|
+
if (!options.quiet) console.log(userSkillAnswer);
|
|
12604
|
+
return userSkillAnswer;
|
|
12605
|
+
}
|
|
12606
|
+
if (/(контакт|адресн)/iu.test(question) && !isExplicitYandexDiskPathDelete(question)) {
|
|
12607
|
+
const yandexContactAnswer = await buildYandexDirectAnswer(question, []);
|
|
12608
|
+
if (yandexContactAnswer) {
|
|
12609
|
+
if (!options.quiet) console.log(yandexContactAnswer);
|
|
12610
|
+
return yandexContactAnswer;
|
|
12611
|
+
}
|
|
11034
12612
|
}
|
|
11035
12613
|
const cloudAnswer = await buildCloudDirectAnswer(question);
|
|
11036
12614
|
if (cloudAnswer) {
|
|
11037
12615
|
if (!options.quiet) console.log(cloudAnswer);
|
|
11038
12616
|
return cloudAnswer;
|
|
11039
12617
|
}
|
|
12618
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, []);
|
|
12619
|
+
if (yandexAnswer) {
|
|
12620
|
+
if (!options.quiet) console.log(yandexAnswer);
|
|
12621
|
+
return yandexAnswer;
|
|
12622
|
+
}
|
|
11040
12623
|
const geoAnswer = await buildGeoDirectAnswer(question);
|
|
11041
12624
|
if (geoAnswer) {
|
|
11042
12625
|
if (!options.quiet) console.log(geoAnswer);
|
|
@@ -11131,6 +12714,9 @@ function isUnsupportedPublicEntityQuestion(normalized) {
|
|
|
11131
12714
|
|
|
11132
12715
|
function buildCasualDirectAnswer(question) {
|
|
11133
12716
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU").trim();
|
|
12717
|
+
if (isCurrentDateTimeQuestion(normalized)) {
|
|
12718
|
+
return formatCurrentDateTimeAnswer(normalized);
|
|
12719
|
+
}
|
|
11134
12720
|
if (/^(кто ты|что ты|какая ты модель|что ты за модель|что за модель|какая модель|назови модель|ты какая модель|ты кто)([?.!\s]*)$/iu.test(normalized)) {
|
|
11135
12721
|
return "Я IOLA, первая городская модель искусственного интеллекта Йошкар-Олы. Работаю локально в CLI и отвечаю по открытым городским данным через проверяемые слои и API.";
|
|
11136
12722
|
}
|
|
@@ -11240,8 +12826,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
11240
12826
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
11241
12827
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
11242
12828
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
11243
|
-
"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_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_calendar_list {start,end}, yandex_contacts_search {query}.",
|
|
11244
|
-
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_delete, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_contacts_create, yandex_contacts_add_email, yandex_calendar_create_event, yandex_telemost_create_event.",
|
|
12829
|
+
"Yandex tools: yandex_identity_me {}, yandex_disk_info {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_stat {path}, yandex_disk_exists {path}, yandex_disk_read_text {path}, yandex_disk_save_text {path,text}, yandex_disk_upload {localPath,remotePath}, yandex_disk_download {remotePath,outputPath}, yandex_disk_move {from,to,confirm}, yandex_disk_copy {from,to,confirm}, yandex_disk_rename {path,name,confirm}, yandex_disk_share {path,confirm}, yandex_disk_share_qr {path,confirm}, yandex_disk_share_email {path,to,contact,subject,text,confirm}, yandex_disk_package_share_email {sourcePath,targetFolder,to,contact,mode,confirm}, yandex_disk_unshare {path}, yandex_disk_delete {path,confirm}, yandex_disk_trash_list {}, yandex_disk_restore {path,confirm}, yandex_disk_empty_trash {confirm}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_send {to,subject,text,confirm}, yandex_mail_reply {uid,text,confirm}, yandex_mail_forward {uid,to,confirm}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_calendar_list {start,end}, yandex_calendar_create_event {title,start,end,location,attendees,confirm}, yandex_contacts_list {limit}, yandex_contacts_search {query}, yandex_contacts_get {query}, yandex_contacts_create {name,email,phone,address,note,confirm}, yandex_contacts_update {query,email,phone,address,note,birthday,org,title,confirm}, yandex_contacts_delete {query,confirm}, yandex_contacts_export_csv {}, yandex_contacts_find_incomplete {}, yandex_contacts_find_duplicates {}, yandex_contacts_backup_to_disk {format,confirm}, yandex_contact_send_mail {contact,subject,text,confirm}, yandex_contact_send_disk_link_qr {contact,path,confirm}, yandex_contact_create_disk_folder {contact,confirm}, yandex_contact_create_calendar_event {contact,start,end,title,confirm}, yandex_contact_create_telemost_event {contact,start,end,title,confirm}.",
|
|
12830
|
+
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_share_qr, yandex_disk_share_email, yandex_disk_package_share_email, yandex_disk_delete, yandex_disk_move, yandex_disk_copy, yandex_disk_rename, yandex_disk_restore, yandex_disk_empty_trash, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_contacts_create, yandex_contacts_update, yandex_contacts_delete, yandex_contacts_add_email, yandex_contacts_add_phone, yandex_contacts_add_address, yandex_contacts_backup_to_disk, yandex_contact_send_mail, yandex_contact_send_disk_link_qr, yandex_contact_create_disk_folder, yandex_contact_create_calendar_event, yandex_contact_create_telemost_event, yandex_calendar_create_event, yandex_telemost_create_event.",
|
|
12831
|
+
"User skill tools: user_skill_create {name,description,instructions,tools,enable,confirm}, user_skill_enable {name}, user_skill_disable {name}, user_skill_delete {name,confirm}, user_skill_list {}. Создавай skill только по явной просьбе пользователя и с confirm=true.",
|
|
11245
12832
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
11246
12833
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
11247
12834
|
`Вопрос: ${question}`,
|
|
@@ -11299,14 +12886,48 @@ function parseJsonObject(text) {
|
|
|
11299
12886
|
|
|
11300
12887
|
function inferToolPlan(question, options = {}) {
|
|
11301
12888
|
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
12889
|
+
if (isCurrentDateTimeQuestion(normalized)) {
|
|
12890
|
+
return { steps: [{ tool: "get_current_date", args: {} }] };
|
|
12891
|
+
}
|
|
12892
|
+
if (/(создай|добавь|сделай).{0,40}(skill|скилл|скил|навык)/iu.test(normalized)) {
|
|
12893
|
+
const name = normalizeUserSkillName(
|
|
12894
|
+
question.match(/(?:skill|скилл|скил|навык)\s+["«]?([^"».,:;\n]+)["»]?/iu)?.[1]
|
|
12895
|
+
|| question.match(/(?:создай|добавь|сделай)\s+["«]?([^"».,:;\n]+?)["»]?\s+(?:skill|скилл|скил|навык)/iu)?.[1]
|
|
12896
|
+
|| "user-skill"
|
|
12897
|
+
);
|
|
12898
|
+
return {
|
|
12899
|
+
steps: [{
|
|
12900
|
+
tool: "user_skill_create",
|
|
12901
|
+
args: {
|
|
12902
|
+
name,
|
|
12903
|
+
description: `Пользовательский skill: ${name}`,
|
|
12904
|
+
instructions: question,
|
|
12905
|
+
enable: true,
|
|
12906
|
+
confirm: true,
|
|
12907
|
+
},
|
|
12908
|
+
}],
|
|
12909
|
+
};
|
|
12910
|
+
}
|
|
11302
12911
|
if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
|
|
11303
12912
|
return { steps: [{ tool: "yandex_identity_me", args: {} }] };
|
|
11304
12913
|
}
|
|
11305
12914
|
if (/(яндекс|диск|облак)/iu.test(normalized)) {
|
|
12915
|
+
const diskPath = extractCloudPath(question) || CLOUD_DEFAULT_REMOTE_DIR;
|
|
12916
|
+
if (/(мест[оа]|сколько.*занято|сколько.*свобод|статус|инфо)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_info", args: {} }] };
|
|
12917
|
+
if (/(корзин|удаленн|удалённ)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_trash_list", args: { limit: 20 } }] };
|
|
11306
12918
|
if (/(создай|сделай).{0,30}папк/iu.test(normalized)) {
|
|
11307
12919
|
const folder = question.match(/папк[ауи]?\s+["«]?([^"»\n]+)["»]?/iu)?.[1]?.trim() || "Новая папка";
|
|
11308
12920
|
return { steps: [{ tool: "yandex_disk_mkdir", args: { path: `${CLOUD_DEFAULT_REMOTE_DIR}/${folder}` } }] };
|
|
11309
12921
|
}
|
|
12922
|
+
if (/(прочитай|открой|содержим|текст)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_read_text", args: { path: diskPath } }] };
|
|
12923
|
+
if (/(скачай|download)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_download", args: { remotePath: diskPath, outputPath: path.basename(diskPath) } }] };
|
|
12924
|
+
if (/(ссылк|поделись|опубликуй)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_share", args: { path: diskPath, confirm: true } }] };
|
|
12925
|
+
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_delete", args: { path: diskPath, confirm: true } }] };
|
|
12926
|
+
if (/(переимен|rename)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_rename", args: { path: diskPath, name: extractCloudNewName(question), confirm: true } }] };
|
|
12927
|
+
if (/(перемести|move|скопируй|копир|copy)/iu.test(normalized)) {
|
|
12928
|
+
const { from, to } = extractCloudTwoPaths(question);
|
|
12929
|
+
return { steps: [{ tool: /(скопируй|копир|copy)/iu.test(normalized) ? "yandex_disk_copy" : "yandex_disk_move", args: { from, to, confirm: true, overwrite: true } }] };
|
|
12930
|
+
}
|
|
11310
12931
|
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_find", args: { query: question, path: CLOUD_DEFAULT_REMOTE_DIR, limit: 20 } }] };
|
|
11311
12932
|
return { steps: [{ tool: "yandex_disk_ls", args: { path: CLOUD_DEFAULT_REMOTE_DIR } }] };
|
|
11312
12933
|
}
|
|
@@ -11325,6 +12946,18 @@ function inferToolPlan(question, options = {}) {
|
|
|
11325
12946
|
return { steps: [{ tool: normalized.includes("телемост") ? "yandex_telemost_create_event" : "yandex_calendar_list", args: { limit: 20 } }] };
|
|
11326
12947
|
}
|
|
11327
12948
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
12949
|
+
if (/(дубликат|повтор)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_find_duplicates", args: { limit: 20 } }] };
|
|
12950
|
+
if (/(неполн|без\s+email|без\s+почт|без\s+телефон|без\s+адрес)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_find_incomplete", args: { limit: 30 } }] };
|
|
12951
|
+
if (/(экспорт|выгруз)/iu.test(normalized)) return { steps: [{ tool: /csv/iu.test(normalized) ? "yandex_contacts_export_csv" : "yandex_contacts_export_vcard", args: {} }] };
|
|
12952
|
+
if (/(резерв|backup|бэкап|диск|яндекс.?диск)/iu.test(normalized) && /(контакт)/iu.test(normalized) && /(сохрани|экспорт|выгруз|резерв|backup|бэкап)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_backup_to_disk", args: { format: /csv/iu.test(normalized) ? "csv" : "vcard", confirm: true } }] };
|
|
12953
|
+
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_create", args: { ...parseYandexContactCreateRequest(question), confirm: true } }] };
|
|
12954
|
+
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_delete", args: { query: cleanupYandexContactActionQuery(question), confirm: true } }] };
|
|
12955
|
+
if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(normalized)) return { steps: [{ tool: "yandex_contact_send_disk_link_qr", args: { contact: cleanupYandexContactActionQuery(question), path: extractCloudPath(question), confirm: true } }] };
|
|
12956
|
+
if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
|
|
12957
|
+
const draft = parseYandexMailSendRequest(question);
|
|
12958
|
+
return { steps: [{ tool: "yandex_contact_send_mail", args: { contact: draft.contactQuery || cleanupYandexContactActionQuery(question), subject: draft.subject, text: draft.text, confirm: true } }] };
|
|
12959
|
+
}
|
|
12960
|
+
if (/(создай|добавь|запланируй).{0,40}(встреч|событи|календар|телемост)/iu.test(normalized)) return { steps: [{ tool: /телемост/iu.test(normalized) ? "yandex_contact_create_telemost_event" : "yandex_contact_create_calendar_event", args: { contact: cleanupYandexContactActionQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
|
|
11328
12961
|
return { steps: [{ tool: "yandex_contacts_search", args: { query: question, limit: 20 } }] };
|
|
11329
12962
|
}
|
|
11330
12963
|
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
@@ -11680,7 +13313,7 @@ function formatToolExecutionError(error, plan) {
|
|
|
11680
13313
|
}
|
|
11681
13314
|
|
|
11682
13315
|
function availableToolNames(options = {}) {
|
|
11683
|
-
const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS]);
|
|
13316
|
+
const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS]);
|
|
11684
13317
|
if (options.files) {
|
|
11685
13318
|
for (const tool of FILE_TOOLS) names.add(tool);
|
|
11686
13319
|
}
|
|
@@ -11749,6 +13382,10 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
11749
13382
|
const result = await executeYandexTool(step.tool, step.args || {});
|
|
11750
13383
|
current = Array.isArray(result) ? result : [result];
|
|
11751
13384
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
13385
|
+
} else if (USER_SKILL_TOOLS.includes(step.tool)) {
|
|
13386
|
+
const result = await executeUserSkillTool(step.tool, step.args || {});
|
|
13387
|
+
current = Array.isArray(result) ? result : [result];
|
|
13388
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
11752
13389
|
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
11753
13390
|
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
11754
13391
|
current = Array.isArray(result) ? result : [result];
|
|
@@ -11789,14 +13426,34 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
11789
13426
|
|
|
11790
13427
|
function getCurrentDateInfo() {
|
|
11791
13428
|
const now = new Date();
|
|
13429
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
|
|
11792
13430
|
return {
|
|
11793
13431
|
name: "текущая дата",
|
|
11794
13432
|
date: new Intl.DateTimeFormat("ru-RU", { dateStyle: "long" }).format(now),
|
|
11795
13433
|
time: new Intl.DateTimeFormat("ru-RU", { timeStyle: "short" }).format(now),
|
|
13434
|
+
weekday: new Intl.DateTimeFormat("ru-RU", { weekday: "long" }).format(now),
|
|
13435
|
+
timezone: timeZone,
|
|
11796
13436
|
iso: now.toISOString(),
|
|
11797
13437
|
};
|
|
11798
13438
|
}
|
|
11799
13439
|
|
|
13440
|
+
function isCurrentDateTimeQuestion(normalized) {
|
|
13441
|
+
const text = String(normalized || "");
|
|
13442
|
+
return /^(?:какая|какой|какое|скажи|подскажи|что)\s+(?:сегодня\s+)?(?:дата|день|число|время|день недели)|^(?:сегодня|сейчас)\??$/iu.test(text)
|
|
13443
|
+
|| /(?:какая\s+сегодня\s+дата|какой\s+сегодня\s+день|который\s+час|сколько\s+времени|текущее\s+время|текущая\s+дата|дата\s+сегодня)/iu.test(text);
|
|
13444
|
+
}
|
|
13445
|
+
|
|
13446
|
+
function formatCurrentDateTimeAnswer(normalized) {
|
|
13447
|
+
const info = getCurrentDateInfo();
|
|
13448
|
+
if (/(время|час|сейчас|сколько)/iu.test(normalized) && !/(дата|день|число)/iu.test(normalized)) {
|
|
13449
|
+
return `Сейчас ${info.time}. Часовой пояс: ${info.timezone}.`;
|
|
13450
|
+
}
|
|
13451
|
+
if (/(день недели)/iu.test(normalized) && !/(дата|число)/iu.test(normalized)) {
|
|
13452
|
+
return `Сегодня ${info.weekday}.`;
|
|
13453
|
+
}
|
|
13454
|
+
return `Сегодня ${info.date}, ${info.weekday}. Время: ${info.time}. Часовой пояс: ${info.timezone}.`;
|
|
13455
|
+
}
|
|
13456
|
+
|
|
11800
13457
|
function getLocalMcpToolNames() {
|
|
11801
13458
|
return mcpTools().map((tool) => `mcp:iola-local:${tool.name}`);
|
|
11802
13459
|
}
|
|
@@ -11880,23 +13537,62 @@ function formatToolResult(result, options) {
|
|
|
11880
13537
|
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
11881
13538
|
}
|
|
11882
13539
|
if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
|
|
13540
|
+
if (row.status === "contact-mail-sent") return `Письмо контакту отправлено: ${row.contact}. Тема: ${row.subject || "-"}`;
|
|
13541
|
+
if (row.status === "contact-disk-link-sent") return `Отправил контакту ${row.contact} ссылку на Яндекс Диск.\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
13542
|
+
if (row.status === "contact-folder-created") return `Папка контакта создана: ${row.remote}\nКарточка: ${row.cardPath}`;
|
|
13543
|
+
if (row.status === "contact-calendar-event-created" || row.status === "contact-telemost-event-created") return `${row.status === "contact-telemost-event-created" ? "Телемост" : "Встреча"} создана: ${row.title || row.uid}. Участник: ${row.attendee || "-"}`;
|
|
13544
|
+
if (row.status === "contacts-backup") return `Контакты сохранены на Яндекс Диск: ${row.remote}. Записей: ${row.rows}.`;
|
|
13545
|
+
if (row.status === "contacts-imported") return `Контакты импортированы из ${row.input}. Создано: ${row.created}, пропущено: ${row.skipped}.`;
|
|
13546
|
+
if (row.status === "birthday-events-created") return `События дней рождения созданы: ${row.created}. Контактов с днем рождения: ${row.totalWithBirthday}.`;
|
|
11883
13547
|
if (row.provider === "yandex-disk" && row.publicUrl) return `Публичная ссылка: ${row.publicUrl}`;
|
|
13548
|
+
if (row.provider === "yandex-disk" && row.status === "shared-with-qr") return `Публичная ссылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
13549
|
+
if (row.provider === "yandex-disk" && row.status === "shared-and-sent") return `Отправил ссылку на Яндекс Диск: ${Array.isArray(row.to) ? row.to.join(", ") : row.to}\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
13550
|
+
if (row.provider === "yandex-disk" && row.status === "package-shared-and-sent") return `${row.mode === "move" ? "Перенес" : "Скопировал"} объектов: ${row.transferred}\nПапка: ${row.targetFolder}\nОтправил: ${Array.isArray(row.to) ? row.to.join(", ") : row.to}\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
13551
|
+
if (row.provider === "yandex-disk" && typeof row.exists === "boolean") return `Яндекс Диск: ${row.path || row.remote} ${row.exists ? "найден" : "не найден"}`;
|
|
13552
|
+
if (row.provider === "yandex-disk" && (row.status === "moved" || row.status === "copied" || row.status === "restored")) return `Яндекс Диск: ${row.status} ${row.from ? `${row.from} -> ` : ""}${row.remote}`;
|
|
13553
|
+
if (row.provider === "yandex-disk" && row.status === "unpublished") return `Яндекс Диск: публичная ссылка снята ${row.remote}`;
|
|
13554
|
+
if (row.provider === "yandex-disk" && row.status === "trash-empty-requested") return `Яндекс Диск: очистка корзины запрошена`;
|
|
13555
|
+
if (row.provider === "yandex-disk" && row.text) return `Яндекс Диск: ${row.remote}\n${String(row.text).slice(0, 2000)}`;
|
|
11884
13556
|
if (row.provider === "yandex-disk" && row.remote) return `Яндекс Диск: ${row.status || "ok"} ${row.remote}`;
|
|
11885
13557
|
if (row.uid && (row.subject || row.from)) return `Письмо #${row.uid}: ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}`;
|
|
11886
13558
|
if (row.status === "moved-to-trash") return `Письмо #${row.uid} перемещено в корзину: ${row.to}`;
|
|
11887
13559
|
if (row.status === "seen" || row.status === "unseen") return `Письмо #${row.uid}: ${row.status === "seen" ? "прочитано" : "непрочитано"}`;
|
|
11888
13560
|
if (row.status === "sent" && row.to) return `Письмо отправлено: ${Array.isArray(row.to) ? row.to.join(", ") : row.to}. Тема: ${row.subject || "-"}`;
|
|
13561
|
+
if (row.status === "contact-mail-sent") return `Письмо контакту отправлено: ${row.contact}. Тема: ${row.subject || "-"}`;
|
|
13562
|
+
if (row.status === "contact-disk-link-sent") return `Отправил контакту ${row.contact} ссылку на Яндекс Диск.\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
13563
|
+
if (row.status === "contact-folder-created") return `Папка контакта создана: ${row.remote}\nКарточка: ${row.cardPath}`;
|
|
13564
|
+
if (row.status === "contact-calendar-event-created" || row.status === "contact-telemost-event-created") return `${row.status === "contact-telemost-event-created" ? "Телемост" : "Встреча"} создана: ${row.title || row.uid}. Участник: ${row.attendee || "-"}`;
|
|
13565
|
+
if (row.status === "contacts-backup") return `Контакты сохранены на Яндекс Диск: ${row.remote}. Записей: ${row.rows}.`;
|
|
13566
|
+
if (row.status === "exported" && row.output) return `Контакты экспортированы: ${row.output}. Записей: ${row.rows}.`;
|
|
13567
|
+
if (row.status === "deleted" && (row.email || row.phone || row.name)) return `Контакт удален: ${formatYandexContact(row)}`;
|
|
13568
|
+
if (row.status === "updated" && (row.email || row.phone || row.name)) return `Контакт обновлен: ${formatYandexContact(row)}`;
|
|
13569
|
+
if (row.status === "created" && (row.email || row.phone || row.name)) return `Контакт создан: ${formatYandexContact(row)}`;
|
|
13570
|
+
if (row.status === "no-email" && row.contact) return `У контакта нет email: ${formatYandexContact(row.contact)}`;
|
|
13571
|
+
if (row.status === "ambiguous" && row.contacts) return [`Нашел несколько контактов. Уточните:`, ...row.contacts.map((contact, index) => `${index + 1}. ${formatYandexContact(contact)}`)].join("\n");
|
|
13572
|
+
if (row.status === "not-found" && row.query !== undefined) return `Контакт не найден: ${row.query}`;
|
|
13573
|
+
if (row.status === "duplicate-group" && row.contacts) return [`Дубликаты (${row.key}):`, ...row.contacts.map((contact, index) => `${index + 1}. ${formatYandexContact(contact)}`)].join("\n");
|
|
13574
|
+
if (row.status === "created" && row.file && row.name) return `Skill создан: ${row.name}\nФайл: ${row.file}${row.enabled ? "\nSkill включен." : ""}`;
|
|
13575
|
+
if ((row.status === "enabled" || row.status === "disabled") && row.name) return `Skill ${row.name}: ${row.status}`;
|
|
13576
|
+
if (row.status === "deleted" && row.name) return `Skill удален: ${row.name}`;
|
|
11889
13577
|
if (row.special || row.delimiter) return `Папка почты: ${row.name}${row.special ? ` (${row.special})` : ""}`;
|
|
11890
13578
|
if (row.login || row.defaultEmail) return `Yandex ID: ${row.displayName || row.login || "-"}${row.defaultEmail ? `, ${row.defaultEmail}` : ""}`;
|
|
11891
13579
|
if (row.title && (row.start || row.end)) return `${row.title}: ${row.start || "-"}${row.end ? ` - ${row.end}` : ""}`;
|
|
11892
|
-
if (row.email || row.phone) return formatYandexContact(row);
|
|
13580
|
+
if (row.email || row.phone || row.emails || row.phones) return formatYandexContact(row);
|
|
11893
13581
|
return `${row.name || row.check || row.inn || "строка"}: ${row.address || row.phone || row.email || row.website || row.count || ""}`;
|
|
11894
13582
|
}).join("\n");
|
|
11895
13583
|
}
|
|
11896
13584
|
|
|
11897
13585
|
function formatYandexContact(row) {
|
|
11898
|
-
const
|
|
11899
|
-
|
|
13586
|
+
const emails = row.emails?.length ? row.emails : (row.email ? [row.email] : []);
|
|
13587
|
+
const phones = row.phones?.length ? row.phones : (row.phone ? [row.phone] : []);
|
|
13588
|
+
const name = row.name && row.name !== emails[0] ? row.name : "";
|
|
13589
|
+
return [
|
|
13590
|
+
name || emails[0] || phones[0] || "Контакт",
|
|
13591
|
+
emails.length ? `email: ${emails.join(", ")}` : "",
|
|
13592
|
+
phones.length ? `тел: ${phones.join(", ")}` : "",
|
|
13593
|
+
row.org ? `орг: ${row.org}` : "",
|
|
13594
|
+
row.address ? `адрес: ${row.address}` : "",
|
|
13595
|
+
].filter(Boolean).join(", ");
|
|
11900
13596
|
}
|
|
11901
13597
|
|
|
11902
13598
|
function applyRuntimeConfig(target, value) {
|
|
@@ -12242,8 +13938,11 @@ async function buildAiMessages(question, dataContext, history, options = {}, con
|
|
|
12242
13938
|
const projectContext = options.bare ? "" : await buildProjectContextText();
|
|
12243
13939
|
const skillsText = options.bare ? "" : await buildSkillsText(config, question, options);
|
|
12244
13940
|
const hasDataContext = dataContext.enabled !== false;
|
|
13941
|
+
const currentDate = getCurrentDateInfo();
|
|
12245
13942
|
const system = [
|
|
12246
13943
|
"Ты терминальный AI-агент городского округа Йошкар-Ола.",
|
|
13944
|
+
`Текущие дата и время CLI: ${currentDate.date}, ${currentDate.weekday}, ${currentDate.time}; часовой пояс: ${currentDate.timezone}; ISO: ${currentDate.iso}.`,
|
|
13945
|
+
"Если пользователь спрашивает про сегодня, завтра, вчера, текущую дату, время или относительные сроки, опирайся на текущие дату и время CLI.",
|
|
12247
13946
|
"Отвечай на русском языке естественно и по смыслу запроса пользователя.",
|
|
12248
13947
|
"Не смешивай языки. Не выдумывай факты, географию и числа.",
|
|
12249
13948
|
"Если пользователь просто здоровается, ответь коротким приветствием и спроси, чем помочь.",
|
|
@@ -13395,12 +15094,12 @@ function parseOptions(args) {
|
|
|
13395
15094
|
|
|
13396
15095
|
for (let index = 0; index < args.length; index += 1) {
|
|
13397
15096
|
const arg = args[index];
|
|
13398
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url") {
|
|
15097
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url" || arg === "--enable") {
|
|
13399
15098
|
result[arg.slice(2)] = true;
|
|
13400
15099
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
13401
15100
|
result.check = true;
|
|
13402
15101
|
result[arg.slice(2)] = true;
|
|
13403
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app") {
|
|
15102
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--instructions" || arg === "--allowed-tools" || arg === "--tool" || arg === "--uses" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app") {
|
|
13404
15103
|
result[arg.slice(2)] = args[index + 1];
|
|
13405
15104
|
index += 1;
|
|
13406
15105
|
} else {
|
|
@@ -13607,6 +15306,209 @@ function findSkill(name, config) {
|
|
|
13607
15306
|
return listSkills(config).find((skill) => skill.name === name);
|
|
13608
15307
|
}
|
|
13609
15308
|
|
|
15309
|
+
function printSkillsList(skills, config) {
|
|
15310
|
+
if (!skills.length) {
|
|
15311
|
+
console.log("Нет данных.");
|
|
15312
|
+
return;
|
|
15313
|
+
}
|
|
15314
|
+
const nameWidth = Math.min(28, Math.max(5, ...skills.map((skill) => visibleLength(skill.name))));
|
|
15315
|
+
console.log(`${padCell("Вкл", 3)} ${padCell("Skill", nameWidth)} Описание`);
|
|
15316
|
+
console.log(`${"-".repeat(3)} ${"-".repeat(nameWidth)} ${"-".repeat(8)}`);
|
|
15317
|
+
for (const skill of skills) {
|
|
15318
|
+
const enabled = isSkillEnabled(config, skill.name) ? "yes" : "no";
|
|
15319
|
+
console.log(`${padCell(enabled, 3)} ${padCell(skill.name, nameWidth)} ${skill.description || "-"}`);
|
|
15320
|
+
}
|
|
15321
|
+
}
|
|
15322
|
+
|
|
15323
|
+
async function executeUserSkillTool(tool, args = {}) {
|
|
15324
|
+
if (tool === "user_skill_list") return listSkills(await loadConfig()).filter((skill) => skill.source === "user");
|
|
15325
|
+
if (tool === "user_skill_create") return userSkillCreate({ ...args, confirm: args.confirm === true });
|
|
15326
|
+
if (tool === "user_skill_enable") return userSkillSetEnabled(args.name, true);
|
|
15327
|
+
if (tool === "user_skill_disable") return userSkillSetEnabled(args.name, false);
|
|
15328
|
+
if (tool === "user_skill_delete") return userSkillDelete(args.name, { confirm: args.confirm === true });
|
|
15329
|
+
throw new Error(`Неизвестный user skill tool: ${tool}`);
|
|
15330
|
+
}
|
|
15331
|
+
|
|
15332
|
+
async function userSkillCreate(args = {}) {
|
|
15333
|
+
if (!args.confirm) throw new Error("Для создания пользовательского skill нужен аргумент confirm=true.");
|
|
15334
|
+
const name = normalizeUserSkillName(args.name || args.skill || args.title);
|
|
15335
|
+
if (!name) throw new Error("Имя skill обязательно.");
|
|
15336
|
+
const description = String(args.description || `Пользовательский skill: ${name}`).trim();
|
|
15337
|
+
const instructions = String(args.instructions || args.text || args.prompt || "").trim();
|
|
15338
|
+
if (!instructions) throw new Error("Инструкции skill обязательны.");
|
|
15339
|
+
const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || "");
|
|
15340
|
+
const dir = path.join(USER_SKILLS_DIR, name);
|
|
15341
|
+
const file = path.join(dir, "SKILL.md");
|
|
15342
|
+
if (existsSync(file) && !args.overwrite) throw new Error(`Skill уже существует: ${name}. Используйте overwrite=true или --force.`);
|
|
15343
|
+
await mkdir(dir, { recursive: true });
|
|
15344
|
+
const body = buildUserSkillMarkdown({ name, description, instructions, tools });
|
|
15345
|
+
await writeFile(file, body, "utf8");
|
|
15346
|
+
let enabled = false;
|
|
15347
|
+
if (args.enable) {
|
|
15348
|
+
await userSkillSetEnabled(name, true);
|
|
15349
|
+
enabled = true;
|
|
15350
|
+
}
|
|
15351
|
+
return { name, description, file, enabled, tools, status: "created" };
|
|
15352
|
+
}
|
|
15353
|
+
|
|
15354
|
+
async function userSkillSetEnabled(name, enabled) {
|
|
15355
|
+
const skillName = normalizeUserSkillName(name);
|
|
15356
|
+
if (!skillName) throw new Error("Имя skill обязательно.");
|
|
15357
|
+
const config = await loadConfig();
|
|
15358
|
+
const skill = findSkill(skillName, config);
|
|
15359
|
+
if (!skill && enabled) throw new Error(`Skill не найден: ${skillName}`);
|
|
15360
|
+
const enabledSet = new Set(config.skills?.enabled || []);
|
|
15361
|
+
if (enabled) enabledSet.add(skillName);
|
|
15362
|
+
else enabledSet.delete(skillName);
|
|
15363
|
+
await saveConfig({ skills: { ...(config.skills || {}), enabled: [...enabledSet] } });
|
|
15364
|
+
return { name: skillName, enabled, status: enabled ? "enabled" : "disabled" };
|
|
15365
|
+
}
|
|
15366
|
+
|
|
15367
|
+
async function userSkillDelete(name, options = {}) {
|
|
15368
|
+
if (!options.confirm) throw new Error("Для удаления пользовательского skill нужен --yes или confirm=true.");
|
|
15369
|
+
const skillName = normalizeUserSkillName(name);
|
|
15370
|
+
if (!skillName) throw new Error("Имя skill обязательно.");
|
|
15371
|
+
const file = path.join(USER_SKILLS_DIR, skillName, "SKILL.md");
|
|
15372
|
+
const dir = path.dirname(file);
|
|
15373
|
+
if (!existsSync(file)) throw new Error(`Пользовательский skill не найден: ${skillName}`);
|
|
15374
|
+
await rm(dir, { recursive: true, force: true });
|
|
15375
|
+
await userSkillSetEnabled(skillName, false);
|
|
15376
|
+
return { name: skillName, status: "deleted" };
|
|
15377
|
+
}
|
|
15378
|
+
|
|
15379
|
+
function normalizeUserSkillName(value) {
|
|
15380
|
+
return String(value || "")
|
|
15381
|
+
.toLocaleLowerCase("ru-RU")
|
|
15382
|
+
.replace(/[^a-z0-9а-яё_-]+/giu, "-")
|
|
15383
|
+
.replace(/^-+|-+$/gu, "")
|
|
15384
|
+
.slice(0, 80);
|
|
15385
|
+
}
|
|
15386
|
+
|
|
15387
|
+
function parseCommaList(value) {
|
|
15388
|
+
if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
|
|
15389
|
+
return String(value || "")
|
|
15390
|
+
.split(/[,\n;]/u)
|
|
15391
|
+
.map((item) => item.trim())
|
|
15392
|
+
.filter(Boolean);
|
|
15393
|
+
}
|
|
15394
|
+
|
|
15395
|
+
function extractUserSkillNameFromQuestion(question) {
|
|
15396
|
+
const text = String(question || "");
|
|
15397
|
+
const quoted = text.match(/[«"]([^»"]+)[»"]/u)?.[1];
|
|
15398
|
+
if (quoted) return normalizeUserSkillName(quoted);
|
|
15399
|
+
const explicit = text.match(/(?:skill|скилл|скил|навык)\s+([a-z0-9а-яё_-]{3,80})/iu)?.[1]
|
|
15400
|
+
|| text.match(/(?:создай|добавь|сделай|включи|выключи|удали|удалить)\s+([a-z0-9а-яё_-]{3,80})\s+(?:skill|скилл|скил|навык)/iu)?.[1];
|
|
15401
|
+
return normalizeUserSkillName(explicit || "");
|
|
15402
|
+
}
|
|
15403
|
+
|
|
15404
|
+
function extractUserSkillDescription(question, name) {
|
|
15405
|
+
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
15406
|
+
const afterColon = text.match(/[:\-]\s*(.+)$/u)?.[1];
|
|
15407
|
+
return (afterColon || `Пользовательский skill: ${name}`).slice(0, 180);
|
|
15408
|
+
}
|
|
15409
|
+
|
|
15410
|
+
function inferUserSkillTools(question) {
|
|
15411
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
15412
|
+
const tools = new Set();
|
|
15413
|
+
if (/(почт|письм|email|e-mail)/iu.test(text)) {
|
|
15414
|
+
tools.add("yandex_mail_list");
|
|
15415
|
+
tools.add("yandex_mail_search");
|
|
15416
|
+
tools.add("yandex_mail_read");
|
|
15417
|
+
}
|
|
15418
|
+
if (/(отправь|отправить|отправляй|ответь|ответить|перешли|переслать|удали|удалить)/iu.test(text) && /(почт|письм|email|e-mail)/iu.test(text)) {
|
|
15419
|
+
tools.add("yandex_mail_send");
|
|
15420
|
+
tools.add("yandex_mail_reply");
|
|
15421
|
+
tools.add("yandex_mail_forward");
|
|
15422
|
+
tools.add("yandex_mail_delete");
|
|
15423
|
+
}
|
|
15424
|
+
if (/(диск|облак|яндекс.?диск)/iu.test(text)) {
|
|
15425
|
+
tools.add("yandex_disk_info");
|
|
15426
|
+
tools.add("yandex_disk_ls");
|
|
15427
|
+
tools.add("yandex_disk_find");
|
|
15428
|
+
tools.add("yandex_disk_stat");
|
|
15429
|
+
tools.add("yandex_disk_exists");
|
|
15430
|
+
tools.add("yandex_disk_read_text");
|
|
15431
|
+
tools.add("yandex_disk_save_text");
|
|
15432
|
+
tools.add("yandex_disk_upload");
|
|
15433
|
+
tools.add("yandex_disk_download");
|
|
15434
|
+
tools.add("yandex_disk_share");
|
|
15435
|
+
tools.add("yandex_disk_unshare");
|
|
15436
|
+
tools.add("yandex_disk_move");
|
|
15437
|
+
tools.add("yandex_disk_copy");
|
|
15438
|
+
tools.add("yandex_disk_rename");
|
|
15439
|
+
tools.add("yandex_disk_delete");
|
|
15440
|
+
tools.add("yandex_disk_trash_list");
|
|
15441
|
+
tools.add("yandex_disk_restore");
|
|
15442
|
+
}
|
|
15443
|
+
if (/(календар|событи|встреч|телемост)/iu.test(text)) {
|
|
15444
|
+
tools.add("yandex_calendar_list");
|
|
15445
|
+
tools.add("yandex_calendar_create_event");
|
|
15446
|
+
tools.add("yandex_telemost_create_event");
|
|
15447
|
+
}
|
|
15448
|
+
if (/(контакт|адресн)/iu.test(text)) {
|
|
15449
|
+
tools.add("yandex_contacts_search");
|
|
15450
|
+
tools.add("yandex_contacts_get");
|
|
15451
|
+
tools.add("yandex_contacts_create");
|
|
15452
|
+
tools.add("yandex_contacts_update");
|
|
15453
|
+
tools.add("yandex_contacts_delete");
|
|
15454
|
+
tools.add("yandex_contacts_add_email");
|
|
15455
|
+
tools.add("yandex_contacts_add_phone");
|
|
15456
|
+
tools.add("yandex_contacts_add_address");
|
|
15457
|
+
tools.add("yandex_contacts_add_note");
|
|
15458
|
+
tools.add("yandex_contacts_add_birthday");
|
|
15459
|
+
tools.add("yandex_contacts_add_org");
|
|
15460
|
+
tools.add("yandex_contacts_export_csv");
|
|
15461
|
+
tools.add("yandex_contacts_find_incomplete");
|
|
15462
|
+
tools.add("yandex_contacts_find_duplicates");
|
|
15463
|
+
tools.add("yandex_contacts_backup_to_disk");
|
|
15464
|
+
tools.add("yandex_contact_send_mail");
|
|
15465
|
+
tools.add("yandex_contact_send_disk_link_qr");
|
|
15466
|
+
tools.add("yandex_contact_create_disk_folder");
|
|
15467
|
+
tools.add("yandex_contact_create_calendar_event");
|
|
15468
|
+
tools.add("yandex_contact_create_telemost_event");
|
|
15469
|
+
}
|
|
15470
|
+
if (/(файл|папк|документ|архив)/iu.test(text)) {
|
|
15471
|
+
tools.add("files_tree");
|
|
15472
|
+
tools.add("files_read");
|
|
15473
|
+
tools.add("files_search");
|
|
15474
|
+
}
|
|
15475
|
+
if (/(запиши|сохрани|создай файл|измени|исправ)/iu.test(text) && /(файл|папк|документ|архив)/iu.test(text)) {
|
|
15476
|
+
tools.add("files_write");
|
|
15477
|
+
tools.add("files_patch");
|
|
15478
|
+
}
|
|
15479
|
+
if (/(школ|сад|детсад|инн|адрес|телефон|открыт)/iu.test(text)) {
|
|
15480
|
+
tools.add("search_data");
|
|
15481
|
+
tools.add("get_card");
|
|
15482
|
+
tools.add("export_report");
|
|
15483
|
+
}
|
|
15484
|
+
if (/(сайт|страниц|браузер|url|ссылка)/iu.test(text)) tools.add("browser_open");
|
|
15485
|
+
return [...tools];
|
|
15486
|
+
}
|
|
15487
|
+
|
|
15488
|
+
function buildUserSkillMarkdown({ name, description, instructions, tools = [] }) {
|
|
15489
|
+
const toolLines = tools.length
|
|
15490
|
+
? ["", "Разрешенные/ожидаемые tools для этого skill:", "", ...tools.map((tool) => `- \`${tool}\``)]
|
|
15491
|
+
: [];
|
|
15492
|
+
return [
|
|
15493
|
+
"---",
|
|
15494
|
+
`name: ${name}`,
|
|
15495
|
+
`description: ${description.replace(/\r?\n/g, " ")}`,
|
|
15496
|
+
"source: user",
|
|
15497
|
+
"---",
|
|
15498
|
+
"",
|
|
15499
|
+
instructions.trim(),
|
|
15500
|
+
"",
|
|
15501
|
+
"Правила безопасности:",
|
|
15502
|
+
"",
|
|
15503
|
+
"- Используй только встроенные tools `iola-cli` и подключенные MCP/Yandex/local-files механизмы.",
|
|
15504
|
+
"- Не выводи секреты, OAuth-токены, API-ключи и пароли.",
|
|
15505
|
+
"- Для записи, удаления, отправки писем, публикации ссылок и изменения внешних сервисов требуется явная просьба пользователя.",
|
|
15506
|
+
"- Если действие неоднозначно, сначала уточни у пользователя цель и место выполнения.",
|
|
15507
|
+
...toolLines,
|
|
15508
|
+
"",
|
|
15509
|
+
].join("\n");
|
|
15510
|
+
}
|
|
15511
|
+
|
|
13610
15512
|
function readSkillMeta(file) {
|
|
13611
15513
|
try {
|
|
13612
15514
|
const text = readFileSyncUtf8(file);
|
|
@@ -15376,6 +17278,10 @@ function sanitizeConfig(config) {
|
|
|
15376
17278
|
if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("local-files") && !next.skills.enabled.includes("personal-docs")) {
|
|
15377
17279
|
next.skills.enabled = [...next.skills.enabled, "personal-docs"];
|
|
15378
17280
|
}
|
|
17281
|
+
next.toolsets = next.toolsets || {};
|
|
17282
|
+
next.toolsets.enabled = [...new Set([...(next.toolsets.enabled || []), "user-skills"])];
|
|
17283
|
+
next.skills = next.skills || {};
|
|
17284
|
+
next.skills.enabled = [...new Set([...(next.skills.enabled || []), "user-skills"])];
|
|
15379
17285
|
if (Array.isArray(next.yandex?.enabledServices)) {
|
|
15380
17286
|
next.yandex.enabledServices = next.yandex.enabledServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
|
|
15381
17287
|
}
|