@iola_adm/iola-cli 0.2.32 → 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 +2046 -100
- package/test/smoke-test.js +1 -1
- 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
|
}
|
|
@@ -4148,8 +4294,13 @@ async function yandexMailCount(options = {}) {
|
|
|
4148
4294
|
async function yandexMailSearch(query, options = {}) {
|
|
4149
4295
|
const normalized = normalizeGeoText(query);
|
|
4150
4296
|
if (!normalized) return yandexMailList(options);
|
|
4297
|
+
const tokens = normalized.split(/\s+/u).filter((token) => token.length > 2 && !/^(про|обо?|где|что|кто|как)$/iu.test(token));
|
|
4151
4298
|
const rows = await yandexMailList({ ...options, limit: Math.max(50, Number(options.limit || 20) * 3) });
|
|
4152
|
-
return rows.filter((row) =>
|
|
4299
|
+
return rows.filter((row) => {
|
|
4300
|
+
const haystack = normalizeGeoText(`${row.from} ${row.subject} ${row.snippet}`);
|
|
4301
|
+
if (haystack.includes(normalized)) return true;
|
|
4302
|
+
return tokens.length > 0 && tokens.every((token) => haystack.includes(token));
|
|
4303
|
+
}).slice(0, Number(options.limit || 20));
|
|
4153
4304
|
}
|
|
4154
4305
|
|
|
4155
4306
|
async function yandexMailWatchTick(options = {}) {
|
|
@@ -4198,6 +4349,78 @@ async function yandexMailWatchDisable() {
|
|
|
4198
4349
|
return { enabled: false };
|
|
4199
4350
|
}
|
|
4200
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
|
+
|
|
4201
4424
|
async function yandexMailRead(uid, options = {}) {
|
|
4202
4425
|
if (!uid) throw new Error("UID письма обязателен.");
|
|
4203
4426
|
const { token, email } = await yandexMailCredentials();
|
|
@@ -4205,7 +4428,7 @@ async function yandexMailRead(uid, options = {}) {
|
|
|
4205
4428
|
try {
|
|
4206
4429
|
await imapAuthenticate(session, email, token);
|
|
4207
4430
|
await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
|
|
4208
|
-
const bodyAccessor = options.markSeen === false ? "BODY.PEEK[
|
|
4431
|
+
const bodyAccessor = options.markSeen === false ? "BODY.PEEK[]" : "BODY[]";
|
|
4209
4432
|
const fetch = await imapCommand(session, `UID FETCH ${Number(uid)} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT MESSAGE-ID REFERENCES)] ${bodyAccessor})`, { timeout: 60000 });
|
|
4210
4433
|
return parseImapFetchSummaries(fetch, { full: true })[0] || { uid, status: "not-found" };
|
|
4211
4434
|
} finally {
|
|
@@ -4646,14 +4869,14 @@ function stripMailBody(value) {
|
|
|
4646
4869
|
const raw = String(value || "").replace(/\r/g, "");
|
|
4647
4870
|
const withoutFetch = raw
|
|
4648
4871
|
.replace(/^\* \d+ FETCH[^\n]*\n?/u, "")
|
|
4649
|
-
.replace(/^BODY
|
|
4872
|
+
.replace(/^BODY(?:\.PEEK)?\[[^\n]*\]\s*\{\d+\}\n?/imu, "")
|
|
4650
4873
|
.replace(/\n\)\s*$/u, "");
|
|
4651
|
-
const bodyMatch = withoutFetch.match(/BODY
|
|
4874
|
+
const bodyMatch = withoutFetch.match(/BODY(?:\.PEEK)?\[[^\n]*\]\s*\{\d+\}\s*([\s\S]*)/iu);
|
|
4652
4875
|
if (bodyMatch?.[1]) return extractMimeText(bodyMatch[1]);
|
|
4653
4876
|
const headerEnd = withoutFetch.indexOf("\n\n");
|
|
4654
4877
|
if (headerEnd < 0) return withoutFetch.replace(/[^\S\n]+/g, " ").trim();
|
|
4655
4878
|
const headers = withoutFetch.slice(0, headerEnd);
|
|
4656
|
-
const body = withoutFetch.slice(headerEnd + 2).replace(/^BODY
|
|
4879
|
+
const body = withoutFetch.slice(headerEnd + 2).replace(/^BODY(?:\.PEEK)?\[[^\n]*\]\s*\{\d+\}\n?/imu, "");
|
|
4657
4880
|
return extractMimeText(`${headers}\n\n${body}`);
|
|
4658
4881
|
}
|
|
4659
4882
|
|
|
@@ -4690,17 +4913,52 @@ function decodeMailPart(part) {
|
|
|
4690
4913
|
const encoding = headers.match(/^Content-Transfer-Encoding:\s*([^\n]+)/imu)?.[1]?.trim().toLocaleLowerCase("en-US") || "";
|
|
4691
4914
|
let decoded = body;
|
|
4692
4915
|
if (encoding === "base64") {
|
|
4693
|
-
|
|
4916
|
+
const clean = body.replace(/\s+/g, "");
|
|
4917
|
+
decoded = /^[A-Z0-9+/]+={0,2}$/iu.test(clean) && clean.length >= 8
|
|
4918
|
+
? Buffer.from(clean, "base64").toString("utf8")
|
|
4919
|
+
: body;
|
|
4694
4920
|
} else if (encoding === "quoted-printable") {
|
|
4695
4921
|
decoded = decodeQuotedPrintable(body);
|
|
4922
|
+
} else {
|
|
4923
|
+
const clean = body.replace(/\s+/g, "");
|
|
4924
|
+
if (/^[A-Z0-9+/]+={0,2}$/iu.test(clean) && clean.length >= 80) {
|
|
4925
|
+
const candidate = Buffer.from(clean, "base64").toString("utf8");
|
|
4926
|
+
if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(candidate)) decoded = candidate;
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
decoded = decodeEmbeddedBase64MailBody(decoded);
|
|
4930
|
+
const cleaned = decoded
|
|
4931
|
+
.replace(/<style\b[\s\S]*?<\/style>/giu, " ")
|
|
4932
|
+
.replace(/<script\b[\s\S]*?<\/script>/giu, " ")
|
|
4933
|
+
.replace(/<[^>]+>/g, " ")
|
|
4934
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]+/gu, " ")
|
|
4935
|
+
.replace(/[^\S\n]+/g, " ")
|
|
4936
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
4937
|
+
.trim();
|
|
4938
|
+
const firstCyrillic = cleaned.search(/[А-Яа-яЁё]{3,}/u);
|
|
4939
|
+
if (firstCyrillic > 0 && firstCyrillic < 120 && cleaned.slice(0, firstCyrillic).includes("�")) {
|
|
4940
|
+
return cleaned.slice(firstCyrillic).trim();
|
|
4696
4941
|
}
|
|
4697
|
-
return
|
|
4942
|
+
return cleaned;
|
|
4698
4943
|
}
|
|
4699
4944
|
|
|
4700
4945
|
function decodeQuotedPrintable(value) {
|
|
4701
4946
|
return Buffer.from(String(value || "").replace(/=\n/gu, "").replace(/=([A-F0-9]{2})/giu, (_, hex) => String.fromCharCode(parseInt(hex, 16))), "binary").toString("utf8");
|
|
4702
4947
|
}
|
|
4703
4948
|
|
|
4949
|
+
function decodeEmbeddedBase64MailBody(value) {
|
|
4950
|
+
const text = String(value || "");
|
|
4951
|
+
if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(text) && !/[A-Z0-9+/]{120,}/u.test(text)) return text;
|
|
4952
|
+
const matches = text.match(/[A-Z0-9+/=\s]{160,}/giu) || [];
|
|
4953
|
+
for (const match of matches) {
|
|
4954
|
+
const clean = match.replace(/\s+/g, "");
|
|
4955
|
+
if (!/^[A-Z0-9+/]+={0,2}$/iu.test(clean) || clean.length < 160) continue;
|
|
4956
|
+
const candidate = Buffer.from(clean, "base64").toString("utf8");
|
|
4957
|
+
if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(candidate)) return candidate;
|
|
4958
|
+
}
|
|
4959
|
+
return text;
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4704
4962
|
async function yandexDavRequest(url, token, options = {}) {
|
|
4705
4963
|
const response = await fetch(url, {
|
|
4706
4964
|
method: options.method || "GET",
|
|
@@ -4736,7 +4994,7 @@ async function yandexCalendarCreateEvent(args = {}) {
|
|
|
4736
4994
|
const end = toIcsDate(args.end || new Date(Date.now() + 7200000).toISOString());
|
|
4737
4995
|
const summary = args.title || args.summary || "Событие IOLA";
|
|
4738
4996
|
const description = args.description || "";
|
|
4739
|
-
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 || [] });
|
|
4740
4998
|
const url = `${baseUrl}${encodeURIComponent(uid)}.ics`;
|
|
4741
4999
|
await yandexDavRequest(url, token, { method: "PUT", ics: true, body: ics, timeout: 45000 });
|
|
4742
5000
|
return { status: "created", uid, title: summary, start: args.start || args.date || "", url };
|
|
@@ -4808,9 +5066,9 @@ async function yandexContactsList(args = {}) {
|
|
|
4808
5066
|
|
|
4809
5067
|
async function yandexContactsSearch(query, args = {}) {
|
|
4810
5068
|
const normalized = normalizeGeoText(query);
|
|
4811
|
-
const rows = await yandexContactsList({ limit: Math.max(
|
|
5069
|
+
const rows = await yandexContactsList({ limit: Math.max(1000, Number(args.limit || 20) * 10) });
|
|
4812
5070
|
if (!normalized) return rows.slice(0, Number(args.limit || 20));
|
|
4813
|
-
return rows.filter((row) =>
|
|
5071
|
+
return rows.filter((row) => contactMatchesQuery(row, normalized)).slice(0, Number(args.limit || 20));
|
|
4814
5072
|
}
|
|
4815
5073
|
|
|
4816
5074
|
async function resolveYandexMailRecipientFromContacts(query) {
|
|
@@ -4831,58 +5089,397 @@ async function yandexContactsAddEmail(query, email, args = {}) {
|
|
|
4831
5089
|
if (!args.confirm) throw new Error("Для изменения контакта нужен аргумент confirm=true.");
|
|
4832
5090
|
if (!query) throw new Error("Укажите имя или часть имени контакта.");
|
|
4833
5091
|
if (!email || !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(String(email))) throw new Error("Укажите корректный email.");
|
|
4834
|
-
const
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
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;
|
|
4841
5135
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4842
|
-
await yandexDavRequest(new URL(contact.href, "https://carddav.yandex.ru/").toString(), token, {
|
|
4843
|
-
|
|
4844
|
-
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4845
|
-
body: updatedCard,
|
|
4846
|
-
timeout: 45000,
|
|
4847
|
-
});
|
|
4848
|
-
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 };
|
|
4849
5138
|
}
|
|
4850
5139
|
|
|
4851
5140
|
async function yandexContactsCreate(args = {}) {
|
|
4852
5141
|
if (!args.confirm) throw new Error("Для создания контакта нужен аргумент confirm=true.");
|
|
4853
5142
|
const name = String(args.name || args.email || "").trim();
|
|
4854
5143
|
const email = String(args.email || "").trim();
|
|
5144
|
+
const phone = String(args.phone || "").trim();
|
|
4855
5145
|
if (!name) throw new Error("Имя контакта обязательно.");
|
|
4856
|
-
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 или телефон.");
|
|
4857
5148
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4858
5149
|
const baseUrl = await yandexContactsBaseUrl(token);
|
|
4859
5150
|
const uid = `${randomUUID()}@iola-cli`;
|
|
4860
|
-
const card =
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
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
|
+
});
|
|
4869
5163
|
await yandexDavRequest(new URL(`${encodeURIComponent(uid)}.vcf`, baseUrl).toString(), token, {
|
|
4870
5164
|
method: "PUT",
|
|
4871
5165
|
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4872
5166
|
body: card,
|
|
4873
5167
|
timeout: 45000,
|
|
4874
5168
|
});
|
|
4875
|
-
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");
|
|
4876
5451
|
}
|
|
4877
5452
|
|
|
4878
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 = {}) {
|
|
4879
5458
|
const text = String(card || "").replace(/\r/g, "").trim();
|
|
4880
5459
|
if (!text.includes("BEGIN:VCARD")) throw new Error("Контакт не похож на vCard.");
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
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);
|
|
4884
5468
|
}
|
|
4885
|
-
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");
|
|
4886
5483
|
}
|
|
4887
5484
|
|
|
4888
5485
|
function escapeVcardValue(value) {
|
|
@@ -4890,10 +5487,28 @@ function escapeVcardValue(value) {
|
|
|
4890
5487
|
}
|
|
4891
5488
|
|
|
4892
5489
|
function contactMatchesQuery(contact, normalizedQuery) {
|
|
4893
|
-
const
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
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);
|
|
4897
5512
|
const contactTokens = contactText.split(/\s+/u).filter(Boolean);
|
|
4898
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))))));
|
|
4899
5514
|
}
|
|
@@ -4902,6 +5517,99 @@ function normalizeContactLookupText(value) {
|
|
|
4902
5517
|
return normalizeGeoText(String(value || "").replace(/\b(?:кому|контакт|письмо|сообщение)\b/giu, " ")).trim();
|
|
4903
5518
|
}
|
|
4904
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
|
+
|
|
4905
5613
|
async function yandexContactsBaseUrl(token) {
|
|
4906
5614
|
const root = "https://carddav.yandex.ru/";
|
|
4907
5615
|
const principalXml = await yandexDavRequest(root, token, {
|
|
@@ -4940,8 +5648,12 @@ async function yandexTelemostCreateEvent(args = {}) {
|
|
|
4940
5648
|
return yandexCalendarCreateEvent({ ...args, description });
|
|
4941
5649
|
}
|
|
4942
5650
|
|
|
4943
|
-
function buildIcsEvent({ uid, start, end, summary, description, location }) {
|
|
5651
|
+
function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [] }) {
|
|
4944
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}`);
|
|
4945
5657
|
return [
|
|
4946
5658
|
"BEGIN:VCALENDAR",
|
|
4947
5659
|
"VERSION:2.0",
|
|
@@ -4954,6 +5666,7 @@ function buildIcsEvent({ uid, start, end, summary, description, location }) {
|
|
|
4954
5666
|
`SUMMARY:${escape(summary)}`,
|
|
4955
5667
|
description ? `DESCRIPTION:${escape(description)}` : "",
|
|
4956
5668
|
location ? `LOCATION:${escape(location)}` : "",
|
|
5669
|
+
...attendeeLines,
|
|
4957
5670
|
"END:VEVENT",
|
|
4958
5671
|
"END:VCALENDAR",
|
|
4959
5672
|
"",
|
|
@@ -5017,19 +5730,75 @@ function extractVcardHrefs(xml) {
|
|
|
5017
5730
|
function parseVCards(xmlOrCards) {
|
|
5018
5731
|
const decoded = decodeXml(stripXmlTags(xmlOrCards)).replace(/\r/g, "");
|
|
5019
5732
|
return decoded.split("BEGIN:VCARD").slice(1).map((chunk) => {
|
|
5020
|
-
const
|
|
5021
|
-
const
|
|
5022
|
-
const
|
|
5023
|
-
|
|
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;
|
|
5024
5755
|
}).filter((item) => item.name || item.email || item.phone);
|
|
5025
5756
|
}
|
|
5026
5757
|
|
|
5027
5758
|
function cleanVcardName(value, fallback = "") {
|
|
5028
|
-
const text =
|
|
5759
|
+
const text = unescapeVcardValue(value).replace(/;/g, " ").replace(/\s+/g, " ").trim();
|
|
5029
5760
|
if (!text || /^[\s;]+$/u.test(String(value || ""))) return fallback || "";
|
|
5030
5761
|
return text;
|
|
5031
5762
|
}
|
|
5032
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
|
+
|
|
5033
5802
|
function normalizeYandexServiceList(values) {
|
|
5034
5803
|
const aliases = {
|
|
5035
5804
|
id: "identity",
|
|
@@ -5238,6 +6007,121 @@ async function yandexDiskFind(query, options = {}) {
|
|
|
5238
6007
|
.slice(0, Number(options.limit || 50));
|
|
5239
6008
|
}
|
|
5240
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
|
+
|
|
5241
6125
|
async function yandexDiskListRecursive(remotePath, options = {}) {
|
|
5242
6126
|
const depth = Number(options.depth || 4);
|
|
5243
6127
|
const limit = Number(options.limit || 200);
|
|
@@ -5285,6 +6169,119 @@ async function yandexDiskShare(remotePath) {
|
|
|
5285
6169
|
return { provider: "yandex-disk", remote: remotePath, publicUrl: payload.public_url || "-" };
|
|
5286
6170
|
}
|
|
5287
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
|
+
|
|
5288
6285
|
async function ensureYandexDiskDir(remotePath, options = {}) {
|
|
5289
6286
|
const normalized = normalizeYandexDiskPath(remotePath || CLOUD_DEFAULT_REMOTE_DIR);
|
|
5290
6287
|
const plain = denormalizeYandexDiskPath(normalized);
|
|
@@ -5305,7 +6302,7 @@ async function ensureYandexDiskDir(remotePath, options = {}) {
|
|
|
5305
6302
|
|
|
5306
6303
|
function normalizeYandexDiskPath(remotePath) {
|
|
5307
6304
|
const text = String(remotePath || CLOUD_DEFAULT_REMOTE_DIR).trim().replace(/\\/g, "/");
|
|
5308
|
-
if (text.startsWith("disk:") || text.startsWith("app:")) return text;
|
|
6305
|
+
if (text.startsWith("disk:") || text.startsWith("app:") || text.startsWith("trash:")) return text;
|
|
5309
6306
|
return text.startsWith("/") ? text : `/${text}`;
|
|
5310
6307
|
}
|
|
5311
6308
|
|
|
@@ -9652,6 +10649,10 @@ function isCronDue(job) {
|
|
|
9652
10649
|
if (everyMinutes) {
|
|
9653
10650
|
return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyMinutes[1]) * 60 * 1000;
|
|
9654
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
|
+
}
|
|
9655
10656
|
if (normalized.includes("каждый день") || normalized.includes("daily")) {
|
|
9656
10657
|
return !lastRun || now.toISOString().slice(0, 10) !== lastRun.toISOString().slice(0, 10);
|
|
9657
10658
|
}
|
|
@@ -10021,19 +11022,35 @@ async function aiAsk(args, context = {}) {
|
|
|
10021
11022
|
if (!options.quiet) console.log(casualAnswer);
|
|
10022
11023
|
return casualAnswer;
|
|
10023
11024
|
}
|
|
10024
|
-
const
|
|
10025
|
-
if (
|
|
11025
|
+
const userSkillAnswer = await buildUserSkillDirectAnswer(question);
|
|
11026
|
+
if (userSkillAnswer) {
|
|
10026
11027
|
if (historyEnabled) {
|
|
10027
|
-
recordAskHistory({ question, answer:
|
|
10028
|
-
appendSessionExchange(sessionId, question,
|
|
11028
|
+
recordAskHistory({ question, answer: userSkillAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
11029
|
+
appendSessionExchange(sessionId, question, userSkillAnswer, dataContext, "");
|
|
10029
11030
|
}
|
|
10030
|
-
emitEvent(options, "answer", { length:
|
|
11031
|
+
emitEvent(options, "answer", { length: userSkillAnswer.length, sessionId, direct: true, skill: true });
|
|
10031
11032
|
if (options.output) {
|
|
10032
11033
|
await assertPermission("writeFiles");
|
|
10033
|
-
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;
|
|
10034
11053
|
}
|
|
10035
|
-
if (!options.quiet) console.log(yandexAnswer);
|
|
10036
|
-
return yandexAnswer;
|
|
10037
11054
|
}
|
|
10038
11055
|
const cloudAnswer = await buildCloudDirectAnswer(question);
|
|
10039
11056
|
if (cloudAnswer) {
|
|
@@ -10049,6 +11066,20 @@ async function aiAsk(args, context = {}) {
|
|
|
10049
11066
|
if (!options.quiet) console.log(cloudAnswer);
|
|
10050
11067
|
return cloudAnswer;
|
|
10051
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
|
+
}
|
|
10052
11083
|
const geoAnswer = await buildGeoDirectAnswer(question);
|
|
10053
11084
|
if (geoAnswer) {
|
|
10054
11085
|
if (historyEnabled) {
|
|
@@ -10174,6 +11205,10 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10174
11205
|
].join("\n");
|
|
10175
11206
|
}
|
|
10176
11207
|
|
|
11208
|
+
if (/(контакт|адресн)/iu.test(normalized) && !mailFollowup && !isExplicitYandexDiskPathDelete(question)) {
|
|
11209
|
+
return await buildYandexContactsDirectAnswer(question, normalized);
|
|
11210
|
+
}
|
|
11211
|
+
|
|
10177
11212
|
if (mailFollowup || /(почт|письм|email|e-mail|спам|чернов|отправлен|исходящ|корзин)/iu.test(normalized)) {
|
|
10178
11213
|
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
10179
11214
|
const minutes = Number(String(question || "").match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 5);
|
|
@@ -10236,7 +11271,7 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10236
11271
|
if (!rows.length) return "В письме не нашел совпадений со слоями школ и детских садов.";
|
|
10237
11272
|
return ["Нашел в городских слоях:", ...rows.map((row, index) => `${index + 1}. ${row.name}${row.inn ? `, ИНН ${row.inn}` : ""}${row.address ? `, ${row.address}` : ""}`)].join("\n");
|
|
10238
11273
|
}
|
|
10239
|
-
if (/(
|
|
11274
|
+
if (/(адрес|карт|место|где)/iu.test(normalized) && /(письм|письма|письме)/iu.test(normalized)) {
|
|
10240
11275
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10241
11276
|
if (!uid) return "Из какого письма взять адрес? Укажите номер из списка или UID.";
|
|
10242
11277
|
const rows = await yandexMailMapAddresses(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
|
|
@@ -10328,6 +11363,38 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10328
11363
|
}
|
|
10329
11364
|
|
|
10330
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
|
+
}
|
|
10331
11398
|
if (/(добав|запиши|сохрани).{0,40}(email|e-mail|почт)/iu.test(normalized)) {
|
|
10332
11399
|
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
10333
11400
|
const query = cleanupYandexContactEmailTarget(question, email);
|
|
@@ -10343,6 +11410,54 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10343
11410
|
}
|
|
10344
11411
|
return `Email добавлен в контакт: ${result.name || query}, ${result.email}.`;
|
|
10345
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
|
+
}
|
|
10346
11461
|
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
10347
11462
|
const result = await yandexContactsStatus();
|
|
10348
11463
|
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
@@ -10360,13 +11475,219 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10360
11475
|
return "";
|
|
10361
11476
|
}
|
|
10362
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
|
+
|
|
10363
11684
|
function isYandexServiceQuestion(normalized) {
|
|
10364
11685
|
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
|
|
10365
11686
|
}
|
|
10366
11687
|
|
|
10367
11688
|
function isYandexIdentityQuestion(normalized) {
|
|
10368
11689
|
const text = String(normalized || "");
|
|
10369
|
-
return /(
|
|
11690
|
+
return /(аккаунт|аккант|акант|акаунт|акк?аунт|профил|логин|кто подключен|какой.*подключ|email|e-mail)/iu.test(text)
|
|
10370
11691
|
&& /(яндекс|яндес|язндекс|язндекс|яндкс|yandex)/iu.test(text);
|
|
10371
11692
|
}
|
|
10372
11693
|
|
|
@@ -10378,7 +11699,7 @@ function isYandexMailFollowupQuestion(normalized, question) {
|
|
|
10378
11699
|
}
|
|
10379
11700
|
|
|
10380
11701
|
function cleanupYandexQuery(question) {
|
|
10381
|
-
const stop = /^(
|
|
11702
|
+
const stop = /^(?:в|на|у|из|для|по|про|о|об|обо|яндекс|yandex|найди|поиск|покажи|посмотри|проверь|почт\p{L}*|письм\p{L}*|календар\p{L}*|контакт\p{L}*)$/iu;
|
|
10382
11703
|
return String(question || "")
|
|
10383
11704
|
.replace(/[?.!]+$/u, "")
|
|
10384
11705
|
.split(/[^\p{L}\p{N}@._+-]+/gu)
|
|
@@ -10445,6 +11766,13 @@ function isYandexMailReadRequest(normalizedQuestion) {
|
|
|
10445
11766
|
|| /(покажи\s+содерж|о чем|о чём|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(normalizedQuestion);
|
|
10446
11767
|
}
|
|
10447
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
|
+
|
|
10448
11776
|
function parseYandexMailReplyRequest(question, previousAssistantText = "") {
|
|
10449
11777
|
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
10450
11778
|
const uid = resolveYandexMailUidFromQuestion(text, previousAssistantText);
|
|
@@ -10502,6 +11830,49 @@ function cleanupYandexContactEmailTarget(question, email) {
|
|
|
10502
11830
|
.trim();
|
|
10503
11831
|
}
|
|
10504
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
|
+
|
|
10505
11876
|
function formatYandexMailSummary(row) {
|
|
10506
11877
|
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
10507
11878
|
}
|
|
@@ -10596,6 +11967,64 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10596
11967
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
10597
11968
|
try {
|
|
10598
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
|
+
}
|
|
10599
12028
|
if (/(созда|сдела|добав).{0,30}(папк|директор)/iu.test(normalized) || /(папк|директор).{0,30}(созда|сдела|добав)/iu.test(normalized)) {
|
|
10600
12029
|
const folderName = extractCloudFolderName(question) || "Новая папка";
|
|
10601
12030
|
const remotePath = normalizeCloudUserPath(folderName, provider);
|
|
@@ -10623,17 +12052,101 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10623
12052
|
].join("\n");
|
|
10624
12053
|
}
|
|
10625
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
|
+
|
|
10626
12082
|
if (/(ссылк|поделись|опубликуй)/iu.test(normalized)) {
|
|
10627
12083
|
const remotePath = extractCloudPath(question);
|
|
10628
12084
|
if (!remotePath) return "Укажите путь к файлу на облачном диске, например: /IOLA/reports/report.md";
|
|
10629
|
-
const
|
|
10630
|
-
|
|
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}).`;
|
|
10631
12144
|
}
|
|
10632
12145
|
|
|
10633
12146
|
if (/(сохрани|запиши).{0,40}(на яндекс диске|в облак|на диск)/iu.test(normalized)) {
|
|
10634
12147
|
const text = cleanupCloudSaveText(question);
|
|
10635
12148
|
if (!text) return "Что сохранить на облачный диск?";
|
|
10636
|
-
const remotePath = `${cloudRootForProvider(provider)}/notes/iola-${timestampForFile()}.txt`;
|
|
12149
|
+
const remotePath = extractCloudPath(question) || `${cloudRootForProvider(provider)}/notes/iola-${timestampForFile()}.txt`;
|
|
10637
12150
|
const tempPath = path.join(CONFIG_DIR, `cloud-save-${Date.now()}.txt`);
|
|
10638
12151
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
10639
12152
|
await writeFile(tempPath, text, "utf8");
|
|
@@ -10651,7 +12164,7 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10651
12164
|
}
|
|
10652
12165
|
|
|
10653
12166
|
function isCloudQuestion(question) {
|
|
10654
|
-
return /(
|
|
12167
|
+
return /(\/IOLA\/|яндекс.?диск|yandex.?disk|облак|облачн|на диск|с диска|в диск|cloud|mail\.?ru|публичн.*ссылк|поделиться.*файл|qr-код|qr\s+код)/iu.test(String(question || ""));
|
|
10655
12168
|
}
|
|
10656
12169
|
|
|
10657
12170
|
function normalizeCloudUserPath(value, provider = "yandex-disk") {
|
|
@@ -10674,16 +12187,112 @@ function extractCloudFolderName(question) {
|
|
|
10674
12187
|
|
|
10675
12188
|
function extractCloudPath(question) {
|
|
10676
12189
|
const text = String(question || "").trim();
|
|
10677
|
-
const pathMatch = text.match(/(?:^|\s)(\/IOLA\/[^\s]+|\/[^\s]+)/iu);
|
|
10678
|
-
if (pathMatch?.[1]) return pathMatch[1];
|
|
10679
12190
|
const quoted = text.match(/["«]([^"»]+)["»]/u);
|
|
10680
|
-
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]);
|
|
10681
12196
|
const afterFolder = text.match(/(?:папк[аеуы]?|файл[ае]?)\s+([^,.!?]+)/iu);
|
|
10682
12197
|
return afterFolder?.[1] ? cleanupCloudObjectName(afterFolder[1]) : "";
|
|
10683
12198
|
}
|
|
10684
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
|
+
|
|
10685
12293
|
function cleanupCloudObjectName(value) {
|
|
10686
12294
|
return String(value || "")
|
|
12295
|
+
.replace(/\s+(?:на\s+яндекс.?диск(?:е)?|на\s+диск(?:е)?|в\s+облак(?:е|о)?).*/iu, " ")
|
|
10687
12296
|
.replace(/\b(?:на|в|у меня|яндекс.?диск(?:е)?|диск(?:е)?|облак(?:е|о)?|создай|сделай|добавь|покажи)\b/giu, " ")
|
|
10688
12297
|
.replace(/\s+/g, " ")
|
|
10689
12298
|
.trim();
|
|
@@ -10691,7 +12300,8 @@ function cleanupCloudObjectName(value) {
|
|
|
10691
12300
|
|
|
10692
12301
|
function cleanupCloudQuery(question) {
|
|
10693
12302
|
return String(question || "")
|
|
10694
|
-
.replace(
|
|
12303
|
+
.replace(/яндекс\s+диск(?:е)?/giu, " ")
|
|
12304
|
+
.replace(/(?:найди|поиск|где лежит|на|в|яндекс|облак(?:е|о)?|диск(?:е)?|файл|документ)/giu, " ")
|
|
10695
12305
|
.replace(/[?.!]+$/u, "")
|
|
10696
12306
|
.replace(/\s+/g, " ")
|
|
10697
12307
|
.trim();
|
|
@@ -10700,6 +12310,7 @@ function cleanupCloudQuery(question) {
|
|
|
10700
12310
|
function cleanupCloudSaveText(question) {
|
|
10701
12311
|
return String(question || "")
|
|
10702
12312
|
.replace(/^.*?(?:сохрани|запиши)\s+/iu, "")
|
|
12313
|
+
.replace(/\s+(?:в|на|как)\s+\/[^\s]+/giu, " ")
|
|
10703
12314
|
.replace(/\s+(?:на яндекс диске|в облак[ео]|на диск).*$/iu, "")
|
|
10704
12315
|
.trim();
|
|
10705
12316
|
}
|
|
@@ -10987,16 +12598,28 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
10987
12598
|
if (!options.quiet) console.log(casualAnswer);
|
|
10988
12599
|
return casualAnswer;
|
|
10989
12600
|
}
|
|
10990
|
-
const
|
|
10991
|
-
if (
|
|
10992
|
-
if (!options.quiet) console.log(
|
|
10993
|
-
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
|
+
}
|
|
10994
12612
|
}
|
|
10995
12613
|
const cloudAnswer = await buildCloudDirectAnswer(question);
|
|
10996
12614
|
if (cloudAnswer) {
|
|
10997
12615
|
if (!options.quiet) console.log(cloudAnswer);
|
|
10998
12616
|
return cloudAnswer;
|
|
10999
12617
|
}
|
|
12618
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, []);
|
|
12619
|
+
if (yandexAnswer) {
|
|
12620
|
+
if (!options.quiet) console.log(yandexAnswer);
|
|
12621
|
+
return yandexAnswer;
|
|
12622
|
+
}
|
|
11000
12623
|
const geoAnswer = await buildGeoDirectAnswer(question);
|
|
11001
12624
|
if (geoAnswer) {
|
|
11002
12625
|
if (!options.quiet) console.log(geoAnswer);
|
|
@@ -11091,6 +12714,9 @@ function isUnsupportedPublicEntityQuestion(normalized) {
|
|
|
11091
12714
|
|
|
11092
12715
|
function buildCasualDirectAnswer(question) {
|
|
11093
12716
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU").trim();
|
|
12717
|
+
if (isCurrentDateTimeQuestion(normalized)) {
|
|
12718
|
+
return formatCurrentDateTimeAnswer(normalized);
|
|
12719
|
+
}
|
|
11094
12720
|
if (/^(кто ты|что ты|какая ты модель|что ты за модель|что за модель|какая модель|назови модель|ты какая модель|ты кто)([?.!\s]*)$/iu.test(normalized)) {
|
|
11095
12721
|
return "Я IOLA, первая городская модель искусственного интеллекта Йошкар-Олы. Работаю локально в CLI и отвечаю по открытым городским данным через проверяемые слои и API.";
|
|
11096
12722
|
}
|
|
@@ -11200,8 +12826,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
11200
12826
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
11201
12827
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
11202
12828
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
11203
|
-
"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}.",
|
|
11204
|
-
"Опасные 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.",
|
|
11205
12832
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
11206
12833
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
11207
12834
|
`Вопрос: ${question}`,
|
|
@@ -11259,14 +12886,48 @@ function parseJsonObject(text) {
|
|
|
11259
12886
|
|
|
11260
12887
|
function inferToolPlan(question, options = {}) {
|
|
11261
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
|
+
}
|
|
11262
12911
|
if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
|
|
11263
12912
|
return { steps: [{ tool: "yandex_identity_me", args: {} }] };
|
|
11264
12913
|
}
|
|
11265
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 } }] };
|
|
11266
12918
|
if (/(создай|сделай).{0,30}папк/iu.test(normalized)) {
|
|
11267
12919
|
const folder = question.match(/папк[ауи]?\s+["«]?([^"»\n]+)["»]?/iu)?.[1]?.trim() || "Новая папка";
|
|
11268
12920
|
return { steps: [{ tool: "yandex_disk_mkdir", args: { path: `${CLOUD_DEFAULT_REMOTE_DIR}/${folder}` } }] };
|
|
11269
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
|
+
}
|
|
11270
12931
|
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_find", args: { query: question, path: CLOUD_DEFAULT_REMOTE_DIR, limit: 20 } }] };
|
|
11271
12932
|
return { steps: [{ tool: "yandex_disk_ls", args: { path: CLOUD_DEFAULT_REMOTE_DIR } }] };
|
|
11272
12933
|
}
|
|
@@ -11285,6 +12946,18 @@ function inferToolPlan(question, options = {}) {
|
|
|
11285
12946
|
return { steps: [{ tool: normalized.includes("телемост") ? "yandex_telemost_create_event" : "yandex_calendar_list", args: { limit: 20 } }] };
|
|
11286
12947
|
}
|
|
11287
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 } }] };
|
|
11288
12961
|
return { steps: [{ tool: "yandex_contacts_search", args: { query: question, limit: 20 } }] };
|
|
11289
12962
|
}
|
|
11290
12963
|
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
@@ -11640,7 +13313,7 @@ function formatToolExecutionError(error, plan) {
|
|
|
11640
13313
|
}
|
|
11641
13314
|
|
|
11642
13315
|
function availableToolNames(options = {}) {
|
|
11643
|
-
const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS]);
|
|
13316
|
+
const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS]);
|
|
11644
13317
|
if (options.files) {
|
|
11645
13318
|
for (const tool of FILE_TOOLS) names.add(tool);
|
|
11646
13319
|
}
|
|
@@ -11709,6 +13382,10 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
11709
13382
|
const result = await executeYandexTool(step.tool, step.args || {});
|
|
11710
13383
|
current = Array.isArray(result) ? result : [result];
|
|
11711
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 });
|
|
11712
13389
|
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
11713
13390
|
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
11714
13391
|
current = Array.isArray(result) ? result : [result];
|
|
@@ -11749,14 +13426,34 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
11749
13426
|
|
|
11750
13427
|
function getCurrentDateInfo() {
|
|
11751
13428
|
const now = new Date();
|
|
13429
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
|
|
11752
13430
|
return {
|
|
11753
13431
|
name: "текущая дата",
|
|
11754
13432
|
date: new Intl.DateTimeFormat("ru-RU", { dateStyle: "long" }).format(now),
|
|
11755
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,
|
|
11756
13436
|
iso: now.toISOString(),
|
|
11757
13437
|
};
|
|
11758
13438
|
}
|
|
11759
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
|
+
|
|
11760
13457
|
function getLocalMcpToolNames() {
|
|
11761
13458
|
return mcpTools().map((tool) => `mcp:iola-local:${tool.name}`);
|
|
11762
13459
|
}
|
|
@@ -11840,23 +13537,62 @@ function formatToolResult(result, options) {
|
|
|
11840
13537
|
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
11841
13538
|
}
|
|
11842
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}.`;
|
|
11843
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)}`;
|
|
11844
13556
|
if (row.provider === "yandex-disk" && row.remote) return `Яндекс Диск: ${row.status || "ok"} ${row.remote}`;
|
|
11845
13557
|
if (row.uid && (row.subject || row.from)) return `Письмо #${row.uid}: ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}`;
|
|
11846
13558
|
if (row.status === "moved-to-trash") return `Письмо #${row.uid} перемещено в корзину: ${row.to}`;
|
|
11847
13559
|
if (row.status === "seen" || row.status === "unseen") return `Письмо #${row.uid}: ${row.status === "seen" ? "прочитано" : "непрочитано"}`;
|
|
11848
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}`;
|
|
11849
13577
|
if (row.special || row.delimiter) return `Папка почты: ${row.name}${row.special ? ` (${row.special})` : ""}`;
|
|
11850
13578
|
if (row.login || row.defaultEmail) return `Yandex ID: ${row.displayName || row.login || "-"}${row.defaultEmail ? `, ${row.defaultEmail}` : ""}`;
|
|
11851
13579
|
if (row.title && (row.start || row.end)) return `${row.title}: ${row.start || "-"}${row.end ? ` - ${row.end}` : ""}`;
|
|
11852
|
-
if (row.email || row.phone) return formatYandexContact(row);
|
|
13580
|
+
if (row.email || row.phone || row.emails || row.phones) return formatYandexContact(row);
|
|
11853
13581
|
return `${row.name || row.check || row.inn || "строка"}: ${row.address || row.phone || row.email || row.website || row.count || ""}`;
|
|
11854
13582
|
}).join("\n");
|
|
11855
13583
|
}
|
|
11856
13584
|
|
|
11857
13585
|
function formatYandexContact(row) {
|
|
11858
|
-
const
|
|
11859
|
-
|
|
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(", ");
|
|
11860
13596
|
}
|
|
11861
13597
|
|
|
11862
13598
|
function applyRuntimeConfig(target, value) {
|
|
@@ -12202,8 +13938,11 @@ async function buildAiMessages(question, dataContext, history, options = {}, con
|
|
|
12202
13938
|
const projectContext = options.bare ? "" : await buildProjectContextText();
|
|
12203
13939
|
const skillsText = options.bare ? "" : await buildSkillsText(config, question, options);
|
|
12204
13940
|
const hasDataContext = dataContext.enabled !== false;
|
|
13941
|
+
const currentDate = getCurrentDateInfo();
|
|
12205
13942
|
const system = [
|
|
12206
13943
|
"Ты терминальный AI-агент городского округа Йошкар-Ола.",
|
|
13944
|
+
`Текущие дата и время CLI: ${currentDate.date}, ${currentDate.weekday}, ${currentDate.time}; часовой пояс: ${currentDate.timezone}; ISO: ${currentDate.iso}.`,
|
|
13945
|
+
"Если пользователь спрашивает про сегодня, завтра, вчера, текущую дату, время или относительные сроки, опирайся на текущие дату и время CLI.",
|
|
12207
13946
|
"Отвечай на русском языке естественно и по смыслу запроса пользователя.",
|
|
12208
13947
|
"Не смешивай языки. Не выдумывай факты, географию и числа.",
|
|
12209
13948
|
"Если пользователь просто здоровается, ответь коротким приветствием и спроси, чем помочь.",
|
|
@@ -13355,12 +15094,12 @@ function parseOptions(args) {
|
|
|
13355
15094
|
|
|
13356
15095
|
for (let index = 0; index < args.length; index += 1) {
|
|
13357
15096
|
const arg = args[index];
|
|
13358
|
-
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") {
|
|
13359
15098
|
result[arg.slice(2)] = true;
|
|
13360
15099
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
13361
15100
|
result.check = true;
|
|
13362
15101
|
result[arg.slice(2)] = true;
|
|
13363
|
-
} 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") {
|
|
13364
15103
|
result[arg.slice(2)] = args[index + 1];
|
|
13365
15104
|
index += 1;
|
|
13366
15105
|
} else {
|
|
@@ -13567,6 +15306,209 @@ function findSkill(name, config) {
|
|
|
13567
15306
|
return listSkills(config).find((skill) => skill.name === name);
|
|
13568
15307
|
}
|
|
13569
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
|
+
|
|
13570
15512
|
function readSkillMeta(file) {
|
|
13571
15513
|
try {
|
|
13572
15514
|
const text = readFileSyncUtf8(file);
|
|
@@ -15336,6 +17278,10 @@ function sanitizeConfig(config) {
|
|
|
15336
17278
|
if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("local-files") && !next.skills.enabled.includes("personal-docs")) {
|
|
15337
17279
|
next.skills.enabled = [...next.skills.enabled, "personal-docs"];
|
|
15338
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"])];
|
|
15339
17285
|
if (Array.isArray(next.yandex?.enabledServices)) {
|
|
15340
17286
|
next.yandex.enabledServices = next.yandex.enabledServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
|
|
15341
17287
|
}
|