@iola_adm/iola-cli 0.2.33 → 0.2.35
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 +79 -7
- package/src/cli.js +2640 -116
- package/wiki/Skills-/320/270-toolsets.md +27 -5
- package/wiki/Yandex-Connector.md +66 -4
- 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 +127 -4
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",
|
|
@@ -180,16 +193,58 @@ const YANDEX_TOOLS = [
|
|
|
180
193
|
"yandex_mail_map_addresses",
|
|
181
194
|
"yandex_mail_create_task",
|
|
182
195
|
"yandex_calendar_status",
|
|
196
|
+
"yandex_calendar_calendars",
|
|
183
197
|
"yandex_calendar_create_event",
|
|
184
198
|
"yandex_calendar_list",
|
|
199
|
+
"yandex_calendar_get",
|
|
200
|
+
"yandex_calendar_search",
|
|
201
|
+
"yandex_calendar_update",
|
|
202
|
+
"yandex_calendar_move",
|
|
203
|
+
"yandex_calendar_delete",
|
|
204
|
+
"yandex_calendar_create_recurring_event",
|
|
205
|
+
"yandex_calendar_add_reminder",
|
|
206
|
+
"yandex_docs_status",
|
|
207
|
+
"yandex_docs_list",
|
|
208
|
+
"yandex_docs_find",
|
|
209
|
+
"yandex_docs_create_text",
|
|
210
|
+
"yandex_docs_read",
|
|
211
|
+
"yandex_docs_share",
|
|
212
|
+
"yandex_docs_rename",
|
|
213
|
+
"yandex_docs_delete",
|
|
214
|
+
"yandex_docs_save_answer",
|
|
185
215
|
"yandex_contacts_status",
|
|
186
216
|
"yandex_contacts_list",
|
|
187
217
|
"yandex_contacts_search",
|
|
218
|
+
"yandex_contacts_get",
|
|
188
219
|
"yandex_contacts_create",
|
|
220
|
+
"yandex_contacts_update",
|
|
221
|
+
"yandex_contacts_delete",
|
|
189
222
|
"yandex_contacts_add_email",
|
|
223
|
+
"yandex_contacts_add_phone",
|
|
224
|
+
"yandex_contacts_add_address",
|
|
225
|
+
"yandex_contacts_add_note",
|
|
226
|
+
"yandex_contacts_add_birthday",
|
|
227
|
+
"yandex_contacts_add_org",
|
|
228
|
+
"yandex_contacts_remove_email",
|
|
229
|
+
"yandex_contacts_remove_phone",
|
|
230
|
+
"yandex_contacts_export_vcard",
|
|
231
|
+
"yandex_contacts_export_csv",
|
|
232
|
+
"yandex_contacts_import_vcard",
|
|
233
|
+
"yandex_contacts_import_csv",
|
|
234
|
+
"yandex_contacts_find_incomplete",
|
|
235
|
+
"yandex_contacts_find_duplicates",
|
|
236
|
+
"yandex_contacts_backup_to_disk",
|
|
237
|
+
"yandex_contacts_birthdays_to_calendar",
|
|
238
|
+
"yandex_contact_send_mail",
|
|
239
|
+
"yandex_contact_send_disk_link_qr",
|
|
240
|
+
"yandex_contact_create_disk_folder",
|
|
241
|
+
"yandex_contact_create_calendar_event",
|
|
242
|
+
"yandex_contact_create_telemost_event",
|
|
243
|
+
"yandex_contact_from_public_entity",
|
|
244
|
+
"yandex_telemost_status",
|
|
190
245
|
"yandex_telemost_create_event",
|
|
191
246
|
];
|
|
192
|
-
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS];
|
|
247
|
+
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
|
|
193
248
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
194
249
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
195
250
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
@@ -247,6 +302,13 @@ const TOOLSETS = {
|
|
|
247
302
|
localTools: Object.fromEntries(ALL_LOCAL_TOOLS.map((tool) => [tool, true])),
|
|
248
303
|
},
|
|
249
304
|
},
|
|
305
|
+
"user-skills": {
|
|
306
|
+
description: "Создание и управление пользовательскими skills на базе встроенных tools.",
|
|
307
|
+
permissions: {
|
|
308
|
+
writeFiles: true,
|
|
309
|
+
localTools: Object.fromEntries(USER_SKILL_TOOLS.map((tool) => [tool, true])),
|
|
310
|
+
},
|
|
311
|
+
},
|
|
250
312
|
};
|
|
251
313
|
const FEATURES = {
|
|
252
314
|
"sqlite-history": { stage: "stable", defaultEnabled: true, description: "Запись истории AI-запросов в SQLite." },
|
|
@@ -390,7 +452,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
390
452
|
codex: true,
|
|
391
453
|
},
|
|
392
454
|
toolsets: {
|
|
393
|
-
enabled: ["data-read", "reports", "sync", "ai", "yandex"],
|
|
455
|
+
enabled: ["data-read", "reports", "sync", "ai", "yandex", "user-skills"],
|
|
394
456
|
},
|
|
395
457
|
files: {
|
|
396
458
|
mode: "locked",
|
|
@@ -404,7 +466,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
404
466
|
suggestions: true,
|
|
405
467
|
},
|
|
406
468
|
skills: {
|
|
407
|
-
enabled: ["education", "open-data", "geo", "personal-docs", "reports", "local-model", "local-files", "browser-agent", "yandex-services"],
|
|
469
|
+
enabled: ["education", "open-data", "geo", "personal-docs", "reports", "local-model", "local-files", "browser-agent", "yandex-services", "user-skills"],
|
|
408
470
|
},
|
|
409
471
|
cloud: {
|
|
410
472
|
activeProvider: "",
|
|
@@ -768,7 +830,7 @@ Usage:
|
|
|
768
830
|
iola settings list|get|validate|doctor|init
|
|
769
831
|
iola wiki [open|links]
|
|
770
832
|
iola context list|show|init
|
|
771
|
-
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
833
|
+
iola skills list|show|paths|create|enable|disable|delete|bundles|bundle|doctor
|
|
772
834
|
iola tools list|toolsets|enable|disable|profile
|
|
773
835
|
iola files status|mode|approvals|tree|read|search|write|patch
|
|
774
836
|
iola cloud setup|status|ls|find|upload|download|share|save|backup
|
|
@@ -2949,20 +3011,7 @@ async function handleSkills(args) {
|
|
|
2949
3011
|
const config = await loadConfig();
|
|
2950
3012
|
|
|
2951
3013
|
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
|
-
]);
|
|
3014
|
+
printSkillsList(listSkills(config), config);
|
|
2966
3015
|
return;
|
|
2967
3016
|
}
|
|
2968
3017
|
|
|
@@ -3016,6 +3065,30 @@ async function handleSkills(args) {
|
|
|
3016
3065
|
return;
|
|
3017
3066
|
}
|
|
3018
3067
|
|
|
3068
|
+
if (action === "create" || action === "new") {
|
|
3069
|
+
const options = parseOptions(args.slice(2));
|
|
3070
|
+
const result = await userSkillCreate({
|
|
3071
|
+
name,
|
|
3072
|
+
description: options.description || "",
|
|
3073
|
+
instructions: options.instructions || options.text || options.prompt || options._.join(" "),
|
|
3074
|
+
tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
|
|
3075
|
+
enable: Boolean(options.enable),
|
|
3076
|
+
overwrite: Boolean(options.force),
|
|
3077
|
+
confirm: true,
|
|
3078
|
+
});
|
|
3079
|
+
console.log(`Skill создан: ${result.name}`);
|
|
3080
|
+
console.log(`Файл: ${result.file}`);
|
|
3081
|
+
if (result.enabled) console.log("Skill включен.");
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
if (action === "delete" || action === "remove" || action === "rm") {
|
|
3086
|
+
const options = parseOptions(args.slice(2));
|
|
3087
|
+
const result = await userSkillDelete(name, { confirm: Boolean(options.yes || options.force) });
|
|
3088
|
+
console.log(`Skill удален: ${result.name}`);
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3019
3092
|
if (action === "enable" || action === "disable") {
|
|
3020
3093
|
if (!name) throw new Error("Имя skill обязательно.");
|
|
3021
3094
|
const enabled = new Set(config.skills?.enabled || []);
|
|
@@ -3026,7 +3099,7 @@ async function handleSkills(args) {
|
|
|
3026
3099
|
return;
|
|
3027
3100
|
}
|
|
3028
3101
|
|
|
3029
|
-
throw new Error("Команды skills: list, paths, show NAME, enable NAME, disable NAME, bundles, bundle enable NAME, doctor.");
|
|
3102
|
+
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
3103
|
}
|
|
3031
3104
|
|
|
3032
3105
|
async function handleTools(args) {
|
|
@@ -3322,6 +3395,11 @@ async function handleYandex(args) {
|
|
|
3322
3395
|
return;
|
|
3323
3396
|
}
|
|
3324
3397
|
|
|
3398
|
+
if (action === "contacts-maintenance" || action === "contacts-watch" || action === "contacts-doctor") {
|
|
3399
|
+
await handleYandexContactsMaintenance([target, ...rest].filter(Boolean));
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3325
3403
|
if (action === "enable" || action === "disable") {
|
|
3326
3404
|
const services = [target, ...rest].filter((item) => item && !String(item).startsWith("--"));
|
|
3327
3405
|
if (services.length === 0) throw new Error("Укажите сервисы. Пример: iola yandex enable disk mail calendar");
|
|
@@ -3358,6 +3436,7 @@ async function handleYandex(args) {
|
|
|
3358
3436
|
iola yandex status|doctor
|
|
3359
3437
|
iola yandex services
|
|
3360
3438
|
iola yandex mail-watch on|off|status|tick [--minutes 5]
|
|
3439
|
+
iola yandex contacts-maintenance on|off|status|tick [--days 7] [--backup]
|
|
3361
3440
|
iola yandex enable disk mail calendar
|
|
3362
3441
|
iola yandex disable mail
|
|
3363
3442
|
iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
|
|
@@ -3428,6 +3507,55 @@ async function handleYandexMailWatch(args = []) {
|
|
|
3428
3507
|
throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
|
|
3429
3508
|
}
|
|
3430
3509
|
|
|
3510
|
+
async function handleYandexContactsMaintenance(args = []) {
|
|
3511
|
+
const [action = "status", ...rest] = args;
|
|
3512
|
+
const options = parseOptions(rest);
|
|
3513
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3514
|
+
const days = Math.max(1, Number(options.days || options.interval || rest.find((item) => /^\d+$/u.test(String(item))) || 7));
|
|
3515
|
+
const result = await yandexContactsMaintenanceEnable(days, { backup: Boolean(options.backup) });
|
|
3516
|
+
console.log(`Проверка контактов включена: каждые ${days} дней.`);
|
|
3517
|
+
console.log(`Backup на Диск: ${result.backup ? "yes" : "no"}.`);
|
|
3518
|
+
console.log("Для работы по расписанию должен запускаться cron tick: вручную, через daemon или Windows Task Scheduler.");
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3522
|
+
await yandexContactsMaintenanceDisable();
|
|
3523
|
+
console.log("Проверка контактов выключена.");
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3527
|
+
const result = await yandexContactsMaintenanceTick({ force: true, backup: options.backup });
|
|
3528
|
+
if (!result.enabled) {
|
|
3529
|
+
console.log("Проверка контактов выключена.");
|
|
3530
|
+
} else {
|
|
3531
|
+
printKeyValue({
|
|
3532
|
+
status: "ok",
|
|
3533
|
+
contacts: result.total,
|
|
3534
|
+
incomplete: result.incomplete,
|
|
3535
|
+
duplicateGroups: result.duplicateGroups,
|
|
3536
|
+
backup: result.backupRemote || "-",
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
if (action === "status" || action === "doctor") {
|
|
3542
|
+
const config = await loadConfig();
|
|
3543
|
+
const maintenance = config.yandex?.contactsMaintenance || {};
|
|
3544
|
+
printKeyValue({
|
|
3545
|
+
enabled: maintenance.enabled ? "yes" : "no",
|
|
3546
|
+
days: maintenance.days || "-",
|
|
3547
|
+
backup: maintenance.backup ? "yes" : "no",
|
|
3548
|
+
lastRunAt: maintenance.lastRunAt || "-",
|
|
3549
|
+
lastTotal: maintenance.lastTotal || "-",
|
|
3550
|
+
lastIncomplete: maintenance.lastIncomplete || "-",
|
|
3551
|
+
lastDuplicateGroups: maintenance.lastDuplicateGroups || "-",
|
|
3552
|
+
cron: listCronJobs().some((job) => job.command === "yandex contacts-maintenance tick") ? "yes" : "no",
|
|
3553
|
+
});
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
throw new Error("Команды: iola yandex contacts-maintenance on --days 7 [--backup] | off | status | tick");
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3431
3559
|
async function setupYandexConnector(args = []) {
|
|
3432
3560
|
const options = parseOptions(args);
|
|
3433
3561
|
const config = await loadConfig();
|
|
@@ -3999,15 +4127,27 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
3999
4127
|
if (tool === "yandex_disk_ls") return yandexDiskList(args.path || args.remotePath || CLOUD_DEFAULT_REMOTE_DIR, { allowMissingRoot: true });
|
|
4000
4128
|
if (tool === "yandex_disk_mkdir") return cloudCreateFolder("yandex-disk", args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/Новая папка`);
|
|
4001
4129
|
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 });
|
|
4130
|
+
if (tool === "yandex_disk_stat") return yandexDiskStat(args.path || args.remotePath || CLOUD_DEFAULT_REMOTE_DIR);
|
|
4131
|
+
if (tool === "yandex_disk_exists") return yandexDiskExists(args.path || args.remotePath || CLOUD_DEFAULT_REMOTE_DIR);
|
|
4132
|
+
if (tool === "yandex_disk_read_text") return yandexDiskReadText(args.path || args.remotePath, { maxBytes: args.maxBytes || args["max-bytes"] });
|
|
4002
4133
|
if (tool === "yandex_disk_save_text") return yandexDiskSaveText(args.text || args.content || "", args.path || args.remotePath);
|
|
4003
4134
|
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
4135
|
if (tool === "yandex_disk_download") return yandexDiskDownload(args.remotePath || args.path, args.outputPath || args.output || path.basename(args.remotePath || args.path || "download"));
|
|
4136
|
+
if (tool === "yandex_disk_move") return yandexDiskMove(args.from || args.source || args.path, args.to || args.target || args.remotePath, args);
|
|
4137
|
+
if (tool === "yandex_disk_copy") return yandexDiskCopy(args.from || args.source || args.path, args.to || args.target || args.remotePath, args);
|
|
4138
|
+
if (tool === "yandex_disk_rename") return yandexDiskRename(args.path || args.remotePath, args.name || args.newName || args.to, args);
|
|
4005
4139
|
if (tool === "yandex_disk_share") {
|
|
4006
4140
|
if (!args.confirm) throw new Error("Для публикации ссылки нужен аргумент confirm=true.");
|
|
4007
4141
|
return yandexDiskShare(args.remotePath || args.path);
|
|
4008
4142
|
}
|
|
4143
|
+
if (tool === "yandex_disk_share_qr") return yandexDiskShareWithQr(args.remotePath || args.path, args);
|
|
4144
|
+
if (tool === "yandex_disk_share_email") return yandexDiskShareEmail(args);
|
|
4145
|
+
if (tool === "yandex_disk_package_share_email") return yandexDiskPackageShareEmail(args);
|
|
4009
4146
|
if (tool === "yandex_disk_unshare") return yandexDiskUnshare(args.remotePath || args.path);
|
|
4010
4147
|
if (tool === "yandex_disk_delete") return yandexDiskDelete(args.remotePath || args.path, args);
|
|
4148
|
+
if (tool === "yandex_disk_trash_list") return yandexDiskTrashList(args.path || args.remotePath || "", args);
|
|
4149
|
+
if (tool === "yandex_disk_restore") return yandexDiskRestore(args.path || args.remotePath, args);
|
|
4150
|
+
if (tool === "yandex_disk_empty_trash") return yandexDiskEmptyTrash(args);
|
|
4011
4151
|
if (tool === "yandex_mail_status") return yandexMailStatus();
|
|
4012
4152
|
if (tool === "yandex_mail_folders") return yandexMailFolders();
|
|
4013
4153
|
if (tool === "yandex_mail_list") return yandexMailList({ mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), limit: args.limit || 10, unread: Boolean(args.unread) });
|
|
@@ -4025,13 +4165,55 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4025
4165
|
if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
|
|
4026
4166
|
if (tool === "yandex_mail_create_task") return yandexMailCreateTask(args.uid || args.id, args);
|
|
4027
4167
|
if (tool === "yandex_calendar_status") return yandexCalendarStatus();
|
|
4168
|
+
if (tool === "yandex_calendar_calendars") return yandexCalendarCalendars(args);
|
|
4028
4169
|
if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
|
|
4029
4170
|
if (tool === "yandex_calendar_list") return yandexCalendarList(args);
|
|
4171
|
+
if (tool === "yandex_calendar_get") return yandexCalendarGet(args.query || args.uid || args.title || "", args);
|
|
4172
|
+
if (tool === "yandex_calendar_search") return yandexCalendarSearch(args.query || args.title || "", args);
|
|
4173
|
+
if (tool === "yandex_calendar_update") return yandexCalendarUpdate(args.query || args.uid || args.title || "", args);
|
|
4174
|
+
if (tool === "yandex_calendar_move") return yandexCalendarMove(args.query || args.uid || args.title || "", args);
|
|
4175
|
+
if (tool === "yandex_calendar_delete") return yandexCalendarDelete(args.query || args.uid || args.title || "", args);
|
|
4176
|
+
if (tool === "yandex_calendar_create_recurring_event") return yandexCalendarCreateRecurringEvent(args);
|
|
4177
|
+
if (tool === "yandex_calendar_add_reminder") return yandexCalendarAddReminder(args.query || args.uid || args.title || "", args);
|
|
4178
|
+
if (tool === "yandex_docs_status") return yandexDocsStatus();
|
|
4179
|
+
if (tool === "yandex_docs_list") return yandexDocsList(args);
|
|
4180
|
+
if (tool === "yandex_docs_find") return yandexDocsFind(args.query || args.name || "", args);
|
|
4181
|
+
if (tool === "yandex_docs_create_text") return yandexDocsCreateText(args);
|
|
4182
|
+
if (tool === "yandex_docs_read") return yandexDocsRead(args.path || args.remotePath || args.query || args.name, args);
|
|
4183
|
+
if (tool === "yandex_docs_share") return yandexDocsShare(args.path || args.remotePath || args.query || args.name, args);
|
|
4184
|
+
if (tool === "yandex_docs_rename") return yandexDocsRename(args.path || args.remotePath || args.query || args.name, args.name || args.newName || args.to, args);
|
|
4185
|
+
if (tool === "yandex_docs_delete") return yandexDocsDelete(args.path || args.remotePath || args.query || args.name, args);
|
|
4186
|
+
if (tool === "yandex_docs_save_answer") return yandexDocsCreateText({ ...args, text: args.text || args.answer || args.content });
|
|
4030
4187
|
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
4031
4188
|
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
4032
4189
|
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
4190
|
+
if (tool === "yandex_contacts_get") return yandexContactsGet(args.query || args.name || args.email || args.phone || "", args);
|
|
4033
4191
|
if (tool === "yandex_contacts_create") return yandexContactsCreate(args);
|
|
4192
|
+
if (tool === "yandex_contacts_update") return yandexContactsUpdate(args.query || args.name || args.email || "", args);
|
|
4193
|
+
if (tool === "yandex_contacts_delete") return yandexContactsDelete(args.query || args.name || args.email || "", args);
|
|
4034
4194
|
if (tool === "yandex_contacts_add_email") return yandexContactsAddEmail(args.query || args.name || "", args.email, args);
|
|
4195
|
+
if (tool === "yandex_contacts_add_phone") return yandexContactsUpdate(args.query || args.name || "", { ...args, phone: args.phone, mode: "add-phone" });
|
|
4196
|
+
if (tool === "yandex_contacts_add_address") return yandexContactsUpdate(args.query || args.name || "", { ...args, address: args.address, mode: "add-address" });
|
|
4197
|
+
if (tool === "yandex_contacts_add_note") return yandexContactsUpdate(args.query || args.name || "", { ...args, note: args.note || args.text, mode: "add-note" });
|
|
4198
|
+
if (tool === "yandex_contacts_add_birthday") return yandexContactsUpdate(args.query || args.name || "", { ...args, birthday: args.birthday || args.date, mode: "add-birthday" });
|
|
4199
|
+
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" });
|
|
4200
|
+
if (tool === "yandex_contacts_remove_email") return yandexContactsUpdate(args.query || args.name || "", { ...args, removeEmail: args.email || true, mode: "remove-email" });
|
|
4201
|
+
if (tool === "yandex_contacts_remove_phone") return yandexContactsUpdate(args.query || args.name || "", { ...args, removePhone: args.phone || true, mode: "remove-phone" });
|
|
4202
|
+
if (tool === "yandex_contacts_export_vcard") return yandexContactsExport("vcard", args);
|
|
4203
|
+
if (tool === "yandex_contacts_export_csv") return yandexContactsExport("csv", args);
|
|
4204
|
+
if (tool === "yandex_contacts_import_vcard") return yandexContactsImport("vcard", args);
|
|
4205
|
+
if (tool === "yandex_contacts_import_csv") return yandexContactsImport("csv", args);
|
|
4206
|
+
if (tool === "yandex_contacts_find_incomplete") return yandexContactsFindIncomplete(args);
|
|
4207
|
+
if (tool === "yandex_contacts_find_duplicates") return yandexContactsFindDuplicates(args);
|
|
4208
|
+
if (tool === "yandex_contacts_backup_to_disk") return yandexContactsBackupToDisk(args);
|
|
4209
|
+
if (tool === "yandex_contacts_birthdays_to_calendar") return yandexContactsBirthdaysToCalendar(args);
|
|
4210
|
+
if (tool === "yandex_contact_send_mail") return yandexContactSendMail(args);
|
|
4211
|
+
if (tool === "yandex_contact_send_disk_link_qr") return yandexContactSendDiskLinkQr(args);
|
|
4212
|
+
if (tool === "yandex_contact_create_disk_folder") return yandexContactCreateDiskFolder(args);
|
|
4213
|
+
if (tool === "yandex_contact_create_calendar_event") return yandexContactCreateCalendarEvent(args);
|
|
4214
|
+
if (tool === "yandex_contact_create_telemost_event") return yandexContactCreateTelemostEvent(args);
|
|
4215
|
+
if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
|
|
4216
|
+
if (tool === "yandex_telemost_status") return yandexTelemostStatus();
|
|
4035
4217
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
4036
4218
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
4037
4219
|
}
|
|
@@ -4203,6 +4385,78 @@ async function yandexMailWatchDisable() {
|
|
|
4203
4385
|
return { enabled: false };
|
|
4204
4386
|
}
|
|
4205
4387
|
|
|
4388
|
+
async function yandexContactsMaintenanceEnable(days = 7, options = {}) {
|
|
4389
|
+
const config = await loadConfig();
|
|
4390
|
+
const safeDays = Math.max(1, Number(days || 7));
|
|
4391
|
+
await saveConfig({
|
|
4392
|
+
yandex: {
|
|
4393
|
+
...(config.yandex || {}),
|
|
4394
|
+
contactsMaintenance: {
|
|
4395
|
+
...(config.yandex?.contactsMaintenance || {}),
|
|
4396
|
+
enabled: true,
|
|
4397
|
+
days: safeDays,
|
|
4398
|
+
backup: Boolean(options.backup),
|
|
4399
|
+
updatedAt: new Date().toISOString(),
|
|
4400
|
+
},
|
|
4401
|
+
},
|
|
4402
|
+
});
|
|
4403
|
+
await upsertCronJob(`каждые ${safeDays} дней`, "yandex contacts-maintenance tick", { replaceCommand: true });
|
|
4404
|
+
return { enabled: true, days: safeDays, backup: Boolean(options.backup) };
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
async function yandexContactsMaintenanceDisable() {
|
|
4408
|
+
const config = await loadConfig();
|
|
4409
|
+
await saveConfig({
|
|
4410
|
+
yandex: {
|
|
4411
|
+
...(config.yandex || {}),
|
|
4412
|
+
contactsMaintenance: {
|
|
4413
|
+
...(config.yandex?.contactsMaintenance || {}),
|
|
4414
|
+
enabled: false,
|
|
4415
|
+
updatedAt: new Date().toISOString(),
|
|
4416
|
+
},
|
|
4417
|
+
},
|
|
4418
|
+
});
|
|
4419
|
+
deleteCronJobsByCommand("yandex contacts-maintenance tick");
|
|
4420
|
+
return { enabled: false };
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
async function yandexContactsMaintenanceTick(options = {}) {
|
|
4424
|
+
const config = await loadConfig();
|
|
4425
|
+
const maintenance = config.yandex?.contactsMaintenance || {};
|
|
4426
|
+
if (!maintenance.enabled && !options.force) return { enabled: false };
|
|
4427
|
+
const contacts = await yandexContactsList({ limit: Number(options.limit || 1000) });
|
|
4428
|
+
const incomplete = await yandexContactsFindIncomplete({ limit: Number(options.limit || 1000) });
|
|
4429
|
+
const duplicates = await yandexContactsFindDuplicates({ limit: Number(options.limit || 1000) });
|
|
4430
|
+
let backupRemote = "";
|
|
4431
|
+
if (options.backup || maintenance.backup) {
|
|
4432
|
+
const backup = await yandexContactsBackupToDisk({ format: "csv", confirm: true, limit: Number(options.limit || 1000) });
|
|
4433
|
+
backupRemote = backup.remote || "";
|
|
4434
|
+
}
|
|
4435
|
+
await saveConfig({
|
|
4436
|
+
yandex: {
|
|
4437
|
+
...(config.yandex || {}),
|
|
4438
|
+
contactsMaintenance: {
|
|
4439
|
+
...maintenance,
|
|
4440
|
+
enabled: maintenance.enabled !== false,
|
|
4441
|
+
lastRunAt: new Date().toISOString(),
|
|
4442
|
+
lastTotal: contacts.length,
|
|
4443
|
+
lastIncomplete: incomplete.length,
|
|
4444
|
+
lastDuplicateGroups: duplicates.length,
|
|
4445
|
+
lastBackupRemote: backupRemote || maintenance.lastBackupRemote || "",
|
|
4446
|
+
},
|
|
4447
|
+
},
|
|
4448
|
+
});
|
|
4449
|
+
return {
|
|
4450
|
+
enabled: true,
|
|
4451
|
+
total: contacts.length,
|
|
4452
|
+
incomplete: incomplete.length,
|
|
4453
|
+
duplicateGroups: duplicates.length,
|
|
4454
|
+
backupRemote,
|
|
4455
|
+
incompletePreview: incomplete.slice(0, 10),
|
|
4456
|
+
duplicatePreview: duplicates.slice(0, 10),
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4206
4460
|
async function yandexMailRead(uid, options = {}) {
|
|
4207
4461
|
if (!uid) throw new Error("UID письма обязателен.");
|
|
4208
4462
|
const { token, email } = await yandexMailCredentials();
|
|
@@ -4762,29 +5016,53 @@ async function yandexDavRequest(url, token, options = {}) {
|
|
|
4762
5016
|
|
|
4763
5017
|
async function yandexCalendarStatus() {
|
|
4764
5018
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4765
|
-
const
|
|
5019
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5020
|
+
const selected = pickYandexCalendar(calendars, {});
|
|
5021
|
+
const url = selected.url;
|
|
4766
5022
|
const text = await yandexDavRequest(url, token, { method: "PROPFIND", headers: { Depth: "0" }, xml: true, body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/></d:prop></d:propfind>" });
|
|
4767
|
-
return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || "calendar" };
|
|
5023
|
+
return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || selected.name || "calendar", calendars: calendars.length };
|
|
5024
|
+
}
|
|
5025
|
+
|
|
5026
|
+
async function yandexCalendarCalendars(args = {}) {
|
|
5027
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5028
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5029
|
+
return calendars.slice(0, Number(args.limit || 50));
|
|
4768
5030
|
}
|
|
4769
5031
|
|
|
4770
5032
|
async function yandexCalendarCreateEvent(args = {}) {
|
|
4771
5033
|
if (!args.confirm) throw new Error("Для создания события в календаре нужен аргумент confirm=true.");
|
|
4772
5034
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4773
|
-
const baseUrl = await yandexCalendarBaseUrl(token);
|
|
5035
|
+
const baseUrl = await yandexCalendarBaseUrl(token, args);
|
|
4774
5036
|
const uid = `${randomUUID()}@iola-cli`;
|
|
4775
5037
|
const start = toIcsDate(args.start || args.date || new Date(Date.now() + 3600000).toISOString());
|
|
4776
5038
|
const end = toIcsDate(args.end || new Date(Date.now() + 7200000).toISOString());
|
|
4777
5039
|
const summary = args.title || args.summary || "Событие IOLA";
|
|
4778
5040
|
const description = args.description || "";
|
|
4779
|
-
const ics = buildIcsEvent({
|
|
5041
|
+
const ics = buildIcsEvent({
|
|
5042
|
+
uid,
|
|
5043
|
+
start,
|
|
5044
|
+
end,
|
|
5045
|
+
summary,
|
|
5046
|
+
description,
|
|
5047
|
+
location: args.location || "",
|
|
5048
|
+
attendees: args.attendees || args.to || [],
|
|
5049
|
+
rrule: args.rrule || buildIcsRrule(args),
|
|
5050
|
+
reminders: normalizeCalendarReminders(args.reminders || args.reminder || args.alarm),
|
|
5051
|
+
});
|
|
4780
5052
|
const url = `${baseUrl}${encodeURIComponent(uid)}.ics`;
|
|
4781
5053
|
await yandexDavRequest(url, token, { method: "PUT", ics: true, body: ics, timeout: 45000 });
|
|
4782
|
-
return { status: "created", uid, title: summary, start: args.start || args.date || "", url };
|
|
5054
|
+
return { status: "calendar-event-created", uid, title: summary, start: args.start || args.date || "", end: args.end || "", url };
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
async function yandexCalendarCreateRecurringEvent(args = {}) {
|
|
5058
|
+
if (!args.confirm) throw new Error("Для создания повторяющегося события нужен аргумент confirm=true.");
|
|
5059
|
+
const rrule = args.rrule || buildIcsRrule({ ...args, repeat: args.repeat || args.frequency || "weekly" });
|
|
5060
|
+
return yandexCalendarCreateEvent({ ...args, rrule, confirm: true });
|
|
4783
5061
|
}
|
|
4784
5062
|
|
|
4785
5063
|
async function yandexCalendarList(args = {}) {
|
|
4786
5064
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4787
|
-
const baseUrl = await yandexCalendarBaseUrl(token);
|
|
5065
|
+
const baseUrl = await yandexCalendarBaseUrl(token, args);
|
|
4788
5066
|
const start = toIcsDate(args.start || new Date().toISOString());
|
|
4789
5067
|
const end = toIcsDate(args.end || new Date(Date.now() + 14 * 86400000).toISOString());
|
|
4790
5068
|
const body = `<?xml version="1.0"?>
|
|
@@ -4793,10 +5071,113 @@ async function yandexCalendarList(args = {}) {
|
|
|
4793
5071
|
<c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VEVENT"><c:time-range start="${start}" end="${end}"/></c:comp-filter></c:comp-filter></c:filter>
|
|
4794
5072
|
</c:calendar-query>`;
|
|
4795
5073
|
const text = await yandexDavRequest(baseUrl, token, { method: "REPORT", headers: { Depth: "1" }, xml: true, body, timeout: 45000 });
|
|
4796
|
-
return parseIcsEvents(text).slice(0, Number(args.limit || 20));
|
|
5074
|
+
return parseIcsEvents(text, { baseUrl }).slice(0, Number(args.limit || 20));
|
|
4797
5075
|
}
|
|
4798
5076
|
|
|
4799
|
-
async function
|
|
5077
|
+
async function yandexCalendarSearch(query, args = {}) {
|
|
5078
|
+
const needle = normalizeGeoText(query || args.query || args.title || "");
|
|
5079
|
+
const rows = await yandexCalendarList({
|
|
5080
|
+
...args,
|
|
5081
|
+
start: args.start || new Date(Date.now() - 90 * 86400000).toISOString(),
|
|
5082
|
+
end: args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
|
|
5083
|
+
limit: Math.max(200, Number(args.limit || 20) * 10),
|
|
5084
|
+
});
|
|
5085
|
+
if (!needle) return rows.slice(0, Number(args.limit || 20));
|
|
5086
|
+
return rows.filter((row) => normalizeGeoText([
|
|
5087
|
+
row.title,
|
|
5088
|
+
row.description,
|
|
5089
|
+
row.location,
|
|
5090
|
+
row.uid,
|
|
5091
|
+
].filter(Boolean).join(" ")).includes(needle)).slice(0, Number(args.limit || 20));
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
async function yandexCalendarGet(query, args = {}) {
|
|
5095
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5096
|
+
if (resolved.status !== "ok") return resolved;
|
|
5097
|
+
return stripCalendarPrivateFields(resolved.event);
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
async function yandexCalendarUpdate(query, args = {}) {
|
|
5101
|
+
if (!args.confirm) throw new Error("Для изменения события нужен аргумент confirm=true.");
|
|
5102
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5103
|
+
if (resolved.status !== "ok") return resolved;
|
|
5104
|
+
const event = resolved.event;
|
|
5105
|
+
const start = toIcsDate(args.start || args.date || event.startIso || event.start);
|
|
5106
|
+
const end = toIcsDate(args.end || event.endIso || event.end || new Date(new Date(args.start || event.startIso || Date.now()).getTime() + 3600000).toISOString());
|
|
5107
|
+
const next = buildIcsEvent({
|
|
5108
|
+
uid: event.uid || `${randomUUID()}@iola-cli`,
|
|
5109
|
+
start,
|
|
5110
|
+
end,
|
|
5111
|
+
summary: args.title || args.summary || event.title || "Событие IOLA",
|
|
5112
|
+
description: args.description ?? event.description ?? "",
|
|
5113
|
+
location: args.location ?? event.location ?? "",
|
|
5114
|
+
attendees: args.attendees || args.to || event.attendees || [],
|
|
5115
|
+
rrule: args.rrule === null ? "" : (args.rrule || event.rrule || buildIcsRrule(args)),
|
|
5116
|
+
reminders: args.reminders || args.reminder || args.alarm ? normalizeCalendarReminders(args.reminders || args.reminder || args.alarm) : event.reminders || [],
|
|
5117
|
+
});
|
|
5118
|
+
await yandexDavRequest(event.url, await requireYandexOAuthToken("organizer", "Яндекс Календарь"), { method: "PUT", ics: true, body: next, timeout: 45000 });
|
|
5119
|
+
return { status: "calendar-event-updated", uid: event.uid, title: args.title || event.title, href: event.href };
|
|
5120
|
+
}
|
|
5121
|
+
|
|
5122
|
+
async function yandexCalendarMove(query, args = {}) {
|
|
5123
|
+
if (!args.confirm) throw new Error("Для переноса события нужен аргумент confirm=true.");
|
|
5124
|
+
const dateTime = args.start || args.date ? {} : extractDateTimeFromText(args.text || args.source_question || "");
|
|
5125
|
+
const start = args.start || args.date || dateTime.start;
|
|
5126
|
+
if (!start) throw new Error("Укажите новую дату/время события.");
|
|
5127
|
+
const end = args.end || dateTime.end || new Date(new Date(start).getTime() + 3600000).toISOString();
|
|
5128
|
+
return yandexCalendarUpdate(query, { ...args, start, end, confirm: true });
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
async function yandexCalendarAddReminder(query, args = {}) {
|
|
5132
|
+
if (!args.confirm) throw new Error("Для добавления напоминания нужен аргумент confirm=true.");
|
|
5133
|
+
const minutes = Number(args.minutes || args.beforeMinutes || String(args.reminder || "").match(/\d+/u)?.[0] || 15);
|
|
5134
|
+
return yandexCalendarUpdate(query, { ...args, reminders: [`-${minutes}`], confirm: true });
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
async function yandexCalendarDelete(query, args = {}) {
|
|
5138
|
+
if (!args.confirm) throw new Error("Для удаления события нужен аргумент confirm=true.");
|
|
5139
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5140
|
+
if (resolved.status !== "ok") return resolved;
|
|
5141
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5142
|
+
await yandexDavRequest(resolved.event.url, token, { method: "DELETE", timeout: 45000 });
|
|
5143
|
+
return { status: "calendar-event-deleted", uid: resolved.event.uid, title: resolved.event.title, href: resolved.event.href };
|
|
5144
|
+
}
|
|
5145
|
+
|
|
5146
|
+
async function resolveYandexCalendarEvent(query, args = {}) {
|
|
5147
|
+
const uid = String(args.uid || "").trim();
|
|
5148
|
+
const href = String(args.href || "").trim();
|
|
5149
|
+
if (href) {
|
|
5150
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5151
|
+
const url = new URL(href, "https://caldav.yandex.ru/").toString();
|
|
5152
|
+
const ics = await yandexDavRequest(url, token, { method: "GET", timeout: 45000 });
|
|
5153
|
+
const event = parseIcsEvents(ics, { baseUrl: url.replace(/[^/]+$/u, "") })[0];
|
|
5154
|
+
return event ? { status: "ok", event: { ...event, href, url, ics } } : { status: "not-found", query: href };
|
|
5155
|
+
}
|
|
5156
|
+
const needle = normalizeGeoText(uid || query || args.query || args.title || "");
|
|
5157
|
+
const rows = await yandexCalendarSearch("", {
|
|
5158
|
+
...args,
|
|
5159
|
+
start: args.startRange || args.start || new Date(Date.now() - 365 * 86400000).toISOString(),
|
|
5160
|
+
end: args.endRange || args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
|
|
5161
|
+
limit: 500,
|
|
5162
|
+
});
|
|
5163
|
+
const matches = rows.filter((row) => {
|
|
5164
|
+
if (uid && row.uid === uid) return true;
|
|
5165
|
+
const haystack = normalizeGeoText([row.title, row.description, row.location, row.start, row.startIso, row.end, row.endIso, row.uid, row.href].filter(Boolean).join(" "));
|
|
5166
|
+
return needle && haystack.includes(needle);
|
|
5167
|
+
});
|
|
5168
|
+
if (!matches.length) return { status: "not-found", kind: "calendar-event", query: query || uid };
|
|
5169
|
+
if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", kind: "calendar-event", query: query || uid, events: matches.slice(0, 10).map(stripCalendarPrivateFields) };
|
|
5170
|
+
return { status: "ok", event: matches[0] };
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
async function yandexCalendarBaseUrl(token, args = {}) {
|
|
5174
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5175
|
+
const selected = pickYandexCalendar(calendars, args);
|
|
5176
|
+
if (!selected) throw new Error("В Яндекс Календаре не найдена календарная коллекция.");
|
|
5177
|
+
return selected.url;
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
async function yandexCalendarCollections(token) {
|
|
4800
5181
|
const root = "https://caldav.yandex.ru/";
|
|
4801
5182
|
const principalXml = await yandexDavRequest(root, token, {
|
|
4802
5183
|
method: "PROPFIND",
|
|
@@ -4820,9 +5201,99 @@ async function yandexCalendarBaseUrl(token) {
|
|
|
4820
5201
|
xml: true,
|
|
4821
5202
|
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>",
|
|
4822
5203
|
});
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
5204
|
+
return extractCalendarCollections(listXml).map((row) => ({ ...row, url: new URL(row.href, root).toString() }));
|
|
5205
|
+
}
|
|
5206
|
+
|
|
5207
|
+
function pickYandexCalendar(calendars, args = {}) {
|
|
5208
|
+
const query = normalizeGeoText(args.calendar || args.calendarName || args.name || "");
|
|
5209
|
+
if (query) {
|
|
5210
|
+
const found = calendars.find((row) => normalizeGeoText(`${row.name} ${row.href}`).includes(query));
|
|
5211
|
+
if (found) return found;
|
|
5212
|
+
}
|
|
5213
|
+
return calendars.find((row) => !/\/(?:inbox|outbox)\//iu.test(row.href)) || calendars[0] || null;
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
async function yandexDocsStatus() {
|
|
5217
|
+
const info = await yandexDiskInfo();
|
|
5218
|
+
await ensureYandexDiskDir(`${CLOUD_DEFAULT_REMOTE_DIR}/docs`, { allowExisting: true });
|
|
5219
|
+
return { status: "ok", provider: "yandex-disk", folder: `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, totalSpace: info.totalSpace, usedSpace: info.usedSpace };
|
|
5220
|
+
}
|
|
5221
|
+
|
|
5222
|
+
async function yandexDocsList(args = {}) {
|
|
5223
|
+
const folder = normalizeCloudUserPath(args.path || args.folder || `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, "yandex-disk");
|
|
5224
|
+
const rows = await yandexDiskListRecursive(folder, { depth: Number(args.depth || 3), limit: Number(args.limit || 100) }).catch(() => []);
|
|
5225
|
+
return rows.filter(isYandexDocumentResource).slice(0, Number(args.limit || 50));
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
async function yandexDocsFind(query, args = {}) {
|
|
5229
|
+
const needle = normalizeGeoText(query || args.query || "");
|
|
5230
|
+
const rows = await yandexDocsList({ ...args, limit: Math.max(200, Number(args.limit || 20) * 10) });
|
|
5231
|
+
if (!needle) return rows.slice(0, Number(args.limit || 20));
|
|
5232
|
+
return rows.filter((row) => normalizeGeoText(`${row.name} ${row.path}`).includes(needle)).slice(0, Number(args.limit || 20));
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5235
|
+
async function yandexDocsCreateText(args = {}) {
|
|
5236
|
+
if (!args.confirm) throw new Error("Для создания документа нужен аргумент confirm=true.");
|
|
5237
|
+
const text = String(args.text || args.content || "").trim();
|
|
5238
|
+
if (!text) throw new Error("Текст документа пустой.");
|
|
5239
|
+
const ext = normalizeYandexDocExtension(args.format || args.ext || path.extname(args.path || args.remotePath || ""));
|
|
5240
|
+
const title = slugYandexDiskName(args.title || args.name || `document-${timestampForFile()}`);
|
|
5241
|
+
const remotePath = normalizeCloudUserPath(args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/docs/${title}${title.endsWith(ext) ? "" : ext}`, "yandex-disk");
|
|
5242
|
+
const saved = await yandexDiskSaveText(text, remotePath);
|
|
5243
|
+
return { ...saved, status: "document-created", title: path.posix.basename(remotePath), remote: remotePath };
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
async function yandexDocsRead(target, args = {}) {
|
|
5247
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5248
|
+
if (doc.status !== "ok") return doc;
|
|
5249
|
+
if (!/\.(txt|md|csv|json|html)$/iu.test(doc.path)) {
|
|
5250
|
+
return { status: "binary-document", remote: doc.path, name: doc.name, message: "Этот документ не текстовый. Его можно скачать, переименовать, удалить или опубликовать ссылкой." };
|
|
5251
|
+
}
|
|
5252
|
+
return yandexDiskReadText(doc.path, args);
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5255
|
+
async function yandexDocsShare(target, args = {}) {
|
|
5256
|
+
if (!args.confirm) throw new Error("Для публикации документа нужен аргумент confirm=true.");
|
|
5257
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5258
|
+
if (doc.status !== "ok") return doc;
|
|
5259
|
+
return yandexDiskShareWithQr(doc.path, { ...args, confirm: true });
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5262
|
+
async function yandexDocsRename(target, newName, args = {}) {
|
|
5263
|
+
if (!args.confirm) throw new Error("Для переименования документа нужен аргумент confirm=true.");
|
|
5264
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5265
|
+
if (doc.status !== "ok") return doc;
|
|
5266
|
+
if (!newName) throw new Error("Укажите новое имя документа.");
|
|
5267
|
+
return yandexDiskRename(doc.path, newName, { ...args, confirm: true, overwrite: args.overwrite !== false });
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
async function yandexDocsDelete(target, args = {}) {
|
|
5271
|
+
if (!args.confirm) throw new Error("Для удаления документа нужен аргумент confirm=true.");
|
|
5272
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5273
|
+
if (doc.status !== "ok") return doc;
|
|
5274
|
+
return yandexDiskDelete(doc.path, args);
|
|
5275
|
+
}
|
|
5276
|
+
|
|
5277
|
+
async function resolveYandexDoc(target, args = {}) {
|
|
5278
|
+
const value = String(target || "").trim();
|
|
5279
|
+
if (value.startsWith("/")) {
|
|
5280
|
+
const statRow = await yandexDiskStat(value);
|
|
5281
|
+
return isYandexDocumentResource(statRow) ? { status: "ok", ...statRow } : { status: "not-document", path: value };
|
|
5282
|
+
}
|
|
5283
|
+
const rows = await yandexDocsFind(value, { ...args, limit: 10 });
|
|
5284
|
+
if (!rows.length) return { status: "not-found", query: value };
|
|
5285
|
+
if (rows.length > 1 && !args.selectFirst) return { status: "ambiguous", query: value, docs: rows.slice(0, 10) };
|
|
5286
|
+
return { status: "ok", ...rows[0] };
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
function isYandexDocumentResource(row = {}) {
|
|
5290
|
+
return row.type !== "dir" && /\.(docx|xlsx|pptx|pdf|txt|md|html|csv|json)$/iu.test(row.name || row.path || "");
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
function normalizeYandexDocExtension(value) {
|
|
5294
|
+
const ext = String(value || "").replace(/^\./u, "").toLocaleLowerCase("en-US");
|
|
5295
|
+
if (["txt", "md", "html", "csv", "json"].includes(ext)) return `.${ext}`;
|
|
5296
|
+
return ".md";
|
|
4826
5297
|
}
|
|
4827
5298
|
|
|
4828
5299
|
async function yandexContactsStatus() {
|
|
@@ -4848,9 +5319,9 @@ async function yandexContactsList(args = {}) {
|
|
|
4848
5319
|
|
|
4849
5320
|
async function yandexContactsSearch(query, args = {}) {
|
|
4850
5321
|
const normalized = normalizeGeoText(query);
|
|
4851
|
-
const rows = await yandexContactsList({ limit: Math.max(
|
|
5322
|
+
const rows = await yandexContactsList({ limit: Math.max(1000, Number(args.limit || 20) * 10) });
|
|
4852
5323
|
if (!normalized) return rows.slice(0, Number(args.limit || 20));
|
|
4853
|
-
return rows.filter((row) =>
|
|
5324
|
+
return rows.filter((row) => contactMatchesQuery(row, normalized)).slice(0, Number(args.limit || 20));
|
|
4854
5325
|
}
|
|
4855
5326
|
|
|
4856
5327
|
async function resolveYandexMailRecipientFromContacts(query) {
|
|
@@ -4871,58 +5342,397 @@ async function yandexContactsAddEmail(query, email, args = {}) {
|
|
|
4871
5342
|
if (!args.confirm) throw new Error("Для изменения контакта нужен аргумент confirm=true.");
|
|
4872
5343
|
if (!query) throw new Error("Укажите имя или часть имени контакта.");
|
|
4873
5344
|
if (!email || !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(String(email))) throw new Error("Укажите корректный email.");
|
|
4874
|
-
const
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
const
|
|
5345
|
+
const result = await yandexContactsUpdate(query, { ...args, email, mode: "add-email" });
|
|
5346
|
+
if (result.status === "updated") return { status: "updated", name: result.name, email };
|
|
5347
|
+
return result;
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
async function yandexContactsGet(query, args = {}) {
|
|
5351
|
+
const resolved = await resolveYandexContact(query, args);
|
|
5352
|
+
if (resolved.status !== "ok") return resolved;
|
|
5353
|
+
const { card: _card, ...contact } = resolved.contact;
|
|
5354
|
+
return { status: "ok", ...contact };
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
async function yandexContactsUpdate(query, args = {}) {
|
|
5358
|
+
if (!args.confirm) throw new Error("Для изменения контакта нужен аргумент confirm=true.");
|
|
5359
|
+
if (!query) throw new Error("Укажите имя, email или телефон контакта.");
|
|
5360
|
+
const resolved = await resolveYandexContact(query, args);
|
|
5361
|
+
if (resolved.status !== "ok") return resolved;
|
|
5362
|
+
const contact = resolved.contact;
|
|
5363
|
+
let card = contact.card;
|
|
5364
|
+
if (args.name || args.fullName) {
|
|
5365
|
+
card = upsertVcardProperty(card, "FN", args.name || args.fullName, { replace: true });
|
|
5366
|
+
card = upsertVcardProperty(card, "N", `${escapeVcardValue(args.name || args.fullName)};;;;`, { replace: true, raw: true });
|
|
5367
|
+
}
|
|
5368
|
+
if (args.email) card = upsertVcardProperty(card, "EMAIL;TYPE=INTERNET", String(args.email).trim(), { replace: args.overwriteEmail || args.overwrite || args.mode === "set-email" });
|
|
5369
|
+
if (args.phone) card = upsertVcardProperty(card, "TEL;TYPE=CELL", String(args.phone).trim(), { replace: args.overwritePhone || args.overwrite || args.mode === "set-phone" });
|
|
5370
|
+
if (args.address) card = upsertVcardProperty(card, "ADR;TYPE=HOME", `;;;;${escapeVcardValue(args.address)};;`, { replace: args.overwriteAddress || args.overwrite || args.mode === "set-address", raw: true });
|
|
5371
|
+
if (args.note) card = upsertVcardProperty(card, "NOTE", args.note, { replace: args.overwriteNote || args.overwrite || args.mode === "set-note" });
|
|
5372
|
+
if (args.birthday) card = upsertVcardProperty(card, "BDAY", normalizeVcardBirthday(args.birthday), { replace: true });
|
|
5373
|
+
if (args.org) card = upsertVcardProperty(card, "ORG", args.org, { replace: args.overwriteOrg || args.overwrite || args.mode === "set-org" });
|
|
5374
|
+
if (args.title) card = upsertVcardProperty(card, "TITLE", args.title, { replace: true });
|
|
5375
|
+
if (args.categories || args.group) card = upsertVcardProperty(card, "CATEGORIES", Array.isArray(args.categories) ? args.categories.join(",") : (args.categories || args.group), { replace: Boolean(args.overwriteCategories) });
|
|
5376
|
+
if (args.removeEmail) card = removeVcardProperty(card, "EMAIL", args.removeEmail === true ? "" : args.removeEmail);
|
|
5377
|
+
if (args.removePhone) card = removeVcardProperty(card, "TEL", args.removePhone === true ? "" : args.removePhone);
|
|
5378
|
+
if (card === contact.card) return { status: "unchanged", name: contact.name, email: contact.email, phone: contact.phone };
|
|
5379
|
+
await saveYandexContactCard(contact.href, card);
|
|
5380
|
+
const parsed = parseVCards(card)[0] || {};
|
|
5381
|
+
return { status: "updated", ...parsed, href: contact.href };
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5384
|
+
async function yandexContactsDelete(query, args = {}) {
|
|
5385
|
+
if (!args.confirm) throw new Error("Для удаления контакта нужен аргумент confirm=true.");
|
|
5386
|
+
const resolved = await resolveYandexContact(query, args);
|
|
5387
|
+
if (resolved.status !== "ok") return resolved;
|
|
4881
5388
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4882
|
-
await yandexDavRequest(new URL(contact.href, "https://carddav.yandex.ru/").toString(), token, {
|
|
4883
|
-
|
|
4884
|
-
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4885
|
-
body: updatedCard,
|
|
4886
|
-
timeout: 45000,
|
|
4887
|
-
});
|
|
4888
|
-
return { status: "updated", name: contact.name, email };
|
|
5389
|
+
await yandexDavRequest(new URL(resolved.contact.href, "https://carddav.yandex.ru/").toString(), token, { method: "DELETE", timeout: 45000 });
|
|
5390
|
+
return { status: "deleted", name: resolved.contact.name, email: resolved.contact.email, phone: resolved.contact.phone };
|
|
4889
5391
|
}
|
|
4890
5392
|
|
|
4891
5393
|
async function yandexContactsCreate(args = {}) {
|
|
4892
5394
|
if (!args.confirm) throw new Error("Для создания контакта нужен аргумент confirm=true.");
|
|
4893
5395
|
const name = String(args.name || args.email || "").trim();
|
|
4894
5396
|
const email = String(args.email || "").trim();
|
|
5397
|
+
const phone = String(args.phone || "").trim();
|
|
4895
5398
|
if (!name) throw new Error("Имя контакта обязательно.");
|
|
4896
|
-
if (
|
|
5399
|
+
if (email && !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email)) throw new Error("Укажите корректный email.");
|
|
5400
|
+
if (!email && !phone) throw new Error("Для контакта нужен хотя бы email или телефон.");
|
|
4897
5401
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4898
5402
|
const baseUrl = await yandexContactsBaseUrl(token);
|
|
4899
5403
|
const uid = `${randomUUID()}@iola-cli`;
|
|
4900
|
-
const card =
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
5404
|
+
const card = buildVcard({
|
|
5405
|
+
uid,
|
|
5406
|
+
name,
|
|
5407
|
+
email,
|
|
5408
|
+
phone,
|
|
5409
|
+
address: args.address,
|
|
5410
|
+
note: args.note,
|
|
5411
|
+
birthday: args.birthday,
|
|
5412
|
+
org: args.org || args.organization,
|
|
5413
|
+
title: args.title || args.position,
|
|
5414
|
+
categories: args.categories || args.group,
|
|
5415
|
+
});
|
|
4909
5416
|
await yandexDavRequest(new URL(`${encodeURIComponent(uid)}.vcf`, baseUrl).toString(), token, {
|
|
4910
5417
|
method: "PUT",
|
|
4911
5418
|
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4912
5419
|
body: card,
|
|
4913
5420
|
timeout: 45000,
|
|
4914
5421
|
});
|
|
4915
|
-
return { status: "created", name, email, uid };
|
|
5422
|
+
return { status: "created", name, email, phone, uid };
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
async function yandexContactsExport(format, args = {}) {
|
|
5426
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 1000), full: true });
|
|
5427
|
+
const safeFormat = format === "csv" ? "csv" : "vcard";
|
|
5428
|
+
const output = args.output || path.join(CONFIG_DIR, `yandex-contacts-${timestampForFile()}.${safeFormat === "csv" ? "csv" : "vcf"}`);
|
|
5429
|
+
const text = safeFormat === "csv"
|
|
5430
|
+
? contactsToCsv(rows)
|
|
5431
|
+
: rows.map((row) => row.card).filter(Boolean).join("\r\n");
|
|
5432
|
+
await mkdir(path.dirname(path.resolve(output)), { recursive: true });
|
|
5433
|
+
await writeFile(output, text, "utf8");
|
|
5434
|
+
saveArtifact("yandex-contacts-export", output, output, { rows: rows.length, format: safeFormat });
|
|
5435
|
+
return { status: "exported", output: path.resolve(output), rows: rows.length, format: safeFormat };
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
async function yandexContactsFindIncomplete(args = {}) {
|
|
5439
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 500) });
|
|
5440
|
+
return rows.filter((row) => {
|
|
5441
|
+
if (args.field === "email") return !row.email;
|
|
5442
|
+
if (args.field === "phone") return !row.phone;
|
|
5443
|
+
if (args.field === "address") return !row.address;
|
|
5444
|
+
return !row.email || !row.phone || !row.name;
|
|
5445
|
+
}).slice(0, Number(args.limit || 50));
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5448
|
+
async function yandexContactsFindDuplicates(args = {}) {
|
|
5449
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 1000) });
|
|
5450
|
+
const groups = new Map();
|
|
5451
|
+
for (const row of rows) {
|
|
5452
|
+
const keys = [
|
|
5453
|
+
row.email ? `email:${row.email.toLocaleLowerCase("en-US")}` : "",
|
|
5454
|
+
row.phone ? `phone:${normalizePhone(row.phone)}` : "",
|
|
5455
|
+
row.name ? `name:${normalizeContactLookupText(row.name)}` : "",
|
|
5456
|
+
].filter((key) => key && !key.endsWith(":"));
|
|
5457
|
+
for (const key of keys) {
|
|
5458
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
5459
|
+
groups.get(key).push(row);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
const seen = new Set();
|
|
5463
|
+
const duplicates = [];
|
|
5464
|
+
for (const [key, group] of groups.entries()) {
|
|
5465
|
+
if (group.length < 2) continue;
|
|
5466
|
+
const signature = group.map((row) => row.href || `${row.name}:${row.email}:${row.phone}`).sort().join("|");
|
|
5467
|
+
if (seen.has(signature)) continue;
|
|
5468
|
+
seen.add(signature);
|
|
5469
|
+
duplicates.push({ status: "duplicate-group", key, count: group.length, contacts: group.slice(0, 10) });
|
|
5470
|
+
}
|
|
5471
|
+
return duplicates.slice(0, Number(args.limit || 20));
|
|
5472
|
+
}
|
|
5473
|
+
|
|
5474
|
+
async function yandexContactsBackupToDisk(args = {}) {
|
|
5475
|
+
if (!args.confirm) throw new Error("Для резервной копии контактов на Диск нужен аргумент confirm=true.");
|
|
5476
|
+
const format = args.format === "csv" ? "csv" : "vcard";
|
|
5477
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 1000), full: true });
|
|
5478
|
+
const remotePath = args.path || `${CLOUD_DEFAULT_REMOTE_DIR}/contacts/yandex-contacts-${timestampForFile()}.${format === "csv" ? "csv" : "vcf"}`;
|
|
5479
|
+
const text = format === "csv" ? contactsToCsv(rows) : rows.map((row) => row.card).filter(Boolean).join("\r\n");
|
|
5480
|
+
const saved = await yandexDiskSaveText(text, remotePath);
|
|
5481
|
+
return { provider: "yandex-disk", status: "contacts-backup", remote: saved.remote, rows: rows.length, format };
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
async function yandexContactsImport(format, args = {}) {
|
|
5485
|
+
if (!args.confirm) throw new Error("Для импорта контактов нужен аргумент confirm=true.");
|
|
5486
|
+
const inputPath = args.path || args.file || args.input;
|
|
5487
|
+
if (!inputPath) throw new Error("Укажите файл для импорта.");
|
|
5488
|
+
const text = await readFile(path.resolve(inputPath), "utf8");
|
|
5489
|
+
const rows = format === "csv" ? parseContactsCsv(text) : parseVCards(text);
|
|
5490
|
+
const existing = await yandexContactsList({ limit: 1000 });
|
|
5491
|
+
const existingEmails = new Set(existing.flatMap((row) => row.emails || []).map((email) => email.toLocaleLowerCase("en-US")));
|
|
5492
|
+
const existingPhones = new Set(existing.flatMap((row) => row.phones || []).map(normalizePhone).filter(Boolean));
|
|
5493
|
+
const created = [];
|
|
5494
|
+
const skipped = [];
|
|
5495
|
+
for (const row of rows.slice(0, Number(args.limit || 200))) {
|
|
5496
|
+
const email = row.email || row.emails?.[0] || "";
|
|
5497
|
+
const phone = row.phone || row.phones?.[0] || "";
|
|
5498
|
+
const emailKey = email.toLocaleLowerCase("en-US");
|
|
5499
|
+
const phoneKey = normalizePhone(phone);
|
|
5500
|
+
if ((!email && !phone) || (emailKey && existingEmails.has(emailKey)) || (phoneKey && existingPhones.has(phoneKey))) {
|
|
5501
|
+
skipped.push(row);
|
|
5502
|
+
continue;
|
|
5503
|
+
}
|
|
5504
|
+
const createdRow = await yandexContactsCreate({
|
|
5505
|
+
name: row.name || email || phone,
|
|
5506
|
+
email,
|
|
5507
|
+
phone,
|
|
5508
|
+
address: row.address,
|
|
5509
|
+
note: row.note,
|
|
5510
|
+
birthday: row.birthday,
|
|
5511
|
+
org: row.org,
|
|
5512
|
+
title: row.title,
|
|
5513
|
+
categories: row.categories,
|
|
5514
|
+
confirm: true,
|
|
5515
|
+
});
|
|
5516
|
+
created.push(createdRow);
|
|
5517
|
+
if (emailKey) existingEmails.add(emailKey);
|
|
5518
|
+
if (phoneKey) existingPhones.add(phoneKey);
|
|
5519
|
+
}
|
|
5520
|
+
return { status: "contacts-imported", format, input: path.resolve(inputPath), created: created.length, skipped: skipped.length };
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
async function yandexContactsBirthdaysToCalendar(args = {}) {
|
|
5524
|
+
if (!args.confirm) throw new Error("Для создания событий дней рождения нужен аргумент confirm=true.");
|
|
5525
|
+
const rows = await yandexContactsList({ limit: Number(args.limit || 1000) });
|
|
5526
|
+
const withBirthday = rows.filter((row) => row.birthday);
|
|
5527
|
+
const created = [];
|
|
5528
|
+
for (const contact of withBirthday.slice(0, Number(args.maxCreate || 50))) {
|
|
5529
|
+
const next = nextBirthdayDate(contact.birthday);
|
|
5530
|
+
if (!next) continue;
|
|
5531
|
+
const result = await yandexCalendarCreateEvent({
|
|
5532
|
+
title: `День рождения: ${contact.name || contact.email || contact.phone}`,
|
|
5533
|
+
start: next.start,
|
|
5534
|
+
end: next.end,
|
|
5535
|
+
description: `Контакт: ${formatYandexContact(contact)}`,
|
|
5536
|
+
confirm: true,
|
|
5537
|
+
});
|
|
5538
|
+
created.push(result);
|
|
5539
|
+
}
|
|
5540
|
+
return { status: "birthday-events-created", created: created.length, totalWithBirthday: withBirthday.length };
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
async function yandexContactSendMail(args = {}) {
|
|
5544
|
+
if (!args.confirm) throw new Error("Для отправки письма контакту нужен аргумент confirm=true.");
|
|
5545
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5546
|
+
if (resolved.status !== "ok") return resolved;
|
|
5547
|
+
const contact = resolved.contact;
|
|
5548
|
+
if (!contact.email) return { status: "no-email", contact: stripYandexContactPrivateFields(contact) };
|
|
5549
|
+
const sent = await yandexMailSend({
|
|
5550
|
+
to: [contact.email],
|
|
5551
|
+
subject: args.subject || "Сообщение от IOLA CLI",
|
|
5552
|
+
text: args.text || args.message || "",
|
|
5553
|
+
confirm: true,
|
|
5554
|
+
});
|
|
5555
|
+
return { status: "contact-mail-sent", contact: contact.name || contact.email, to: sent.to, subject: sent.subject };
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
async function yandexContactSendDiskLinkQr(args = {}) {
|
|
5559
|
+
if (!args.confirm) throw new Error("Для отправки ссылки контакту нужен аргумент confirm=true.");
|
|
5560
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5561
|
+
if (resolved.status !== "ok") return resolved;
|
|
5562
|
+
const contact = resolved.contact;
|
|
5563
|
+
if (!contact.email) return { status: "no-email", contact: stripYandexContactPrivateFields(contact) };
|
|
5564
|
+
const result = await yandexDiskShareEmail({
|
|
5565
|
+
remotePath: args.path || args.remotePath || args.target,
|
|
5566
|
+
to: [contact.email],
|
|
5567
|
+
subject: args.subject,
|
|
5568
|
+
text: args.text || args.message,
|
|
5569
|
+
confirm: true,
|
|
5570
|
+
});
|
|
5571
|
+
return { ...result, status: "contact-disk-link-sent", contact: contact.name || contact.email };
|
|
5572
|
+
}
|
|
5573
|
+
|
|
5574
|
+
async function yandexContactCreateDiskFolder(args = {}) {
|
|
5575
|
+
if (!args.confirm) throw new Error("Для создания папки контакта нужен аргумент confirm=true.");
|
|
5576
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5577
|
+
if (resolved.status !== "ok") return resolved;
|
|
5578
|
+
const contact = resolved.contact;
|
|
5579
|
+
const folder = args.path || `${CLOUD_DEFAULT_REMOTE_DIR}/contacts/${slugYandexDiskName(contact.name || contact.email || contact.phone || "contact")}`;
|
|
5580
|
+
await cloudCreateFolder("yandex-disk", folder);
|
|
5581
|
+
const cardPath = path.posix.join(folder, "contact.vcf");
|
|
5582
|
+
await yandexDiskSaveText(contact.card, cardPath);
|
|
5583
|
+
const note = [
|
|
5584
|
+
`Контакт: ${contact.name || "-"}`,
|
|
5585
|
+
`Email: ${(contact.emails || []).join(", ") || "-"}`,
|
|
5586
|
+
`Телефон: ${(contact.phones || []).join(", ") || "-"}`,
|
|
5587
|
+
contact.address ? `Адрес: ${contact.address}` : "",
|
|
5588
|
+
contact.org ? `Организация: ${contact.org}` : "",
|
|
5589
|
+
contact.title ? `Должность: ${contact.title}` : "",
|
|
5590
|
+
contact.note ? `Заметка: ${contact.note}` : "",
|
|
5591
|
+
].filter(Boolean).join("\n");
|
|
5592
|
+
await yandexDiskSaveText(note, path.posix.join(folder, "README.txt"));
|
|
5593
|
+
return { provider: "yandex-disk", status: "contact-folder-created", contact: contact.name || contact.email, remote: folder, cardPath };
|
|
5594
|
+
}
|
|
5595
|
+
|
|
5596
|
+
async function yandexContactCreateCalendarEvent(args = {}) {
|
|
5597
|
+
if (!args.confirm) throw new Error("Для создания встречи с контактом нужен аргумент confirm=true.");
|
|
5598
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5599
|
+
if (resolved.status !== "ok") return resolved;
|
|
5600
|
+
const contact = resolved.contact;
|
|
5601
|
+
if (!contact.email) return { status: "no-email", contact: stripYandexContactPrivateFields(contact) };
|
|
5602
|
+
const result = await yandexCalendarCreateEvent({
|
|
5603
|
+
...args,
|
|
5604
|
+
title: args.title || `Встреча: ${contact.name || contact.email}`,
|
|
5605
|
+
description: [args.description || "", `Контакт: ${contact.name || "-"}`, `Email: ${contact.email}`].filter(Boolean).join("\n"),
|
|
5606
|
+
attendees: [contact.email],
|
|
5607
|
+
confirm: true,
|
|
5608
|
+
});
|
|
5609
|
+
return { ...result, status: "contact-calendar-event-created", contact: contact.name || contact.email, attendee: contact.email };
|
|
5610
|
+
}
|
|
5611
|
+
|
|
5612
|
+
async function yandexContactCreateTelemostEvent(args = {}) {
|
|
5613
|
+
const result = await yandexContactCreateCalendarEvent({
|
|
5614
|
+
...args,
|
|
5615
|
+
title: args.title || `Телемост: ${args.contact || args.name || args.query || "контакт"}`,
|
|
5616
|
+
description: [args.description || "", "Телемост: создайте ссылку в Яндекс Календаре, если интерфейс календаря предложит видеовстречу."].filter(Boolean).join("\n"),
|
|
5617
|
+
confirm: true,
|
|
5618
|
+
});
|
|
5619
|
+
return { ...result, status: "contact-telemost-event-created" };
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
async function yandexContactFromPublicEntity(args = {}) {
|
|
5623
|
+
if (!args.confirm) throw new Error("Для создания контакта из городского слоя нужен аргумент confirm=true.");
|
|
5624
|
+
const layer = normalizeEntityLayer(args.layer || (/(сад|детсад)/iu.test(args.query || "") ? "kindergartens" : "schools"));
|
|
5625
|
+
const sourceQuery = args.query || args.name || args.inn || "";
|
|
5626
|
+
const number = String(sourceQuery).match(/№?\s*(\d{1,4})/u)?.[1] || "";
|
|
5627
|
+
const query = number
|
|
5628
|
+
? (layer === "kindergartens" ? `детский сад ${number}` : `школа ${number}`)
|
|
5629
|
+
: sourceQuery;
|
|
5630
|
+
const rows = await searchPublicEntities({ layer, query, limit: 5 });
|
|
5631
|
+
if (!rows.length) return { status: "not-found", query: args.query || args.name || args.inn || "" };
|
|
5632
|
+
const entity = rows[0];
|
|
5633
|
+
const name = args.contactName || entity.name || entity.fns_short_name || args.query;
|
|
5634
|
+
const email = entity.email || "";
|
|
5635
|
+
const phone = entity.phone || "";
|
|
5636
|
+
if (!email && !phone) return { status: "no-contact-fields", entity };
|
|
5637
|
+
return yandexContactsCreate({
|
|
5638
|
+
name,
|
|
5639
|
+
email,
|
|
5640
|
+
phone,
|
|
5641
|
+
address: entity.address,
|
|
5642
|
+
org: entity.name,
|
|
5643
|
+
note: `Создано из открытого слоя ${layer}. ИНН: ${entity.inn || "-"}.`,
|
|
5644
|
+
confirm: true,
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
async function resolveYandexContact(query, args = {}) {
|
|
5649
|
+
const rows = await yandexContactsList({ limit: Math.max(500, Number(args.limit || 100)), full: true });
|
|
5650
|
+
const normalized = normalizeContactLookupText(query || args.query || args.name || args.email || args.phone || "");
|
|
5651
|
+
const matches = rows.filter((row) => contactMatchesQuery(row, normalized)).slice(0, 20);
|
|
5652
|
+
if (!matches.length) return { status: "not-found", query };
|
|
5653
|
+
const exact = pickExactYandexContactMatch(matches, query, args);
|
|
5654
|
+
if (exact) return { status: "ok", contact: exact };
|
|
5655
|
+
if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", query, contacts: matches.map(stripYandexContactPrivateFields) };
|
|
5656
|
+
return { status: "ok", contact: matches[0] };
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
function pickExactYandexContactMatch(matches, query, args = {}) {
|
|
5660
|
+
const text = String(query || args.email || args.phone || args.name || "").trim();
|
|
5661
|
+
const email = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0]?.toLocaleLowerCase("en-US");
|
|
5662
|
+
if (email) return matches.find((row) => row.emails?.some((item) => item.toLocaleLowerCase("en-US") === email));
|
|
5663
|
+
const phone = normalizePhone(args.phone || text);
|
|
5664
|
+
if (phone.length >= 7) return matches.find((row) => row.phones?.some((item) => normalizePhone(item).endsWith(phone) || phone.endsWith(normalizePhone(item))));
|
|
5665
|
+
const normalized = normalizeContactLookupText(text);
|
|
5666
|
+
return matches.find((row) => normalizeContactLookupText(row.name) === normalized) || null;
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
function stripYandexContactPrivateFields(row = {}) {
|
|
5670
|
+
const { card: _card, ...rest } = row;
|
|
5671
|
+
return rest;
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5674
|
+
async function saveYandexContactCard(href, card) {
|
|
5675
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
5676
|
+
await yandexDavRequest(new URL(href, "https://carddav.yandex.ru/").toString(), token, {
|
|
5677
|
+
method: "PUT",
|
|
5678
|
+
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
5679
|
+
body: ensureVcardCrlf(card),
|
|
5680
|
+
timeout: 45000,
|
|
5681
|
+
});
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5684
|
+
function buildVcard(args = {}) {
|
|
5685
|
+
const uid = args.uid || `${randomUUID()}@iola-cli`;
|
|
5686
|
+
const lines = [
|
|
5687
|
+
"BEGIN:VCARD",
|
|
5688
|
+
"VERSION:3.0",
|
|
5689
|
+
`UID:${uid}`,
|
|
5690
|
+
`FN:${escapeVcardValue(args.name || args.email || args.phone || "Контакт")}`,
|
|
5691
|
+
`N:${escapeVcardValue(args.name || args.email || args.phone || "Контакт")};;;;`,
|
|
5692
|
+
args.email ? `EMAIL;TYPE=INTERNET:${String(args.email).trim()}` : "",
|
|
5693
|
+
args.phone ? `TEL;TYPE=CELL:${escapeVcardValue(args.phone)}` : "",
|
|
5694
|
+
args.address ? `ADR;TYPE=HOME:;;;;${escapeVcardValue(args.address)};;` : "",
|
|
5695
|
+
args.org ? `ORG:${escapeVcardValue(args.org)}` : "",
|
|
5696
|
+
args.title ? `TITLE:${escapeVcardValue(args.title)}` : "",
|
|
5697
|
+
args.birthday ? `BDAY:${normalizeVcardBirthday(args.birthday)}` : "",
|
|
5698
|
+
args.note ? `NOTE:${escapeVcardValue(args.note)}` : "",
|
|
5699
|
+
args.categories ? `CATEGORIES:${escapeVcardValue(Array.isArray(args.categories) ? args.categories.join(",") : args.categories)}` : "",
|
|
5700
|
+
"END:VCARD",
|
|
5701
|
+
"",
|
|
5702
|
+
].filter((line) => line !== "");
|
|
5703
|
+
return lines.join("\r\n");
|
|
4916
5704
|
}
|
|
4917
5705
|
|
|
4918
5706
|
function upsertVcardEmail(card, email, options = {}) {
|
|
5707
|
+
return upsertVcardProperty(card, "EMAIL;TYPE=INTERNET", email, { replace: Boolean(options.overwrite) });
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
function upsertVcardProperty(card, property, value, options = {}) {
|
|
4919
5711
|
const text = String(card || "").replace(/\r/g, "").trim();
|
|
4920
5712
|
if (!text.includes("BEGIN:VCARD")) throw new Error("Контакт не похож на vCard.");
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
5713
|
+
const propName = String(property || "").split(";")[0].toLocaleUpperCase("en-US");
|
|
5714
|
+
const rawValue = options.raw ? String(value || "") : escapeVcardValue(value);
|
|
5715
|
+
if (!rawValue) return text;
|
|
5716
|
+
const line = `${property}:${rawValue}`;
|
|
5717
|
+
const pattern = new RegExp(`^${escapeRegExp(propName)}[^:]*:[^\\n]*`, "imu");
|
|
5718
|
+
if (pattern.test(text)) {
|
|
5719
|
+
if (!options.replace) return text;
|
|
5720
|
+
return text.replace(pattern, line);
|
|
4924
5721
|
}
|
|
4925
|
-
return text.replace(/\nEND:VCARD/iu, `\
|
|
5722
|
+
return text.replace(/\nEND:VCARD/iu, `\n${line}\nEND:VCARD`);
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5725
|
+
function removeVcardProperty(card, property, value = "") {
|
|
5726
|
+
const text = String(card || "").replace(/\r/g, "").trim();
|
|
5727
|
+
const propName = String(property || "").split(";")[0].toLocaleUpperCase("en-US");
|
|
5728
|
+
const lines = text.split(/\n/u);
|
|
5729
|
+
const needle = String(value || "").toLocaleLowerCase("ru-RU");
|
|
5730
|
+
const next = lines.filter((line) => {
|
|
5731
|
+
if (!new RegExp(`^${escapeRegExp(propName)}(?:[;:]|$)`, "iu").test(line)) return true;
|
|
5732
|
+
if (!needle) return false;
|
|
5733
|
+
return !line.toLocaleLowerCase("ru-RU").includes(needle);
|
|
5734
|
+
});
|
|
5735
|
+
return next.join("\n");
|
|
4926
5736
|
}
|
|
4927
5737
|
|
|
4928
5738
|
function escapeVcardValue(value) {
|
|
@@ -4930,10 +5740,28 @@ function escapeVcardValue(value) {
|
|
|
4930
5740
|
}
|
|
4931
5741
|
|
|
4932
5742
|
function contactMatchesQuery(contact, normalizedQuery) {
|
|
4933
|
-
const
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
5743
|
+
const query = normalizeContactLookupText(normalizedQuery);
|
|
5744
|
+
const contactText = normalizeContactLookupText([
|
|
5745
|
+
contact.name,
|
|
5746
|
+
contact.email,
|
|
5747
|
+
...(contact.emails || []),
|
|
5748
|
+
contact.phone,
|
|
5749
|
+
...(contact.phones || []),
|
|
5750
|
+
contact.address,
|
|
5751
|
+
contact.org,
|
|
5752
|
+
contact.title,
|
|
5753
|
+
contact.note,
|
|
5754
|
+
contact.categories,
|
|
5755
|
+
].filter(Boolean).join(" "));
|
|
5756
|
+
const queryPhone = normalizePhone(normalizedQuery);
|
|
5757
|
+
if (queryPhone.length >= 7 && [...(contact.phones || []), contact.phone].some((phone) => {
|
|
5758
|
+
const contactPhone = normalizePhone(phone);
|
|
5759
|
+
return contactPhone.length >= 7 && (contactPhone.includes(queryPhone) || queryPhone.includes(contactPhone));
|
|
5760
|
+
})) return true;
|
|
5761
|
+
const normalizedQueryText = query;
|
|
5762
|
+
if (!contactText || !normalizedQueryText) return false;
|
|
5763
|
+
if (contactText.includes(normalizedQueryText)) return true;
|
|
5764
|
+
const queryTokens = normalizedQueryText.split(/\s+/u).filter(Boolean);
|
|
4937
5765
|
const contactTokens = contactText.split(/\s+/u).filter(Boolean);
|
|
4938
5766
|
return queryTokens.every((queryToken) => contactTokens.some((contactToken) => contactToken.startsWith(queryToken.slice(0, Math.max(4, Math.min(queryToken.length, 6)))) || queryToken.startsWith(contactToken.slice(0, Math.max(4, Math.min(contactToken.length, 6))))));
|
|
4939
5767
|
}
|
|
@@ -4942,6 +5770,99 @@ function normalizeContactLookupText(value) {
|
|
|
4942
5770
|
return normalizeGeoText(String(value || "").replace(/\b(?:кому|контакт|письмо|сообщение)\b/giu, " ")).trim();
|
|
4943
5771
|
}
|
|
4944
5772
|
|
|
5773
|
+
function normalizePhone(value) {
|
|
5774
|
+
return String(value || "").replace(/[^\d]+/g, "");
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5777
|
+
function normalizeVcardBirthday(value) {
|
|
5778
|
+
const text = String(value || "").trim();
|
|
5779
|
+
const iso = text.match(/^(\d{4})-(\d{2})-(\d{2})$/u);
|
|
5780
|
+
if (iso) return `${iso[1]}-${iso[2]}-${iso[3]}`;
|
|
5781
|
+
const ru = text.match(/^(\d{1,2})[.\-/](\d{1,2})(?:[.\-/](\d{2,4}))?$/u);
|
|
5782
|
+
if (ru) {
|
|
5783
|
+
const year = ru[3] ? (ru[3].length === 2 ? `20${ru[3]}` : ru[3]) : "1900";
|
|
5784
|
+
return `${year}-${String(ru[2]).padStart(2, "0")}-${String(ru[1]).padStart(2, "0")}`;
|
|
5785
|
+
}
|
|
5786
|
+
return text;
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
function contactsToCsv(rows) {
|
|
5790
|
+
const headers = ["name", "email", "emails", "phone", "phones", "address", "org", "title", "birthday", "note", "categories"];
|
|
5791
|
+
return [
|
|
5792
|
+
headers.join(","),
|
|
5793
|
+
...rows.map((row) => headers.map((key) => contactsCsvCell(Array.isArray(row[key]) ? row[key].join("; ") : row[key])).join(",")),
|
|
5794
|
+
].join("\n");
|
|
5795
|
+
}
|
|
5796
|
+
|
|
5797
|
+
function contactsCsvCell(value) {
|
|
5798
|
+
return `"${String(value || "").replace(/"/g, '""')}"`;
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
function parseContactsCsv(text) {
|
|
5802
|
+
const rows = parseSimpleCsv(text);
|
|
5803
|
+
return rows.map((row) => ({
|
|
5804
|
+
name: row.name || row.Name || row["Имя"] || row["ФИО"] || "",
|
|
5805
|
+
email: row.email || row.Email || row["Почта"] || row["Email"] || "",
|
|
5806
|
+
phone: row.phone || row.Phone || row["Телефон"] || "",
|
|
5807
|
+
address: row.address || row.Address || row["Адрес"] || "",
|
|
5808
|
+
org: row.org || row.organization || row["Организация"] || "",
|
|
5809
|
+
title: row.title || row.position || row["Должность"] || "",
|
|
5810
|
+
birthday: row.birthday || row.Birthday || row["День рождения"] || "",
|
|
5811
|
+
note: row.note || row.Note || row["Заметка"] || "",
|
|
5812
|
+
categories: row.categories || row.group || row["Группа"] || "",
|
|
5813
|
+
})).filter((row) => row.name || row.email || row.phone);
|
|
5814
|
+
}
|
|
5815
|
+
|
|
5816
|
+
function parseSimpleCsv(text) {
|
|
5817
|
+
const lines = String(text || "").replace(/\r/g, "").split("\n").filter((line) => line.trim());
|
|
5818
|
+
if (!lines.length) return [];
|
|
5819
|
+
const headers = splitContactsCsvLine(lines[0]).map((header) => header.trim());
|
|
5820
|
+
return lines.slice(1).map((line) => {
|
|
5821
|
+
const values = splitContactsCsvLine(line);
|
|
5822
|
+
return Object.fromEntries(headers.map((header, index) => [header, values[index] || ""]));
|
|
5823
|
+
});
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
function splitContactsCsvLine(line) {
|
|
5827
|
+
const cells = [];
|
|
5828
|
+
let current = "";
|
|
5829
|
+
let quoted = false;
|
|
5830
|
+
for (let index = 0; index < String(line || "").length; index += 1) {
|
|
5831
|
+
const char = line[index];
|
|
5832
|
+
const next = line[index + 1];
|
|
5833
|
+
if (char === "\"" && quoted && next === "\"") {
|
|
5834
|
+
current += "\"";
|
|
5835
|
+
index += 1;
|
|
5836
|
+
} else if (char === "\"") {
|
|
5837
|
+
quoted = !quoted;
|
|
5838
|
+
} else if (char === "," && !quoted) {
|
|
5839
|
+
cells.push(current);
|
|
5840
|
+
current = "";
|
|
5841
|
+
} else {
|
|
5842
|
+
current += char;
|
|
5843
|
+
}
|
|
5844
|
+
}
|
|
5845
|
+
cells.push(current);
|
|
5846
|
+
return cells.map((cell) => cell.trim());
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
function nextBirthdayDate(value) {
|
|
5850
|
+
const normalized = normalizeVcardBirthday(value);
|
|
5851
|
+
const match = normalized.match(/^\d{4}-(\d{2})-(\d{2})$/u)
|
|
5852
|
+
|| normalized.match(/^\d{4}(\d{2})(\d{2})$/u)
|
|
5853
|
+
|| normalized.match(/^(\d{2})-(\d{2})$/u)
|
|
5854
|
+
|| normalized.match(/^(\d{2})(\d{2})$/u);
|
|
5855
|
+
if (!match) return null;
|
|
5856
|
+
const now = new Date();
|
|
5857
|
+
let date = new Date(now.getFullYear(), Number(match[1]) - 1, Number(match[2]), 9, 0, 0);
|
|
5858
|
+
if (date < now) date = new Date(now.getFullYear() + 1, Number(match[1]) - 1, Number(match[2]), 9, 0, 0);
|
|
5859
|
+
return { start: date.toISOString(), end: new Date(date.getTime() + 3600000).toISOString() };
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
function escapeRegExp(value) {
|
|
5863
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5864
|
+
}
|
|
5865
|
+
|
|
4945
5866
|
async function yandexContactsBaseUrl(token) {
|
|
4946
5867
|
const root = "https://carddav.yandex.ru/";
|
|
4947
5868
|
const principalXml = await yandexDavRequest(root, token, {
|
|
@@ -4971,21 +5892,92 @@ async function yandexContactsBaseUrl(token) {
|
|
|
4971
5892
|
return new URL(addressBookHref, root).toString();
|
|
4972
5893
|
}
|
|
4973
5894
|
|
|
5895
|
+
async function yandexTelemostStatus() {
|
|
5896
|
+
const calendar = await yandexCalendarStatus();
|
|
5897
|
+
return {
|
|
5898
|
+
status: "calendar-fallback",
|
|
5899
|
+
provider: "yandex-telemost",
|
|
5900
|
+
calendar: calendar.displayName,
|
|
5901
|
+
message: "Для обычного OAuth-подключения CLI использует календарное событие. Прямой Telemost API проверяется отдельно и может быть доступен только аккаунтам/организациям Яндекс 360.",
|
|
5902
|
+
};
|
|
5903
|
+
}
|
|
5904
|
+
|
|
4974
5905
|
async function yandexTelemostCreateEvent(args = {}) {
|
|
5906
|
+
if (!args.confirm) throw new Error("Для создания встречи нужен аргумент confirm=true.");
|
|
5907
|
+
const telemost = await tryCreateYandexTelemostMeeting(args).catch((error) => ({
|
|
5908
|
+
status: "telemost-api-unavailable",
|
|
5909
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5910
|
+
}));
|
|
4975
5911
|
const description = [
|
|
4976
5912
|
args.description || "",
|
|
4977
5913
|
"",
|
|
4978
|
-
"Телемост:
|
|
5914
|
+
telemost.joinUrl ? `Ссылка Телемоста: ${telemost.joinUrl}` : "Телемост: прямое создание ссылки через API недоступно для текущего OAuth-подключения. Событие создано в календаре как встреча; ссылку можно добавить вручную в Яндекс Календаре.",
|
|
4979
5915
|
].join("\n").trim();
|
|
4980
|
-
|
|
5916
|
+
const event = await yandexCalendarCreateEvent({
|
|
5917
|
+
...args,
|
|
5918
|
+
title: args.title || args.summary || "Телемост IOLA",
|
|
5919
|
+
description,
|
|
5920
|
+
location: telemost.joinUrl || args.location || "Яндекс Телемост",
|
|
5921
|
+
confirm: true,
|
|
5922
|
+
});
|
|
5923
|
+
return { ...event, status: telemost.joinUrl ? "telemost-event-created" : "telemost-calendar-fallback-created", telemost };
|
|
4981
5924
|
}
|
|
4982
5925
|
|
|
4983
|
-
function
|
|
5926
|
+
async function tryCreateYandexTelemostMeeting(args = {}) {
|
|
5927
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Телемост");
|
|
5928
|
+
const endpoints = [
|
|
5929
|
+
"https://cloud-api.yandex.net/v1/telemost/meetings",
|
|
5930
|
+
"https://api360.yandex.net/v1/telemost/meetings",
|
|
5931
|
+
];
|
|
5932
|
+
let lastError = "";
|
|
5933
|
+
for (const endpoint of endpoints) {
|
|
5934
|
+
const response = await fetch(endpoint, {
|
|
5935
|
+
method: "POST",
|
|
5936
|
+
headers: {
|
|
5937
|
+
Authorization: `OAuth ${token}`,
|
|
5938
|
+
"content-type": "application/json",
|
|
5939
|
+
},
|
|
5940
|
+
body: JSON.stringify({
|
|
5941
|
+
title: args.title || args.summary || "Телемост IOLA",
|
|
5942
|
+
description: args.description || "",
|
|
5943
|
+
}),
|
|
5944
|
+
signal: AbortSignal.timeout(15000),
|
|
5945
|
+
}).catch((error) => ({ ok: false, status: 0, statusText: error.message, text: async () => error.message }));
|
|
5946
|
+
const text = await response.text().catch(() => "");
|
|
5947
|
+
if (response.ok) {
|
|
5948
|
+
const payload = text ? JSON.parse(text) : {};
|
|
5949
|
+
return {
|
|
5950
|
+
status: "telemost-api-created",
|
|
5951
|
+
endpoint,
|
|
5952
|
+
id: payload.id || payload.meeting_id || "",
|
|
5953
|
+
joinUrl: payload.join_url || payload.url || payload.link || payload.meeting_url || "",
|
|
5954
|
+
raw: payload,
|
|
5955
|
+
};
|
|
5956
|
+
}
|
|
5957
|
+
lastError = `${endpoint}: ${response.status} ${response.statusText} ${sanitizeSecretFromText(text.slice(0, 500), token)}`;
|
|
5958
|
+
if (![404, 405].includes(Number(response.status))) break;
|
|
5959
|
+
}
|
|
5960
|
+
throw new Error(lastError || "Telemost API недоступен.");
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5963
|
+
function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [], rrule = "", reminders = [] }) {
|
|
4984
5964
|
const escape = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
|
|
5965
|
+
const attendeeLines = (Array.isArray(attendees) ? attendees : [attendees])
|
|
5966
|
+
.map((email) => String(email || "").trim())
|
|
5967
|
+
.filter((email) => /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email))
|
|
5968
|
+
.map((email) => `ATTENDEE;CN=${escape(email)};ROLE=REQ-PARTICIPANT:mailto:${email}`);
|
|
5969
|
+
const reminderBlocks = normalizeCalendarReminders(reminders).map((minutes) => [
|
|
5970
|
+
"BEGIN:VALARM",
|
|
5971
|
+
`TRIGGER:-PT${Math.max(1, Math.abs(Number(minutes) || 15))}M`,
|
|
5972
|
+
"ACTION:DISPLAY",
|
|
5973
|
+
`DESCRIPTION:${escape(summary || "Напоминание")}`,
|
|
5974
|
+
"END:VALARM",
|
|
5975
|
+
].join("\r\n"));
|
|
4985
5976
|
return [
|
|
4986
5977
|
"BEGIN:VCALENDAR",
|
|
4987
5978
|
"VERSION:2.0",
|
|
4988
5979
|
"PRODID:-//IOLA CLI//Yandex Calendar//RU",
|
|
5980
|
+
"CALSCALE:GREGORIAN",
|
|
4989
5981
|
"BEGIN:VEVENT",
|
|
4990
5982
|
`UID:${uid}`,
|
|
4991
5983
|
`DTSTAMP:${toIcsDate(new Date().toISOString())}`,
|
|
@@ -4994,6 +5986,9 @@ function buildIcsEvent({ uid, start, end, summary, description, location }) {
|
|
|
4994
5986
|
`SUMMARY:${escape(summary)}`,
|
|
4995
5987
|
description ? `DESCRIPTION:${escape(description)}` : "",
|
|
4996
5988
|
location ? `LOCATION:${escape(location)}` : "",
|
|
5989
|
+
rrule ? `RRULE:${rrule}` : "",
|
|
5990
|
+
...attendeeLines,
|
|
5991
|
+
...reminderBlocks,
|
|
4997
5992
|
"END:VEVENT",
|
|
4998
5993
|
"END:VCALENDAR",
|
|
4999
5994
|
"",
|
|
@@ -5006,15 +6001,105 @@ function toIcsDate(value) {
|
|
|
5006
6001
|
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/u, "Z");
|
|
5007
6002
|
}
|
|
5008
6003
|
|
|
5009
|
-
function parseIcsEvents(xmlOrIcs) {
|
|
5010
|
-
const
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
6004
|
+
function parseIcsEvents(xmlOrIcs, options = {}) {
|
|
6005
|
+
const text = String(xmlOrIcs || "");
|
|
6006
|
+
const responses = text.split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
6007
|
+
if (responses.length) {
|
|
6008
|
+
return responses.flatMap((response) => {
|
|
6009
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
6010
|
+
const calendarData = response.match(/<[^:>]*:?calendar-data[^>]*>([\s\S]*?)<\/[^:>]*:?calendar-data>/iu)?.[1] || response;
|
|
6011
|
+
return parseIcsEvents(calendarData, options).map((row) => ({
|
|
6012
|
+
...row,
|
|
6013
|
+
href: row.href || href,
|
|
6014
|
+
url: row.url || (href ? new URL(href, options.baseUrl || "https://caldav.yandex.ru/").toString() : ""),
|
|
6015
|
+
}));
|
|
6016
|
+
});
|
|
6017
|
+
}
|
|
6018
|
+
const decoded = decodeXml(stripXmlTags(text));
|
|
6019
|
+
return decoded.split("BEGIN:VEVENT").slice(1).map((chunk) => {
|
|
6020
|
+
const lines = unfoldIcsLines(`BEGIN:VEVENT${chunk}`);
|
|
6021
|
+
const uid = icsValue(lines, "UID");
|
|
6022
|
+
const start = icsValue(lines, "DTSTART");
|
|
6023
|
+
const end = icsValue(lines, "DTEND");
|
|
6024
|
+
return {
|
|
6025
|
+
uid,
|
|
6026
|
+
title: unescapeIcsValue(icsValue(lines, "SUMMARY")),
|
|
6027
|
+
start,
|
|
6028
|
+
end,
|
|
6029
|
+
startIso: icsDateToIso(start),
|
|
6030
|
+
endIso: icsDateToIso(end),
|
|
6031
|
+
location: unescapeIcsValue(icsValue(lines, "LOCATION")),
|
|
6032
|
+
description: unescapeIcsValue(icsValue(lines, "DESCRIPTION")),
|
|
6033
|
+
rrule: icsValue(lines, "RRULE"),
|
|
6034
|
+
attendees: icsValues(lines, "ATTENDEE").map((value) => value.replace(/^mailto:/iu, "")),
|
|
6035
|
+
reminders: parseIcsReminderMinutes(lines),
|
|
6036
|
+
ics: decoded.includes("BEGIN:VCALENDAR") ? decoded : "",
|
|
6037
|
+
};
|
|
6038
|
+
}).filter((item) => item.uid || item.title);
|
|
6039
|
+
}
|
|
6040
|
+
|
|
6041
|
+
function unfoldIcsLines(value) {
|
|
6042
|
+
return String(value || "")
|
|
6043
|
+
.replace(/\r/g, "")
|
|
6044
|
+
.replace(/\n[ \t]/g, "")
|
|
6045
|
+
.split(/\n/u)
|
|
6046
|
+
.map((line) => line.trim())
|
|
6047
|
+
.filter(Boolean);
|
|
6048
|
+
}
|
|
6049
|
+
|
|
6050
|
+
function icsValues(lines, property) {
|
|
6051
|
+
const prop = String(property || "").toLocaleUpperCase("en-US");
|
|
6052
|
+
return lines
|
|
6053
|
+
.filter((line) => line.toLocaleUpperCase("en-US").startsWith(`${prop};`) || line.toLocaleUpperCase("en-US").startsWith(`${prop}:`))
|
|
6054
|
+
.map((line) => line.slice(line.indexOf(":") + 1).trim())
|
|
6055
|
+
.filter(Boolean);
|
|
6056
|
+
}
|
|
6057
|
+
|
|
6058
|
+
function icsValue(lines, property) {
|
|
6059
|
+
return icsValues(lines, property)[0] || "";
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
function unescapeIcsValue(value) {
|
|
6063
|
+
return String(value || "")
|
|
6064
|
+
.replace(/\\n/giu, "\n")
|
|
6065
|
+
.replace(/\\,/gu, ",")
|
|
6066
|
+
.replace(/\\;/gu, ";")
|
|
6067
|
+
.replace(/\\\\/gu, "\\")
|
|
6068
|
+
.trim();
|
|
6069
|
+
}
|
|
6070
|
+
|
|
6071
|
+
function icsDateToIso(value) {
|
|
6072
|
+
const text = String(value || "").trim();
|
|
6073
|
+
const match = text.match(/^(\d{4})(\d{2})(\d{2})T?(\d{2})?(\d{2})?(\d{2})?Z?$/u);
|
|
6074
|
+
if (!match) return "";
|
|
6075
|
+
const iso = `${match[1]}-${match[2]}-${match[3]}T${match[4] || "00"}:${match[5] || "00"}:${match[6] || "00"}${text.endsWith("Z") ? "Z" : ""}`;
|
|
6076
|
+
const date = new Date(iso);
|
|
6077
|
+
return Number.isNaN(date.getTime()) ? "" : date.toISOString();
|
|
6078
|
+
}
|
|
6079
|
+
|
|
6080
|
+
function parseIcsReminderMinutes(lines) {
|
|
6081
|
+
return lines
|
|
6082
|
+
.filter((line) => /^TRIGGER:/iu.test(line))
|
|
6083
|
+
.map((line) => Number(line.match(/PT(\d+)M/iu)?.[1] || 0))
|
|
6084
|
+
.filter(Boolean);
|
|
6085
|
+
}
|
|
6086
|
+
|
|
6087
|
+
function normalizeCalendarReminders(value) {
|
|
6088
|
+
const rows = Array.isArray(value) ? value : String(value || "").split(/[;,]/u);
|
|
6089
|
+
return rows.map((item) => Number(String(item).match(/\d+/u)?.[0] || 0)).filter(Boolean);
|
|
6090
|
+
}
|
|
6091
|
+
|
|
6092
|
+
function buildIcsRrule(args = {}) {
|
|
6093
|
+
const repeat = String(args.repeat || args.frequency || "").toLocaleLowerCase("ru-RU");
|
|
6094
|
+
if (!repeat && !args.count && !args.until) return "";
|
|
6095
|
+
const freq = /день|daily|day/iu.test(repeat) ? "DAILY"
|
|
6096
|
+
: /месяц|monthly|month/iu.test(repeat) ? "MONTHLY"
|
|
6097
|
+
: /год|year|yearly|annual/iu.test(repeat) ? "YEARLY"
|
|
6098
|
+
: "WEEKLY";
|
|
6099
|
+
const parts = [`FREQ=${freq}`];
|
|
6100
|
+
if (args.count) parts.push(`COUNT=${Number(args.count)}`);
|
|
6101
|
+
if (args.until) parts.push(`UNTIL=${toIcsDate(args.until)}`);
|
|
6102
|
+
return parts.join(";");
|
|
5018
6103
|
}
|
|
5019
6104
|
|
|
5020
6105
|
function extractDavHref(xml, propertyName) {
|
|
@@ -5033,6 +6118,24 @@ function extractCalendarCollectionHref(xml) {
|
|
|
5033
6118
|
return "";
|
|
5034
6119
|
}
|
|
5035
6120
|
|
|
6121
|
+
function extractCalendarCollections(xml) {
|
|
6122
|
+
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
6123
|
+
const rows = [];
|
|
6124
|
+
for (const response of responses) {
|
|
6125
|
+
if (!/<[^:>]*:?calendar(?:\s|>|\/)/iu.test(response)) continue;
|
|
6126
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
6127
|
+
if (!href || /\/(?:inbox|outbox)\//iu.test(href)) continue;
|
|
6128
|
+
const name = stripXmlTags(response.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "").trim() || path.posix.basename(href.replace(/\/$/u, ""));
|
|
6129
|
+
rows.push({ provider: "yandex-calendar", href, name, type: "calendar" });
|
|
6130
|
+
}
|
|
6131
|
+
return rows;
|
|
6132
|
+
}
|
|
6133
|
+
|
|
6134
|
+
function stripCalendarPrivateFields(row = {}) {
|
|
6135
|
+
const { ics: _ics, url: _url, ...rest } = row;
|
|
6136
|
+
return rest;
|
|
6137
|
+
}
|
|
6138
|
+
|
|
5036
6139
|
function extractAddressBookCollectionHref(xml) {
|
|
5037
6140
|
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
5038
6141
|
for (const response of responses) {
|
|
@@ -5057,19 +6160,75 @@ function extractVcardHrefs(xml) {
|
|
|
5057
6160
|
function parseVCards(xmlOrCards) {
|
|
5058
6161
|
const decoded = decodeXml(stripXmlTags(xmlOrCards)).replace(/\r/g, "");
|
|
5059
6162
|
return decoded.split("BEGIN:VCARD").slice(1).map((chunk) => {
|
|
5060
|
-
const
|
|
5061
|
-
const
|
|
5062
|
-
const
|
|
5063
|
-
|
|
6163
|
+
const card = `BEGIN:VCARD\n${chunk}`.replace(/\n+/g, "\n").trim();
|
|
6164
|
+
const lines = unfoldVcardLines(chunk);
|
|
6165
|
+
const emails = vcardValues(lines, "EMAIL");
|
|
6166
|
+
const phones = vcardValues(lines, "TEL");
|
|
6167
|
+
const name = cleanVcardName(vcardValue(lines, "FN") || vcardValue(lines, "N"), emails[0] || phones[0] || "");
|
|
6168
|
+
const address = cleanVcardAddress(vcardValue(lines, "ADR"));
|
|
6169
|
+
const row = {
|
|
6170
|
+
uid: vcardValue(lines, "UID"),
|
|
6171
|
+
name,
|
|
6172
|
+
email: emails[0] || "",
|
|
6173
|
+
emails,
|
|
6174
|
+
phone: phones[0] || "",
|
|
6175
|
+
phones,
|
|
6176
|
+
address,
|
|
6177
|
+
org: cleanVcardName(vcardValue(lines, "ORG")),
|
|
6178
|
+
title: cleanVcardName(vcardValue(lines, "TITLE")),
|
|
6179
|
+
note: unescapeVcardValue(vcardValue(lines, "NOTE")),
|
|
6180
|
+
birthday: vcardValue(lines, "BDAY"),
|
|
6181
|
+
categories: vcardValue(lines, "CATEGORIES"),
|
|
6182
|
+
card: ensureVcardCrlf(card),
|
|
6183
|
+
};
|
|
6184
|
+
return row;
|
|
5064
6185
|
}).filter((item) => item.name || item.email || item.phone);
|
|
5065
6186
|
}
|
|
5066
6187
|
|
|
5067
6188
|
function cleanVcardName(value, fallback = "") {
|
|
5068
|
-
const text =
|
|
6189
|
+
const text = unescapeVcardValue(value).replace(/;/g, " ").replace(/\s+/g, " ").trim();
|
|
5069
6190
|
if (!text || /^[\s;]+$/u.test(String(value || ""))) return fallback || "";
|
|
5070
6191
|
return text;
|
|
5071
6192
|
}
|
|
5072
6193
|
|
|
6194
|
+
function unfoldVcardLines(value) {
|
|
6195
|
+
return String(value || "")
|
|
6196
|
+
.replace(/\r/g, "")
|
|
6197
|
+
.replace(/\n[ \t]/g, "")
|
|
6198
|
+
.split(/\n/u)
|
|
6199
|
+
.map((line) => line.trim())
|
|
6200
|
+
.filter(Boolean);
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6203
|
+
function vcardValues(lines, property) {
|
|
6204
|
+
const prop = String(property || "").toLocaleUpperCase("en-US");
|
|
6205
|
+
return lines
|
|
6206
|
+
.filter((line) => line.toLocaleUpperCase("en-US").startsWith(`${prop};`) || line.toLocaleUpperCase("en-US").startsWith(`${prop}:`))
|
|
6207
|
+
.map((line) => unescapeVcardValue(line.slice(line.indexOf(":") + 1).trim()))
|
|
6208
|
+
.filter(Boolean);
|
|
6209
|
+
}
|
|
6210
|
+
|
|
6211
|
+
function vcardValue(lines, property) {
|
|
6212
|
+
return vcardValues(lines, property)[0] || "";
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
function cleanVcardAddress(value) {
|
|
6216
|
+
return unescapeVcardValue(value).split(";").map((part) => part.trim()).filter(Boolean).join(", ");
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
function unescapeVcardValue(value) {
|
|
6220
|
+
return String(value || "")
|
|
6221
|
+
.replace(/\\n/giu, "\n")
|
|
6222
|
+
.replace(/\\,/gu, ",")
|
|
6223
|
+
.replace(/\\;/gu, ";")
|
|
6224
|
+
.replace(/\\\\/gu, "\\")
|
|
6225
|
+
.trim();
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
function ensureVcardCrlf(value) {
|
|
6229
|
+
return String(value || "").replace(/\r/g, "").replace(/\n/g, "\r\n").trim() + "\r\n";
|
|
6230
|
+
}
|
|
6231
|
+
|
|
5073
6232
|
function normalizeYandexServiceList(values) {
|
|
5074
6233
|
const aliases = {
|
|
5075
6234
|
id: "identity",
|
|
@@ -5278,6 +6437,121 @@ async function yandexDiskFind(query, options = {}) {
|
|
|
5278
6437
|
.slice(0, Number(options.limit || 50));
|
|
5279
6438
|
}
|
|
5280
6439
|
|
|
6440
|
+
async function yandexDiskStat(remotePath) {
|
|
6441
|
+
if (!remotePath) throw new Error("Путь на Яндекс Диске обязателен.");
|
|
6442
|
+
const payload = await yandexDiskRequest("GET", "/resources", {
|
|
6443
|
+
query: {
|
|
6444
|
+
path: normalizeYandexDiskPath(remotePath),
|
|
6445
|
+
fields: "name,path,type,size,created,modified,mime_type,public_url,preview,md5,sha256,embedded",
|
|
6446
|
+
},
|
|
6447
|
+
});
|
|
6448
|
+
return formatYandexDiskResource(payload);
|
|
6449
|
+
}
|
|
6450
|
+
|
|
6451
|
+
async function yandexDiskExists(remotePath) {
|
|
6452
|
+
try {
|
|
6453
|
+
const stat = await yandexDiskStat(remotePath);
|
|
6454
|
+
return { provider: "yandex-disk", path: stat.path, exists: true, type: stat.type, name: stat.name };
|
|
6455
|
+
} catch (error) {
|
|
6456
|
+
if (/404|DiskNotFoundError|Path not found/iu.test(String(error?.message || ""))) {
|
|
6457
|
+
return { provider: "yandex-disk", path: remotePath, exists: false };
|
|
6458
|
+
}
|
|
6459
|
+
throw error;
|
|
6460
|
+
}
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
async function yandexDiskReadText(remotePath, options = {}) {
|
|
6464
|
+
if (!remotePath) throw new Error("Путь к текстовому файлу на Яндекс Диске обязателен.");
|
|
6465
|
+
const maxBytes = Number(options.maxBytes || 200000);
|
|
6466
|
+
const download = await yandexDiskRequest("GET", "/resources/download", { query: { path: normalizeYandexDiskPath(remotePath) } });
|
|
6467
|
+
const response = await fetch(download.href, { signal: AbortSignal.timeout(120000) });
|
|
6468
|
+
if (!response.ok) throw new Error(`Yandex Disk read failed: ${response.status} ${response.statusText}`);
|
|
6469
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
6470
|
+
if (buffer.length > maxBytes) throw new Error(`Файл слишком большой для чтения: ${buffer.length} байт. Лимит: ${maxBytes}.`);
|
|
6471
|
+
return { provider: "yandex-disk", remote: remotePath, text: buffer.toString("utf8"), size: buffer.length };
|
|
6472
|
+
}
|
|
6473
|
+
|
|
6474
|
+
async function yandexDiskMove(from, to, options = {}) {
|
|
6475
|
+
if (!options.confirm) throw new Error("Для перемещения на Яндекс Диске нужен аргумент confirm=true.");
|
|
6476
|
+
if (!from || !to) throw new Error("Для перемещения нужны from и to.");
|
|
6477
|
+
await ensureYandexDiskDir(path.dirname(to), { allowExisting: true });
|
|
6478
|
+
await yandexDiskRequest("POST", "/resources/move", {
|
|
6479
|
+
query: { from: normalizeYandexDiskPath(from), path: normalizeYandexDiskPath(to), overwrite: Boolean(options.overwrite) },
|
|
6480
|
+
timeout: 60000,
|
|
6481
|
+
});
|
|
6482
|
+
return { provider: "yandex-disk", status: "moved", from, remote: to };
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
async function yandexDiskCopy(from, to, options = {}) {
|
|
6486
|
+
if (!options.confirm) throw new Error("Для копирования на Яндекс Диске нужен аргумент confirm=true.");
|
|
6487
|
+
if (!from || !to) throw new Error("Для копирования нужны from и to.");
|
|
6488
|
+
await ensureYandexDiskDir(path.dirname(to), { allowExisting: true });
|
|
6489
|
+
await yandexDiskRequest("POST", "/resources/copy", {
|
|
6490
|
+
query: { from: normalizeYandexDiskPath(from), path: normalizeYandexDiskPath(to), overwrite: Boolean(options.overwrite) },
|
|
6491
|
+
timeout: 60000,
|
|
6492
|
+
});
|
|
6493
|
+
return { provider: "yandex-disk", status: "copied", from, remote: to };
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6496
|
+
async function yandexDiskRename(remotePath, newName, options = {}) {
|
|
6497
|
+
if (!remotePath || !newName) throw new Error("Для переименования нужны путь и новое имя.");
|
|
6498
|
+
const current = denormalizeYandexDiskPath(normalizeYandexDiskPath(remotePath));
|
|
6499
|
+
const target = path.posix.join(path.posix.dirname(current), slugYandexDiskName(newName));
|
|
6500
|
+
return yandexDiskMove(remotePath, target, { ...options, confirm: true });
|
|
6501
|
+
}
|
|
6502
|
+
|
|
6503
|
+
async function yandexDiskTrashList(remotePath = "", options = {}) {
|
|
6504
|
+
const query = { limit: Number(options.limit || 100) };
|
|
6505
|
+
if (remotePath) query.path = normalizeYandexDiskPath(remotePath);
|
|
6506
|
+
const payload = await yandexDiskRequest("GET", "/trash/resources", { query });
|
|
6507
|
+
const items = payload._embedded?.items || [];
|
|
6508
|
+
return items.map(formatYandexDiskResource);
|
|
6509
|
+
}
|
|
6510
|
+
|
|
6511
|
+
async function yandexDiskRestore(remotePath, options = {}) {
|
|
6512
|
+
if (!options.confirm) throw new Error("Для восстановления из корзины нужен аргумент confirm=true.");
|
|
6513
|
+
if (!remotePath) throw new Error("Путь в корзине обязателен.");
|
|
6514
|
+
await yandexDiskRequest("PUT", "/trash/resources/restore", {
|
|
6515
|
+
query: { path: normalizeYandexDiskPath(remotePath), name: options.name || "", overwrite: Boolean(options.overwrite) },
|
|
6516
|
+
timeout: 60000,
|
|
6517
|
+
});
|
|
6518
|
+
return { provider: "yandex-disk", status: "restored", remote: remotePath };
|
|
6519
|
+
}
|
|
6520
|
+
|
|
6521
|
+
async function yandexDiskEmptyTrash(options = {}) {
|
|
6522
|
+
if (!options.confirm) throw new Error("Для очистки корзины нужен аргумент confirm=true.");
|
|
6523
|
+
await yandexDiskRequest("DELETE", "/trash/resources", {
|
|
6524
|
+
query: { path: options.path ? normalizeYandexDiskPath(options.path) : "" },
|
|
6525
|
+
timeout: 60000,
|
|
6526
|
+
});
|
|
6527
|
+
return { provider: "yandex-disk", status: "trash-empty-requested", remote: options.path || "trash" };
|
|
6528
|
+
}
|
|
6529
|
+
|
|
6530
|
+
function formatYandexDiskResource(item = {}) {
|
|
6531
|
+
return {
|
|
6532
|
+
provider: "yandex-disk",
|
|
6533
|
+
type: item.type === "dir" ? "dir" : "file",
|
|
6534
|
+
name: item.name || path.basename(item.path || ""),
|
|
6535
|
+
path: denormalizeYandexDiskPath(item.path || ""),
|
|
6536
|
+
remote: denormalizeYandexDiskPath(item.path || ""),
|
|
6537
|
+
size: item.size || 0,
|
|
6538
|
+
mimeType: item.mime_type || "",
|
|
6539
|
+
created: item.created || "",
|
|
6540
|
+
modified: item.modified || "",
|
|
6541
|
+
publicUrl: item.public_url || "",
|
|
6542
|
+
md5: item.md5 || "",
|
|
6543
|
+
sha256: item.sha256 || "",
|
|
6544
|
+
};
|
|
6545
|
+
}
|
|
6546
|
+
|
|
6547
|
+
function slugYandexDiskName(value) {
|
|
6548
|
+
return String(value || "")
|
|
6549
|
+
.replace(/[\\/:*?"<>|]+/gu, "-")
|
|
6550
|
+
.replace(/\s+/gu, " ")
|
|
6551
|
+
.trim()
|
|
6552
|
+
|| `item-${timestampForFile()}`;
|
|
6553
|
+
}
|
|
6554
|
+
|
|
5281
6555
|
async function yandexDiskListRecursive(remotePath, options = {}) {
|
|
5282
6556
|
const depth = Number(options.depth || 4);
|
|
5283
6557
|
const limit = Number(options.limit || 200);
|
|
@@ -5325,6 +6599,119 @@ async function yandexDiskShare(remotePath) {
|
|
|
5325
6599
|
return { provider: "yandex-disk", remote: remotePath, publicUrl: payload.public_url || "-" };
|
|
5326
6600
|
}
|
|
5327
6601
|
|
|
6602
|
+
async function yandexDiskShareWithQr(remotePath, options = {}) {
|
|
6603
|
+
if (!options.confirm) throw new Error("Для публикации ссылки и QR нужен аргумент confirm=true.");
|
|
6604
|
+
if (!remotePath) throw new Error("Путь на Яндекс Диске обязателен.");
|
|
6605
|
+
const shared = await yandexDiskShare(remotePath);
|
|
6606
|
+
const qrRemotePath = options.qrPath || buildYandexDiskQrPath(remotePath);
|
|
6607
|
+
const tempPath = path.join(CONFIG_DIR, `qr-${Date.now()}.png`);
|
|
6608
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
6609
|
+
try {
|
|
6610
|
+
await createQrPng(shared.publicUrl, tempPath);
|
|
6611
|
+
await yandexDiskUpload(tempPath, qrRemotePath, { overwrite: true });
|
|
6612
|
+
} finally {
|
|
6613
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
6614
|
+
}
|
|
6615
|
+
const qrShared = await yandexDiskShare(qrRemotePath);
|
|
6616
|
+
return {
|
|
6617
|
+
provider: "yandex-disk",
|
|
6618
|
+
status: "shared-with-qr",
|
|
6619
|
+
remote: remotePath,
|
|
6620
|
+
publicUrl: shared.publicUrl,
|
|
6621
|
+
qrRemote: qrRemotePath,
|
|
6622
|
+
qrPublicUrl: qrShared.publicUrl,
|
|
6623
|
+
};
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
async function yandexDiskShareEmail(args = {}) {
|
|
6627
|
+
if (!args.confirm) throw new Error("Для отправки ссылки по почте нужен аргумент confirm=true.");
|
|
6628
|
+
const remotePath = args.remotePath || args.path || args.targetFolder || args.target;
|
|
6629
|
+
if (!remotePath) throw new Error("Путь на Яндекс Диске обязателен.");
|
|
6630
|
+
const to = await resolveYandexShareRecipients(args);
|
|
6631
|
+
const shared = await yandexDiskShareWithQr(remotePath, { ...args, confirm: true });
|
|
6632
|
+
const subject = args.subject || `Ссылка на Яндекс Диск: ${path.posix.basename(remotePath) || "материалы"}`;
|
|
6633
|
+
const text = [
|
|
6634
|
+
args.text || args.message || "Здравствуйте. Направляю ссылку на материалы.",
|
|
6635
|
+
"",
|
|
6636
|
+
`Ссылка: ${shared.publicUrl}`,
|
|
6637
|
+
`QR-код: ${shared.qrPublicUrl}`,
|
|
6638
|
+
"",
|
|
6639
|
+
"QR-код сохранен на Яндекс Диске:",
|
|
6640
|
+
shared.qrRemote,
|
|
6641
|
+
].join("\n");
|
|
6642
|
+
const sent = await yandexMailSend({ to, subject, text, confirm: true });
|
|
6643
|
+
return { ...shared, status: "shared-and-sent", to: sent.to, subject };
|
|
6644
|
+
}
|
|
6645
|
+
|
|
6646
|
+
async function yandexDiskPackageShareEmail(args = {}) {
|
|
6647
|
+
if (!args.confirm) throw new Error("Для пакетной отправки папки нужен аргумент confirm=true.");
|
|
6648
|
+
const sourcePath = args.sourcePath || args.source || args.from;
|
|
6649
|
+
const targetFolder = args.targetFolder || args.target || args.to;
|
|
6650
|
+
if (!sourcePath || !targetFolder) throw new Error("Нужны sourcePath и targetFolder.");
|
|
6651
|
+
await cloudCreateFolder("yandex-disk", targetFolder);
|
|
6652
|
+
const items = await yandexDiskList(sourcePath);
|
|
6653
|
+
const mode = /move|перен/iu.test(args.mode || "") ? "move" : "copy";
|
|
6654
|
+
const transferred = [];
|
|
6655
|
+
for (const item of items.slice(0, Number(args.limit || 100))) {
|
|
6656
|
+
const destination = path.posix.join(targetFolder, item.name);
|
|
6657
|
+
if (mode === "move") {
|
|
6658
|
+
await yandexDiskMove(item.path, destination, { confirm: true, overwrite: args.overwrite !== false });
|
|
6659
|
+
} else {
|
|
6660
|
+
await yandexDiskCopy(item.path, destination, { confirm: true, overwrite: args.overwrite !== false });
|
|
6661
|
+
}
|
|
6662
|
+
transferred.push({ from: item.path, to: destination, type: item.type });
|
|
6663
|
+
}
|
|
6664
|
+
const shared = await yandexDiskShareEmail({
|
|
6665
|
+
remotePath: targetFolder,
|
|
6666
|
+
to: args.to,
|
|
6667
|
+
email: args.email,
|
|
6668
|
+
contact: args.contact || args.contactQuery,
|
|
6669
|
+
subject: args.subject || `Материалы на Яндекс Диске: ${path.posix.basename(targetFolder)}`,
|
|
6670
|
+
text: args.text || args.message || `Собрал материалы в папку ${targetFolder}.`,
|
|
6671
|
+
confirm: true,
|
|
6672
|
+
});
|
|
6673
|
+
return {
|
|
6674
|
+
...shared,
|
|
6675
|
+
status: "package-shared-and-sent",
|
|
6676
|
+
sourcePath,
|
|
6677
|
+
targetFolder,
|
|
6678
|
+
mode,
|
|
6679
|
+
transferred: transferred.length,
|
|
6680
|
+
};
|
|
6681
|
+
}
|
|
6682
|
+
|
|
6683
|
+
async function resolveYandexShareRecipients(args = {}) {
|
|
6684
|
+
const emails = Array.isArray(args.to) ? args.to : String(args.to || args.email || "").split(/[;,]/u).map((item) => item.trim()).filter(Boolean);
|
|
6685
|
+
if (emails.length) return emails;
|
|
6686
|
+
const contactQuery = args.contact || args.contactQuery || args.name || "";
|
|
6687
|
+
if (!contactQuery) throw new Error("Укажите email или контакт получателя.");
|
|
6688
|
+
const result = await resolveYandexMailRecipientFromContacts(contactQuery);
|
|
6689
|
+
if (result.status === "not-found") throw new Error(`В Яндекс Контактах не нашел: ${contactQuery}. Укажите email вручную.`);
|
|
6690
|
+
if (result.status === "no-email") throw new Error(`Контакт найден, но email не указан: ${result.contact.name}. Укажите email вручную.`);
|
|
6691
|
+
if (result.status === "ambiguous") {
|
|
6692
|
+
const rows = result.contacts.map((contact, index) => `${index + 1}. ${contact.name || contact.email || "Контакт"}${contact.email ? `, ${contact.email}` : ", email не указан"}`).join("\n");
|
|
6693
|
+
throw new Error(`Нашел несколько контактов для "${contactQuery}". Уточните получателя:\n${rows}`);
|
|
6694
|
+
}
|
|
6695
|
+
return [result.contact.email];
|
|
6696
|
+
}
|
|
6697
|
+
|
|
6698
|
+
async function createQrPng(text, outputPath) {
|
|
6699
|
+
const QRCode = await import("qrcode");
|
|
6700
|
+
await QRCode.toFile(outputPath, String(text || ""), {
|
|
6701
|
+
type: "png",
|
|
6702
|
+
errorCorrectionLevel: "M",
|
|
6703
|
+
margin: 2,
|
|
6704
|
+
width: 512,
|
|
6705
|
+
});
|
|
6706
|
+
}
|
|
6707
|
+
|
|
6708
|
+
function buildYandexDiskQrPath(remotePath) {
|
|
6709
|
+
const current = denormalizeYandexDiskPath(normalizeYandexDiskPath(remotePath));
|
|
6710
|
+
const dir = path.posix.dirname(current);
|
|
6711
|
+
const base = path.posix.basename(current).replace(/\.[^.]+$/u, "");
|
|
6712
|
+
return path.posix.join(dir, `${slugYandexDiskName(base || "link")}-qr.png`);
|
|
6713
|
+
}
|
|
6714
|
+
|
|
5328
6715
|
async function ensureYandexDiskDir(remotePath, options = {}) {
|
|
5329
6716
|
const normalized = normalizeYandexDiskPath(remotePath || CLOUD_DEFAULT_REMOTE_DIR);
|
|
5330
6717
|
const plain = denormalizeYandexDiskPath(normalized);
|
|
@@ -5345,7 +6732,7 @@ async function ensureYandexDiskDir(remotePath, options = {}) {
|
|
|
5345
6732
|
|
|
5346
6733
|
function normalizeYandexDiskPath(remotePath) {
|
|
5347
6734
|
const text = String(remotePath || CLOUD_DEFAULT_REMOTE_DIR).trim().replace(/\\/g, "/");
|
|
5348
|
-
if (text.startsWith("disk:") || text.startsWith("app:")) return text;
|
|
6735
|
+
if (text.startsWith("disk:") || text.startsWith("app:") || text.startsWith("trash:")) return text;
|
|
5349
6736
|
return text.startsWith("/") ? text : `/${text}`;
|
|
5350
6737
|
}
|
|
5351
6738
|
|
|
@@ -9692,6 +11079,10 @@ function isCronDue(job) {
|
|
|
9692
11079
|
if (everyMinutes) {
|
|
9693
11080
|
return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyMinutes[1]) * 60 * 1000;
|
|
9694
11081
|
}
|
|
11082
|
+
const everyDays = normalized.match(/кажд(?:ые|ую)\s+(\d+)\s*(?:дн|день|дня|дней)/u) || normalized.match(/every\s+(\d+)\s*(?:d|day|days)/u);
|
|
11083
|
+
if (everyDays) {
|
|
11084
|
+
return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyDays[1]) * 24 * 60 * 60 * 1000;
|
|
11085
|
+
}
|
|
9695
11086
|
if (normalized.includes("каждый день") || normalized.includes("daily")) {
|
|
9696
11087
|
return !lastRun || now.toISOString().slice(0, 10) !== lastRun.toISOString().slice(0, 10);
|
|
9697
11088
|
}
|
|
@@ -10061,19 +11452,35 @@ async function aiAsk(args, context = {}) {
|
|
|
10061
11452
|
if (!options.quiet) console.log(casualAnswer);
|
|
10062
11453
|
return casualAnswer;
|
|
10063
11454
|
}
|
|
10064
|
-
const
|
|
10065
|
-
if (
|
|
11455
|
+
const userSkillAnswer = await buildUserSkillDirectAnswer(question);
|
|
11456
|
+
if (userSkillAnswer) {
|
|
10066
11457
|
if (historyEnabled) {
|
|
10067
|
-
recordAskHistory({ question, answer:
|
|
10068
|
-
appendSessionExchange(sessionId, question,
|
|
11458
|
+
recordAskHistory({ question, answer: userSkillAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
11459
|
+
appendSessionExchange(sessionId, question, userSkillAnswer, dataContext, "");
|
|
10069
11460
|
}
|
|
10070
|
-
emitEvent(options, "answer", { length:
|
|
11461
|
+
emitEvent(options, "answer", { length: userSkillAnswer.length, sessionId, direct: true, skill: true });
|
|
10071
11462
|
if (options.output) {
|
|
10072
11463
|
await assertPermission("writeFiles");
|
|
10073
|
-
await writeFile(options.output,
|
|
11464
|
+
await writeFile(options.output, userSkillAnswer, "utf8");
|
|
11465
|
+
}
|
|
11466
|
+
if (!options.quiet) console.log(userSkillAnswer);
|
|
11467
|
+
return userSkillAnswer;
|
|
11468
|
+
}
|
|
11469
|
+
if (/(контакт|адресн)/iu.test(question) && !isExplicitYandexDiskPathDelete(question)) {
|
|
11470
|
+
const yandexContactAnswer = await buildYandexDirectAnswer(question, context.history || history);
|
|
11471
|
+
if (yandexContactAnswer) {
|
|
11472
|
+
if (historyEnabled) {
|
|
11473
|
+
recordAskHistory({ question, answer: yandexContactAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
11474
|
+
appendSessionExchange(sessionId, question, yandexContactAnswer, dataContext, "");
|
|
11475
|
+
}
|
|
11476
|
+
emitEvent(options, "answer", { length: yandexContactAnswer.length, sessionId, direct: true, yandex: true });
|
|
11477
|
+
if (options.output) {
|
|
11478
|
+
await assertPermission("writeFiles");
|
|
11479
|
+
await writeFile(options.output, yandexContactAnswer, "utf8");
|
|
11480
|
+
}
|
|
11481
|
+
if (!options.quiet) console.log(yandexContactAnswer);
|
|
11482
|
+
return yandexContactAnswer;
|
|
10074
11483
|
}
|
|
10075
|
-
if (!options.quiet) console.log(yandexAnswer);
|
|
10076
|
-
return yandexAnswer;
|
|
10077
11484
|
}
|
|
10078
11485
|
const cloudAnswer = await buildCloudDirectAnswer(question);
|
|
10079
11486
|
if (cloudAnswer) {
|
|
@@ -10089,6 +11496,20 @@ async function aiAsk(args, context = {}) {
|
|
|
10089
11496
|
if (!options.quiet) console.log(cloudAnswer);
|
|
10090
11497
|
return cloudAnswer;
|
|
10091
11498
|
}
|
|
11499
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, context.history || history);
|
|
11500
|
+
if (yandexAnswer) {
|
|
11501
|
+
if (historyEnabled) {
|
|
11502
|
+
recordAskHistory({ question, answer: yandexAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
11503
|
+
appendSessionExchange(sessionId, question, yandexAnswer, dataContext, "");
|
|
11504
|
+
}
|
|
11505
|
+
emitEvent(options, "answer", { length: yandexAnswer.length, sessionId, direct: true, yandex: true });
|
|
11506
|
+
if (options.output) {
|
|
11507
|
+
await assertPermission("writeFiles");
|
|
11508
|
+
await writeFile(options.output, yandexAnswer, "utf8");
|
|
11509
|
+
}
|
|
11510
|
+
if (!options.quiet) console.log(yandexAnswer);
|
|
11511
|
+
return yandexAnswer;
|
|
11512
|
+
}
|
|
10092
11513
|
const geoAnswer = await buildGeoDirectAnswer(question);
|
|
10093
11514
|
if (geoAnswer) {
|
|
10094
11515
|
if (historyEnabled) {
|
|
@@ -10214,6 +11635,10 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10214
11635
|
].join("\n");
|
|
10215
11636
|
}
|
|
10216
11637
|
|
|
11638
|
+
if (/(контакт|адресн)/iu.test(normalized) && !mailFollowup && !isExplicitYandexDiskPathDelete(question)) {
|
|
11639
|
+
return await buildYandexContactsDirectAnswer(question, normalized);
|
|
11640
|
+
}
|
|
11641
|
+
|
|
10217
11642
|
if (mailFollowup || /(почт|письм|email|e-mail|спам|чернов|отправлен|исходящ|корзин)/iu.test(normalized)) {
|
|
10218
11643
|
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
10219
11644
|
const minutes = Number(String(question || "").match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 5);
|
|
@@ -10361,13 +11786,128 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10361
11786
|
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
10362
11787
|
}
|
|
10363
11788
|
|
|
10364
|
-
if (/(
|
|
11789
|
+
if (/(документ|документы|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
|
|
11790
|
+
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
11791
|
+
const result = await yandexDocsStatus();
|
|
11792
|
+
return `Яндекс Документы через Диск подключены. Папка: ${result.folder}.`;
|
|
11793
|
+
}
|
|
11794
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
|
|
11795
|
+
const text = extractShareMessage(question) || cleanupCloudSaveText(question);
|
|
11796
|
+
if (!text) return "Укажите текст документа. Пример: создай документ на Яндекс Диске текст: ...";
|
|
11797
|
+
const result = await yandexDocsCreateText({
|
|
11798
|
+
title: extractYandexDocTitle(question),
|
|
11799
|
+
text,
|
|
11800
|
+
format: /html/iu.test(normalized) ? "html" : /txt|текстов/iu.test(normalized) ? "txt" : "md",
|
|
11801
|
+
confirm: true,
|
|
11802
|
+
});
|
|
11803
|
+
return `Документ создан на Яндекс Диске: ${result.remote}.`;
|
|
11804
|
+
}
|
|
11805
|
+
if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
|
|
11806
|
+
const result = await yandexDocsRead(extractCloudPath(question) || cleanupYandexQuery(question), {});
|
|
11807
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11808
|
+
}
|
|
11809
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
|
|
11810
|
+
const result = await yandexDocsShare(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
|
|
11811
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11812
|
+
}
|
|
11813
|
+
if (/(переимен|rename)/iu.test(normalized)) {
|
|
11814
|
+
const result = await yandexDocsRename(extractCloudPath(question) || cleanupYandexQuery(question), extractCloudNewName(question), { confirm: true });
|
|
11815
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11816
|
+
}
|
|
11817
|
+
if (/(удали|удалить)/iu.test(normalized)) {
|
|
11818
|
+
const result = await yandexDocsDelete(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
|
|
11819
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11820
|
+
}
|
|
11821
|
+
const rows = /(найди|поиск)/iu.test(normalized)
|
|
11822
|
+
? await yandexDocsFind(cleanupYandexQuery(question), { limit: 20 })
|
|
11823
|
+
: await yandexDocsList({ limit: 20 });
|
|
11824
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
11825
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
11826
|
+
}
|
|
11827
|
+
|
|
11828
|
+
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
11829
|
+
if (/(статус|проверь|работает|доступ)/iu.test(normalized) && /(календар|телемост)/iu.test(normalized)) {
|
|
11830
|
+
const result = /телемост/iu.test(normalized) ? await yandexTelemostStatus() : await yandexCalendarStatus();
|
|
11831
|
+
return /телемост/iu.test(normalized)
|
|
11832
|
+
? `${result.message} Календарь: ${result.calendar || "-"}.`
|
|
11833
|
+
: `Яндекс Календарь подключен: ${result.displayName || result.url}. Календарей: ${result.calendars || 1}.`;
|
|
11834
|
+
}
|
|
11835
|
+
if (/(какие|список).{0,30}(календар|календари)|календари/iu.test(normalized)) {
|
|
11836
|
+
const rows = await yandexCalendarCalendars({ limit: 20 });
|
|
11837
|
+
if (!rows.length) return "Календари Яндекса не найдены.";
|
|
11838
|
+
return ["Яндекс Календари:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.href}`)].join("\n");
|
|
11839
|
+
}
|
|
11840
|
+
if (/(напомин|уведом)/iu.test(normalized) && /(добав|постав|создай)/iu.test(normalized)) {
|
|
11841
|
+
const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
|
|
11842
|
+
const result = await yandexCalendarAddReminder(cleanupCalendarEventQuery(question), { minutes, confirm: true });
|
|
11843
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11844
|
+
}
|
|
11845
|
+
if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
|
|
11846
|
+
const dateTime = extractDateTimeFromText(question);
|
|
11847
|
+
const title = extractCalendarTitle(question) || (/телемост/iu.test(normalized) ? "Телемост IOLA" : "Событие IOLA");
|
|
11848
|
+
const args = { ...dateTime, title, description: extractShareMessage(question) || "", confirm: true };
|
|
11849
|
+
const result = /телемост/iu.test(normalized)
|
|
11850
|
+
? await yandexTelemostCreateEvent(args)
|
|
11851
|
+
: /кажд|еженед|ежеднев|ежемесяч|повтор/iu.test(normalized)
|
|
11852
|
+
? await yandexCalendarCreateRecurringEvent({ ...args, ...extractCalendarRepeat(question), confirm: true })
|
|
11853
|
+
: await yandexCalendarCreateEvent(args);
|
|
11854
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11855
|
+
}
|
|
11856
|
+
if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) {
|
|
11857
|
+
const result = await yandexCalendarMove(cleanupCalendarEventQuery(question), { ...extractDateTimeFromText(question), confirm: true });
|
|
11858
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11859
|
+
}
|
|
11860
|
+
if (/(переимен|измени\s+назв|смени\s+назв)/iu.test(normalized)) {
|
|
11861
|
+
const result = await yandexCalendarUpdate(cleanupCalendarEventQuery(question), { title: extractCalendarTitle(question), confirm: true });
|
|
11862
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11863
|
+
}
|
|
11864
|
+
if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) {
|
|
11865
|
+
const result = await yandexCalendarDelete(cleanupCalendarEventQuery(question), { confirm: true });
|
|
11866
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11867
|
+
}
|
|
11868
|
+
if (/(найди|поиск|где|покажи).{0,40}(событи|встреч|телемост)/iu.test(normalized)) {
|
|
11869
|
+
const rows = await yandexCalendarSearch(cleanupCalendarEventQuery(question), { limit: 20 });
|
|
11870
|
+
if (!rows.length) return "События в Яндекс Календаре по запросу не найдены.";
|
|
11871
|
+
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
|
|
11872
|
+
}
|
|
10365
11873
|
const rows = await yandexCalendarList({ limit: 10 });
|
|
10366
11874
|
if (!rows.length) return "В ближайшие дни событий в Яндекс Календаре не найдено.";
|
|
10367
|
-
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.start || "-"}`)].join("\n");
|
|
11875
|
+
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
|
|
10368
11876
|
}
|
|
10369
11877
|
|
|
10370
11878
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
11879
|
+
if (/(дубликат|повтор)/iu.test(normalized)) {
|
|
11880
|
+
const rows = await yandexContactsFindDuplicates({ limit: 20 });
|
|
11881
|
+
if (!rows.length) return "Дубликаты контактов не найдены.";
|
|
11882
|
+
return rows.map((row) => formatToolResult({ rows: [row], outputs: [] }, {})).join("\n");
|
|
11883
|
+
}
|
|
11884
|
+
if (/(неполн|без\s+email|без\s+почт|без\s+телефон|без\s+адрес)/iu.test(normalized)) {
|
|
11885
|
+
const field = /без\s+(?:email|почт)/iu.test(normalized) ? "email" : /без\s+телефон/iu.test(normalized) ? "phone" : /без\s+адрес/iu.test(normalized) ? "address" : "";
|
|
11886
|
+
const rows = await yandexContactsFindIncomplete({ field, limit: 30 });
|
|
11887
|
+
if (!rows.length) return "Неполные контакты по этому признаку не найдены.";
|
|
11888
|
+
return ["Неполные контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
|
|
11889
|
+
}
|
|
11890
|
+
if (/(экспорт|выгруз|сохрани|резерв|backup|бэкап)/iu.test(normalized) && /(диск|яндекс.?диск|облак)/iu.test(normalized)) {
|
|
11891
|
+
const result = await yandexContactsBackupToDisk({ format: /csv/iu.test(normalized) ? "csv" : "vcard", confirm: true });
|
|
11892
|
+
return `Контакты сохранены на Яндекс Диск: ${result.remote}. Записей: ${result.rows}.`;
|
|
11893
|
+
}
|
|
11894
|
+
if (/(экспорт|выгруз)/iu.test(normalized)) {
|
|
11895
|
+
const result = await yandexContactsExport(/csv/iu.test(normalized) ? "csv" : "vcard", {});
|
|
11896
|
+
return `Контакты экспортированы: ${result.output}. Записей: ${result.rows}.`;
|
|
11897
|
+
}
|
|
11898
|
+
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(normalized)) {
|
|
11899
|
+
const draft = parseYandexContactCreateRequest(question);
|
|
11900
|
+
if (!draft.name || (!draft.email && !draft.phone)) return "Для создания контакта укажите имя и email или телефон.";
|
|
11901
|
+
const result = await yandexContactsCreate({ ...draft, confirm: true });
|
|
11902
|
+
return `Контакт создан: ${formatYandexContact(result)}.`;
|
|
11903
|
+
}
|
|
11904
|
+
if (/(удали|удалить).{0,40}контакт/iu.test(normalized)) {
|
|
11905
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
11906
|
+
const result = await yandexContactsDelete(query, { confirm: true });
|
|
11907
|
+
if (result.status === "ambiguous") return [`Нашел несколько контактов. Уточните:`, ...result.contacts.map((contact, index) => `${index + 1}. ${formatYandexContact(contact)}`)].join("\n");
|
|
11908
|
+
if (result.status === "not-found") return `Контакт не найден: ${query}.`;
|
|
11909
|
+
return `Контакт удален: ${formatYandexContact(result)}.`;
|
|
11910
|
+
}
|
|
10371
11911
|
if (/(добав|запиши|сохрани).{0,40}(email|e-mail|почт)/iu.test(normalized)) {
|
|
10372
11912
|
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
10373
11913
|
const query = cleanupYandexContactEmailTarget(question, email);
|
|
@@ -10383,6 +11923,54 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10383
11923
|
}
|
|
10384
11924
|
return `Email добавлен в контакт: ${result.name || query}, ${result.email}.`;
|
|
10385
11925
|
}
|
|
11926
|
+
if (/(добав|запиши|сохрани).{0,40}(телефон|номер)/iu.test(normalized)) {
|
|
11927
|
+
const phone = String(question || "").match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || "";
|
|
11928
|
+
const query = cleanupYandexContactActionQuery(question.replace(phone, " "));
|
|
11929
|
+
if (!phone || !query) return "Укажите контакт и телефон.";
|
|
11930
|
+
const result = await yandexContactsUpdate(query, { phone, mode: "add-phone", confirm: true });
|
|
11931
|
+
if (result.status !== "updated") return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11932
|
+
return `Телефон добавлен: ${formatYandexContact(result)}.`;
|
|
11933
|
+
}
|
|
11934
|
+
if (/(добав|запиши|сохрани).{0,40}(адрес)/iu.test(normalized)) {
|
|
11935
|
+
const address = question.match(/(?:адрес|по адресу)\s*:?\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
11936
|
+
const query = cleanupYandexContactActionQuery(question.replace(address, " "));
|
|
11937
|
+
if (!address || !query) return "Укажите контакт и адрес.";
|
|
11938
|
+
const result = await yandexContactsUpdate(query, { address, mode: "add-address", confirm: true });
|
|
11939
|
+
if (result.status !== "updated") return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11940
|
+
return `Адрес добавлен: ${formatYandexContact(result)}.`;
|
|
11941
|
+
}
|
|
11942
|
+
if (/(добав|запиши|сохрани).{0,40}(заметк|коммент|примеч)/iu.test(normalized)) {
|
|
11943
|
+
const note = question.match(/(?:заметк\p{L}*|коммент\p{L}*|примеч\p{L}*)\s*:?\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
11944
|
+
const query = cleanupYandexContactActionQuery(question.replace(note, " "));
|
|
11945
|
+
if (!note || !query) return "Укажите контакт и текст заметки.";
|
|
11946
|
+
const result = await yandexContactsUpdate(query, { note, mode: "add-note", confirm: true });
|
|
11947
|
+
if (result.status !== "updated") return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11948
|
+
return `Заметка добавлена: ${formatYandexContact(result)}.`;
|
|
11949
|
+
}
|
|
11950
|
+
if (/(создай|сделай).{0,40}(папк).{0,40}(контакт)/iu.test(normalized)) {
|
|
11951
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
11952
|
+
const result = await yandexContactCreateDiskFolder({ query, confirm: true });
|
|
11953
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11954
|
+
}
|
|
11955
|
+
if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(normalized)) {
|
|
11956
|
+
const remotePath = extractCloudPath(question);
|
|
11957
|
+
const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
|
|
11958
|
+
const result = await yandexContactSendDiskLinkQr({ contact, path: remotePath, confirm: true });
|
|
11959
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11960
|
+
}
|
|
11961
|
+
if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
|
|
11962
|
+
const draft = parseYandexMailSendRequest(question);
|
|
11963
|
+
const result = await yandexContactSendMail({ contact: draft.contactQuery || cleanupYandexContactActionQuery(question), subject: draft.subject, text: draft.text, confirm: true });
|
|
11964
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11965
|
+
}
|
|
11966
|
+
if (/(создай|добавь|запланируй).{0,40}(встреч|событи|календар|телемост)/iu.test(normalized)) {
|
|
11967
|
+
const dateTime = extractDateTimeFromText(question);
|
|
11968
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
11969
|
+
const result = /телемост/iu.test(normalized)
|
|
11970
|
+
? await yandexContactCreateTelemostEvent({ query, ...dateTime, confirm: true })
|
|
11971
|
+
: await yandexContactCreateCalendarEvent({ query, ...dateTime, confirm: true });
|
|
11972
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11973
|
+
}
|
|
10386
11974
|
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
10387
11975
|
const result = await yandexContactsStatus();
|
|
10388
11976
|
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
@@ -10400,8 +11988,214 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10400
11988
|
return "";
|
|
10401
11989
|
}
|
|
10402
11990
|
|
|
11991
|
+
async function buildYandexContactsDirectAnswer(question, normalized = "") {
|
|
11992
|
+
const text = normalized || String(question || "").toLocaleLowerCase("ru-RU");
|
|
11993
|
+
if (/(авто|автомат|регуляр|кажд|период|обслужив|проверяй|проверку)/iu.test(text) && /(контакт)/iu.test(text) && /(включ|запусти|начни|поставь|создай)/iu.test(text)) {
|
|
11994
|
+
const days = Number(String(question || "").match(/(\d+)\s*(?:дн|день|дня|дней)/iu)?.[1] || 7);
|
|
11995
|
+
const result = await yandexContactsMaintenanceEnable(days, { backup: /(backup|бэкап|резерв|диск)/iu.test(text) });
|
|
11996
|
+
return `Регулярная проверка контактов включена: каждые ${result.days} дней. Backup на Диск: ${result.backup ? "yes" : "no"}.`;
|
|
11997
|
+
}
|
|
11998
|
+
if (/(авто|автомат|регуляр|кажд|период|обслужив|проверяй|проверку)/iu.test(text) && /(контакт)/iu.test(text) && /(выключ|отключ|останов|убери)/iu.test(text)) {
|
|
11999
|
+
await yandexContactsMaintenanceDisable();
|
|
12000
|
+
return "Регулярная проверка контактов выключена.";
|
|
12001
|
+
}
|
|
12002
|
+
if (/(обслужив|провер|диагност|doctor)/iu.test(text) && /(контакт)/iu.test(text) && /(сейчас|запусти|сделай|проверь)/iu.test(text)) {
|
|
12003
|
+
const result = await yandexContactsMaintenanceTick({ force: true, backup: /(backup|бэкап|резерв)/iu.test(text) });
|
|
12004
|
+
return [
|
|
12005
|
+
"Проверка контактов выполнена:",
|
|
12006
|
+
`Всего: ${result.total}`,
|
|
12007
|
+
`Неполных: ${result.incomplete}`,
|
|
12008
|
+
`Групп дубликатов: ${result.duplicateGroups}`,
|
|
12009
|
+
result.backupRemote ? `Backup: ${result.backupRemote}` : "",
|
|
12010
|
+
].filter(Boolean).join("\n");
|
|
12011
|
+
}
|
|
12012
|
+
if (/(дубликат|повтор)/iu.test(text)) {
|
|
12013
|
+
const rows = await yandexContactsFindDuplicates({ limit: 20 });
|
|
12014
|
+
if (!rows.length) return "Дубликаты контактов не найдены.";
|
|
12015
|
+
return rows.map((row) => formatToolResult({ rows: [row], outputs: [] }, {})).join("\n");
|
|
12016
|
+
}
|
|
12017
|
+
if (/(неполн|без\s+email|без\s+почт|без\s+телефон|без\s+адрес)/iu.test(text)) {
|
|
12018
|
+
const field = /без\s+(?:email|почт)/iu.test(text) ? "email" : /без\s+телефон/iu.test(text) ? "phone" : /без\s+адрес/iu.test(text) ? "address" : "";
|
|
12019
|
+
const rows = await yandexContactsFindIncomplete({ field, limit: 30 });
|
|
12020
|
+
if (!rows.length) return "Неполные контакты по этому признаку не найдены.";
|
|
12021
|
+
return ["Неполные контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
|
|
12022
|
+
}
|
|
12023
|
+
if (/(экспорт|выгруз|сохрани|резерв|backup|бэкап)/iu.test(text) && /(диск|яндекс.?диск|облак)/iu.test(text)) {
|
|
12024
|
+
const result = await yandexContactsBackupToDisk({ format: /csv/iu.test(text) ? "csv" : "vcard", confirm: true });
|
|
12025
|
+
return `Контакты сохранены на Яндекс Диск: ${result.remote}. Записей: ${result.rows}.`;
|
|
12026
|
+
}
|
|
12027
|
+
if (/(импорт|импортируй|загрузи).{0,40}контакт/iu.test(text)) {
|
|
12028
|
+
const inputPath = extractLocalInputPath(question) || question.match(/(?:из|файл)\s+([^\s]+(?:\.csv|\.vcf|\.vcard))/iu)?.[1] || "";
|
|
12029
|
+
if (!inputPath) return "Укажите файл CSV или vCard для импорта контактов.";
|
|
12030
|
+
const result = await yandexContactsImport(/\.csv$/iu.test(inputPath) ? "csv" : "vcard", { path: inputPath, confirm: true });
|
|
12031
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12032
|
+
}
|
|
12033
|
+
if (/(дн[еиь]\s+рожден|день\s+рождени|дней\s+рождени|birthday)/iu.test(text) && /(календар|событи|напомин)/iu.test(text)) {
|
|
12034
|
+
const result = await yandexContactsBirthdaysToCalendar({ confirm: true });
|
|
12035
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12036
|
+
}
|
|
12037
|
+
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(text) && /(школ|детск\w*\s+сад|детсад|садик|инн)/iu.test(text)) {
|
|
12038
|
+
const result = await yandexContactFromPublicEntity({ query: cleanupYandexContactActionQuery(question), confirm: true });
|
|
12039
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12040
|
+
}
|
|
12041
|
+
if (/(экспорт|выгруз)/iu.test(text)) {
|
|
12042
|
+
const result = await yandexContactsExport(/csv/iu.test(text) ? "csv" : "vcard", {});
|
|
12043
|
+
return `Контакты экспортированы: ${result.output}. Записей: ${result.rows}.`;
|
|
12044
|
+
}
|
|
12045
|
+
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(text)) {
|
|
12046
|
+
const draft = parseYandexContactCreateRequest(question);
|
|
12047
|
+
if (!draft.name || (!draft.email && !draft.phone)) return "Для создания контакта укажите имя и email или телефон.";
|
|
12048
|
+
const result = await yandexContactsCreate({ ...draft, confirm: true });
|
|
12049
|
+
return `Контакт создан: ${formatYandexContact(result)}.`;
|
|
12050
|
+
}
|
|
12051
|
+
if (/(удали|удалить).{0,40}контакт/iu.test(text)) {
|
|
12052
|
+
const query = extractEmailAddress(question) || String(question || "").match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || cleanupYandexContactActionQuery(question);
|
|
12053
|
+
const result = await yandexContactsDelete(query, { confirm: true });
|
|
12054
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12055
|
+
}
|
|
12056
|
+
if (/(добав|запиши|сохрани).{0,40}(email|e-mail|почт)/iu.test(text)) {
|
|
12057
|
+
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
12058
|
+
const query = cleanupYandexContactEmailTarget(question, email);
|
|
12059
|
+
if (!email || !query) return "Укажите контакт и email. Пример: добавь email petrov@example.com к контакту Петров.";
|
|
12060
|
+
const result = await yandexContactsAddEmail(query, email, { confirm: true });
|
|
12061
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12062
|
+
}
|
|
12063
|
+
if (/(удали|удалить|убери).{0,40}(email|e-mail|почт)/iu.test(text)) {
|
|
12064
|
+
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
12065
|
+
const query = cleanupYandexContactEmailTarget(question, email) || cleanupYandexContactActionQuery(question);
|
|
12066
|
+
if (!query) return "Укажите контакт, у которого удалить email.";
|
|
12067
|
+
const result = await yandexContactsUpdate(query, { removeEmail: email || true, mode: "remove-email", confirm: true });
|
|
12068
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12069
|
+
}
|
|
12070
|
+
if (/(добав|запиши|сохрани).{0,40}(телефон|номер)/iu.test(text)) {
|
|
12071
|
+
const phone = String(question || "").match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || "";
|
|
12072
|
+
const query = cleanupYandexContactActionQuery(question.replace(phone, " "));
|
|
12073
|
+
if (!phone || !query) return "Укажите контакт и телефон.";
|
|
12074
|
+
const result = await yandexContactsUpdate(query, { phone, mode: "add-phone", confirm: true });
|
|
12075
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12076
|
+
}
|
|
12077
|
+
if (/(удали|удалить|убери).{0,40}(телефон|номер)/iu.test(text)) {
|
|
12078
|
+
const phone = String(question || "").match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || "";
|
|
12079
|
+
const query = cleanupYandexContactActionQuery(question.replace(phone, " "));
|
|
12080
|
+
if (!query) return "Укажите контакт, у которого удалить телефон.";
|
|
12081
|
+
const result = await yandexContactsUpdate(query, { removePhone: phone || true, mode: "remove-phone", confirm: true });
|
|
12082
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12083
|
+
}
|
|
12084
|
+
if (/(переимен|измени\s+имя|смени\s+имя)/iu.test(text) && /(контакт)/iu.test(text)) {
|
|
12085
|
+
const newName = question.match(/(?:в|на|как)\s+["«]?([^"».,!?]+)["»]?\s*$/iu)?.[1]?.trim() || "";
|
|
12086
|
+
const query = cleanupYandexContactActionQuery(question.replace(newName, " "));
|
|
12087
|
+
if (!query || !newName) return "Укажите контакт и новое имя.";
|
|
12088
|
+
const result = await yandexContactsUpdate(query, { name: newName, confirm: true });
|
|
12089
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12090
|
+
}
|
|
12091
|
+
if (/(добав|запиши|сохрани).{0,40}(день\s+рожд|дат[ау]\s+рожд|birthday)/iu.test(text)) {
|
|
12092
|
+
const birthday = question.match(/(\d{1,2}[.\-/]\d{1,2}(?:[.\-/]\d{2,4})?|\d{4}-\d{2}-\d{2})/u)?.[1] || "";
|
|
12093
|
+
const query = cleanupYandexContactActionQuery(question.replace(birthday, " "));
|
|
12094
|
+
if (!birthday || !query) return "Укажите контакт и дату рождения.";
|
|
12095
|
+
const result = await yandexContactsUpdate(query, { birthday, mode: "add-birthday", confirm: true });
|
|
12096
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12097
|
+
}
|
|
12098
|
+
if (/(добав|запиши|сохрани).{0,40}(организац|должност)/iu.test(text)) {
|
|
12099
|
+
const org = question.match(/(?:организац\p{L}*|орг)\s*:?\s*(.*?)(?=\s+должност\p{L}*\s*:|$)/iu)?.[1]?.trim() || "";
|
|
12100
|
+
const title = question.match(/должност\p{L}*\s*:?\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
12101
|
+
const query = cleanupYandexContactActionQuery(question.replace(org, " ").replace(title, " "));
|
|
12102
|
+
if (!query || (!org && !title)) return "Укажите контакт и организацию или должность.";
|
|
12103
|
+
const result = await yandexContactsUpdate(query, { org, title, mode: "add-org", confirm: true });
|
|
12104
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12105
|
+
}
|
|
12106
|
+
if (/(добав|запиши|сохрани).{0,40}(адрес)/iu.test(text)) {
|
|
12107
|
+
const address = question.match(/(?:адрес|по адресу)\s*:?\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
12108
|
+
const query = cleanupYandexContactActionQuery(question.replace(address, " "));
|
|
12109
|
+
if (!address || !query) return "Укажите контакт и адрес.";
|
|
12110
|
+
const result = await yandexContactsUpdate(query, { address, mode: "add-address", confirm: true });
|
|
12111
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12112
|
+
}
|
|
12113
|
+
if (/(добав|запиши|сохрани).{0,40}(заметк|коммент|примеч)/iu.test(text)) {
|
|
12114
|
+
const note = question.match(/(?:заметка|комментарий|примечание)\s*:\s*(.+)$/iu)?.[1]?.trim() || "";
|
|
12115
|
+
const query = cleanupYandexContactActionQuery(question.replace(note, " "));
|
|
12116
|
+
if (!note || !query) return "Укажите контакт и текст заметки.";
|
|
12117
|
+
const result = await yandexContactsUpdate(query, { note, mode: "add-note", confirm: true });
|
|
12118
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12119
|
+
}
|
|
12120
|
+
if (/(создай|сделай).{0,40}(папк).{0,40}(контакт)/iu.test(text)) {
|
|
12121
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
12122
|
+
const result = await yandexContactCreateDiskFolder({ query, confirm: true });
|
|
12123
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12124
|
+
}
|
|
12125
|
+
if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(text)) {
|
|
12126
|
+
const remotePath = extractCloudPath(question);
|
|
12127
|
+
const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
|
|
12128
|
+
const result = await yandexContactSendDiskLinkQr({ contact, path: remotePath, confirm: true });
|
|
12129
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12130
|
+
}
|
|
12131
|
+
if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(text)) {
|
|
12132
|
+
const draft = parseYandexMailSendRequest(question);
|
|
12133
|
+
const result = await yandexContactSendMail({ contact: draft.contactQuery || cleanupYandexContactActionQuery(question), subject: draft.subject, text: draft.text, confirm: true });
|
|
12134
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12135
|
+
}
|
|
12136
|
+
if (/(создай|добавь|запланируй).{0,40}(встреч|событи|календар|телемост)/iu.test(text)) {
|
|
12137
|
+
const dateTime = extractDateTimeFromText(question);
|
|
12138
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
12139
|
+
const result = /телемост/iu.test(text)
|
|
12140
|
+
? await yandexContactCreateTelemostEvent({ query, ...dateTime, confirm: true })
|
|
12141
|
+
: await yandexContactCreateCalendarEvent({ query, ...dateTime, confirm: true });
|
|
12142
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12143
|
+
}
|
|
12144
|
+
if (/(статус|проверь|работает|доступ)/iu.test(text)) {
|
|
12145
|
+
const result = await yandexContactsStatus();
|
|
12146
|
+
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
12147
|
+
}
|
|
12148
|
+
const query = cleanupYandexQuery(question);
|
|
12149
|
+
const rows = /(найди|поиск|покажи|посмотри)/iu.test(text) && query
|
|
12150
|
+
? await yandexContactsSearch(query, { limit: 10 })
|
|
12151
|
+
: await yandexContactsList({ limit: 20 });
|
|
12152
|
+
if (!rows.length) return "Контакты по запросу не найдены.";
|
|
12153
|
+
return ["Яндекс Контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
|
|
12154
|
+
}
|
|
12155
|
+
|
|
12156
|
+
async function buildUserSkillDirectAnswer(question) {
|
|
12157
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
12158
|
+
if (!/(skill|скилл|скил|навык)/iu.test(normalized)) return "";
|
|
12159
|
+
if (/(создай|добавь|сделай|create|new)/iu.test(normalized)) {
|
|
12160
|
+
const name = extractUserSkillNameFromQuestion(question) || "user-skill";
|
|
12161
|
+
const result = await userSkillCreate({
|
|
12162
|
+
name,
|
|
12163
|
+
description: extractUserSkillDescription(question, name),
|
|
12164
|
+
instructions: question,
|
|
12165
|
+
tools: inferUserSkillTools(question),
|
|
12166
|
+
enable: true,
|
|
12167
|
+
confirm: true,
|
|
12168
|
+
});
|
|
12169
|
+
return `Skill создан: ${result.name}\nФайл: ${result.file}\nSkill включен.`;
|
|
12170
|
+
}
|
|
12171
|
+
if (/(выключ|отключ|disable)/iu.test(normalized)) {
|
|
12172
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
12173
|
+
if (!name) return "Какой skill выключить? Укажите имя.";
|
|
12174
|
+
const result = await userSkillSetEnabled(name, false);
|
|
12175
|
+
return `Skill ${result.name}: disabled`;
|
|
12176
|
+
}
|
|
12177
|
+
if (/(включ|enable)/iu.test(normalized)) {
|
|
12178
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
12179
|
+
if (!name) return "Какой skill включить? Укажите имя.";
|
|
12180
|
+
const result = await userSkillSetEnabled(name, true);
|
|
12181
|
+
return `Skill ${result.name}: enabled`;
|
|
12182
|
+
}
|
|
12183
|
+
if (/(удали|удалить|remove|delete)/iu.test(normalized)) {
|
|
12184
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
12185
|
+
if (!name) return "Какой skill удалить? Укажите имя.";
|
|
12186
|
+
const result = await userSkillDelete(name, { confirm: true });
|
|
12187
|
+
return `Skill удален: ${result.name}`;
|
|
12188
|
+
}
|
|
12189
|
+
if (/(список|покажи|какие|list)/iu.test(normalized)) {
|
|
12190
|
+
const skills = listSkills(await loadConfig()).filter((skill) => skill.source === "user");
|
|
12191
|
+
if (!skills.length) return "Пользовательских skills пока нет.";
|
|
12192
|
+
return ["Пользовательские skills:", ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)].join("\n");
|
|
12193
|
+
}
|
|
12194
|
+
return "";
|
|
12195
|
+
}
|
|
12196
|
+
|
|
10403
12197
|
function isYandexServiceQuestion(normalized) {
|
|
10404
|
-
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex
|
|
12198
|
+
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
|
|
10405
12199
|
}
|
|
10406
12200
|
|
|
10407
12201
|
function isYandexIdentityQuestion(normalized) {
|
|
@@ -10485,6 +12279,13 @@ function isYandexMailReadRequest(normalizedQuestion) {
|
|
|
10485
12279
|
|| /(покажи\s+содерж|о чем|о чём|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(normalizedQuestion);
|
|
10486
12280
|
}
|
|
10487
12281
|
|
|
12282
|
+
function isExplicitYandexDiskPathDelete(question) {
|
|
12283
|
+
const text = String(question || "");
|
|
12284
|
+
return /(удали|удалить|перемести\s+в\s+корзин)/iu.test(text)
|
|
12285
|
+
&& /(яндекс.?диск|диск|облак|\/IOLA\/)/iu.test(text)
|
|
12286
|
+
&& Boolean(extractCloudPath(text));
|
|
12287
|
+
}
|
|
12288
|
+
|
|
10488
12289
|
function parseYandexMailReplyRequest(question, previousAssistantText = "") {
|
|
10489
12290
|
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
10490
12291
|
const uid = resolveYandexMailUidFromQuestion(text, previousAssistantText);
|
|
@@ -10542,6 +12343,49 @@ function cleanupYandexContactEmailTarget(question, email) {
|
|
|
10542
12343
|
.trim();
|
|
10543
12344
|
}
|
|
10544
12345
|
|
|
12346
|
+
function parseYandexContactCreateRequest(question) {
|
|
12347
|
+
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
12348
|
+
const email = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
12349
|
+
const phone = text.match(/(?:\+?\d[\d\s()\-]{6,}\d)/u)?.[0]?.trim() || "";
|
|
12350
|
+
const nameMatch = text.match(/(?:контакт|контакта)\s+["«]?([^"»:,]+)["»]?/iu)
|
|
12351
|
+
|| text.match(/(?:создай|добавь|запиши|сохрани)\s+["«]?([^"»:,]+?)["»]?\s+(?:контакт|email|телефон|номер|почт)/iu);
|
|
12352
|
+
const address = text.match(/(?:адрес|по адресу)\s*:?\s*(.*?)(?=\s+(?:заметка|телефон|email|почта|организация|должность)\s*:|$)/iu)?.[1]?.trim() || "";
|
|
12353
|
+
const note = text.match(/(?:заметка|примечание)\s*:?\s*(.*)$/iu)?.[1]?.trim() || "";
|
|
12354
|
+
const org = text.match(/(?:организация|орг)\s*:?\s*(.*?)(?=\s+(?:заметка|телефон|email|почта|должность)\s*:|$)/iu)?.[1]?.trim() || "";
|
|
12355
|
+
const title = text.match(/(?:должность)\s*:?\s*(.*?)(?=\s+(?:заметка|телефон|email|почта|организация)\s*:|$)/iu)?.[1]?.trim() || "";
|
|
12356
|
+
const rawName = (nameMatch?.[1] || text.replace(email, " ").replace(phone, " "))
|
|
12357
|
+
.replace(/\b(?:email|e-mail|почта|телефон|номер|адрес|заметка|организация|должность)\b[\s\S]*$/iu, "")
|
|
12358
|
+
.trim();
|
|
12359
|
+
return {
|
|
12360
|
+
name: cleanupYandexContactQuery(rawName),
|
|
12361
|
+
email,
|
|
12362
|
+
phone,
|
|
12363
|
+
address,
|
|
12364
|
+
note,
|
|
12365
|
+
org,
|
|
12366
|
+
title,
|
|
12367
|
+
};
|
|
12368
|
+
}
|
|
12369
|
+
|
|
12370
|
+
function cleanupYandexContactActionQuery(question) {
|
|
12371
|
+
const stopWords = new Set([
|
|
12372
|
+
"и", "к", "ко", "с", "со", "у", "для", "из", "на", "в", "во", "сегодня", "завтра", "послезавтра", "час", "часа", "часов", "день", "дня", "рождения", "рождение", "дату", "дата", "диске", "облаке", "контакт", "контакта", "контакту", "контактом", "адресная", "книга", "создай", "сделай",
|
|
12373
|
+
"добавь", "добавить", "запиши", "сохрани", "удали", "удалить", "папку", "папка", "отправь", "пошли",
|
|
12374
|
+
"напиши", "письмо", "сообщение", "ссылку", "ссылка", "qr", "qr-код", "диск", "яндекс", "телемост",
|
|
12375
|
+
"встречу", "встреча", "событие", "календарь", "телефон", "номер", "адрес", "заметка", "заметку", "комментарий",
|
|
12376
|
+
"примечание", "тема", "текст",
|
|
12377
|
+
]);
|
|
12378
|
+
return String(question || "")
|
|
12379
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, " ")
|
|
12380
|
+
.replace(/(?:\+?\d[\d\s()\-]{6,}\d)/gu, " ")
|
|
12381
|
+
.replace(/\/[^\s"»]+/gu, " ")
|
|
12382
|
+
.replace(/[,:;.!?«»"()]+/gu, " ")
|
|
12383
|
+
.split(/\s+/u)
|
|
12384
|
+
.filter((token) => token && !stopWords.has(token.toLocaleLowerCase("ru-RU")))
|
|
12385
|
+
.join(" ")
|
|
12386
|
+
.trim();
|
|
12387
|
+
}
|
|
12388
|
+
|
|
10545
12389
|
function formatYandexMailSummary(row) {
|
|
10546
12390
|
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
10547
12391
|
}
|
|
@@ -10636,6 +12480,96 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10636
12480
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
10637
12481
|
try {
|
|
10638
12482
|
const provider = await getCloudProvider();
|
|
12483
|
+
if (provider === "yandex-disk" && /(документ|docs|360)/iu.test(normalized)) {
|
|
12484
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
|
|
12485
|
+
const result = await yandexDocsShare(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
|
|
12486
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12487
|
+
}
|
|
12488
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
|
|
12489
|
+
const text = extractShareMessage(question) || cleanupCloudSaveText(question);
|
|
12490
|
+
if (!text) return "Укажите текст документа.";
|
|
12491
|
+
const result = await yandexDocsCreateText({ title: extractYandexDocTitle(question), text, confirm: true });
|
|
12492
|
+
return `Документ создан на Яндекс Диске: ${result.remote}.`;
|
|
12493
|
+
}
|
|
12494
|
+
if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
|
|
12495
|
+
const result = await yandexDocsRead(extractCloudPath(question) || cleanupCloudQuery(question), {});
|
|
12496
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12497
|
+
}
|
|
12498
|
+
if (/(переимен|rename)/iu.test(normalized)) {
|
|
12499
|
+
const result = await yandexDocsRename(extractCloudPath(question) || cleanupCloudQuery(question), extractCloudNewName(question), { confirm: true });
|
|
12500
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12501
|
+
}
|
|
12502
|
+
if (/(удали|удалить)/iu.test(normalized)) {
|
|
12503
|
+
const result = await yandexDocsDelete(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
|
|
12504
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12505
|
+
}
|
|
12506
|
+
if (/(найди|поиск)/iu.test(normalized)) {
|
|
12507
|
+
const rows = await yandexDocsFind(cleanupCloudQuery(question), { limit: 20 });
|
|
12508
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
12509
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
12510
|
+
}
|
|
12511
|
+
const rows = await yandexDocsList({ limit: 20 });
|
|
12512
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
12513
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
12514
|
+
}
|
|
12515
|
+
if (provider === "yandex-disk" && /(мест[оа]|сколько.*занято|сколько.*свобод|инфо|статус)/iu.test(normalized)) {
|
|
12516
|
+
const info = await yandexDiskInfo();
|
|
12517
|
+
return [
|
|
12518
|
+
"Яндекс Диск:",
|
|
12519
|
+
`Занято: ${formatBytes(info.usedSpace)} из ${formatBytes(info.totalSpace)}.`,
|
|
12520
|
+
`Корзина: ${formatBytes(info.trashSize)}.`,
|
|
12521
|
+
].join("\n");
|
|
12522
|
+
}
|
|
12523
|
+
if (provider === "yandex-disk" && /(перенеси|перемести|скопируй|копир).{0,80}(из|с).{0,30}папк/iu.test(normalized) && /(ссылк|qr|qr-код|почт|email|контакт|@)/iu.test(normalized)) {
|
|
12524
|
+
const packageRequest = parseYandexDiskPackageRequest(question);
|
|
12525
|
+
if (!packageRequest.sourcePath || !packageRequest.targetFolder) return "Укажите исходную и целевую папку на Яндекс Диске.";
|
|
12526
|
+
if (!packageRequest.email && !packageRequest.contact) return "Укажите email или контакт, кому отправить ссылку.";
|
|
12527
|
+
const result = await yandexDiskPackageShareEmail({
|
|
12528
|
+
...packageRequest,
|
|
12529
|
+
confirm: true,
|
|
12530
|
+
});
|
|
12531
|
+
return [
|
|
12532
|
+
`${result.mode === "move" ? "Перенес" : "Скопировал"} объектов: ${result.transferred}.`,
|
|
12533
|
+
`Папка: ${result.targetFolder}`,
|
|
12534
|
+
`Отправил: ${result.to.join(", ")}.`,
|
|
12535
|
+
`Ссылка: ${result.publicUrl}`,
|
|
12536
|
+
`QR-код: ${result.qrPublicUrl}`,
|
|
12537
|
+
].join("\n");
|
|
12538
|
+
}
|
|
12539
|
+
if (provider === "yandex-disk" && /(корзин|удаленн|удалённ)/iu.test(normalized) && /(покажи|список|что|есть)/iu.test(normalized)) {
|
|
12540
|
+
const rows = await yandexDiskTrashList("", { limit: 20 });
|
|
12541
|
+
if (!rows.length) return "Корзина Яндекс Диска пуста.";
|
|
12542
|
+
return ["Корзина Яндекс Диска:", ...rows.map((row, index) => `${index + 1}. ${row.type === "dir" ? "папка" : "файл"} ${row.name} — ${row.path}`)].join("\n");
|
|
12543
|
+
}
|
|
12544
|
+
if (provider === "yandex-disk" && /(есть\s+ли|существует|проверь).{0,40}(файл|папк|\/)/iu.test(normalized)) {
|
|
12545
|
+
const remotePath = extractCloudPath(question);
|
|
12546
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12547
|
+
const result = await yandexDiskExists(normalizeCloudUserPath(remotePath, provider));
|
|
12548
|
+
return result.exists ? `На Яндекс Диске найдено: ${result.path} (${result.type}).` : `На Яндекс Диске не найдено: ${result.path}.`;
|
|
12549
|
+
}
|
|
12550
|
+
if (provider === "yandex-disk" && /(свойств|карточк|метадан|размер|информац).{0,40}(файл|папк|\/)/iu.test(normalized)) {
|
|
12551
|
+
const remotePath = extractCloudPath(question);
|
|
12552
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12553
|
+
const result = await yandexDiskStat(normalizeCloudUserPath(remotePath, provider));
|
|
12554
|
+
return [
|
|
12555
|
+
`${result.type === "dir" ? "Папка" : "Файл"}: ${result.name}`,
|
|
12556
|
+
`Путь: ${result.path}`,
|
|
12557
|
+
`Размер: ${formatBytes(result.size)}`,
|
|
12558
|
+
result.modified ? `Изменен: ${result.modified}` : "",
|
|
12559
|
+
result.mimeType ? `Тип: ${result.mimeType}` : "",
|
|
12560
|
+
result.publicUrl ? `Публичная ссылка: ${result.publicUrl}` : "",
|
|
12561
|
+
].filter(Boolean).join("\n");
|
|
12562
|
+
}
|
|
12563
|
+
if (provider === "yandex-disk" && /(очисти|очистить).{0,30}корзин/iu.test(normalized)) {
|
|
12564
|
+
const result = await yandexDiskEmptyTrash({ confirm: true });
|
|
12565
|
+
return `Очистка корзины запрошена: ${result.remote}.`;
|
|
12566
|
+
}
|
|
12567
|
+
if (provider === "yandex-disk" && /(восстанов|верни).{0,40}(корзин|диск|файл|папк)/iu.test(normalized)) {
|
|
12568
|
+
const remotePath = extractCloudPath(question);
|
|
12569
|
+
if (!remotePath) return "Укажите путь объекта в корзине Яндекс Диска.";
|
|
12570
|
+
const result = await yandexDiskRestore(normalizeCloudUserPath(remotePath, provider), { confirm: true });
|
|
12571
|
+
return `Восстановил из корзины: ${result.remote}.`;
|
|
12572
|
+
}
|
|
10639
12573
|
if (/(созда|сдела|добав).{0,30}(папк|директор)/iu.test(normalized) || /(папк|директор).{0,30}(созда|сдела|добав)/iu.test(normalized)) {
|
|
10640
12574
|
const folderName = extractCloudFolderName(question) || "Новая папка";
|
|
10641
12575
|
const remotePath = normalizeCloudUserPath(folderName, provider);
|
|
@@ -10663,17 +12597,101 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10663
12597
|
].join("\n");
|
|
10664
12598
|
}
|
|
10665
12599
|
|
|
12600
|
+
if (provider === "yandex-disk" && /(сними|убери|закрой).{0,30}(ссылк|публик)/iu.test(normalized)) {
|
|
12601
|
+
const remotePath = extractCloudPath(question);
|
|
12602
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12603
|
+
const result = await yandexDiskUnshare(normalizeCloudUserPath(remotePath, provider));
|
|
12604
|
+
return `Публичная ссылка снята: ${result.remote}.`;
|
|
12605
|
+
}
|
|
12606
|
+
|
|
12607
|
+
if (provider === "yandex-disk" && /(отправь|отправить|пошли|перешли).{0,80}(ссылк|qr|qr-код|код)/iu.test(normalized) && /(почт|email|e-mail|контакт|@)/iu.test(normalized)) {
|
|
12608
|
+
const remotePath = extractCloudPath(question);
|
|
12609
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12610
|
+
const recipient = extractShareRecipient(question);
|
|
12611
|
+
if (!recipient.email && !recipient.contact) return "Укажите email или имя контакта, кому отправить ссылку.";
|
|
12612
|
+
const result = await yandexDiskShareEmail({
|
|
12613
|
+
remotePath: normalizeCloudUserPath(remotePath, provider),
|
|
12614
|
+
to: recipient.email,
|
|
12615
|
+
contact: recipient.contact,
|
|
12616
|
+
subject: extractMailSubject(question) || "",
|
|
12617
|
+
text: extractShareMessage(question) || "",
|
|
12618
|
+
confirm: true,
|
|
12619
|
+
});
|
|
12620
|
+
return [
|
|
12621
|
+
`Отправил ссылку на Яндекс Диск: ${result.to.join(", ")}.`,
|
|
12622
|
+
`Ссылка: ${result.publicUrl}`,
|
|
12623
|
+
`QR-код: ${result.qrPublicUrl}`,
|
|
12624
|
+
].join("\n");
|
|
12625
|
+
}
|
|
12626
|
+
|
|
10666
12627
|
if (/(ссылк|поделись|опубликуй)/iu.test(normalized)) {
|
|
10667
12628
|
const remotePath = extractCloudPath(question);
|
|
10668
12629
|
if (!remotePath) return "Укажите путь к файлу на облачном диске, например: /IOLA/reports/report.md";
|
|
10669
|
-
const
|
|
10670
|
-
|
|
12630
|
+
const withQr = provider === "yandex-disk" && /(qr|qr-код|код)/iu.test(normalized);
|
|
12631
|
+
const result = withQr
|
|
12632
|
+
? await yandexDiskShareWithQr(normalizeCloudUserPath(remotePath, provider), { confirm: true })
|
|
12633
|
+
: await cloudShare(provider, normalizeCloudUserPath(remotePath, provider));
|
|
12634
|
+
return withQr
|
|
12635
|
+
? `Публичная ссылка: ${result.publicUrl}\nQR-код: ${result.qrPublicUrl}`
|
|
12636
|
+
: `Публичная ссылка: ${result.publicUrl}`;
|
|
12637
|
+
}
|
|
12638
|
+
|
|
12639
|
+
if (provider === "yandex-disk" && /(прочитай|открой|покажи содержим|текст)/iu.test(normalized) && /(файл|\.txt|\.md|\.json|\.csv)/iu.test(normalized) && !/(сохрани|запиши)/iu.test(normalized)) {
|
|
12640
|
+
const remotePath = extractCloudPath(question);
|
|
12641
|
+
if (!remotePath) return "Укажите путь к текстовому файлу на Яндекс Диске.";
|
|
12642
|
+
const result = await yandexDiskReadText(normalizeCloudUserPath(remotePath, provider));
|
|
12643
|
+
return [`Файл ${result.remote}:`, result.text.slice(0, 4000)].join("\n");
|
|
12644
|
+
}
|
|
12645
|
+
|
|
12646
|
+
if (provider === "yandex-disk" && /(скачай|загрузи\s+с\s+диска|сохрани\s+на\s+комп)/iu.test(normalized)) {
|
|
12647
|
+
const remotePath = extractCloudPath(question);
|
|
12648
|
+
if (!remotePath) return "Укажите путь к файлу на Яндекс Диске.";
|
|
12649
|
+
const outputPath = extractLocalOutputPath(question) || path.basename(remotePath);
|
|
12650
|
+
const result = await yandexDiskDownload(normalizeCloudUserPath(remotePath, provider), outputPath);
|
|
12651
|
+
return `Скачал файл с Яндекс Диска: ${result.local}`;
|
|
12652
|
+
}
|
|
12653
|
+
|
|
12654
|
+
if (provider === "yandex-disk" && /(загрузи|отправь|положи).{0,40}(на яндекс.?диск|на диск|в облак)/iu.test(normalized)) {
|
|
12655
|
+
const localPath = extractLocalInputPath(question);
|
|
12656
|
+
if (!localPath) return "Укажите локальный путь к файлу для загрузки на Яндекс Диск.";
|
|
12657
|
+
const remotePath = extractCloudPath(question) || `${cloudRootForProvider(provider)}/${path.basename(localPath)}`;
|
|
12658
|
+
const result = await yandexDiskUpload(localPath, normalizeCloudUserPath(remotePath, provider), { overwrite: true });
|
|
12659
|
+
return `Загрузил файл на Яндекс Диск: ${result.remote}`;
|
|
12660
|
+
}
|
|
12661
|
+
|
|
12662
|
+
if (provider === "yandex-disk" && /(?:^|\s)(?:переименуй|переименовать|переименовать|rename)(?:\s|$)/iu.test(normalized)) {
|
|
12663
|
+
const remotePath = extractCloudPath(question);
|
|
12664
|
+
const newName = extractCloudNewName(question);
|
|
12665
|
+
if (!remotePath || !newName) return "Укажите путь и новое имя. Пример: переименуй /IOLA/a.txt в b.txt на Яндекс Диске.";
|
|
12666
|
+
const result = await yandexDiskRename(normalizeCloudUserPath(remotePath, provider), newName, { confirm: true, overwrite: true });
|
|
12667
|
+
return `Переименовал на Яндекс Диске: ${result.from} -> ${result.remote}`;
|
|
12668
|
+
}
|
|
12669
|
+
|
|
12670
|
+
if (provider === "yandex-disk" && /(перемести|move)/iu.test(normalized)) {
|
|
12671
|
+
const { from, to } = extractCloudTwoPaths(question);
|
|
12672
|
+
if (!from || !to) return "Укажите откуда и куда переместить на Яндекс Диске.";
|
|
12673
|
+
const result = await yandexDiskMove(normalizeCloudUserPath(from, provider), normalizeCloudUserPath(to, provider), { confirm: true, overwrite: true });
|
|
12674
|
+
return `Переместил на Яндекс Диске: ${result.from} -> ${result.remote}`;
|
|
12675
|
+
}
|
|
12676
|
+
|
|
12677
|
+
if (provider === "yandex-disk" && /(скопируй|копир|copy)/iu.test(normalized)) {
|
|
12678
|
+
const { from, to } = extractCloudTwoPaths(question);
|
|
12679
|
+
if (!from || !to) return "Укажите откуда и куда скопировать на Яндекс Диске.";
|
|
12680
|
+
const result = await yandexDiskCopy(normalizeCloudUserPath(from, provider), normalizeCloudUserPath(to, provider), { confirm: true, overwrite: true });
|
|
12681
|
+
return `Скопировал на Яндекс Диске: ${result.from} -> ${result.remote}`;
|
|
12682
|
+
}
|
|
12683
|
+
|
|
12684
|
+
if (provider === "yandex-disk" && /(удали|удалить|перемести.*корзин)/iu.test(normalized)) {
|
|
12685
|
+
const remotePath = extractCloudPath(question);
|
|
12686
|
+
if (!remotePath) return "Укажите путь к файлу или папке на Яндекс Диске.";
|
|
12687
|
+
const result = await yandexDiskDelete(normalizeCloudUserPath(remotePath, provider), { confirm: true, permanently: /навсегда|окончательно|безвозвратно/iu.test(normalized) });
|
|
12688
|
+
return `Удалил на Яндекс Диске: ${result.remote} (${result.status}).`;
|
|
10671
12689
|
}
|
|
10672
12690
|
|
|
10673
12691
|
if (/(сохрани|запиши).{0,40}(на яндекс диске|в облак|на диск)/iu.test(normalized)) {
|
|
10674
12692
|
const text = cleanupCloudSaveText(question);
|
|
10675
12693
|
if (!text) return "Что сохранить на облачный диск?";
|
|
10676
|
-
const remotePath = `${cloudRootForProvider(provider)}/notes/iola-${timestampForFile()}.txt`;
|
|
12694
|
+
const remotePath = extractCloudPath(question) || `${cloudRootForProvider(provider)}/notes/iola-${timestampForFile()}.txt`;
|
|
10677
12695
|
const tempPath = path.join(CONFIG_DIR, `cloud-save-${Date.now()}.txt`);
|
|
10678
12696
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
10679
12697
|
await writeFile(tempPath, text, "utf8");
|
|
@@ -10691,7 +12709,7 @@ async function buildCloudDirectAnswer(question) {
|
|
|
10691
12709
|
}
|
|
10692
12710
|
|
|
10693
12711
|
function isCloudQuestion(question) {
|
|
10694
|
-
return /(
|
|
12712
|
+
return /(\/IOLA\/|яндекс.?диск|yandex.?disk|облак|облачн|на диск|с диска|в диск|cloud|mail\.?ru|публичн.*ссылк|поделиться.*файл|qr-код|qr\s+код)/iu.test(String(question || ""));
|
|
10695
12713
|
}
|
|
10696
12714
|
|
|
10697
12715
|
function normalizeCloudUserPath(value, provider = "yandex-disk") {
|
|
@@ -10714,16 +12732,156 @@ function extractCloudFolderName(question) {
|
|
|
10714
12732
|
|
|
10715
12733
|
function extractCloudPath(question) {
|
|
10716
12734
|
const text = String(question || "").trim();
|
|
10717
|
-
const pathMatch = text.match(/(?:^|\s)(\/IOLA\/[^\s]+|\/[^\s]+)/iu);
|
|
10718
|
-
if (pathMatch?.[1]) return pathMatch[1];
|
|
10719
12735
|
const quoted = text.match(/["«]([^"»]+)["»]/u);
|
|
10720
|
-
if (quoted?.[1]) return quoted[1];
|
|
12736
|
+
if (quoted?.[1]) return cleanupCloudPathCandidate(quoted[1]);
|
|
12737
|
+
const iolaPath = text.match(/(?:^|\s)(\/IOLA\/.+)$/iu)?.[1];
|
|
12738
|
+
if (iolaPath) return cleanupCloudPathCandidate(iolaPath);
|
|
12739
|
+
const pathMatch = text.match(/(?:^|\s)(\/IOLA\/[^\s]+|\/[^\s]+)/iu);
|
|
12740
|
+
if (pathMatch?.[1]) return cleanupCloudPathCandidate(pathMatch[1]);
|
|
10721
12741
|
const afterFolder = text.match(/(?:папк[аеуы]?|файл[ае]?)\s+([^,.!?]+)/iu);
|
|
10722
12742
|
return afterFolder?.[1] ? cleanupCloudObjectName(afterFolder[1]) : "";
|
|
10723
12743
|
}
|
|
10724
12744
|
|
|
12745
|
+
function cleanupCloudPathCandidate(value) {
|
|
12746
|
+
return String(value || "")
|
|
12747
|
+
.replace(/\s+(?:по\s+почт[еуы]|на\s+почт[уые]|контакту|получател[юя]|кому|с\s+темой|тема\s*:|текст\s*:).*$/iu, "")
|
|
12748
|
+
.replace(/\s+(?:на\s+яндекс.?диск(?:е)?|на\s+диск(?:е)?|в\s+облак(?:е|о)?).*/iu, "")
|
|
12749
|
+
.replace(/[.!?]+$/u, "")
|
|
12750
|
+
.trim();
|
|
12751
|
+
}
|
|
12752
|
+
|
|
12753
|
+
function extractCloudTwoPaths(question) {
|
|
12754
|
+
const text = String(question || "").trim();
|
|
12755
|
+
const quoted = [...text.matchAll(/["«]([^"»]+)["»]/gu)].map((match) => match[1]).filter(Boolean);
|
|
12756
|
+
if (quoted.length >= 2) return { from: quoted[0], to: quoted[1] };
|
|
12757
|
+
const paths = [...text.matchAll(/(?:^|\s)(\/[^\s,;]+)/gu)].map((match) => match[1]).filter(Boolean);
|
|
12758
|
+
if (paths.length >= 2) return { from: paths[0], to: paths[1] };
|
|
12759
|
+
const match = text.match(/(?:из|с|откуда)\s+(.+?)\s+(?:в|на|куда)\s+(.+?)(?:\s+на\s+яндекс|\s+на\s+диск|\s+в\s+облак|$)/iu)
|
|
12760
|
+
|| text.match(/(?:перемести|скопируй|копируй|copy|move)\s+(.+?)\s+(?:в|на|куда)\s+(.+?)(?:\s+на\s+яндекс|\s+на\s+диск|\s+в\s+облак|$)/iu);
|
|
12761
|
+
return match ? { from: cleanupCloudObjectName(match[1]), to: cleanupCloudObjectName(match[2]) } : { from: "", to: "" };
|
|
12762
|
+
}
|
|
12763
|
+
|
|
12764
|
+
function extractCloudNewName(question) {
|
|
12765
|
+
const text = String(question || "").trim();
|
|
12766
|
+
return text.match(/(?:в|на|как)\s+["«]?([^"».,!?/\\]+(?:\.[a-z0-9а-яё]+)?)["»]?\s*(?:на\s+яндекс|на\s+диск|в\s+облак|$)/iu)?.[1]?.trim()
|
|
12767
|
+
|| text.match(/(?:нов(?:ое|ый|ым)?\s+им(?:я|енем)|названи(?:е|ем))\s+["«]?([^"».,!?/\\]+)["»]?/iu)?.[1]?.trim()
|
|
12768
|
+
|| "";
|
|
12769
|
+
}
|
|
12770
|
+
|
|
12771
|
+
function extractLocalInputPath(question) {
|
|
12772
|
+
const text = String(question || "").trim();
|
|
12773
|
+
const quoted = [...text.matchAll(/["«]([^"»]+)["»]/gu)].map((match) => match[1]).find((item) => /^[a-z]:[\\/]|\.{0,2}[\\/]/iu.test(item));
|
|
12774
|
+
if (quoted) return quoted;
|
|
12775
|
+
return text.match(/([a-z]:[\\/][^"»\s]+|\.\.?[\\/][^"»\s]+)/iu)?.[1] || "";
|
|
12776
|
+
}
|
|
12777
|
+
|
|
12778
|
+
function extractLocalOutputPath(question) {
|
|
12779
|
+
const text = String(question || "").trim();
|
|
12780
|
+
return text.match(/(?:в|на|как|куда)\s+([a-z]:[\\/][^"»\s]+|\.\.?[\\/][^"»\s]+)/iu)?.[1] || "";
|
|
12781
|
+
}
|
|
12782
|
+
|
|
12783
|
+
function extractShareRecipient(question) {
|
|
12784
|
+
const text = String(question || "");
|
|
12785
|
+
const email = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
12786
|
+
if (email) return { email, contact: "" };
|
|
12787
|
+
const contact = text.match(/(?:контакт[ау]?|получател[юя]|кому|для|почт[уе])\s+["«]?([^"».,;!?@]+)["»]?/iu)?.[1]
|
|
12788
|
+
|| text.match(/(?:отправь|отправить|пошли|перешли)\s+([^"».,;!?@]+?)\s+(?:ссылк|qr|qr-код|код)/iu)?.[1]
|
|
12789
|
+
|| "";
|
|
12790
|
+
return { email: "", contact: cleanupYandexContactQuery(contact) };
|
|
12791
|
+
}
|
|
12792
|
+
|
|
12793
|
+
function extractMailSubject(question) {
|
|
12794
|
+
return String(question || "").match(/(?:тема|subject)\s*:\s*(.*?)(?=\s+(?:текст|сообщение|body)\s*:|$)/iu)?.[1]?.trim() || "";
|
|
12795
|
+
}
|
|
12796
|
+
|
|
12797
|
+
function extractShareMessage(question) {
|
|
12798
|
+
return String(question || "").match(/(?:текст|text|сообщение|body)\s*:\s*(.*)$/iu)?.[1]?.trim() || "";
|
|
12799
|
+
}
|
|
12800
|
+
|
|
12801
|
+
function extractYandexDocTitle(question) {
|
|
12802
|
+
const text = String(question || "");
|
|
12803
|
+
const raw = text.match(/(?:названи(?:е|ем)|имя|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
12804
|
+
|| text.match(/(?:документ|файл)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
12805
|
+
|| "";
|
|
12806
|
+
return raw.replace(/\s+(?:текст|text|body|сообщение)\s*:?.*$/iu, "").trim();
|
|
12807
|
+
}
|
|
12808
|
+
|
|
12809
|
+
function extractCalendarTitle(question) {
|
|
12810
|
+
const text = String(question || "");
|
|
12811
|
+
const raw = text.match(/(?:тема|названи(?:е|ем)|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
12812
|
+
|| text.match(/(?:событи[ея]|встреч[ауи]|телемост)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
12813
|
+
|| "";
|
|
12814
|
+
return raw
|
|
12815
|
+
.replace(/\s+(?:сегодня|завтра|послезавтра)(?:\s|$).*$/iu, "")
|
|
12816
|
+
.replace(/\s+(?:в\s+\d{1,2}(?::\d{2})?|на\s+\d{1,2}[.\-/]\d{1,2}[\d.\-/]*)(?:\s|$).*$/iu, "")
|
|
12817
|
+
.trim();
|
|
12818
|
+
}
|
|
12819
|
+
|
|
12820
|
+
function extractCalendarRepeat(question) {
|
|
12821
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
12822
|
+
const count = Number(text.match(/(\d+)\s*(?:раз|повтор)/iu)?.[1] || 0);
|
|
12823
|
+
return {
|
|
12824
|
+
repeat: /ежеднев|каждый\s+день/iu.test(text) ? "daily"
|
|
12825
|
+
: /ежемесяч|каждый\s+месяц/iu.test(text) ? "monthly"
|
|
12826
|
+
: /ежегод|каждый\s+год/iu.test(text) ? "yearly"
|
|
12827
|
+
: "weekly",
|
|
12828
|
+
count: count || undefined,
|
|
12829
|
+
};
|
|
12830
|
+
}
|
|
12831
|
+
|
|
12832
|
+
function cleanupCalendarEventQuery(question) {
|
|
12833
|
+
return String(question || "")
|
|
12834
|
+
.replace(/(?:создай|добавь|запланируй|назначь|перенеси|перемести|измени|смени|удали|удалить|отмени|отменить|найди|поиск|покажи|добавь\s+напоминание|поставь\s+напоминание)/giu, " ")
|
|
12835
|
+
.replace(/(?:^|\s)(?:событи\p{L}*|встреч\p{L}*|телемост\p{L}*|календар\p{L}*|напомин\p{L}*|уведом\p{L}*|яндекс|на|к|ко|в|во|сегодня|завтра|послезавтра|час|часа|часов|минут|минуты)(?=\s|$)/giu, " ")
|
|
12836
|
+
.replace(/\d{1,2}[.\-/]\d{1,2}(?:[.\-/]\d{2,4})?/gu, " ")
|
|
12837
|
+
.replace(/\d{1,2}[:.]\d{2}/gu, " ")
|
|
12838
|
+
.replace(/(?:^|\s)\d{1,3}(?=\s|$)/gu, " ")
|
|
12839
|
+
.replace(/[,:;.!?«»"()]+/gu, " ")
|
|
12840
|
+
.replace(/\s+/g, " ")
|
|
12841
|
+
.trim();
|
|
12842
|
+
}
|
|
12843
|
+
|
|
12844
|
+
function parseYandexDiskPackageRequest(question) {
|
|
12845
|
+
const text = String(question || "").trim();
|
|
12846
|
+
const quoted = [...text.matchAll(/["«]([^"»]+)["»]/gu)].map((match) => match[1]);
|
|
12847
|
+
const paths = [...text.matchAll(/(?:^|\s)(\/[^\s,;]+)/gu)].map((match) => match[1]);
|
|
12848
|
+
const sourcePath = text.match(/(?:из|с)\s+папк[иы]?\s+([^,;]+?)(?=\s+(?:создай|сделай|и|отправь|перешли|на\s+яндекс)|[,;]|$)/iu)?.[1]?.trim()
|
|
12849
|
+
|| quoted[1]
|
|
12850
|
+
|| paths[1]
|
|
12851
|
+
|| "";
|
|
12852
|
+
const targetFolder = text.match(/(?:создай|сделай)\s+папк[уи]?\s+([^,;]+?)(?=\s+(?:на\s+яндекс|и|,|$)|[,;]|$)/iu)?.[1]?.trim()
|
|
12853
|
+
|| text.match(/(?:в|куда|целев\w*)\s+папк[уи]?\s+([^,;]+?)(?=\s+(?:на\s+яндекс|и|,|$))/iu)?.[1]?.trim()
|
|
12854
|
+
|| quoted[0]
|
|
12855
|
+
|| paths[0]
|
|
12856
|
+
|| "";
|
|
12857
|
+
const recipient = extractShareRecipient(question);
|
|
12858
|
+
return {
|
|
12859
|
+
sourcePath: normalizeCloudUserPath(cleanupCloudObjectName(sourcePath), "yandex-disk"),
|
|
12860
|
+
targetFolder: normalizeCloudUserPath(cleanupCloudObjectName(targetFolder), "yandex-disk"),
|
|
12861
|
+
mode: /(перенеси|перемести|move)/iu.test(text) ? "move" : "copy",
|
|
12862
|
+
email: recipient.email,
|
|
12863
|
+
contact: recipient.contact,
|
|
12864
|
+
subject: extractMailSubject(question),
|
|
12865
|
+
text: extractShareMessage(question),
|
|
12866
|
+
};
|
|
12867
|
+
}
|
|
12868
|
+
|
|
12869
|
+
function formatBytes(value) {
|
|
12870
|
+
const bytes = Number(value || 0);
|
|
12871
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 Б";
|
|
12872
|
+
const units = ["Б", "КБ", "МБ", "ГБ", "ТБ"];
|
|
12873
|
+
let size = bytes;
|
|
12874
|
+
let index = 0;
|
|
12875
|
+
while (size >= 1024 && index < units.length - 1) {
|
|
12876
|
+
size /= 1024;
|
|
12877
|
+
index += 1;
|
|
12878
|
+
}
|
|
12879
|
+
return `${size.toFixed(size >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
|
|
12880
|
+
}
|
|
12881
|
+
|
|
10725
12882
|
function cleanupCloudObjectName(value) {
|
|
10726
12883
|
return String(value || "")
|
|
12884
|
+
.replace(/\s+(?:на\s+яндекс.?диск(?:е)?|на\s+диск(?:е)?|в\s+облак(?:е|о)?).*/iu, " ")
|
|
10727
12885
|
.replace(/\b(?:на|в|у меня|яндекс.?диск(?:е)?|диск(?:е)?|облак(?:е|о)?|создай|сделай|добавь|покажи)\b/giu, " ")
|
|
10728
12886
|
.replace(/\s+/g, " ")
|
|
10729
12887
|
.trim();
|
|
@@ -10731,7 +12889,8 @@ function cleanupCloudObjectName(value) {
|
|
|
10731
12889
|
|
|
10732
12890
|
function cleanupCloudQuery(question) {
|
|
10733
12891
|
return String(question || "")
|
|
10734
|
-
.replace(
|
|
12892
|
+
.replace(/яндекс\s+диск(?:е)?/giu, " ")
|
|
12893
|
+
.replace(/(?:найди|поиск|где лежит|на|в|яндекс|облак(?:е|о)?|диск(?:е)?|файл|документ)/giu, " ")
|
|
10735
12894
|
.replace(/[?.!]+$/u, "")
|
|
10736
12895
|
.replace(/\s+/g, " ")
|
|
10737
12896
|
.trim();
|
|
@@ -10740,6 +12899,7 @@ function cleanupCloudQuery(question) {
|
|
|
10740
12899
|
function cleanupCloudSaveText(question) {
|
|
10741
12900
|
return String(question || "")
|
|
10742
12901
|
.replace(/^.*?(?:сохрани|запиши)\s+/iu, "")
|
|
12902
|
+
.replace(/\s+(?:в|на|как)\s+\/[^\s]+/giu, " ")
|
|
10743
12903
|
.replace(/\s+(?:на яндекс диске|в облак[ео]|на диск).*$/iu, "")
|
|
10744
12904
|
.trim();
|
|
10745
12905
|
}
|
|
@@ -11027,16 +13187,28 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
11027
13187
|
if (!options.quiet) console.log(casualAnswer);
|
|
11028
13188
|
return casualAnswer;
|
|
11029
13189
|
}
|
|
11030
|
-
const
|
|
11031
|
-
if (
|
|
11032
|
-
if (!options.quiet) console.log(
|
|
11033
|
-
return
|
|
13190
|
+
const userSkillAnswer = await buildUserSkillDirectAnswer(question);
|
|
13191
|
+
if (userSkillAnswer) {
|
|
13192
|
+
if (!options.quiet) console.log(userSkillAnswer);
|
|
13193
|
+
return userSkillAnswer;
|
|
13194
|
+
}
|
|
13195
|
+
if (/(контакт|адресн)/iu.test(question) && !isExplicitYandexDiskPathDelete(question)) {
|
|
13196
|
+
const yandexContactAnswer = await buildYandexDirectAnswer(question, []);
|
|
13197
|
+
if (yandexContactAnswer) {
|
|
13198
|
+
if (!options.quiet) console.log(yandexContactAnswer);
|
|
13199
|
+
return yandexContactAnswer;
|
|
13200
|
+
}
|
|
11034
13201
|
}
|
|
11035
13202
|
const cloudAnswer = await buildCloudDirectAnswer(question);
|
|
11036
13203
|
if (cloudAnswer) {
|
|
11037
13204
|
if (!options.quiet) console.log(cloudAnswer);
|
|
11038
13205
|
return cloudAnswer;
|
|
11039
13206
|
}
|
|
13207
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, []);
|
|
13208
|
+
if (yandexAnswer) {
|
|
13209
|
+
if (!options.quiet) console.log(yandexAnswer);
|
|
13210
|
+
return yandexAnswer;
|
|
13211
|
+
}
|
|
11040
13212
|
const geoAnswer = await buildGeoDirectAnswer(question);
|
|
11041
13213
|
if (geoAnswer) {
|
|
11042
13214
|
if (!options.quiet) console.log(geoAnswer);
|
|
@@ -11131,6 +13303,9 @@ function isUnsupportedPublicEntityQuestion(normalized) {
|
|
|
11131
13303
|
|
|
11132
13304
|
function buildCasualDirectAnswer(question) {
|
|
11133
13305
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU").trim();
|
|
13306
|
+
if (isCurrentDateTimeQuestion(normalized)) {
|
|
13307
|
+
return formatCurrentDateTimeAnswer(normalized);
|
|
13308
|
+
}
|
|
11134
13309
|
if (/^(кто ты|что ты|какая ты модель|что ты за модель|что за модель|какая модель|назови модель|ты какая модель|ты кто)([?.!\s]*)$/iu.test(normalized)) {
|
|
11135
13310
|
return "Я IOLA, первая городская модель искусственного интеллекта Йошкар-Олы. Работаю локально в CLI и отвечаю по открытым городским данным через проверяемые слои и API.";
|
|
11136
13311
|
}
|
|
@@ -11240,8 +13415,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
11240
13415
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
11241
13416
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
11242
13417
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
11243
|
-
"Yandex tools: yandex_identity_me {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_save_text {path,text}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_calendar_list {start,end}, yandex_contacts_search {query}.",
|
|
11244
|
-
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_delete, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_contacts_create, yandex_contacts_add_email, yandex_calendar_create_event, yandex_telemost_create_event.",
|
|
13418
|
+
"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_calendars {}, yandex_calendar_list {start,end}, yandex_calendar_search {query,start,end}, yandex_calendar_get {query}, yandex_calendar_create_event {title,start,end,location,attendees,reminders,confirm}, yandex_calendar_update {query,title,start,end,location,description,reminders,confirm}, yandex_calendar_move {query,start,end,confirm}, yandex_calendar_delete {query,confirm}, yandex_docs_list {path}, yandex_docs_find {query}, yandex_docs_create_text {title,text,format,confirm}, yandex_docs_read {path|query}, yandex_docs_share {path|query,confirm}, yandex_docs_rename {path|query,name,confirm}, yandex_docs_delete {path|query,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}.",
|
|
13419
|
+
"Опасные 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_calendar_update, yandex_calendar_move, yandex_calendar_delete, yandex_calendar_add_reminder, yandex_docs_create_text, yandex_docs_share, yandex_docs_rename, yandex_docs_delete, yandex_telemost_create_event.",
|
|
13420
|
+
"User skill tools: user_skill_create {name,description,instructions,tools,enable,confirm}, user_skill_enable {name}, user_skill_disable {name}, user_skill_delete {name,confirm}, user_skill_list {}. Создавай skill только по явной просьбе пользователя и с confirm=true.",
|
|
11245
13421
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
11246
13422
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
11247
13423
|
`Вопрос: ${question}`,
|
|
@@ -11299,14 +13475,48 @@ function parseJsonObject(text) {
|
|
|
11299
13475
|
|
|
11300
13476
|
function inferToolPlan(question, options = {}) {
|
|
11301
13477
|
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
13478
|
+
if (isCurrentDateTimeQuestion(normalized)) {
|
|
13479
|
+
return { steps: [{ tool: "get_current_date", args: {} }] };
|
|
13480
|
+
}
|
|
13481
|
+
if (/(создай|добавь|сделай).{0,40}(skill|скилл|скил|навык)/iu.test(normalized)) {
|
|
13482
|
+
const name = normalizeUserSkillName(
|
|
13483
|
+
question.match(/(?:skill|скилл|скил|навык)\s+["«]?([^"».,:;\n]+)["»]?/iu)?.[1]
|
|
13484
|
+
|| question.match(/(?:создай|добавь|сделай)\s+["«]?([^"».,:;\n]+?)["»]?\s+(?:skill|скилл|скил|навык)/iu)?.[1]
|
|
13485
|
+
|| "user-skill"
|
|
13486
|
+
);
|
|
13487
|
+
return {
|
|
13488
|
+
steps: [{
|
|
13489
|
+
tool: "user_skill_create",
|
|
13490
|
+
args: {
|
|
13491
|
+
name,
|
|
13492
|
+
description: `Пользовательский skill: ${name}`,
|
|
13493
|
+
instructions: question,
|
|
13494
|
+
enable: true,
|
|
13495
|
+
confirm: true,
|
|
13496
|
+
},
|
|
13497
|
+
}],
|
|
13498
|
+
};
|
|
13499
|
+
}
|
|
11302
13500
|
if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
|
|
11303
13501
|
return { steps: [{ tool: "yandex_identity_me", args: {} }] };
|
|
11304
13502
|
}
|
|
11305
13503
|
if (/(яндекс|диск|облак)/iu.test(normalized)) {
|
|
13504
|
+
const diskPath = extractCloudPath(question) || CLOUD_DEFAULT_REMOTE_DIR;
|
|
13505
|
+
if (/(мест[оа]|сколько.*занято|сколько.*свобод|статус|инфо)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_info", args: {} }] };
|
|
13506
|
+
if (/(корзин|удаленн|удалённ)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_trash_list", args: { limit: 20 } }] };
|
|
11306
13507
|
if (/(создай|сделай).{0,30}папк/iu.test(normalized)) {
|
|
11307
13508
|
const folder = question.match(/папк[ауи]?\s+["«]?([^"»\n]+)["»]?/iu)?.[1]?.trim() || "Новая папка";
|
|
11308
13509
|
return { steps: [{ tool: "yandex_disk_mkdir", args: { path: `${CLOUD_DEFAULT_REMOTE_DIR}/${folder}` } }] };
|
|
11309
13510
|
}
|
|
13511
|
+
if (/(прочитай|открой|содержим|текст)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_read_text", args: { path: diskPath } }] };
|
|
13512
|
+
if (/(скачай|download)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_download", args: { remotePath: diskPath, outputPath: path.basename(diskPath) } }] };
|
|
13513
|
+
if (/(ссылк|поделись|опубликуй)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_share", args: { path: diskPath, confirm: true } }] };
|
|
13514
|
+
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_delete", args: { path: diskPath, confirm: true } }] };
|
|
13515
|
+
if (/(переимен|rename)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_rename", args: { path: diskPath, name: extractCloudNewName(question), confirm: true } }] };
|
|
13516
|
+
if (/(перемести|move|скопируй|копир|copy)/iu.test(normalized)) {
|
|
13517
|
+
const { from, to } = extractCloudTwoPaths(question);
|
|
13518
|
+
return { steps: [{ tool: /(скопируй|копир|copy)/iu.test(normalized) ? "yandex_disk_copy" : "yandex_disk_move", args: { from, to, confirm: true, overwrite: true } }] };
|
|
13519
|
+
}
|
|
11310
13520
|
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_find", args: { query: question, path: CLOUD_DEFAULT_REMOTE_DIR, limit: 20 } }] };
|
|
11311
13521
|
return { steps: [{ tool: "yandex_disk_ls", args: { path: CLOUD_DEFAULT_REMOTE_DIR } }] };
|
|
11312
13522
|
}
|
|
@@ -11321,10 +13531,39 @@ function inferToolPlan(question, options = {}) {
|
|
|
11321
13531
|
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
|
|
11322
13532
|
return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
|
|
11323
13533
|
}
|
|
13534
|
+
if (/(документ|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
|
|
13535
|
+
const target = extractCloudPath(question) || cleanupYandexQuery(question);
|
|
13536
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_create_text", args: { title: extractYandexDocTitle(question), text: extractShareMessage(question) || cleanupCloudSaveText(question), confirm: true } }] };
|
|
13537
|
+
if (/(прочитай|открой|текст)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_read", args: { query: target } }] };
|
|
13538
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_share", args: { query: target, confirm: true } }] };
|
|
13539
|
+
if (/(переимен|rename)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_rename", args: { query: target, name: extractCloudNewName(question), confirm: true } }] };
|
|
13540
|
+
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_delete", args: { query: target, confirm: true } }] };
|
|
13541
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_find", args: { query: cleanupYandexQuery(question), limit: 20 } }] };
|
|
13542
|
+
return { steps: [{ tool: "yandex_docs_list", args: { limit: 20 } }] };
|
|
13543
|
+
}
|
|
11324
13544
|
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
11325
|
-
|
|
13545
|
+
if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
|
|
13546
|
+
const dateTime = extractDateTimeFromText(question);
|
|
13547
|
+
return { steps: [{ tool: /телемост/iu.test(normalized) ? "yandex_telemost_create_event" : "yandex_calendar_create_event", args: { ...dateTime, title: extractCalendarTitle(question) || (/телемост/iu.test(normalized) ? "Телемост IOLA" : "Событие IOLA"), confirm: true } }] };
|
|
13548
|
+
}
|
|
13549
|
+
if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_move", args: { query: cleanupCalendarEventQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
|
|
13550
|
+
if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_delete", args: { query: cleanupCalendarEventQuery(question), confirm: true } }] };
|
|
13551
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_search", args: { query: cleanupCalendarEventQuery(question), limit: 20 } }] };
|
|
13552
|
+
return { steps: [{ tool: "yandex_calendar_list", args: { limit: 20 } }] };
|
|
11326
13553
|
}
|
|
11327
13554
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
13555
|
+
if (/(дубликат|повтор)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_find_duplicates", args: { limit: 20 } }] };
|
|
13556
|
+
if (/(неполн|без\s+email|без\s+почт|без\s+телефон|без\s+адрес)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_find_incomplete", args: { limit: 30 } }] };
|
|
13557
|
+
if (/(экспорт|выгруз)/iu.test(normalized)) return { steps: [{ tool: /csv/iu.test(normalized) ? "yandex_contacts_export_csv" : "yandex_contacts_export_vcard", args: {} }] };
|
|
13558
|
+
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 } }] };
|
|
13559
|
+
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_create", args: { ...parseYandexContactCreateRequest(question), confirm: true } }] };
|
|
13560
|
+
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_delete", args: { query: cleanupYandexContactActionQuery(question), confirm: true } }] };
|
|
13561
|
+
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 } }] };
|
|
13562
|
+
if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
|
|
13563
|
+
const draft = parseYandexMailSendRequest(question);
|
|
13564
|
+
return { steps: [{ tool: "yandex_contact_send_mail", args: { contact: draft.contactQuery || cleanupYandexContactActionQuery(question), subject: draft.subject, text: draft.text, confirm: true } }] };
|
|
13565
|
+
}
|
|
13566
|
+
if (/(создай|добавь|запланируй).{0,40}(встреч|событи|календар|телемост)/iu.test(normalized)) return { steps: [{ tool: /телемост/iu.test(normalized) ? "yandex_contact_create_telemost_event" : "yandex_contact_create_calendar_event", args: { contact: cleanupYandexContactActionQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
|
|
11328
13567
|
return { steps: [{ tool: "yandex_contacts_search", args: { query: question, limit: 20 } }] };
|
|
11329
13568
|
}
|
|
11330
13569
|
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
@@ -11680,7 +13919,7 @@ function formatToolExecutionError(error, plan) {
|
|
|
11680
13919
|
}
|
|
11681
13920
|
|
|
11682
13921
|
function availableToolNames(options = {}) {
|
|
11683
|
-
const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS]);
|
|
13922
|
+
const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS]);
|
|
11684
13923
|
if (options.files) {
|
|
11685
13924
|
for (const tool of FILE_TOOLS) names.add(tool);
|
|
11686
13925
|
}
|
|
@@ -11749,6 +13988,10 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
11749
13988
|
const result = await executeYandexTool(step.tool, step.args || {});
|
|
11750
13989
|
current = Array.isArray(result) ? result : [result];
|
|
11751
13990
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
13991
|
+
} else if (USER_SKILL_TOOLS.includes(step.tool)) {
|
|
13992
|
+
const result = await executeUserSkillTool(step.tool, step.args || {});
|
|
13993
|
+
current = Array.isArray(result) ? result : [result];
|
|
13994
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
11752
13995
|
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
11753
13996
|
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
11754
13997
|
current = Array.isArray(result) ? result : [result];
|
|
@@ -11789,14 +14032,34 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
11789
14032
|
|
|
11790
14033
|
function getCurrentDateInfo() {
|
|
11791
14034
|
const now = new Date();
|
|
14035
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
|
|
11792
14036
|
return {
|
|
11793
14037
|
name: "текущая дата",
|
|
11794
14038
|
date: new Intl.DateTimeFormat("ru-RU", { dateStyle: "long" }).format(now),
|
|
11795
14039
|
time: new Intl.DateTimeFormat("ru-RU", { timeStyle: "short" }).format(now),
|
|
14040
|
+
weekday: new Intl.DateTimeFormat("ru-RU", { weekday: "long" }).format(now),
|
|
14041
|
+
timezone: timeZone,
|
|
11796
14042
|
iso: now.toISOString(),
|
|
11797
14043
|
};
|
|
11798
14044
|
}
|
|
11799
14045
|
|
|
14046
|
+
function isCurrentDateTimeQuestion(normalized) {
|
|
14047
|
+
const text = String(normalized || "");
|
|
14048
|
+
return /^(?:какая|какой|какое|скажи|подскажи|что)\s+(?:сегодня\s+)?(?:дата|день|число|время|день недели)|^(?:сегодня|сейчас)\??$/iu.test(text)
|
|
14049
|
+
|| /(?:какая\s+сегодня\s+дата|какой\s+сегодня\s+день|который\s+час|сколько\s+времени|текущее\s+время|текущая\s+дата|дата\s+сегодня)/iu.test(text);
|
|
14050
|
+
}
|
|
14051
|
+
|
|
14052
|
+
function formatCurrentDateTimeAnswer(normalized) {
|
|
14053
|
+
const info = getCurrentDateInfo();
|
|
14054
|
+
if (/(время|час|сейчас|сколько)/iu.test(normalized) && !/(дата|день|число)/iu.test(normalized)) {
|
|
14055
|
+
return `Сейчас ${info.time}. Часовой пояс: ${info.timezone}.`;
|
|
14056
|
+
}
|
|
14057
|
+
if (/(день недели)/iu.test(normalized) && !/(дата|число)/iu.test(normalized)) {
|
|
14058
|
+
return `Сегодня ${info.weekday}.`;
|
|
14059
|
+
}
|
|
14060
|
+
return `Сегодня ${info.date}, ${info.weekday}. Время: ${info.time}. Часовой пояс: ${info.timezone}.`;
|
|
14061
|
+
}
|
|
14062
|
+
|
|
11800
14063
|
function getLocalMcpToolNames() {
|
|
11801
14064
|
return mcpTools().map((tool) => `mcp:iola-local:${tool.name}`);
|
|
11802
14065
|
}
|
|
@@ -11880,23 +14143,74 @@ function formatToolResult(result, options) {
|
|
|
11880
14143
|
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
11881
14144
|
}
|
|
11882
14145
|
if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
|
|
14146
|
+
if (row.status === "calendar-event-created" || row.status === "telemost-event-created" || row.status === "telemost-calendar-fallback-created") {
|
|
14147
|
+
return `${row.status === "calendar-event-created" ? "Событие" : "Телемост"} создан: ${row.title || row.uid}${row.start ? `, ${row.start}` : ""}${row.telemost?.joinUrl ? `\nСсылка: ${row.telemost.joinUrl}` : row.status === "telemost-calendar-fallback-created" ? "\nПрямая ссылка Телемоста через API недоступна, создано событие календаря." : ""}`;
|
|
14148
|
+
}
|
|
14149
|
+
if (row.status === "calendar-event-updated") return `Событие обновлено: ${row.title || row.uid}`;
|
|
14150
|
+
if (row.status === "calendar-event-deleted") return `Событие удалено: ${row.title || row.uid}`;
|
|
14151
|
+
if (row.status === "ambiguous" && row.events) return [`Нашел несколько событий. Уточните:`, ...row.events.map((event, index) => `${index + 1}. ${event.title || event.uid} — ${event.startIso || event.start || "-"}`)].join("\n");
|
|
14152
|
+
if (row.status === "not-found" && row.kind === "calendar-event") return `Событие не найдено: ${row.query}`;
|
|
14153
|
+
if (row.status === "document-created") return `Документ создан на Яндекс Диске: ${row.remote}`;
|
|
14154
|
+
if (row.status === "binary-document") return `${row.name || row.remote}: ${row.message}`;
|
|
14155
|
+
if (row.status === "not-document") return `Это не документ: ${row.path}`;
|
|
14156
|
+
if (row.status === "ambiguous" && row.docs) return [`Нашел несколько документов. Уточните:`, ...row.docs.map((doc, index) => `${index + 1}. ${doc.name} — ${doc.path}`)].join("\n");
|
|
14157
|
+
if (row.status === "not-found" && row.docs !== undefined) return `Документ не найден: ${row.query}`;
|
|
14158
|
+
if (row.status === "contact-mail-sent") return `Письмо контакту отправлено: ${row.contact}. Тема: ${row.subject || "-"}`;
|
|
14159
|
+
if (row.status === "contact-disk-link-sent") return `Отправил контакту ${row.contact} ссылку на Яндекс Диск.\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
14160
|
+
if (row.status === "contact-folder-created") return `Папка контакта создана: ${row.remote}\nКарточка: ${row.cardPath}`;
|
|
14161
|
+
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 || "-"}`;
|
|
14162
|
+
if (row.status === "contacts-backup") return `Контакты сохранены на Яндекс Диск: ${row.remote}. Записей: ${row.rows}.`;
|
|
14163
|
+
if (row.status === "contacts-imported") return `Контакты импортированы из ${row.input}. Создано: ${row.created}, пропущено: ${row.skipped}.`;
|
|
14164
|
+
if (row.status === "birthday-events-created") return `События дней рождения созданы: ${row.created}. Контактов с днем рождения: ${row.totalWithBirthday}.`;
|
|
11883
14165
|
if (row.provider === "yandex-disk" && row.publicUrl) return `Публичная ссылка: ${row.publicUrl}`;
|
|
14166
|
+
if (row.provider === "yandex-disk" && row.status === "shared-with-qr") return `Публичная ссылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
14167
|
+
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}`;
|
|
14168
|
+
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}`;
|
|
14169
|
+
if (row.provider === "yandex-disk" && typeof row.exists === "boolean") return `Яндекс Диск: ${row.path || row.remote} ${row.exists ? "найден" : "не найден"}`;
|
|
14170
|
+
if (row.provider === "yandex-disk" && (row.status === "moved" || row.status === "copied" || row.status === "restored")) return `Яндекс Диск: ${row.status} ${row.from ? `${row.from} -> ` : ""}${row.remote}`;
|
|
14171
|
+
if (row.provider === "yandex-disk" && row.status === "unpublished") return `Яндекс Диск: публичная ссылка снята ${row.remote}`;
|
|
14172
|
+
if (row.provider === "yandex-disk" && row.status === "trash-empty-requested") return `Яндекс Диск: очистка корзины запрошена`;
|
|
14173
|
+
if (row.provider === "yandex-disk" && row.text) return `Яндекс Диск: ${row.remote}\n${String(row.text).slice(0, 2000)}`;
|
|
11884
14174
|
if (row.provider === "yandex-disk" && row.remote) return `Яндекс Диск: ${row.status || "ok"} ${row.remote}`;
|
|
11885
14175
|
if (row.uid && (row.subject || row.from)) return `Письмо #${row.uid}: ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}`;
|
|
11886
14176
|
if (row.status === "moved-to-trash") return `Письмо #${row.uid} перемещено в корзину: ${row.to}`;
|
|
11887
14177
|
if (row.status === "seen" || row.status === "unseen") return `Письмо #${row.uid}: ${row.status === "seen" ? "прочитано" : "непрочитано"}`;
|
|
11888
14178
|
if (row.status === "sent" && row.to) return `Письмо отправлено: ${Array.isArray(row.to) ? row.to.join(", ") : row.to}. Тема: ${row.subject || "-"}`;
|
|
14179
|
+
if (row.status === "contact-mail-sent") return `Письмо контакту отправлено: ${row.contact}. Тема: ${row.subject || "-"}`;
|
|
14180
|
+
if (row.status === "contact-disk-link-sent") return `Отправил контакту ${row.contact} ссылку на Яндекс Диск.\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
14181
|
+
if (row.status === "contact-folder-created") return `Папка контакта создана: ${row.remote}\nКарточка: ${row.cardPath}`;
|
|
14182
|
+
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 || "-"}`;
|
|
14183
|
+
if (row.status === "contacts-backup") return `Контакты сохранены на Яндекс Диск: ${row.remote}. Записей: ${row.rows}.`;
|
|
14184
|
+
if (row.status === "exported" && row.output) return `Контакты экспортированы: ${row.output}. Записей: ${row.rows}.`;
|
|
14185
|
+
if (row.status === "deleted" && (row.email || row.phone || row.name)) return `Контакт удален: ${formatYandexContact(row)}`;
|
|
14186
|
+
if (row.status === "updated" && (row.email || row.phone || row.name)) return `Контакт обновлен: ${formatYandexContact(row)}`;
|
|
14187
|
+
if (row.status === "created" && (row.email || row.phone || row.name)) return `Контакт создан: ${formatYandexContact(row)}`;
|
|
14188
|
+
if (row.status === "no-email" && row.contact) return `У контакта нет email: ${formatYandexContact(row.contact)}`;
|
|
14189
|
+
if (row.status === "ambiguous" && row.contacts) return [`Нашел несколько контактов. Уточните:`, ...row.contacts.map((contact, index) => `${index + 1}. ${formatYandexContact(contact)}`)].join("\n");
|
|
14190
|
+
if (row.status === "not-found" && row.query !== undefined) return `Контакт не найден: ${row.query}`;
|
|
14191
|
+
if (row.status === "duplicate-group" && row.contacts) return [`Дубликаты (${row.key}):`, ...row.contacts.map((contact, index) => `${index + 1}. ${formatYandexContact(contact)}`)].join("\n");
|
|
14192
|
+
if (row.status === "created" && row.file && row.name) return `Skill создан: ${row.name}\nФайл: ${row.file}${row.enabled ? "\nSkill включен." : ""}`;
|
|
14193
|
+
if ((row.status === "enabled" || row.status === "disabled") && row.name) return `Skill ${row.name}: ${row.status}`;
|
|
14194
|
+
if (row.status === "deleted" && row.name) return `Skill удален: ${row.name}`;
|
|
11889
14195
|
if (row.special || row.delimiter) return `Папка почты: ${row.name}${row.special ? ` (${row.special})` : ""}`;
|
|
11890
14196
|
if (row.login || row.defaultEmail) return `Yandex ID: ${row.displayName || row.login || "-"}${row.defaultEmail ? `, ${row.defaultEmail}` : ""}`;
|
|
11891
14197
|
if (row.title && (row.start || row.end)) return `${row.title}: ${row.start || "-"}${row.end ? ` - ${row.end}` : ""}`;
|
|
11892
|
-
if (row.email || row.phone) return formatYandexContact(row);
|
|
14198
|
+
if (row.email || row.phone || row.emails || row.phones) return formatYandexContact(row);
|
|
11893
14199
|
return `${row.name || row.check || row.inn || "строка"}: ${row.address || row.phone || row.email || row.website || row.count || ""}`;
|
|
11894
14200
|
}).join("\n");
|
|
11895
14201
|
}
|
|
11896
14202
|
|
|
11897
14203
|
function formatYandexContact(row) {
|
|
11898
|
-
const
|
|
11899
|
-
|
|
14204
|
+
const emails = row.emails?.length ? row.emails : (row.email ? [row.email] : []);
|
|
14205
|
+
const phones = row.phones?.length ? row.phones : (row.phone ? [row.phone] : []);
|
|
14206
|
+
const name = row.name && row.name !== emails[0] ? row.name : "";
|
|
14207
|
+
return [
|
|
14208
|
+
name || emails[0] || phones[0] || "Контакт",
|
|
14209
|
+
emails.length ? `email: ${emails.join(", ")}` : "",
|
|
14210
|
+
phones.length ? `тел: ${phones.join(", ")}` : "",
|
|
14211
|
+
row.org ? `орг: ${row.org}` : "",
|
|
14212
|
+
row.address ? `адрес: ${row.address}` : "",
|
|
14213
|
+
].filter(Boolean).join(", ");
|
|
11900
14214
|
}
|
|
11901
14215
|
|
|
11902
14216
|
function applyRuntimeConfig(target, value) {
|
|
@@ -12242,8 +14556,11 @@ async function buildAiMessages(question, dataContext, history, options = {}, con
|
|
|
12242
14556
|
const projectContext = options.bare ? "" : await buildProjectContextText();
|
|
12243
14557
|
const skillsText = options.bare ? "" : await buildSkillsText(config, question, options);
|
|
12244
14558
|
const hasDataContext = dataContext.enabled !== false;
|
|
14559
|
+
const currentDate = getCurrentDateInfo();
|
|
12245
14560
|
const system = [
|
|
12246
14561
|
"Ты терминальный AI-агент городского округа Йошкар-Ола.",
|
|
14562
|
+
`Текущие дата и время CLI: ${currentDate.date}, ${currentDate.weekday}, ${currentDate.time}; часовой пояс: ${currentDate.timezone}; ISO: ${currentDate.iso}.`,
|
|
14563
|
+
"Если пользователь спрашивает про сегодня, завтра, вчера, текущую дату, время или относительные сроки, опирайся на текущие дату и время CLI.",
|
|
12247
14564
|
"Отвечай на русском языке естественно и по смыслу запроса пользователя.",
|
|
12248
14565
|
"Не смешивай языки. Не выдумывай факты, географию и числа.",
|
|
12249
14566
|
"Если пользователь просто здоровается, ответь коротким приветствием и спроси, чем помочь.",
|
|
@@ -13395,12 +15712,12 @@ function parseOptions(args) {
|
|
|
13395
15712
|
|
|
13396
15713
|
for (let index = 0; index < args.length; index += 1) {
|
|
13397
15714
|
const arg = args[index];
|
|
13398
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url") {
|
|
15715
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url" || arg === "--enable") {
|
|
13399
15716
|
result[arg.slice(2)] = true;
|
|
13400
15717
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
13401
15718
|
result.check = true;
|
|
13402
15719
|
result[arg.slice(2)] = true;
|
|
13403
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app") {
|
|
15720
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--instructions" || arg === "--allowed-tools" || arg === "--tool" || arg === "--uses" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app") {
|
|
13404
15721
|
result[arg.slice(2)] = args[index + 1];
|
|
13405
15722
|
index += 1;
|
|
13406
15723
|
} else {
|
|
@@ -13607,6 +15924,209 @@ function findSkill(name, config) {
|
|
|
13607
15924
|
return listSkills(config).find((skill) => skill.name === name);
|
|
13608
15925
|
}
|
|
13609
15926
|
|
|
15927
|
+
function printSkillsList(skills, config) {
|
|
15928
|
+
if (!skills.length) {
|
|
15929
|
+
console.log("Нет данных.");
|
|
15930
|
+
return;
|
|
15931
|
+
}
|
|
15932
|
+
const nameWidth = Math.min(28, Math.max(5, ...skills.map((skill) => visibleLength(skill.name))));
|
|
15933
|
+
console.log(`${padCell("Вкл", 3)} ${padCell("Skill", nameWidth)} Описание`);
|
|
15934
|
+
console.log(`${"-".repeat(3)} ${"-".repeat(nameWidth)} ${"-".repeat(8)}`);
|
|
15935
|
+
for (const skill of skills) {
|
|
15936
|
+
const enabled = isSkillEnabled(config, skill.name) ? "yes" : "no";
|
|
15937
|
+
console.log(`${padCell(enabled, 3)} ${padCell(skill.name, nameWidth)} ${skill.description || "-"}`);
|
|
15938
|
+
}
|
|
15939
|
+
}
|
|
15940
|
+
|
|
15941
|
+
async function executeUserSkillTool(tool, args = {}) {
|
|
15942
|
+
if (tool === "user_skill_list") return listSkills(await loadConfig()).filter((skill) => skill.source === "user");
|
|
15943
|
+
if (tool === "user_skill_create") return userSkillCreate({ ...args, confirm: args.confirm === true });
|
|
15944
|
+
if (tool === "user_skill_enable") return userSkillSetEnabled(args.name, true);
|
|
15945
|
+
if (tool === "user_skill_disable") return userSkillSetEnabled(args.name, false);
|
|
15946
|
+
if (tool === "user_skill_delete") return userSkillDelete(args.name, { confirm: args.confirm === true });
|
|
15947
|
+
throw new Error(`Неизвестный user skill tool: ${tool}`);
|
|
15948
|
+
}
|
|
15949
|
+
|
|
15950
|
+
async function userSkillCreate(args = {}) {
|
|
15951
|
+
if (!args.confirm) throw new Error("Для создания пользовательского skill нужен аргумент confirm=true.");
|
|
15952
|
+
const name = normalizeUserSkillName(args.name || args.skill || args.title);
|
|
15953
|
+
if (!name) throw new Error("Имя skill обязательно.");
|
|
15954
|
+
const description = String(args.description || `Пользовательский skill: ${name}`).trim();
|
|
15955
|
+
const instructions = String(args.instructions || args.text || args.prompt || "").trim();
|
|
15956
|
+
if (!instructions) throw new Error("Инструкции skill обязательны.");
|
|
15957
|
+
const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || "");
|
|
15958
|
+
const dir = path.join(USER_SKILLS_DIR, name);
|
|
15959
|
+
const file = path.join(dir, "SKILL.md");
|
|
15960
|
+
if (existsSync(file) && !args.overwrite) throw new Error(`Skill уже существует: ${name}. Используйте overwrite=true или --force.`);
|
|
15961
|
+
await mkdir(dir, { recursive: true });
|
|
15962
|
+
const body = buildUserSkillMarkdown({ name, description, instructions, tools });
|
|
15963
|
+
await writeFile(file, body, "utf8");
|
|
15964
|
+
let enabled = false;
|
|
15965
|
+
if (args.enable) {
|
|
15966
|
+
await userSkillSetEnabled(name, true);
|
|
15967
|
+
enabled = true;
|
|
15968
|
+
}
|
|
15969
|
+
return { name, description, file, enabled, tools, status: "created" };
|
|
15970
|
+
}
|
|
15971
|
+
|
|
15972
|
+
async function userSkillSetEnabled(name, enabled) {
|
|
15973
|
+
const skillName = normalizeUserSkillName(name);
|
|
15974
|
+
if (!skillName) throw new Error("Имя skill обязательно.");
|
|
15975
|
+
const config = await loadConfig();
|
|
15976
|
+
const skill = findSkill(skillName, config);
|
|
15977
|
+
if (!skill && enabled) throw new Error(`Skill не найден: ${skillName}`);
|
|
15978
|
+
const enabledSet = new Set(config.skills?.enabled || []);
|
|
15979
|
+
if (enabled) enabledSet.add(skillName);
|
|
15980
|
+
else enabledSet.delete(skillName);
|
|
15981
|
+
await saveConfig({ skills: { ...(config.skills || {}), enabled: [...enabledSet] } });
|
|
15982
|
+
return { name: skillName, enabled, status: enabled ? "enabled" : "disabled" };
|
|
15983
|
+
}
|
|
15984
|
+
|
|
15985
|
+
async function userSkillDelete(name, options = {}) {
|
|
15986
|
+
if (!options.confirm) throw new Error("Для удаления пользовательского skill нужен --yes или confirm=true.");
|
|
15987
|
+
const skillName = normalizeUserSkillName(name);
|
|
15988
|
+
if (!skillName) throw new Error("Имя skill обязательно.");
|
|
15989
|
+
const file = path.join(USER_SKILLS_DIR, skillName, "SKILL.md");
|
|
15990
|
+
const dir = path.dirname(file);
|
|
15991
|
+
if (!existsSync(file)) throw new Error(`Пользовательский skill не найден: ${skillName}`);
|
|
15992
|
+
await rm(dir, { recursive: true, force: true });
|
|
15993
|
+
await userSkillSetEnabled(skillName, false);
|
|
15994
|
+
return { name: skillName, status: "deleted" };
|
|
15995
|
+
}
|
|
15996
|
+
|
|
15997
|
+
function normalizeUserSkillName(value) {
|
|
15998
|
+
return String(value || "")
|
|
15999
|
+
.toLocaleLowerCase("ru-RU")
|
|
16000
|
+
.replace(/[^a-z0-9а-яё_-]+/giu, "-")
|
|
16001
|
+
.replace(/^-+|-+$/gu, "")
|
|
16002
|
+
.slice(0, 80);
|
|
16003
|
+
}
|
|
16004
|
+
|
|
16005
|
+
function parseCommaList(value) {
|
|
16006
|
+
if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
|
|
16007
|
+
return String(value || "")
|
|
16008
|
+
.split(/[,\n;]/u)
|
|
16009
|
+
.map((item) => item.trim())
|
|
16010
|
+
.filter(Boolean);
|
|
16011
|
+
}
|
|
16012
|
+
|
|
16013
|
+
function extractUserSkillNameFromQuestion(question) {
|
|
16014
|
+
const text = String(question || "");
|
|
16015
|
+
const quoted = text.match(/[«"]([^»"]+)[»"]/u)?.[1];
|
|
16016
|
+
if (quoted) return normalizeUserSkillName(quoted);
|
|
16017
|
+
const explicit = text.match(/(?:skill|скилл|скил|навык)\s+([a-z0-9а-яё_-]{3,80})/iu)?.[1]
|
|
16018
|
+
|| text.match(/(?:создай|добавь|сделай|включи|выключи|удали|удалить)\s+([a-z0-9а-яё_-]{3,80})\s+(?:skill|скилл|скил|навык)/iu)?.[1];
|
|
16019
|
+
return normalizeUserSkillName(explicit || "");
|
|
16020
|
+
}
|
|
16021
|
+
|
|
16022
|
+
function extractUserSkillDescription(question, name) {
|
|
16023
|
+
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
16024
|
+
const afterColon = text.match(/[:\-]\s*(.+)$/u)?.[1];
|
|
16025
|
+
return (afterColon || `Пользовательский skill: ${name}`).slice(0, 180);
|
|
16026
|
+
}
|
|
16027
|
+
|
|
16028
|
+
function inferUserSkillTools(question) {
|
|
16029
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
16030
|
+
const tools = new Set();
|
|
16031
|
+
if (/(почт|письм|email|e-mail)/iu.test(text)) {
|
|
16032
|
+
tools.add("yandex_mail_list");
|
|
16033
|
+
tools.add("yandex_mail_search");
|
|
16034
|
+
tools.add("yandex_mail_read");
|
|
16035
|
+
}
|
|
16036
|
+
if (/(отправь|отправить|отправляй|ответь|ответить|перешли|переслать|удали|удалить)/iu.test(text) && /(почт|письм|email|e-mail)/iu.test(text)) {
|
|
16037
|
+
tools.add("yandex_mail_send");
|
|
16038
|
+
tools.add("yandex_mail_reply");
|
|
16039
|
+
tools.add("yandex_mail_forward");
|
|
16040
|
+
tools.add("yandex_mail_delete");
|
|
16041
|
+
}
|
|
16042
|
+
if (/(диск|облак|яндекс.?диск)/iu.test(text)) {
|
|
16043
|
+
tools.add("yandex_disk_info");
|
|
16044
|
+
tools.add("yandex_disk_ls");
|
|
16045
|
+
tools.add("yandex_disk_find");
|
|
16046
|
+
tools.add("yandex_disk_stat");
|
|
16047
|
+
tools.add("yandex_disk_exists");
|
|
16048
|
+
tools.add("yandex_disk_read_text");
|
|
16049
|
+
tools.add("yandex_disk_save_text");
|
|
16050
|
+
tools.add("yandex_disk_upload");
|
|
16051
|
+
tools.add("yandex_disk_download");
|
|
16052
|
+
tools.add("yandex_disk_share");
|
|
16053
|
+
tools.add("yandex_disk_unshare");
|
|
16054
|
+
tools.add("yandex_disk_move");
|
|
16055
|
+
tools.add("yandex_disk_copy");
|
|
16056
|
+
tools.add("yandex_disk_rename");
|
|
16057
|
+
tools.add("yandex_disk_delete");
|
|
16058
|
+
tools.add("yandex_disk_trash_list");
|
|
16059
|
+
tools.add("yandex_disk_restore");
|
|
16060
|
+
}
|
|
16061
|
+
if (/(календар|событи|встреч|телемост)/iu.test(text)) {
|
|
16062
|
+
tools.add("yandex_calendar_list");
|
|
16063
|
+
tools.add("yandex_calendar_create_event");
|
|
16064
|
+
tools.add("yandex_telemost_create_event");
|
|
16065
|
+
}
|
|
16066
|
+
if (/(контакт|адресн)/iu.test(text)) {
|
|
16067
|
+
tools.add("yandex_contacts_search");
|
|
16068
|
+
tools.add("yandex_contacts_get");
|
|
16069
|
+
tools.add("yandex_contacts_create");
|
|
16070
|
+
tools.add("yandex_contacts_update");
|
|
16071
|
+
tools.add("yandex_contacts_delete");
|
|
16072
|
+
tools.add("yandex_contacts_add_email");
|
|
16073
|
+
tools.add("yandex_contacts_add_phone");
|
|
16074
|
+
tools.add("yandex_contacts_add_address");
|
|
16075
|
+
tools.add("yandex_contacts_add_note");
|
|
16076
|
+
tools.add("yandex_contacts_add_birthday");
|
|
16077
|
+
tools.add("yandex_contacts_add_org");
|
|
16078
|
+
tools.add("yandex_contacts_export_csv");
|
|
16079
|
+
tools.add("yandex_contacts_find_incomplete");
|
|
16080
|
+
tools.add("yandex_contacts_find_duplicates");
|
|
16081
|
+
tools.add("yandex_contacts_backup_to_disk");
|
|
16082
|
+
tools.add("yandex_contact_send_mail");
|
|
16083
|
+
tools.add("yandex_contact_send_disk_link_qr");
|
|
16084
|
+
tools.add("yandex_contact_create_disk_folder");
|
|
16085
|
+
tools.add("yandex_contact_create_calendar_event");
|
|
16086
|
+
tools.add("yandex_contact_create_telemost_event");
|
|
16087
|
+
}
|
|
16088
|
+
if (/(файл|папк|документ|архив)/iu.test(text)) {
|
|
16089
|
+
tools.add("files_tree");
|
|
16090
|
+
tools.add("files_read");
|
|
16091
|
+
tools.add("files_search");
|
|
16092
|
+
}
|
|
16093
|
+
if (/(запиши|сохрани|создай файл|измени|исправ)/iu.test(text) && /(файл|папк|документ|архив)/iu.test(text)) {
|
|
16094
|
+
tools.add("files_write");
|
|
16095
|
+
tools.add("files_patch");
|
|
16096
|
+
}
|
|
16097
|
+
if (/(школ|сад|детсад|инн|адрес|телефон|открыт)/iu.test(text)) {
|
|
16098
|
+
tools.add("search_data");
|
|
16099
|
+
tools.add("get_card");
|
|
16100
|
+
tools.add("export_report");
|
|
16101
|
+
}
|
|
16102
|
+
if (/(сайт|страниц|браузер|url|ссылка)/iu.test(text)) tools.add("browser_open");
|
|
16103
|
+
return [...tools];
|
|
16104
|
+
}
|
|
16105
|
+
|
|
16106
|
+
function buildUserSkillMarkdown({ name, description, instructions, tools = [] }) {
|
|
16107
|
+
const toolLines = tools.length
|
|
16108
|
+
? ["", "Разрешенные/ожидаемые tools для этого skill:", "", ...tools.map((tool) => `- \`${tool}\``)]
|
|
16109
|
+
: [];
|
|
16110
|
+
return [
|
|
16111
|
+
"---",
|
|
16112
|
+
`name: ${name}`,
|
|
16113
|
+
`description: ${description.replace(/\r?\n/g, " ")}`,
|
|
16114
|
+
"source: user",
|
|
16115
|
+
"---",
|
|
16116
|
+
"",
|
|
16117
|
+
instructions.trim(),
|
|
16118
|
+
"",
|
|
16119
|
+
"Правила безопасности:",
|
|
16120
|
+
"",
|
|
16121
|
+
"- Используй только встроенные tools `iola-cli` и подключенные MCP/Yandex/local-files механизмы.",
|
|
16122
|
+
"- Не выводи секреты, OAuth-токены, API-ключи и пароли.",
|
|
16123
|
+
"- Для записи, удаления, отправки писем, публикации ссылок и изменения внешних сервисов требуется явная просьба пользователя.",
|
|
16124
|
+
"- Если действие неоднозначно, сначала уточни у пользователя цель и место выполнения.",
|
|
16125
|
+
...toolLines,
|
|
16126
|
+
"",
|
|
16127
|
+
].join("\n");
|
|
16128
|
+
}
|
|
16129
|
+
|
|
13610
16130
|
function readSkillMeta(file) {
|
|
13611
16131
|
try {
|
|
13612
16132
|
const text = readFileSyncUtf8(file);
|
|
@@ -13652,7 +16172,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
|
13652
16172
|
if (enabled.has("geo") && isGeoQuestion(normalized)) selected.add("geo");
|
|
13653
16173
|
const cloudQuestion = isCloudQuestion(normalized);
|
|
13654
16174
|
if (enabled.has("personal-docs") && cloudQuestion) selected.add("personal-docs");
|
|
13655
|
-
if (enabled.has("yandex-services") && /(
|
|
16175
|
+
if (enabled.has("yandex-services") && /(яндекс|диск|почт|письм|календар|контакт|телемост|документ|docs|360|облако)/iu.test(normalized)) selected.add("yandex-services");
|
|
13656
16176
|
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
13657
16177
|
if (enabled.has("local-files") && !cloudQuestion && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
13658
16178
|
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|
|
@@ -15376,6 +17896,10 @@ function sanitizeConfig(config) {
|
|
|
15376
17896
|
if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("local-files") && !next.skills.enabled.includes("personal-docs")) {
|
|
15377
17897
|
next.skills.enabled = [...next.skills.enabled, "personal-docs"];
|
|
15378
17898
|
}
|
|
17899
|
+
next.toolsets = next.toolsets || {};
|
|
17900
|
+
next.toolsets.enabled = [...new Set([...(next.toolsets.enabled || []), "user-skills"])];
|
|
17901
|
+
next.skills = next.skills || {};
|
|
17902
|
+
next.skills.enabled = [...new Set([...(next.skills.enabled || []), "user-skills"])];
|
|
15379
17903
|
if (Array.isArray(next.yandex?.enabledServices)) {
|
|
15380
17904
|
next.yandex.enabledServices = next.yandex.enabledServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
|
|
15381
17905
|
}
|