evolcore 0.0.2 → 0.0.4
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/CHANGELOG.md +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
package/dist/cli/init-channel.js
CHANGED
|
@@ -16,7 +16,9 @@ import { ipcQuery } from '../ipc.js';
|
|
|
16
16
|
import { cmdStart } from './daemon-commands.js';
|
|
17
17
|
import * as platform from '../utils/cross-platform.js';
|
|
18
18
|
import { cleanupInstances } from '../utils/instance-registry.js';
|
|
19
|
-
import { bindContactAlias } from '../config/contact-book.js';
|
|
19
|
+
import { bindContactAlias, getContactSnapshot } from '../config/contact-book.js';
|
|
20
|
+
import { listStaticAgentAdmins } from '../config/peer-role-resolver.js';
|
|
21
|
+
import { setupRawKeyListener } from './raw-key-input.js';
|
|
20
22
|
function ask(rl, question) {
|
|
21
23
|
return new Promise(resolve => rl.question(question, resolve));
|
|
22
24
|
}
|
|
@@ -198,26 +200,12 @@ async function pollBindStatusWithKeyboard(taskId, expiresAt) {
|
|
|
198
200
|
let quit = false;
|
|
199
201
|
let skip = false;
|
|
200
202
|
const p = resolvePaths();
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const handler = (key) => {
|
|
208
|
-
if (key === 'q' || key === '\u0003')
|
|
209
|
-
quit = true;
|
|
210
|
-
if (key === 's' || key === 'S')
|
|
211
|
-
skip = true;
|
|
212
|
-
};
|
|
213
|
-
process.stdin.on('data', handler);
|
|
214
|
-
return () => {
|
|
215
|
-
process.stdin.removeListener('data', handler);
|
|
216
|
-
process.stdin.setRawMode(false);
|
|
217
|
-
// Don't pause stdin - let the next readline/prompt handle it naturally
|
|
218
|
-
};
|
|
219
|
-
};
|
|
220
|
-
const cleanup = setupKeyListener();
|
|
203
|
+
const cleanup = setupRawKeyListener((key) => {
|
|
204
|
+
if (key === 'q' || key === '\u0003')
|
|
205
|
+
quit = true;
|
|
206
|
+
if (key === 's' || key === 'S')
|
|
207
|
+
skip = true;
|
|
208
|
+
});
|
|
221
209
|
try {
|
|
222
210
|
while (Date.now() < expiresAt) {
|
|
223
211
|
if (quit)
|
|
@@ -305,26 +293,12 @@ async function runQrRegistrationFlow() {
|
|
|
305
293
|
console.log('请用飞书/Lark 扫描上方二维码...\n');
|
|
306
294
|
console.log('按 q 退出 | 按 s 跳过扫码手动输入 appId/appSecret\n');
|
|
307
295
|
let userAction = null;
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const handler = (key) => {
|
|
315
|
-
if (key === 'q' || key === '\u0003')
|
|
316
|
-
userAction = QUIT;
|
|
317
|
-
if (key === 's')
|
|
318
|
-
userAction = SKIP;
|
|
319
|
-
};
|
|
320
|
-
process.stdin.on('data', handler);
|
|
321
|
-
return () => {
|
|
322
|
-
process.stdin.removeListener('data', handler);
|
|
323
|
-
process.stdin.setRawMode(false);
|
|
324
|
-
// Don't pause stdin - let the next readline/prompt handle it naturally
|
|
325
|
-
};
|
|
326
|
-
};
|
|
327
|
-
const cleanup = setupKeyListener();
|
|
296
|
+
const cleanup = setupRawKeyListener((key) => {
|
|
297
|
+
if (key === 'q' || key === '\u0003')
|
|
298
|
+
userAction = QUIT;
|
|
299
|
+
if (key === 's')
|
|
300
|
+
userAction = SKIP;
|
|
301
|
+
});
|
|
328
302
|
const startedAt = Date.now();
|
|
329
303
|
let pollIntervalSeconds = Number(beginResult.interval ?? 5);
|
|
330
304
|
const expireInSeconds = Number(beginResult.expires_in ?? beginResult.expire_in ?? 600);
|
|
@@ -410,6 +384,7 @@ export async function cmdInitFeishu() {
|
|
|
410
384
|
let agentConfig = null;
|
|
411
385
|
let choice = null;
|
|
412
386
|
let contactBinding = null;
|
|
387
|
+
let operation = 'configure';
|
|
413
388
|
try {
|
|
414
389
|
aid = await pickAgentForChannel(rl);
|
|
415
390
|
if (!aid)
|
|
@@ -420,16 +395,25 @@ export async function cmdInitFeishu() {
|
|
|
420
395
|
return;
|
|
421
396
|
}
|
|
422
397
|
const existing = (agentConfig.channels || []).filter(c => c.type === 'feishu');
|
|
423
|
-
choice = await pickInstanceWithinAgent(rl, 'feishu', existing);
|
|
398
|
+
choice = await pickInstanceWithinAgent(rl, 'feishu', existing, { confirmOverwrite: false });
|
|
424
399
|
if (choice === null)
|
|
425
400
|
return;
|
|
426
|
-
|
|
401
|
+
if (choice.action === 'overwrite') {
|
|
402
|
+
operation = await pickExistingChannelOperation(rl, '飞书', choice.name);
|
|
403
|
+
if (operation === 'configure' && !await confirmOverwriteChannel(rl, '飞书', choice.name))
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'feishu');
|
|
427
407
|
if (!contactBinding)
|
|
428
408
|
return;
|
|
429
409
|
}
|
|
430
410
|
finally {
|
|
431
411
|
rl.close();
|
|
432
412
|
}
|
|
413
|
+
if (operation === 'rebind-contact') {
|
|
414
|
+
await registerFeishuContactBind(aid, contactBindChannelKey('feishu', aid, choice.name), contactBinding);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
433
417
|
console.log('\n正在获取飞书登录二维码...\n');
|
|
434
418
|
let result;
|
|
435
419
|
try {
|
|
@@ -470,10 +454,18 @@ export async function cmdInitFeishu() {
|
|
|
470
454
|
console.log(` Login Open ID: ${result.openId}`);
|
|
471
455
|
if (result.domain !== 'unknown')
|
|
472
456
|
console.log(` Domain: ${result.domain}`);
|
|
473
|
-
|
|
457
|
+
if (result.openId) {
|
|
458
|
+
bindQrActorToContact(aid, contactBinding, 'feishu', result.openId, commitResult.channelKey);
|
|
459
|
+
}
|
|
460
|
+
else if (commitResult.reloaded) {
|
|
461
|
+
await registerFeishuContactBind(aid, commitResult.channelKey, contactBinding);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
console.log(' ⚠ 服务未运行或热重载失败,无法注册飞书绑定码;请启动服务后重新执行 ec init feishu。');
|
|
465
|
+
}
|
|
474
466
|
// Show unified init success message
|
|
475
467
|
const { generateInitSuccessMessage } = await import('../utils/welcome.js');
|
|
476
|
-
console.log(generateInitSuccessMessage('feishu',
|
|
468
|
+
console.log(generateInitSuccessMessage('feishu', contactBinding !== null));
|
|
477
469
|
}
|
|
478
470
|
// ==================== WeChat ====================
|
|
479
471
|
const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
@@ -571,6 +563,7 @@ export async function cmdInitWechat() {
|
|
|
571
563
|
let agentConfig = null;
|
|
572
564
|
let choice = null;
|
|
573
565
|
let contactBinding = null;
|
|
566
|
+
let operation = 'configure';
|
|
574
567
|
try {
|
|
575
568
|
aid = await pickAgentForChannel(rl);
|
|
576
569
|
if (!aid)
|
|
@@ -581,16 +574,25 @@ export async function cmdInitWechat() {
|
|
|
581
574
|
return;
|
|
582
575
|
}
|
|
583
576
|
const existing = (agentConfig.channels || []).filter(c => c.type === 'wechat');
|
|
584
|
-
choice = await pickInstanceWithinAgent(rl, 'wechat', existing);
|
|
577
|
+
choice = await pickInstanceWithinAgent(rl, 'wechat', existing, { confirmOverwrite: false });
|
|
585
578
|
if (choice === null)
|
|
586
579
|
return;
|
|
587
|
-
|
|
580
|
+
if (choice.action === 'overwrite') {
|
|
581
|
+
operation = await pickExistingChannelOperation(rl, '微信', choice.name);
|
|
582
|
+
if (operation === 'configure' && !await confirmOverwriteChannel(rl, '微信', choice.name))
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'wechat');
|
|
588
586
|
if (!contactBinding)
|
|
589
587
|
return;
|
|
590
588
|
}
|
|
591
589
|
finally {
|
|
592
590
|
rl.close();
|
|
593
591
|
}
|
|
592
|
+
if (operation === 'rebind-contact') {
|
|
593
|
+
await registerWechatContactBind(aid, contactBindChannelKey('wechat', aid, choice.name), contactBinding);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
594
596
|
console.log('\n正在获取微信登录二维码...\n');
|
|
595
597
|
const result = await runWechatQrFlow();
|
|
596
598
|
if (!result) {
|
|
@@ -607,10 +609,18 @@ export async function cmdInitWechat() {
|
|
|
607
609
|
const commitResult = await commitChannel(aid, channel, choice.action);
|
|
608
610
|
if (!commitResult)
|
|
609
611
|
return;
|
|
610
|
-
|
|
612
|
+
if (firstNonEmpty(result.userId)) {
|
|
613
|
+
bindQrActorToContact(aid, contactBinding, 'wechat', result.userId, commitResult.channelKey);
|
|
614
|
+
}
|
|
615
|
+
else if (commitResult.reloaded) {
|
|
616
|
+
await registerWechatContactBind(aid, commitResult.channelKey, contactBinding);
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
console.log(' ⚠ 服务未运行或热重载失败,无法注册微信绑定码;请启动服务后重新执行 ec init wechat。');
|
|
620
|
+
}
|
|
611
621
|
// Show unified init success message
|
|
612
622
|
const { generateInitSuccessMessage } = await import('../utils/welcome.js');
|
|
613
|
-
console.log(generateInitSuccessMessage('wechat',
|
|
623
|
+
console.log(generateInitSuccessMessage('wechat', contactBinding !== null));
|
|
614
624
|
}
|
|
615
625
|
// ==================== AUN ====================
|
|
616
626
|
export async function cmdInitAun() {
|
|
@@ -752,26 +762,12 @@ async function runDingtalkQrFlow() {
|
|
|
752
762
|
console.log('提示: 扫码页面标注 "OpenClaw" 是钉钉生态接入桥,可放心使用。\n');
|
|
753
763
|
console.log('按 q 退出 | 按 s 跳过扫码手动输入\n');
|
|
754
764
|
let userAction = null;
|
|
755
|
-
const
|
|
756
|
-
if (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const handler = (key) => {
|
|
762
|
-
if (key === 'q' || key === '\u0003')
|
|
763
|
-
userAction = QUIT;
|
|
764
|
-
if (key === 's')
|
|
765
|
-
userAction = SKIP;
|
|
766
|
-
};
|
|
767
|
-
process.stdin.on('data', handler);
|
|
768
|
-
return () => {
|
|
769
|
-
process.stdin.removeListener('data', handler);
|
|
770
|
-
process.stdin.setRawMode(false);
|
|
771
|
-
// Don't pause stdin - let the next readline/prompt handle it naturally
|
|
772
|
-
};
|
|
773
|
-
};
|
|
774
|
-
const cleanup = setupKeyListener();
|
|
765
|
+
const cleanup = setupRawKeyListener((key) => {
|
|
766
|
+
if (key === 'q' || key === '\u0003')
|
|
767
|
+
userAction = QUIT;
|
|
768
|
+
if (key === 's')
|
|
769
|
+
userAction = SKIP;
|
|
770
|
+
});
|
|
775
771
|
const pollInterval = Math.max(Number(beginData.interval ?? 3), 2);
|
|
776
772
|
const expiresIn = Number(beginData.expires_in ?? 7200);
|
|
777
773
|
const startedAt = Date.now();
|
|
@@ -828,6 +824,7 @@ export async function cmdInitDingtalk() {
|
|
|
828
824
|
let agentConfig = null;
|
|
829
825
|
let choice = null;
|
|
830
826
|
let contactBinding = null;
|
|
827
|
+
let operation = 'configure';
|
|
831
828
|
try {
|
|
832
829
|
aid = await pickAgentForChannel(rl);
|
|
833
830
|
if (!aid)
|
|
@@ -838,16 +835,25 @@ export async function cmdInitDingtalk() {
|
|
|
838
835
|
return;
|
|
839
836
|
}
|
|
840
837
|
const existing = (agentConfig.channels || []).filter(c => c.type === 'dingtalk');
|
|
841
|
-
choice = await pickInstanceWithinAgent(rl, 'dingtalk', existing);
|
|
838
|
+
choice = await pickInstanceWithinAgent(rl, 'dingtalk', existing, { confirmOverwrite: false });
|
|
842
839
|
if (choice === null)
|
|
843
840
|
return;
|
|
844
|
-
|
|
841
|
+
if (choice.action === 'overwrite') {
|
|
842
|
+
operation = await pickExistingChannelOperation(rl, '钉钉', choice.name);
|
|
843
|
+
if (operation === 'configure' && !await confirmOverwriteChannel(rl, '钉钉', choice.name))
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'dingtalk');
|
|
845
847
|
if (!contactBinding)
|
|
846
848
|
return;
|
|
847
849
|
}
|
|
848
850
|
finally {
|
|
849
851
|
rl.close();
|
|
850
852
|
}
|
|
853
|
+
if (operation === 'rebind-contact') {
|
|
854
|
+
await registerDingtalkContactBind(aid, contactBindChannelKey('dingtalk', aid, choice.name), contactBinding);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
851
857
|
console.log('\n正在获取钉钉登录二维码...\n');
|
|
852
858
|
let result;
|
|
853
859
|
try {
|
|
@@ -904,7 +910,7 @@ export async function cmdInitDingtalk() {
|
|
|
904
910
|
console.log(' ⚠ 服务未运行或热重载失败,无法注册钉钉绑定码;请启动服务后重新执行 ec init dingtalk 并再次扫码绑定。');
|
|
905
911
|
}
|
|
906
912
|
const { generateInitSuccessMessage } = await import('../utils/welcome.js');
|
|
907
|
-
console.log(generateInitSuccessMessage('dingtalk',
|
|
913
|
+
console.log(generateInitSuccessMessage('dingtalk', contactBinding !== null));
|
|
908
914
|
}
|
|
909
915
|
// ==================== QQBot ====================
|
|
910
916
|
const QQBOT_PORTAL_HOST = 'q.qq.com';
|
|
@@ -968,26 +974,12 @@ async function runQQBotBindFlow() {
|
|
|
968
974
|
console.log('请用 QQ 扫描上方二维码绑定机器人...\n');
|
|
969
975
|
console.log('按 q 退出 | 按 s 跳过扫码手动输入\n');
|
|
970
976
|
let userAction = null;
|
|
971
|
-
const
|
|
972
|
-
if (
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const handler = (key) => {
|
|
978
|
-
if (key === 'q' || key === '\u0003')
|
|
979
|
-
userAction = QUIT;
|
|
980
|
-
if (key === 's')
|
|
981
|
-
userAction = SKIP;
|
|
982
|
-
};
|
|
983
|
-
process.stdin.on('data', handler);
|
|
984
|
-
return () => {
|
|
985
|
-
process.stdin.removeListener('data', handler);
|
|
986
|
-
process.stdin.setRawMode(false);
|
|
987
|
-
// Don't pause stdin - let the next readline/prompt handle it naturally
|
|
988
|
-
};
|
|
989
|
-
};
|
|
990
|
-
const cleanup = setupKeyListener();
|
|
977
|
+
const cleanup = setupRawKeyListener((key) => {
|
|
978
|
+
if (key === 'q' || key === '\u0003')
|
|
979
|
+
userAction = QUIT;
|
|
980
|
+
if (key === 's')
|
|
981
|
+
userAction = SKIP;
|
|
982
|
+
});
|
|
991
983
|
const startedAt = Date.now();
|
|
992
984
|
try {
|
|
993
985
|
// Step 3: Poll for bind result
|
|
@@ -1051,6 +1043,7 @@ export async function cmdInitQQBot() {
|
|
|
1051
1043
|
let agentConfig = null;
|
|
1052
1044
|
let choice = null;
|
|
1053
1045
|
let contactBinding = null;
|
|
1046
|
+
let operation = 'configure';
|
|
1054
1047
|
try {
|
|
1055
1048
|
aid = await pickAgentForChannel(rl);
|
|
1056
1049
|
if (!aid)
|
|
@@ -1061,16 +1054,25 @@ export async function cmdInitQQBot() {
|
|
|
1061
1054
|
return;
|
|
1062
1055
|
}
|
|
1063
1056
|
const existing = (agentConfig.channels || []).filter(c => c.type === 'qqbot');
|
|
1064
|
-
choice = await pickInstanceWithinAgent(rl, 'qqbot', existing);
|
|
1057
|
+
choice = await pickInstanceWithinAgent(rl, 'qqbot', existing, { confirmOverwrite: false });
|
|
1065
1058
|
if (choice === null)
|
|
1066
1059
|
return;
|
|
1067
|
-
|
|
1060
|
+
if (choice.action === 'overwrite') {
|
|
1061
|
+
operation = await pickExistingChannelOperation(rl, 'QQ', choice.name);
|
|
1062
|
+
if (operation === 'configure' && !await confirmOverwriteChannel(rl, 'QQ', choice.name))
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'qqbot');
|
|
1068
1066
|
if (!contactBinding)
|
|
1069
1067
|
return;
|
|
1070
1068
|
}
|
|
1071
1069
|
finally {
|
|
1072
1070
|
rl.close();
|
|
1073
1071
|
}
|
|
1072
|
+
if (operation === 'rebind-contact') {
|
|
1073
|
+
await registerQQBotContactBind(aid, contactBindChannelKey('qqbot', aid, choice.name), contactBinding);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1074
1076
|
console.log('\n正在创建 QQ 机器人绑定任务...\n');
|
|
1075
1077
|
let result;
|
|
1076
1078
|
try {
|
|
@@ -1120,9 +1122,17 @@ export async function cmdInitQQBot() {
|
|
|
1120
1122
|
if (!commitResult)
|
|
1121
1123
|
return;
|
|
1122
1124
|
console.log(` App ID: ${result.appId}`);
|
|
1123
|
-
|
|
1125
|
+
if (firstNonEmpty(result.userOpenId)) {
|
|
1126
|
+
bindQrActorToContact(aid, contactBinding, 'qqbot', result.userOpenId, commitResult.channelKey);
|
|
1127
|
+
}
|
|
1128
|
+
else if (commitResult.reloaded) {
|
|
1129
|
+
await registerQQBotContactBind(aid, commitResult.channelKey, contactBinding);
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
console.log(' ⚠ 服务未运行或热重载失败,无法注册 QQ 绑定码;请启动服务后重新执行 ec init qqbot。');
|
|
1133
|
+
}
|
|
1124
1134
|
const { generateInitSuccessMessage } = await import('../utils/welcome.js');
|
|
1125
|
-
console.log(generateInitSuccessMessage('qqbot',
|
|
1135
|
+
console.log(generateInitSuccessMessage('qqbot', contactBinding !== null));
|
|
1126
1136
|
}
|
|
1127
1137
|
// ==================== WeCom (企业微信) ====================
|
|
1128
1138
|
export async function cmdInitWecom() {
|
|
@@ -1130,6 +1140,8 @@ export async function cmdInitWecom() {
|
|
|
1130
1140
|
let aid = null;
|
|
1131
1141
|
let agentConfig = null;
|
|
1132
1142
|
let choice = null;
|
|
1143
|
+
let contactBinding = null;
|
|
1144
|
+
let operation = 'configure';
|
|
1133
1145
|
let botId = '';
|
|
1134
1146
|
let secret = '';
|
|
1135
1147
|
try {
|
|
@@ -1142,25 +1154,71 @@ export async function cmdInitWecom() {
|
|
|
1142
1154
|
return;
|
|
1143
1155
|
}
|
|
1144
1156
|
const existing = (agentConfig.channels || []).filter(c => c.type === 'wecom');
|
|
1145
|
-
choice = await pickInstanceWithinAgent(rl, 'wecom', existing);
|
|
1157
|
+
choice = await pickInstanceWithinAgent(rl, 'wecom', existing, { confirmOverwrite: false });
|
|
1146
1158
|
if (choice === null)
|
|
1147
1159
|
return;
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
if (!botId)
|
|
1153
|
-
console.log(' ⚠ 不能为空');
|
|
1160
|
+
if (choice.action === 'overwrite') {
|
|
1161
|
+
operation = await pickExistingChannelOperation(rl, '企微', choice.name);
|
|
1162
|
+
if (operation === 'configure' && !await confirmOverwriteChannel(rl, '企微', choice.name))
|
|
1163
|
+
return;
|
|
1154
1164
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1165
|
+
contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'wecom');
|
|
1166
|
+
if (!contactBinding)
|
|
1167
|
+
return;
|
|
1168
|
+
if (operation !== 'rebind-contact') {
|
|
1169
|
+
console.log('\n企业微信 AI Bot 配置');
|
|
1170
|
+
const method = (await ask(rl, ' 接入方式:1. 扫码创建(推荐) 2. 手工输入 [1]: ')).trim() || '1';
|
|
1171
|
+
if (method === '1') {
|
|
1172
|
+
try {
|
|
1173
|
+
const { createWecomQrSession, pollWecomQrCredentials } = await import('../channels/wecom.js');
|
|
1174
|
+
const session = await createWecomQrSession();
|
|
1175
|
+
const qrterm = await import('qrcode-terminal');
|
|
1176
|
+
console.log('\n请使用企业微信扫描以下二维码:\n');
|
|
1177
|
+
await new Promise(resolve => {
|
|
1178
|
+
qrterm.default.generate(session.authUrl, { small: true }, (qr) => {
|
|
1179
|
+
console.log(qr);
|
|
1180
|
+
resolve();
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
console.log(`也可在浏览器打开:${session.browserUrl}`);
|
|
1184
|
+
console.log('等待扫码确认,二维码 5 分钟内有效...\n');
|
|
1185
|
+
const credentials = await pollWecomQrCredentials(session.scode, {
|
|
1186
|
+
onStatus: status => {
|
|
1187
|
+
if (status !== 'waiting')
|
|
1188
|
+
console.log(` 扫码状态: ${status}`);
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
1191
|
+
botId = credentials.botId;
|
|
1192
|
+
secret = credentials.secret;
|
|
1193
|
+
console.log(' ✓ 已取得 Bot ID 和 Secret');
|
|
1194
|
+
}
|
|
1195
|
+
catch (error) {
|
|
1196
|
+
console.log(` ⚠ 扫码接入失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
1197
|
+
console.log(' 将切换到手工输入模式。\n');
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (!botId || !secret) {
|
|
1201
|
+
console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
|
|
1202
|
+
}
|
|
1203
|
+
while (!botId) {
|
|
1204
|
+
botId = (await ask(rl, ' Bot ID: ')).trim();
|
|
1205
|
+
if (!botId)
|
|
1206
|
+
console.log(' ⚠ 不能为空');
|
|
1207
|
+
}
|
|
1208
|
+
while (!secret) {
|
|
1209
|
+
secret = (await ask(rl, ' Secret: ')).trim();
|
|
1210
|
+
if (!secret)
|
|
1211
|
+
console.log(' ⚠ 不能为空');
|
|
1212
|
+
}
|
|
1159
1213
|
}
|
|
1160
1214
|
}
|
|
1161
1215
|
finally {
|
|
1162
1216
|
rl.close();
|
|
1163
1217
|
}
|
|
1218
|
+
if (operation === 'rebind-contact') {
|
|
1219
|
+
await registerWecomContactBind(aid, contactBindChannelKey('wecom', aid, choice.name), contactBinding);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1164
1222
|
const channel = {
|
|
1165
1223
|
type: 'wecom',
|
|
1166
1224
|
name: choice.name,
|
|
@@ -1168,10 +1226,18 @@ export async function cmdInitWecom() {
|
|
|
1168
1226
|
botId,
|
|
1169
1227
|
secret,
|
|
1170
1228
|
};
|
|
1171
|
-
await commitChannel(aid, channel, choice.action);
|
|
1229
|
+
const commitResult = await commitChannel(aid, channel, choice.action);
|
|
1230
|
+
if (!commitResult)
|
|
1231
|
+
return;
|
|
1172
1232
|
console.log(` Bot ID: ${botId}`);
|
|
1233
|
+
if (commitResult.reloaded) {
|
|
1234
|
+
await registerWecomContactBind(aid, commitResult.channelKey, contactBinding);
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
console.log(' ⚠ 服务未运行或热重载失败,无法注册企微绑定码;请启动服务后重新执行 ec init wecom。');
|
|
1238
|
+
}
|
|
1173
1239
|
const { generateInitSuccessMessage } = await import('../utils/welcome.js');
|
|
1174
|
-
console.log(generateInitSuccessMessage('wecom',
|
|
1240
|
+
console.log(generateInitSuccessMessage('wecom', contactBinding !== null));
|
|
1175
1241
|
}
|
|
1176
1242
|
// ==================== Shared helpers for per-agent init <channel> ====================
|
|
1177
1243
|
/**
|
|
@@ -1211,24 +1277,42 @@ async function pickAgentForChannel(rl) {
|
|
|
1211
1277
|
*
|
|
1212
1278
|
* If existing.length === 0 → returns { action:'add', name:'main' } directly.
|
|
1213
1279
|
*/
|
|
1214
|
-
async function pickInstanceWithinAgent(rl, channelType, existing) {
|
|
1280
|
+
async function pickInstanceWithinAgent(rl, channelType, existing, options) {
|
|
1215
1281
|
if (existing.length === 0) {
|
|
1216
1282
|
return { action: 'add', name: 'main' };
|
|
1217
1283
|
}
|
|
1218
1284
|
const view = existing.map((c, i) => ({ ...c, name: c.name, originalIndex: i }));
|
|
1219
|
-
return await selectInstance(rl, channelType, view);
|
|
1285
|
+
return await selectInstance(rl, channelType, view, options);
|
|
1220
1286
|
}
|
|
1221
|
-
async function
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1287
|
+
async function pickExistingChannelOperation(rl, displayName, channelName) {
|
|
1288
|
+
console.log(`\n请选择对${displayName}实例 ${channelName} 的操作:`);
|
|
1289
|
+
console.log(' a. 覆盖机器人配置(重新扫码或输入 App ID / App Secret)');
|
|
1290
|
+
console.log(' b. 重新绑定联系人(保留机器人配置,生成一次性 /bind 验证码)');
|
|
1291
|
+
while (true) {
|
|
1292
|
+
const choice = (await ask(rl, '请选择 [a/b]: ')).trim().toLowerCase();
|
|
1293
|
+
if (choice === 'a')
|
|
1294
|
+
return 'configure';
|
|
1295
|
+
if (choice === 'b')
|
|
1296
|
+
return 'rebind-contact';
|
|
1297
|
+
console.log('无效选择,请输入 a 或 b');
|
|
1227
1298
|
}
|
|
1228
|
-
|
|
1299
|
+
}
|
|
1300
|
+
async function confirmOverwriteChannel(rl, displayName, channelName) {
|
|
1301
|
+
const confirm = (await ask(rl, `⚠️ 即将覆盖${displayName}实例 ${channelName} 的机器人配置,确认?(y/N) `)).trim().toLowerCase();
|
|
1302
|
+
if (confirm === 'y' || confirm === 'yes')
|
|
1303
|
+
return true;
|
|
1304
|
+
console.log('已取消');
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
async function pickContactForChannelBinding(rl, agentConfig, channelType) {
|
|
1308
|
+
const candidates = preferredContactBindingCandidates(agentConfig);
|
|
1309
|
+
const contacts = getContactSnapshot(agentConfig.aid).contacts;
|
|
1310
|
+
console.log(`\n请选择 ${channelType} 账号要绑定到的联系人 AID:`);
|
|
1229
1311
|
candidates.forEach((candidate, index) => {
|
|
1230
1312
|
console.log(` ${index + 1}. ${candidate.primaryId} (${candidate.label})`);
|
|
1231
1313
|
});
|
|
1314
|
+
const manualChoice = candidates.length + 1;
|
|
1315
|
+
console.log(` ${manualChoice}. 手工输入已有联系人 AID`);
|
|
1232
1316
|
const defaultIndex = candidates.length === 1 ? 1 : null;
|
|
1233
1317
|
const prompt = defaultIndex ? `请选择 [${defaultIndex}]: ` : '请选择: ';
|
|
1234
1318
|
while (true) {
|
|
@@ -1237,14 +1321,26 @@ async function pickOwnerAdminForChannelBinding(rl, agentConfig, channelType) {
|
|
|
1237
1321
|
if (Number.isInteger(selected) && selected >= 1 && selected <= candidates.length) {
|
|
1238
1322
|
return candidates[selected - 1];
|
|
1239
1323
|
}
|
|
1240
|
-
|
|
1324
|
+
if (selected === manualChoice) {
|
|
1325
|
+
const primaryId = (await ask(rl, '请输入要绑定的联系人 AID: ')).trim();
|
|
1326
|
+
if (isValidAid(primaryId) && contacts.has(primaryId))
|
|
1327
|
+
return { primaryId, label: 'contact' };
|
|
1328
|
+
if (isValidAid(primaryId)) {
|
|
1329
|
+
console.log('该 AID 不是当前 Agent 的联系人,请先添加联系人或将其设为 Owner。');
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
console.log('AID 格式无效,请重新输入。');
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
console.log(`无效选择,请输入 1-${manualChoice}`);
|
|
1241
1336
|
}
|
|
1242
1337
|
}
|
|
1243
|
-
function
|
|
1338
|
+
function preferredContactBindingCandidates(agentConfig) {
|
|
1339
|
+
const contacts = getContactSnapshot(agentConfig.aid).contacts;
|
|
1244
1340
|
const labels = new Map();
|
|
1245
1341
|
const add = (value, label) => {
|
|
1246
1342
|
const aid = String(value || '').trim();
|
|
1247
|
-
if (!isValidAid(aid))
|
|
1343
|
+
if (!isValidAid(aid) || !contacts.has(aid))
|
|
1248
1344
|
return;
|
|
1249
1345
|
let set = labels.get(aid);
|
|
1250
1346
|
if (!set) {
|
|
@@ -1255,7 +1351,7 @@ function ownerAdminContactCandidates(agentConfig) {
|
|
|
1255
1351
|
};
|
|
1256
1352
|
for (const owner of agentConfig.owners ?? [])
|
|
1257
1353
|
add(owner, 'owner');
|
|
1258
|
-
for (const admin of agentConfig.
|
|
1354
|
+
for (const admin of listStaticAgentAdmins(agentConfig.aid))
|
|
1259
1355
|
add(admin, 'admin');
|
|
1260
1356
|
return [...labels.entries()].map(([primaryId, roles]) => ({
|
|
1261
1357
|
primaryId,
|
|
@@ -1273,7 +1369,7 @@ function bindQrActorToContact(aid, binding, channelType, actorId, channelKey) {
|
|
|
1273
1369
|
try {
|
|
1274
1370
|
const changed = bindContactAlias(aid, binding.primaryId, channelType, normalizedActor, channelKey);
|
|
1275
1371
|
const action = changed ? '已写入' : '已存在';
|
|
1276
|
-
console.log(` ✓ 身份映射${action}: ${
|
|
1372
|
+
console.log(` ✓ 身份映射${action}: ${channelKey}:${encodeURIComponent(normalizedActor)} -> ${binding.primaryId}`);
|
|
1277
1373
|
}
|
|
1278
1374
|
catch (error) {
|
|
1279
1375
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1281,23 +1377,40 @@ function bindQrActorToContact(aid, binding, channelType, actorId, channelKey) {
|
|
|
1281
1377
|
}
|
|
1282
1378
|
}
|
|
1283
1379
|
async function registerDingtalkContactBind(aid, channelName, binding) {
|
|
1380
|
+
await registerChannelContactBind('dingtalk', '钉钉', aid, channelName, binding);
|
|
1381
|
+
}
|
|
1382
|
+
function contactBindChannelKey(channelType, aid, channelName) {
|
|
1383
|
+
return `${channelType}#${aid}#${channelName}`;
|
|
1384
|
+
}
|
|
1385
|
+
async function registerFeishuContactBind(aid, channelName, binding) {
|
|
1386
|
+
await registerChannelContactBind('feishu', '飞书', aid, channelName, binding);
|
|
1387
|
+
}
|
|
1388
|
+
async function registerWechatContactBind(aid, channelName, binding) {
|
|
1389
|
+
await registerChannelContactBind('wechat', '微信', aid, channelName, binding);
|
|
1390
|
+
}
|
|
1391
|
+
async function registerQQBotContactBind(aid, channelName, binding) {
|
|
1392
|
+
await registerChannelContactBind('qqbot', 'QQ', aid, channelName, binding);
|
|
1393
|
+
}
|
|
1394
|
+
async function registerWecomContactBind(aid, channelName, binding) {
|
|
1395
|
+
await registerChannelContactBind('wecom', '企微', aid, channelName, binding);
|
|
1396
|
+
}
|
|
1397
|
+
async function registerChannelContactBind(channelType, displayName, aid, channelName, binding) {
|
|
1284
1398
|
if (!binding)
|
|
1285
1399
|
return;
|
|
1286
1400
|
const result = await ipcQuery(resolvePaths().socket, {
|
|
1287
|
-
type:
|
|
1401
|
+
type: `${channelType}.contact-bind.register`,
|
|
1288
1402
|
selfAid: aid,
|
|
1289
1403
|
channelName,
|
|
1290
1404
|
primaryId: binding.primaryId,
|
|
1291
1405
|
}, 3000);
|
|
1292
1406
|
if (!result?.ok) {
|
|
1293
|
-
console.log(` ⚠
|
|
1294
|
-
console.log(
|
|
1407
|
+
console.log(` ⚠ ${displayName}绑定码注册失败: ${result?.error || 'daemon 不可用'}`);
|
|
1408
|
+
console.log(` 请重新执行 ec init ${channelType} 并再次绑定。`);
|
|
1295
1409
|
return;
|
|
1296
1410
|
}
|
|
1297
1411
|
const expiresAt = new Date(Number(result.expiresAt)).toLocaleString();
|
|
1298
|
-
console.log(` ✓
|
|
1299
|
-
console.log(` 请在 10
|
|
1300
|
-
console.log(' 非纯数字或位数不对只会提醒格式,不计入错误次数;6 位数字输错最多 5 次。');
|
|
1412
|
+
console.log(` ✓ ${displayName}身份绑定码: ${result.code}`);
|
|
1413
|
+
console.log(` 请在 10 分钟内私聊${displayName}机器人,发送:/bind ${result.code}。有效期至: ${expiresAt}`);
|
|
1301
1414
|
}
|
|
1302
1415
|
function firstNonEmpty(...values) {
|
|
1303
1416
|
for (const value of values) {
|